Team LiB
Previous Section Next Section

7.1. Defining Abstract Data Types

 

The Sales_item class that we used in Chapter 1 is an abstract data type. We use a Sales_item object by using its interface (i.e., the operations described in § 1.5.1 (p. 20)). We have no access to the data members stored in a Sales_item object. Indeed, we don’t even know what data members that class has.

 

Our Sales_data class (§ 2.6.1, p. 72) is not an abstract data type. It lets users of the class access its data members and forces users to write their own operations. To make Sales_data an abstract type, we need to define operations for users of Sales_data to use. Once Sales_data defines its own operations, we can encapsulate (that is, hide) its data members.

 

7.1.1. Designing the Sales_data Class

 
Image

Ultimately, we want Sales_data to support the same set of operations as the Sales_item class. The Sales_item class had one member function1.5.2, p. 23), named isbn, and supported the +, =, +=, <<, and >> operators.

 

We’ll learn how to define our own operators in Chapter 14. For now, we’ll define ordinary (named) functions for these operations. For reasons that we will explain in § 14.1 (p. 555), the functions that do addition and IO will not be members of Sales_data. Instead, we’ll define those functions as ordinary functions. The function that handles compound assignment will be a member, and for reasons we’ll explain in § 7.1.5 (p. 267), our class doesn’t need to define assignment.

 

Thus, the interface to Sales_data consists of the following operations:

 

• An isbn member function to return the object’s ISBN

 

• A combine member function to add one Sales_data object into another

 

• A function named add to add two Sales_data objects

 

• A read function to read data from an istream into a Sales_data object

 

• A print function to print the value of a Sales_data object on an ostream

 

Key Concept: Different Kinds of Programming Roles

Programmers tend to think about the people who will run their applications as users. Similarly a class designer designs and implements a class for users of that class. In this case, the user is a programmer, not the ultimate user of the application.

 

When we refer to a user, the context makes it clear which kind of user is meant. If we speak of user code or the user of the Sales_data class, we mean a programmer who is using a class. If we speak of the user of the bookstore application, we mean the manager of the store who is running the application.

 

In simple applications, the user of a class and the designer of the class might be one and the same person. Even in such cases, it is useful to keep the roles distinct. When we design the interface of a class, we should think about how easy it will be to use the class. When we use the class, we shouldn’t think about how the class works.

 

Authors of successful applications do a good job of understanding and implementing the needs of the application’s users. Similarly, good class designers pay close attention to the needs of the programmers who will use the class. A well-designed class has an interface that is intuitive and easy to use and has an implementation that is efficient enough for its intended use.

 

 
Using the Revised Sales_data Class
 

Before we think about how to implement our class, let’s look at how we can use our interface functions. As one example, we can use these functions to write a version of the bookstore program from § 1.6 (p. 24) that works with Sales_data objects rather than Sales_items:

 

 

Sales_data total;         // variable to hold the running sum
if (read(cin, total))  {  // read the first transaction
    Sales_data trans;     // variable to hold data for the next transaction
    while(read(cin, trans)) {      //  read the remaining transactions
        if (total.isbn() == trans.isbn())   // check the isbns
            total.combine(trans);  // update the running total
        else {
            print(cout, total) << endl;  // print the results
            total = trans;               // process the next book
        }
    }
    print(cout, total) << endl;          // print the last transaction
} else {                                 // there was no input
    cerr << "No data?!" << endl;         // notify the user
}

 

We start by defining a Sales_data object to hold the running total. Inside the if condition, we call read to read the first transaction into total. This condition works like other loops we’ve written that used the >> operator. Like the >> operator, our read function will return its stream parameter, which the condition checks (§ 4.11.2, p. 162). If the read fails, we fall through to the else to print an error message.

 

If there are data to read, we define trans, which we’ll use to hold each transaction. The condition in the while also checks the stream returned by read. So long as the input operations in read succeed, the condition succeeds and we have another transaction to process.

 

Inside the while, we call the isbn members of total and trans to fetch their respective ISBNs. If total and trans refer to the same book, we call combine to add the components of trans into the running total in total. If trans represents a new book, we call print to print the total for the previous book. Because print returns a reference to its stream parameter, we can use the result of print as the left-hand operand of the <<. We do so to print a newline following the output generated by print. We next assign trans to total, thus setting up to process the records for the next book in the file.

 

After we have exhausted the input, we have to remember to print the data for the last transaction, which we do in the call to print following the while loop.

 

Exercises Section 7.1.1

 

Exercise 7.1: Write a version of the transaction-processing program from § 1.6 (p. 24) using the Sales_data class you defined for the exercises in § 2.6.1 (p. 72).

 

 

7.1.2. Defining the Revised Sales_data Class

 
Image

Our revised class will have the same data members as the version we defined in § 2.6.1 (p. 72): bookNo, a string representing the ISBN; units_sold, an unsigned that says how many copies of the book were sold; and revenue, a double representing the total revenue for those sales.

 

As we’ve seen, our class will also have two member functions, combine and isbn. In addition, we’ll give Sales_data another member function to return the average price at which the books were sold. This function, which we’ll name avg_price, isn’t intended for general use. It will be part of the implementation, not part of the interface.

 

We define (§ 6.1, p. 202) and declare (§ 6.1.2, p. 206) member functions similarly to ordinary functions. Member functions must be declared inside the class. Member functions may be defined inside the class itself or outside the class body. Nonmember functions that are part of the interface, such as add, read, and print, are declared and defined outside the class.

 

With this knowledge, we’re ready to write our revised version of Sales_data:

 

 

struct Sales_data {
    // new members: operations on Sales_data objects
    std::string isbn() const { return bookNo; }
    Sales_data& combine(const Sales_data&);
    double avg_price() const;
    // data members are unchanged from § 2.6.1 (p. 72)
    std::string bookNo;
    unsigned units_sold = 0;
    double revenue = 0.0;
};
// nonmember Sales_data interface functions
Sales_data add(const Sales_data&, const Sales_data&);
std::ostream &print(std::ostream&, const Sales_data&);
std::istream &read(std::istream&, Sales_data&);

 

Image Note

Functions defined in the class are implicitly inline6.5.2, p. 238).

 

 
Defining Member Functions
 

Although every member must be declared inside its class, we can define a member function’s body either inside or outside of the class body. In Sales_data, isbn is defined inside the class; combine and avg_price will be defined elsewhere.

 

We’ll start by explaining the isbn function, which returns a string and has an empty parameter list:

 

 

std::string isbn() const { return bookNo; }

 

As with any function, the body of a member function is a block. In this case, the block contains a single return statement that returns the bookNo data member of a Sales_data object. The interesting thing about this function is how it gets the object from which to fetch the bookNo member.

 
Introducing this
 
Image

Let’s look again at a call to the isbn member function:

 

total.isbn()

 

Here we use the dot operator (§ 4.6, p. 150) to fetch the isbn member of the object named total, which we then call.

 

With one exception that we’ll cover in § 7.6 (p. 300), when we call a member function we do so on behalf of an object. When isbn refers to members of Sales_data (e.g., bookNo), it is referring implicitly to the members of the object on which the function was called. In this call, when isbn returns bookNo, it is implicitly returning total.bookNo.

 

Member functions access the object on which they were called through an extra, implicit parameter named this. When we call a member function, this is initialized with the address of the object on which the function was invoked. For example, when we call

 

total.isbn()

 

the compiler passes the address of total to the implicit this parameter in isbn. It is as if the compiler rewrites this call as

 

 

// pseudo-code illustration of how a call to a member function is translated
Sales_data::isbn(&total)

 

which calls the isbn member of Sales_data passing the address of total.

 

Inside a member function, we can refer directly to the members of the object on which the function was called. We do not have to use a member access operator to use the members of the object to which this points. Any direct use of a member of the class is assumed to be an implicit reference through this. That is, when isbn uses bookNo, it is implicitly using the member to which this points. It is as if we had written this->bookNo.

 

The this parameter is defined for us implicitly. Indeed, it is illegal for us to define a parameter or variable named this. Inside the body of a member function, we can use this. It would be legal, although unnecessary, to define isbn as

 

 

std::string isbn() const { return this->bookNo; }

 

Because this is intended to always refer to “this” object, this is a const pointer (§ 2.4.2, p. 62). We cannot change the address that this holds.

 
Introducing const Member Functions
 

The other important part about the isbn function is the keyword const that follows the parameter list. The purpose of that const is to modify the type of the implicit this pointer.

 

By default, the type of this is a const pointer to the nonconst version of the class type. For example, by default, the type of this in a Sales_data member function is Sales_data *const. Although this is implicit, it follows the normal initialization rules, which means that (by default) we cannot bind this to a const object (§ 2.4.2, p. 62). This fact, in turn, means that we cannot call an ordinary member function on a const object.

 

If isbn were an ordinary function and if this were an ordinary pointer parameter, we would declare this as const Sales_data *const. After all, the body of isbn doesn’t change the object to which this points, so our function would be more flexible if this were a pointer to const6.2.3, p. 213).

 

However, this is implicit and does not appear in the parameter list. There is no place to indicate that this should be a pointer to const. The language resolves this problem by letting us put const after the parameter list of a member function. A const following the parameter list indicates that this is a pointer to const. Member functions that use const in this way are const member functions.

 

We can think of the body of isbn as if it were written as

 

 

// pseudo-code illustration of how the implicit this pointer is used
// this code is illegal: we may not explicitly define the this pointer ourselves
// note that this is a pointer to const because isbn is a const member
std::string Sales_data::isbn(const Sales_data *const this)
{ return this->isbn; }

 

The fact that this is a pointer to const means that const member functions cannot change the object on which they are called. Thus, isbn may read but not write to the data members of the objects on which it is called.

 

Image Note

Objects that are const, and references or pointers to const objects, may call only const member functions.

 

 
Class Scope and Member Functions
 

Recall that a class is itself a scope (§ 2.6.1, p. 72). The definitions of the member functions of a class are nested inside the scope of the class itself. Hence, isbn’s use of the name bookNo is resolved as the data member defined inside Sales_data.

 

It is worth noting that isbn can use bookNo even though bookNo is defined after isbn. As we’ll see in § 7.4.1 (p. 283), the compiler processes classes in two steps—the member declarations are compiled first, after which the member function bodies, if any, are processed. Thus, member function bodies may use other members of their class regardless of where in the class those members appear.

 
Defining a Member Function outside the Class
 

As with any other function, when we define a member function outside the class body, the member’s definition must match its declaration. That is, the return type, parameter list, and name must match the declaration in the class body. If the member was declared as a const member function, then the definition must also specify const after the parameter list. The name of a member defined outside the class must include the name of the class of which it is a member:

 

 

double Sales_data::avg_price() const {
    if (units_sold)
        return revenue/units_sold;
    else
        return 0;
}

 

The function name, Sales_data::avg_price, uses the scope operator (§ 1.2, p. 8) to say that we are defining the function named avg_price that is declared in the scope of the Sales_data class. Once the compiler sees the function name, the rest of the code is interpreted as being inside the scope of the class. Thus, when avg_price refers to revenue and units_sold, it is implicitly referring to the members of Sales_data.

 
Defining a Function to Return “This” Object
 

The combine function is intended to act like the compound assignment operator, +=. The object on which this function is called represents the left-hand operand of the assignment. The right-hand operand is passed as an explicit argument:

 

 

Sales_data& Sales_data::combine(const Sales_data &rhs)
{
    units_sold += rhs.units_sold; // add the members of rhs into
    revenue += rhs.revenue;       // the members of ''this'' object
    return *this; // return the object on which the function was called
}

 

When our transaction-processing program calls

 

 

total.combine(trans); // update the running total

 

the address of total is bound to the implicit this parameter and rhs is bound to trans. Thus, when combine executes

 

 

units_sold += rhs.units_sold; // add the members of rhs into

 

the effect is to add total.units_sold and trans.units_sold, storing the result back into total.units_sold.

 

The interesting part about this function is its return type and the return statement. Ordinarily, when we define a function that operates like a built-in operator, our function should mimic the behavior of that operator. The built-in assignment operators return their left-hand operand as an lvalue (§ 4.4, p. 144). To return an lvalue, our combine function must return a reference (§ 6.3.2, p. 226). Because the left-hand operand is a Sales_data object, the return type is Sales_data&.

 

As we’ve seen, we do not need to use the implicit this pointer to access the members of the object on which a member function is executing. However, we do need to use this to access the object as a whole:

 

 

return *this; // return the object on which the function was called

 

Here the return statement dereferences this to obtain the object on which the function is executing. That is, for the call above, we return a reference to total.

 

Exercises Section 7.1.2

 

Exercise 7.2: Add the combine and isbn members to the Sales_data class you wrote for the exercises in § 2.6.2 (p. 76).

 

Exercise 7.3: Revise your transaction-processing program from § 7.1.1 (p. 256) to use these members.

 

Exercise 7.4: Write a class named Person that represents the name and address of a person. Use a string to hold each of these elements. Subsequent exercises will incrementally add features to this class.

Exercise 7.5: Provide operations in your Person class to return the name and address. Should these functions be const? Explain your choice.


 

7.1.3. Defining Nonmember Class-Related Functions

 
Image

Class authors often define auxiliary functions, such as our add, read, and print functions. Although such functions define operations that are conceptually part of the interface of the class, they are not part of the class itself.

 

We define nonmember functions as we would any other function. As with any other function, we normally separate the declaration of the function from its definition (§ 6.1.2, p. 206). Functions that are conceptually part of a class, but not defined inside the class, are typically declared (but not defined) in the same header as the class itself. That way users need to include only one file to use any part of the interface.

 

Image Note

Ordinarily, nonmember functions that are part of the interface of a class should be declared in the same header as the class itself.

 

 
Defining the read and print Functions
 

The read and print functions do the same job as the code in § 2.6.2 (p. 75) and not surprisingly, the bodies of our functions look a lot like the code presented there:

 

 

// input transactions contain ISBN, number of copies sold, and sales price
istream &read(istream &is, Sales_data &item)
{
    double price = 0;
    is >> item.bookNo >> item.units_sold >> price;
    item.revenue = price * item.units_sold;
    return is;
}
ostream &print(ostream &os, const Sales_data &item)
{
    os << item.isbn() << " " << item.units_sold << " "
       << item.revenue << " " << item.avg_price();
    return os;
}

 

The read function reads data from the given stream into the given object. The print function prints the contents of the given object on the given stream.

 

However, there are two points worth noting about these functions. First, both read and write take a reference to their respective IO class types. The IO classes are types that cannot be copied, so we may only pass them by reference (§ 6.2.2, p. 210). Moreover, reading or writing to a stream changes that stream, so both functions take ordinary references, not references to const.

 

The second thing to note is that print does not print a newline. Ordinarily, functions that do output should do minimal formatting. That way user code can decide whether the newline is needed.

 
Defining the add Function
 

The add function takes two Sales_data objects and returns a new Sales_data representing their sum:

 

 

Sales_data add(const Sales_data &lhs, const Sales_data &rhs)
{
    Sales_data sum = lhs;  // copy data members from lhs into sum
    sum.combine(rhs);      // add data members from rhs into sum
    return sum;
}

 

In the body of the function we define a new Sales_data object named sum to hold the sum of our two transactions. We initialize sum as a copy of lhs. By default, copying a class object copies that object’s members. After the copy, the bookNo, units_sold, and revenue members of sum will have the same values as those in lhs. Next we call combine to add the units_sold and revenue members of rhs into sum. When we’re done, we return a copy of sum.

 

Exercises Section 7.1.3

 

Exercise 7.6: Define your own versions of the add, read, and print functions.

Exercise 7.7: Rewrite the transaction-processing program you wrote for the exercises in § 7.1.2 (p. 260) to use these new functions.

 

Exercise 7.8: Why does read define its Sales_data parameter as a plain reference and print define its parameter as a reference to const?

Exercise 7.9: Add operations to read and print Person objects to the code you wrote for the exercises in § 7.1.2 (p. 260).

 

Exercise 7.10: What does the condition in the following if statement do?

if (read(read(cin, data1), data2))

 

 

7.1.4. Constructors

 
Image

Each class defines how objects of its type can be initialized. Classes control object initialization by defining one or more special member functions known as constructors. The job of a constructor is to initialize the data members of a class object. A constructor is run whenever an object of a class type is created.

 

In this section, we’ll introduce the basics of how to define a constructor. Constructors are a surprisingly complex topic. Indeed, we’ll have more to say about constructors in § 7.5 (p. 288), § 15.7 (p. 622), and § 18.1.3 (p. 777), and in Chapter 13.

 

Constructors have the same name as the class. Unlike other functions, constructors have no return type. Like other functions, constructors have a (possibly empty) parameter list and a (possibly empty) function body. A class can have multiple constructors. Like any other overloaded function (§ 6.4, p. 230), the constructors must differ from each other in the number or types of their parameters.

 

Unlike other member functions, constructors may not be declared as const7.1.2, p. 258). When we create a const object of a class type, the object does not assume its “constness” until after the constructor completes the object’s initialization. Thus, constructors can write to const objects during their construction.

 
The Synthesized Default Constructor
 
Image

Our Sales_data class does not define any constructors, yet the programs we’ve written that use Sales_data objects compile and run correctly. As an example, the program on page 255 defined two objects:

 

 

Sales_data total;     // variable to hold the running sum
Sales_data trans;     // variable to hold data for the next transaction

 

The question naturally arises: How are total and trans initialized?

 

We did not supply an initializer for these objects, so we know that they are default initialized (§ 2.2.1, p. 43). Classes control default initialization by defining a special constructor, known as the default constructor. The default constructor is one that takes no arguments.

 

As we’ll, see the default constructor is special in various ways, one of which is that if our class does not explicitly define any constructors, the compiler will implicitly define the default constructor for us

 

The compiler-generated constructor is known as the synthesized default constructor. For most classes, this synthesized constructor initializes each data member of the class as follows:

 

• If there is an in-class initializer (§ 2.6.1, p. 73), use it to initialize the member.

 

• Otherwise, default-initialize (§ 2.2.1, p. 43) the member.

 

Because Sales_data provides initializers for units_sold and revenue, the synthesized default constructor uses those values to initialize those members. It default initializes bookNo to the empty string.

 
Some Classes Cannot Rely on the Synthesized Default Constructor
 

Only fairly simple classes—such as the current definition of Sales_data—can rely on the synthesized default constructor. The most common reason that a class must define its own default constructor is that the compiler generates the default for us only if we do not define any other constructors for the class. If we define any constructors, the class will not have a default constructor unless we define that constructor ourselves. The basis for this rule is that if a class requires control to initialize an object in one case, then the class is likely to require control in all cases.

 

Image Note

The compiler generates a default constructor automatically only if a class declares no constructors.

 

 

A second reason to define the default constructor is that for some classes, the synthesized default constructor does the wrong thing. Remember that objects of built-in or compound type (such as arrays and pointers) that are defined inside a block have undefined value when they are default initialized (§ 2.2.1, p. 43). The same rule applies to members of built-in type that are default initialized. Therefore, classes that have members of built-in or compound type should ordinarily either initialize those members inside the class or define their own version of the default constructor. Otherwise, users could create objects with members that have undefined value.

 

Image Warning

Classes that have members of built-in or compound type usually should rely on the synthesized default constructor only if all such members have in-class initializers.

 

 

A third reason that some classes must define their own default constructor is that sometimes the compiler is unable to synthesize one. For example, if a class has a member that has a class type, and that class doesn’t have a default constructor, then the compiler can’t initialize that member. For such classes, we must define our own version of the default constructor. Otherwise, the class will not have a usable default constructor. We’ll see in § 13.1.6 (p. 508) additional circumstances that prevent the compiler from generating an appropriate default constructor.

 
Defining the Sales_data Constructors
 

For our Sales_data class we’ll define four constructors with the following parameters:

 

• An istream& from which to read a transaction.

 

• A const string& representing an ISBN, an unsigned representing the count of how many books were sold, and a double representing the price at which the books sold.

 

• A const string& representing an ISBN. This constructor will use default values for the other members.

 

• An empty parameter list (i.e., the default constructor) which as we’ve just seen we must define because we have defined other constructors.

 

Adding these members to our class, we now have

 

 

struct Sales_data {
    // constructors added
    Sales_data() = default;
    Sales_data(const std::string &s): bookNo(s) { }
    Sales_data(const std::string &s, unsigned n, double p):
               bookNo(s), units_sold(n), revenue(p*n) { }
    Sales_data(std::istream &);
    // other members as before
    std::string isbn() const { return bookNo; }
    Sales_data& combine(const Sales_data&);
    double avg_price() const;
    std::string bookNo;
    unsigned units_sold = 0;
    double revenue = 0.0;
};

 
What = default Means
 

We’ll start by explaining the default constructor:

 

Sales_data() = default;

 

First, note that this constructor defines the default constructor because it takes no arguments. We are defining this constructor only because we want to provide other constructors as well as the default constructor. We want this constructor to do exactly the same work as the synthesized version we had been using.

 

Under the new standard, if we want the default behavior, we can ask the compiler to generate the constructor for us by writing = default after the parameter list. The = default can appear with the declaration inside the class body or on the definition outside the class body. Like any other function, if the = default appears inside the class body, the default constructor will be inlined; if it appears on the definition outside the class, the member will not be inlined by default.

 
Image

Image Warning

The default constructor works for Sales_data only because we provide initializers for the data members with built-in type. If your compiler does not support in-class initializers, your default constructor should use the constructor initializer list (described immediately following) to initialize every member of the class.

 

 
Constructor Initializer List
 

Next we’ll look at the other two constructors that were defined inside the class:

 

 

Sales_data(const std::string &s): bookNo(s) { }
Sales_data(const std::string &s, unsigned n, double p):
           bookNo(s), units_sold(n), revenue(p*n) { }

 

The new parts in these definitions are the colon and the code between it and the curly braces that define the (empty) function bodies. This new part is a constructor initializer list, which specifies initial values for one or more data members of the object being created. The constructor initializer is a list of member names, each of which is followed by that member’s initial value in parentheses (or inside curly braces). Multiple member initializations are separated by commas.

 

The constructor that has three parameters uses its first two parameters to initialize the bookNo and units_sold members. The initializer for revenue is calculated by multiplying the number of books sold by the price per book.

 

The constructor that has a single string parameter uses that string to initialize bookNo but does not explicitly initialize the units_sold and revenue members. When a member is omitted from the constructor initializer list, it is implicitly initialized using the same process as is used by the synthesized default constructor. In this case, those members are initialized by the in-class initializers. Thus, the constructor that takes a string is equivalent to

 

 

// has the same behavior as the original constructor defined above
Sales_data(const std::string &s):
           bookNo(s), units_sold(0), revenue(0){ }

 

It is usually best for a constructor to use an in-class initializer if one exists and gives the member the correct value. On the other hand, if your compiler does not yet support in-class initializers, then every constructor should explicitly initialize every member of built-in type.

 

Image Best Practices

Constructors should not override in-class initializers except to use a different initial value. If you can’t use in-class initializers, each constructor should explicitly initialize every member of built-in type.

 

 

It is worth noting that both constructors have empty function bodies. The only work these constructors need to do is give the data members their values. If there is no further work, then the function body is empty.

 
Defining a Constructor outside the Class Body
 

Unlike our other constructors, the constructor that takes an istream does have work to do. Inside its function body, this constructor calls read to give the data members new values:

 

 

Sales_data::Sales_data(std::istream &is)
{
    read(is, *this); // read will read a transaction from is into this object
}

 

Constructors have no return type, so this definition starts with the name of the function we are defining. As with any other member function, when we define a constructor outside of the class body, we must specify the class of which the constructor is a member. Thus, Sales_data::Sales_data says that we’re defining the Sales_data member named Sales_data. This member is a constructor because it has the same name as its class.

 

In this constructor there is no constructor initializer list, although technically speaking, it would be more correct to say that the constructor initializer list is empty. Even though the constructor initializer list is empty, the members of this object are still initialized before the constructor body is executed.

 

Members that do not appear in the constructor initializer list are initialized by the corresponding in-class initializer (if there is one) or are default initialized. For Sales_data that means that when the function body starts executing, bookNo will be the empty string, and units_sold and revenue will both be 0.

 

To understand the call to read, remember that read’s second parameter is a reference to a Sales_data object. In § 7.1.2 (p. 259), we noted that we use this to access the object as a whole, rather than a member of the object. In this case, we use *this to pass “this” object as an argument to the read function.

 

Exercises Section 7.1.4

 

Exercise 7.11: Add constructors to your Sales_data class and write a program to use each of the constructors.

Exercise 7.12: Move the definition of the Sales_data constructor that takes an istream into the body of the Sales_data class.

Exercise 7.13: Rewrite the program from page 255 to use the istream constructor.

 

Exercise 7.14: Write a version of the default constructor that explicitly initializes the members to the values we have provided as in-class initializers.

Exercise 7.15: Add appropriate constructors to your Person class.


 

7.1.5. Copy, Assignment, and Destruction

 
Image

In addition to defining how objects of the class type are initialized, classes also control what happens when we copy, assign, or destroy objects of the class type. Objects are copied in several contexts, such as when we initialize a variable or when we pass or return an object by value (§ 6.2.1, p. 209, and § 6.3.2, p. 224). Objects are assigned when we use the assignment operator (§ 4.4, p. 144). Objects are destroyed when they cease to exist, such as when a local object is destroyed on exit from the block in which it was created (§ 6.1.1, p. 204). Objects stored in a vector (or an array) are destroyed when that vector (or array) is destroyed.

 

If we do not define these operations, the compiler will synthesize them for us. Ordinarily, the versions that the compiler generates for us execute by copying, assigning, or destroying each member of the object. For example, in our bookstore program in § 7.1.1 (p. 255), when the compiler executes this assignment

 

 

total = trans;               // process the next book

 

it executes as if we had written

 

 

// default assignment for Sales_data is equivalent to:
total.bookNo = trans.bookNo;
total.units_sold = trans.units_sold;
total.revenue = trans.revenue;

 

We’ll show how we can define our own versions of these operations in Chapter 13.

 
Some Classes Cannot Rely on the Synthesized Versions
 
Image

Although the compiler will synthesize the copy, assignment, and destruction operations for us, it is important to understand that for some classes the default versions do not behave appropriately. In particular, the synthesized versions are unlikely to work correctly for classes that allocate resources that reside outside the class objects themselves. As one example, in Chapter 12 we’ll see how C++ programs allocate and manage dynamic memory. As we’ll see in § 13.1.4 (p. 504), classes that manage dynamic memory, generally cannot rely on the synthesized versions of these operations.

 

However, it is worth noting that many classes that need dynamic memory can (and generally should) use a vector or a string to manage the necessary storage. Classes that use vectors and strings avoid the complexities involved in allocating and deallocating memory.

 

Moreover, the synthesized versions for copy, assignment, and destruction work correctly for classes that have vector or string members. When we copy or assign an object that has a vector member, the vector class takes care of copying or assigning the elements in that member. When the object is destroyed, the vector member is destroyed, which in turn destroys the elements in the vector. Similarly for strings.

 

Image Warning

Until you know how to define the operations covered in Chapter 13, the resources your classes allocate should be stored directly as data members of the class.

 

 
Team LiB
Previous Section Next Section