Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Professional C++ [eng].pdf
Скачиваний:
284
Добавлен:
16.08.2013
Размер:
11.09 Mб
Скачать

Gaining Proficiency with Classes and Objects

Objects on the Heap

You can also dynamically allocate objects using new:

SpreadsheetCell* myCellp = new SpreadsheetCell();

myCellp->setValue(3.7);

cout << “cell 1: “ << myCellp->getValue() << “ “ << myCellp->getString() << endl;

delete myCellp;

When you create an object on the heap, you call its methods and access its members through the “arrow” operator: ->. The arrow combines dereferencing (*) and method or member access (.). You could use those two operators instead, but doing so would be stylistically awkward:

SpreadsheetCell* myCellp = new SpreadsheetCell();

(*myCellp).setValue(3.7);

cout << “cell 1: “ << (*myCellp).getValue() << “ “ << (*myCellp).getString() << endl;

delete myCellp;

Just as you must free other memory that you allocate on the heap, you must free the memory for objects that you allocate on the heap by calling delete on the objects.

If you allocate an object with new, free it with delete when you are finished with it.

Object Life Cycles

The object life cycle involves three activities: creation, destruction, and assignment. Every object is created, but not every object encounters the other two “life events.” It is important to understand how and when objects are created, destroyed, and assigned, and how you can customize these behaviors.

Object Creation

Objects are created at the point you declare them (if they’re on the stack) or when you explicitly allocate space for them with new or new[].

It is often helpful to give variables initial values as you declare them. For example, you should usually initialize integer variables to 0 like this:

int x = 0, y = 0;

Similarly, you should give initial values to objects. You can provide this functionality by declaring and writing a special method called a constructor, in which you can perform initialization work for the object. Whenever an object is created, one of its constructers is executed.

165

Chapter 8

C++ programmers often call a constructor a “ctor.”

Writing Constructors

Here is a first attempt at adding a constructor to the SpreadsheetCell class:

class SpreadsheetCell

{

public:

SpreadsheetCell(double initialValue); void setValue(double inValue); double getValue();

void setString(string inString); string getString();

protected:

string doubleToString(double inValue); double stringToDouble(string inString);

double mValue; string mString;

};

Note that the constructor has the same name as the name of the class and does not have a return type. These facts are always true about constructors. Just as you must provide implementations for normal methods, you must provide an implementation for the constructor:

SpreadsheetCell::SpreadsheetCell(double initialValue)

{

setValue(initialValue);

}

The SpreadsheetCell constructor is a method of the SpreadsheetCell class, so C++ requires the normal SpreadsheetCell:: scope resolution phrase before the method name. The method name itself is also

SpreadsheetCell, so the code ends up with the funny looking SpreadsheetCell::SpreadsheetCell. The implementation simply makes a call to setValue() in order to set both the numeric and text representations.

Using Constructors

Using the constructor creates an object and initializes its values. You can use constructors with both stack-based and heap-based allocation.

Constructors on the Stack

When you allocate a SpreadsheetCell object on the stack, you use the constructor like this:

SpreadsheetCell myCell(5), anotherCell(4);

cout << “cell 1: “ << myCell.getValue() << endl; cout << “cell 2: “ << anotherCell.getValue() << endl;

166

Gaining Proficiency with Classes and Objects

Note that you do NOT call the SpreadsheetCell constructor explicitly. For example, do not use something like the following:

SpreadsheetCell myCell.SpreadsheetCell(5); // WILL NOT COMPILE!

Similarly, you cannot call the constructor later. The following is also incorrect:

SpreadsheetCell myCell;

myCell.SpreadsheetCell(5); // WILL NOT COMPILE!

Again, the only correct way to use the constructor on the stack is like this:

SpreadsheetCell myCell(5);

Constructors on the Heap

When you dynamically allocate a SpreadsheetCell object, you use the constructor like this:

SpreadsheetCell *myCellp = new SpreadsheetCell(5);

SpreadsheetCell *anotherCellp;

anotherCellp = new SpreadsheetCell(4); delete anotherCellp;

Note that you can declare a pointer to a SpreadsheetCell object without calling the constructor immediately, which is different from objects on the stack, where the constructor is called at the point of declaration.

As usual, remember to call delete on the objects that you dynamically allocate with new!

Providing Multiple Constructors

You can provide more than one constructor in a class. All constructors have the same name (the name of the class), but different constructors must take a different number of arguments or different argument types.

In the SpreadsheetCell class, it is helpful to have two constructors: one to take an initial double value and one to take an initial string value. Here is the class definition with the second constructor:

class SpreadsheetCell

{

public:

SpreadsheetCell(double initialValue); SpreadsheetCell(string initialValue); void setValue(double inValue); double getValue();

void setString(string inString); string getString();

protected:

string doubleToString(double inValue); double stringToDouble(string inString);

double mValue; string mString;

};

167

Chapter 8

Here is the implementation of the second constructor:

SpreadsheetCell::SpreadsheetCell(string initialValue)

{

setString(initialValue);

}

And here is some code that uses the two different constructors:

SpreadsheetCell aThirdCell(“test”); // Uses string-arg ctor SpreadsheetCell aFourthCell(4.4); // Uses double-arg ctor

SpreadsheetCell* aThirdCellp = new SpreadsheetCell(“4.4”); // string-arg ctor cout << “aThirdCell: “ << aThirdCell.getValue() << endl;

cout << “aFourthCell: “ << aFourthCell.getValue() << endl; cout << “aThirdCellp: “ << aThirdCellp->getValue() << endl; delete aThirdCellp;

When you have multiple constructors, it is tempting to attempt to implement one constructor in terms of another. For example, you might want to call the double constructor from the string constructor as follows:

SpreadsheetCell::SpreadsheetCell(string initialValue)

{

SpreadsheetCell(stringToDouble(initialValue));

}

That seems to make sense. After all, you can call normal class methods from within other methods. The code will compile, link, and run, but will not do what you expect. The explicit call to the SpreadsheetCell constructor actually creates a new temporary unnamed object of type SpreadsheetCell. It does not call the constructor for the object that you are supposed to be initializing.

Don’t attempt to call one constructor of a class from another.

Default Constructors

A default constructor is a constructor that takes no arguments. It is also called a 0-argument constructor. With a default constructor, you can give reasonable initial values to data members even though the client did not specify them.

Here is part of the SpreadsheetCell class definition with a default constructor:

class SpreadsheetCell

{

public:

SpreadsheetCell(); SpreadsheetCell(double initialValue); SpreadsheetCell(string initialValue);

// Remainder of the class definition omitted for brevity

};

168

Gaining Proficiency with Classes and Objects

Here is a first crack at an implementation of the default constructor:

SpreadsheetCell::SpreadsheetCell()

{

mValue = 0; mString = “”;

}

You use the default constructor on the stack like this:

SpreadsheetCell myCell; myCell.setValue(6);

cout << “cell 1: “ << myCell.getValue() << endl;

The preceding code creates a new SpreadsheetCell called myCell, sets its value, and prints out its value. Unlike other constructors for stack-based objects, you do not call the default constructor with function-call syntax. Based on the syntax for other constructors, you might be tempted to call the default constructor like this:

SpreadsheetCell myCell();

//

WRONG, but will compile.

myCell.setValue(6);

//

However, this line will not compile.

cout << “cell 1: “ << myCell.getValue() << endl;

Unfortunately, the line attempting to call the default constructor will compile. The line following it will not compile. The problem is that your compiler thinks the first line is actually a function declaration for a function with the name myCell that takes zero arguments and returns a SpreadsheetCell object.

When it gets to the second line, it thinks that you’re trying to use a function name as an object!

When creating an object on the stack, omit parenthesis for the default constructor.

However, when you use the default constructor with a heap-based object allocation, you are required to use function-call syntax:

SpreadsheetCell* myCellp = new SpreadsheetCell(); // Note the function-call syntax

Don’t waste a lot of time pondering why C++ requires different syntax for heap-based versus stackbased object allocation with a default constructor. It’s just one of those things that makes C++ such an exciting language to learn.

Compiler-Generated Default Constructor

If your class doesn’t provide a default constructor, you cannot create objects of that class without specifying arguments. For example, suppose that you have the following SpreadsheetCell class definition:

class SpreadsheetCell

{

public:

169

Chapter 8

SpreadsheetCell(double initialValue); // No default constructor

SpreadsheetCell(string initialValue); void setValue(double inValue); double getValue();

void setString(string inString); string getString();

protected:

string doubleToString(double inValue); double stringToDouble(string inString);

double mValue; string mString;

};

With the preceding definition, the following code will not compile:

SpreadsheetCell myCell; myCell.setValue(6);

But that code used to work! What’s wrong here? Nothing is wrong. Since you didn’t declare a default constructor, you can’t construct an object without specifying arguments.

The real question is why the code used to work. The reason is that if you don’t specify any constructors, the compiler will write one for you that doesn’t take any arguments. This compiler-generated default constructor calls the default constructor on all object members of the class, but does not initialize the language primitives such as int and double. Nonetheless, it allows you to create objects of that class. However, if you declare a default constructor, or any other constructor, the compiler no longer generates a default constructor for you.

A default constructor is the same thing as a 0-argument constructor. The term “default constructor” does not refer only to the constructor that is automatically generated if you fail to declare any constructors.

When You Need a Default Constructor

Consider arrays of objects. The act of creating an array of objects accomplishes two tasks: it allocates contiguous memory space for all the objects and it calls the default constructor on each object. C++ fails to provide any syntax to tell the array creation code directly to call a different constructor. For example, if you do not define a default constructor for the SpreadsheetCell class, the following code does not compile:

SpreadsheetCell cells[3]; // FAILS compilation without a default constructor

SpreadsheetCell* myCellp = new SpreadsheetCell[10]; // Also FAILS

You can circumvent this restriction for stack-based arrays by using initializers like this:

SpreadsheetCell cells[3] = {SpreadsheetCell(0), SpreadsheetCell(23),

SpreadsheetCell(41)};

170

Gaining Proficiency with Classes and Objects

However, it is usually easier to ensure that your class has a default constructor if you intend to create arrays of objects of that class.

Default constructors are also useful when you want to create objects of that class inside other classes, which is shown in the following section, Initializer Lists.

Finally, default constructors are convenient when the class serves as a base class of an inheritance hierarchy. In that case, it’s convenient for subclasses to initialize superclasses via their default constructors. Chapter 10 covers this issue in more detail.

Initializer Lists

C++ provides an alternative method for initializing data members in the constructor, called the initializer list. Here is the 0-argument SpreadsheetCell constructor rewritten to use the initializer list syntax:

SpreadsheetCell::SpreadsheetCell() : mValue(0), mString(“”)

{

}

As you can see, the initializer list lies between the constructor argument list and the opening brace for the body of the constructor. The list starts with a colon and is separated by commas. Each element in the list is an initialization of a data member using function notation or a call to a superclass constructor (see Chapter 10).

Initializing data members with an initializer list provides different behavior than does initializing data members inside the constructor body itself. When C++ creates an object, it must create all the data members of the object before calling the constructor. As part of creating these data members, it must call a constructor on any of them that are themselves objects. By the time you assign a value to an object inside your constructor body, you are not actually constructing that object. You are only modifying its value. An initializer list allows you to provide initial values for data members as they are created, which is more efficient than assigning values to them later. Interestingly, the default initialization for strings gives them the empty string; so explicitly initializing mString to the empty string as shown in the preceding example is superfluous.

Initializer lists allow initialization of data members at the time of their creation.

Even if you don’t care about efficiency, you might want to use initializer lists if you find that they look “cleaner.” Some programmers prefer the more common syntax of assigning initial values in the body of the constructor. However, several data types must be initialized in an initializer list. The following table summarizes them:

Data Type

Explanation

 

 

const data members

You cannot legally assign a value to a const variable

 

after it is created. Any value must be supplied at the time

 

of creation.

Reference data members

References cannot exist without referring to something.

 

 

 

Table continued on following page

171

Chapter 8

Data Type

Explanation

Object data members for which there is no default constructor

C++ attempts to initialize member objects using a default constructor. If no default constructor exists, it cannot initialize the object.

Superclasses without default

[Covered in Chapter 10]

constructors

 

There is one important caveat with initializer lists: they initialize data members in the order that they appear in the class definition, not their order in the initializer list. For example, suppose you rewrite your SpreadsheetCell string constructor to use initializer lists like this:

SpreadsheetCell::SpreadsheetCell(string initialValue) :

 

mString(initialValue), mValue(stringToDouble(mString))

// INCORRECT ORDER!

{

 

}

 

The code will compile (although some compilers issue a warning), but the program does not work correctly. You might assume that mString will be initialized before mValue because mString is listed first in the initialier list. But C++ doesn’t work that way. The SpreadsheetCell class declares mValue before mString:

class SpreadsheetCell

{

public:

//Code omitted for brevity protected:

//Code omitted for brevity double mValue;

string mString;

};

Thus, the initializer list tried to initialize mValue before mString. However, the code to initialize mValue tries to use the value of mString, which is not yet initialized! The solution in this case is to use the initialValue argument instead of mString when initializing mValue. You should also swap their order in the initializer list to avoid confusion:

SpreadsheetCell::SpreadsheetCell(string initialValue) : mValue(stringToDouble(initialValue)), mString(initialValue)

{

}

Initializer lists initialize data members in their declared order in the class definition, not their order in the list.

Copy Constructors

There is a special constructor in C++ called a copy constructor that allows you to create an object that is an exact copy of another object. If you don’t write a copy constructor yourself, C++ generates one for you

172

Gaining Proficiency with Classes and Objects

that initializes each data member in the new object from its equivalent data member in the source object. For object data members, this initialization means that their copy constructors are called.

Here is the declaration for a copy constructor in the SpreadsheetCell class:

class SpreadsheetCell

{

public:

SpreadsheetCell(); SpreadsheetCell(double initialValue); SpreadsheetCell(string initialValue);

SpreadsheetCell(const SpreadsheetCell& src); void setValue(double inValue);

double getValue();

void setString(string inString); string getString();

protected:

string doubleToString(double inValue); double stringToDouble(string inString);

double mValue; string mString;

};

The copy constructor takes a const reference to the source object. Like other constructors, it does not return a value. Inside the constructor, you should copy all the data fields from the source object. Technically, of course, you can do whatever you want in the constructor, but it’s generally a good idea to follow expected behavior and initialize the new object to be a copy of the old one. Here is a sample implementation of the SpreadsheetCell copy constructor:

SpreadsheetCell::SpreadsheetCell(const SpreadsheetCell& src) : mValue(src.mValue), mString(src.mString)

{

}

Note the use of the initializer list. The difference between setting values in the initializer list and in the copy constructor body is examined below in the section on assignment.

The compiler-generated SpreadsheetCell copy constructor is identical to the one shown above. Thus, for simplicity, you could omit the explicit copy constructor and rely on the compiler-generated one. Chapter 10 describes some types of classes for which a compiler-generated copy constructor is insufficient.

When the Copy Constructor Is Called

The default semantics for passing arguments to functions in C++ is pass-by-value. That means that the function or method receives a copy of the variable, not the variable itself. Thus, whenever you pass an object to a function or method the compiler calls the copy constructor of the new object to initialize it.

173

Chapter 8

For example, recall that the definition of the setString() method in the SpreadsheetCell class looks like this:

void SpreadsheetCell::setString(string inString)

{

mString = inString;

mValue = stringToDouble(mString);

}

Recall, also, that the C++ string is actually a class, not a built-in type. When your code makes a call to setString() passing a string argument, the string parameter inString is initialized with a call to its copy constructor. The argument to the copy construction is the string you passed to setString(). In the following example, the string copy constructor is executed for the inString object in setString() with name as its parameter.

SpreadsheetCell myCell; string name = “heading one”;

myCell.setString(name); // Copies name

When the setString() method finishes, inString is destroyed. Because it was only a copy of name, name remains intact.

The copy constructor is also called whenever you return an object from a function or method. In this case, the compiler creates a temporary, unnamed, object through its copy constructor. Chaper 17 explores the impact of temporary objects in more detail.

Calling the Copy Constructor Explicitly

You can use the copy constructor explicitly as well. It is often useful to be able to construct one object as an exact copy of another. For example, you might want to create a copy of a SpreadsheetCell object like this:

SpreadsheetCell myCell2(4);

SpreadsheetCell anotherCell(myCell2); // anotherCell now has the values of myCell2

Passing Objects by Reference

In order to avoid copying objects when you pass them to functions and methods you can declare that the function or method takes a reference to the object. Passing objects by reference is usually more efficient than passing them by value, because only the address of the object is copied, not the entire contents of the object. Additionally, pass-by-reference avoids problems with dynamic memory allocation in objects, which we will discuss in Chapter 9.

Pass objects by const reference instead of by value.

When you pass an object by reference, the function or method using the object reference could change the original object. When you’re only using pass-by-reference for efficiency, you should preclude this possibility by declaring the object const as well. Here is the SpreadsheetCell class definition in which string objects are passed const reference:

174

Gaining Proficiency with Classes and Objects

class SpreadsheetCell

{

public:

SpreadsheetCell(); SpreadsheetCell(double initialValue);

SpreadsheetCell(const string& initialValue); SpreadsheetCell(const SpreadsheetCell& src); void setValue(double inValue);

double getValue();

void setString(const string& inString); string getString();

protected:

string doubleToString(double inValue);

double stringToDouble(const string& inString);

double mValue; string mString;

};

Here is the implementation for setString(). Note that the method body remains the same; only the parameter type is different.

void SpreadsheetCell::setString(const string& inString)

{

mString = inString;

mValue = stringToDouble(mString);

}

The SpreadsheetCell methods that return a string still return it by value. Returning a reference to a data member is risky because the reference is valid only as long as the object is “alive.” Once the object is destroyed, the reference is invalid. However, there are sometimes legitimate reasons to return references to data members, as you will see later in this chapter and in subsequent chapters.

Summary of Compiler-Generated Constructors

The compiler will automatically generate a 0-argument constructor and a copy constructor for every class. However, the constructors you define yourself replace these constructors according to the following rules:

 

. . . then the compiler

. . . and you can

 

If you define . . .

generates . . .

create an object . . .

Example

 

 

 

 

[no constructors]

A 0-argument

With no arguments.

SpreadsheetCell

 

constructor

As a copy of another

cell;

 

A copy constructor

object.

SpreadsheetCell

 

 

 

myCell(cell);

A 0-argument

A copy constructor

With no arguments.

SpreadsheetCell

constructor only

 

As a copy of another

cell;

 

 

object.

SpreadsheetCell

 

 

 

myCell(cell);

 

 

 

 

 

 

 

Table continued on following page

175