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
FundamentalUltimately, we want Sales_data
to support the same set of operations as the Sales_item
class. The Sales_item
class had one member function (§ 1.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 oneSales_data
object into another - A function named
add
to add twoSales_data
objects - A
read
function to read data from anistream
into aSales_data
object - A
print
function to print the value of aSales_data
object on anostream
INFO
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.
C++ programmers tend to speak of users interchangeably as users of the application or users of a class.
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_item
s:
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.
INFO
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
FundamentalOur 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&);
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
TrickyLet’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 const
(§ 6.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.
INFO
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 afterisbn
. 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
.
INFO
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
FundamentalClass 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.
INFO
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
.
INFO
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
FundamentalEach 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 const
(§ 7.1.2, p. 258). When we create a const
object of a class type, the object does not assume its “const
ness” until after the constructor completes the object’s initialization. Thus, constructors can write to const
objects during their construction.
The Synthesized Default Constructor
TrickyOur 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.
INFO
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.
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, anunsigned
representing the count of how many books were sold, and adouble
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.
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.
TIP
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.
INFO
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
FundamentalIn 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
TrickyAlthough 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 vector
s and string
s 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 string
s.
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.