Gaining Proficiency with Classes and Objects - Coding the Professional Way - Professional C++ (2014)

Professional C++ (2014)

Part IIICoding the Professional Way

· CHAPTER 7: Gaining Proficiency with Classes and Objects

· CHAPTER 8: Mastering Classes and Objects

· CHAPTER 9: Discovering Inheritance Techniques

· CHAPTER 10: C++ Quirks, Oddities, and Incidentals

· CHAPTER 11: Writing Generic Code with Templates

· CHAPTER 12: Demystifying C++ I/O

· CHAPTER 13: Handling Errors

· CHAPTER 14: Overloading C++ Operators

· CHAPTER 15: Overview of the C++ Standard Library

· CHAPTER 16: Understanding Containers and Iterators

· CHAPTER 17: Mastering STL Algorithms

· CHAPTER 18: String Localization and Regular Expressions

· CHAPTER 19: Additional Library Utilities

Chapter 7Gaining Proficiency with Classes and Objects

WHAT’S IN THIS CHAPTER?

· How to write your own classes with methods and data members

· How to control access to your methods and data members

· How to use objects on the stack and on the heap

· What the life cycle of an object is

· How to write code that is executed when an object is created or destroyed

· How to write code to copy or assign objects

WROX.COM DOWNLOADS FOR THIS CHAPTER

Please note that all the code examples for this chapter are available as a part of this chapter’s code download on the book’s website at www.wrox.com/go/proc++3e on the Download Code tab.

As an object-oriented language, C++ provides facilities for using objects and for writing object definitions, called classes. You can certainly write programs in C++ without classes and objects, but by doing so, you do not take advantage of the most fundamental and useful aspect of the language; writing a C++ program without classes is like traveling to Paris and eating at McDonald’s. In order to use classes and objects effectively, you must understand their syntax and capabilities.

Chapter 1 reviewed the basic syntax of class definitions. Chapter 5 introduced the object-oriented approach to programming in C++ and presented specific design strategies for classes and objects. This chapter describes the fundamental concepts involved in using classes and objects, including writing class definitions, defining methods, using objects on the stack and the heap, writing constructors, default constructors, compiler-generated constructors, constructor initializers (known as ctor-initializers), copy constructors, initializer-list constructors, destructors, and assignment operators. Even if you are already comfortable with classes and objects, you should skim this chapter because it contains various tidbits of information with which you might not yet be familiar.

INTRODUCING THE SPREADSHEET EXAMPLE

This chapter and the next present a runnable example of a simple spreadsheet application. A spreadsheet is a two-dimensional grid of “cells,” and each cell contains a number or string. Professional spreadsheets such as Microsoft Excel provide the ability to perform mathematical operations, such as calculating the sum of the values of a set of cells. The spreadsheet example in these chapters does not attempt to challenge Microsoft in the marketplace, but is useful for illustrating the issues of classes and objects.

The spreadsheet application uses two basic classes: Spreadsheet and SpreadsheetCell. Each Spreadsheet object contains SpreadsheetCell objects. In addition, a SpreadsheetApplication class manages a collection of Spreadsheets. This chapter focuses on the SpreadsheetCell. Chapter 8 develops the Spreadsheet and SpreadsheetApplication classes.

NOTE This chapter shows several different versions of the SpreadsheetCell class in order to introduce concepts gradually. Thus, the various attempts at the class throughout the chapter do not always illustrate the “best” way to do every aspect of class writing. In particular, the early examples omit important features that would normally be included, but have not yet been introduced. You can download the final version of the class as described in the Introduction.

WRITING CLASSES

When you write a class you specify the behaviors, or methods, that will apply to objects of that class and the properties, or data members, that each object will contain.

There are two components in the process of writing classes: defining the classes themselves and defining their methods.

Class Definitions

Here is a first attempt at a simple SpreadsheetCell class, in which each cell can store only a single number:

class SpreadsheetCell

{

public:

void setValue(double inValue);

double getValue() const;

private:

double mValue;

};

As described in Chapter 1, every class definition begins with the keyword class and the name of the class. A class definition is a statement in C++, so it must end with a semicolon. If you fail to terminate your class definition with a semicolon, your compiler will probably give you several errors, most of which will appear to be completely unrelated.

Class definitions usually go in a file named after the class. For example, the SpreadsheetCell class definition can be put in a file called SpreadsheetCell.h. This rule is not enforced and you are free to call your file however you like.

Class Members

A class can have a number of members. A member is either a member function (which in turn is either a method, constructor, or destructor), or a member variable also called data member.

The two lines that look like function prototypes declare the methods that this class supports:

void setValue(double inValue);

double getValue() const;

Chapter 1 points out that it is always a good idea to declare member functions that do not change the object as const.

The line that looks like a variable declaration declares the data member for this class.

double mValue;

A class defines the member functions and data members that apply. They apply only to a specific instance of the class, which is an object. The only exception to this rule are static members, explained in Chapter 8. Classes define concepts; objects contain real bits. So, each object will contain its own value for the mValue variable. The implementation of the member functions is shared across all objects. Classes can contain any number of member functions and data members. You cannot give a data member the same name as a member function.

Access Control

Every member in a class is subject to one of three access specifiers: public, protected, or private. An access specifier applies to all member declarations that follow it, until the next access specifier. In the SpreadsheetCell class, the setValue() and getValue() methods have public access, while the mValue data member has private access.

The default access specifier for classes is private: all member declarations before the first access specifier have the private access specification. For example, moving the public access specifier below the setValue() method declaration gives the setValue() methodprivate access instead of public:

class SpreadsheetCell

{

void setValue(double inValue); // now has private access

public:

double getValue() const;

private:

double mValue;

};

In C++, a struct can have methods just like a class. In fact, the only difference is that the default access specifier for a struct is public while the default for a class is private. For example, the SpreadsheetCell class can be rewritten using a struct as follows:

struct SpreadsheetCell

{

void setValue(double inValue);

double getValue() const;

private:

double mValue;

};

The following table summarizes the meanings of the three access specifiers:

ACCESS SPECIFICATION

MEANING

WHEN TO USE

public

Any code can call a public member function or access a public data member of an object.

Behaviors (methods) that you want clients to use.
Access methods for private and protected data members.

protected

Any member function of the class can call protected member functions and access protected data members. Member functions of a derived class can access protected members of a base class.

“Helper” methods that you do not want clients to use.

private

Only member functions of the class can call private member functions and access private data members. Member functions in derived classes cannot access private members from a base class.

Everything should be private by default, especially data members. You can provide protected getters and setters if you only want to allow derived classes to access them, and provide public getters and setters if you want clients to access them.

Order of Declarations

You can declare your members and access control specifiers in any order: C++ does not impose any restrictions, such as member functions before data members or public before private. Additionally, you can repeat access specifiers. For example, the SpreadsheetCelldefinition could look like the following:

class SpreadsheetCell

{

public:

void setValue(double inValue);

private:

double mValue;

public:

double getValue() const;

};

However, for clarity it is a good idea to group public, protected, and private declarations, and to group member functions and data members within those declarations.

Defining Methods

The preceding definition for the SpreadsheetCell class is enough for you to create objects of the class. However, if you try to call the setValue() or getValue() methods, your linker will complain that those methods are not defined. That’s because the class definition specifies the prototypes for the methods, but does not define their implementations. Just as you write both a prototype and a definition for a stand-alone function, you must write a prototype and a definition for a method. Note that the class definition must precede the method definitions. Usually the class definition goes in a header file, and the method definitions go in a source file that includes that header. Here are the definitions for the two methods of the SpreadsheetCell class:

#include "SpreadsheetCell.h"

void SpreadsheetCell::setValue(double inValue)

{

mValue = inValue;

}

double SpreadsheetCell::getValue() const

{

return mValue;

}

Note that the name of the class followed by two colons precedes each method name:

void SpreadsheetCell::setValue(double inValue)

The :: is called the scope resolution operator. In this context, the syntax tells the compiler that the coming definition of the setValue() method is part of the SpreadsheetCell class. Note also that you do not repeat the access specification when you define the method.

NOTE If you are using the Microsoft Visual C++ IDE, you will notice that by default all cpp files start with:

#include "stdafx.h"

In a VC++ project, by default, every cpp file should start with this line and your own includes must follow this. If you place your own includes before the stdafx.h include, they will appear to have no effect and you will get all kinds of compilation errors. This deals with the concept of precompiled header files, which is outside the scope of this book. Consult the Microsoft documentation on precompiled header files to learn the details.

Accessing Data Members

Non-static methods of a class, such as setValue() and getValue(), are always executed on behalf of a specific object of that class. Inside the method body, you have access to all the data members of the class for that object. In the previous definition for setValue(), the following line changes the mValue variable inside whatever object calls the method:

mValue = inValue;

If setValue() is called for two different objects, the same line of code (executed once for each object) changes the variable in two different objects.

Calling Other Methods

You can call methods of a class from inside another method. For example, consider an extension to the SpreadsheetCell class. Real spreadsheet applications allow text data as well as numbers in the cells. When you try to interpret a text cell as a number, the spreadsheet tries to convert the text to a number. If the text does not represent a valid number, the cell value is ignored. In this program, strings that are not numbers will generate a cell value of 0. Here is a first stab at a class definition for a SpreadsheetCell that supports text data:

#include <string>

class SpreadsheetCell

{

public:

void setValue(double inValue);

double getValue() const;

void setString(const std::string& inString);

const std::string& getString() const;

private:

std::string doubleToString(double inValue) const;

double stringToDouble(const std::string& inString) const;

double mValue;

std::string mString;

};

This version of the class stores both text and numerical representations of the data. If the client sets the data as a string, it is converted to a double, and a double is converted to a string. If the text is not a valid number, the double value is 0. Note that having both a text and a numerical representation in this class is only for illustrative purposes. You should avoid storing redundant data. This deficiency of the SpreadsheetCell class is resolved in Chapter 9. This class definition shows two new methods to set and retrieve the text representation of the cell and two new private helper methods to convert a double to a string and vice versa. These helper methods use string streams, which are covered in detail in Chapter 12. Here are the implementations of all the methods.

#include "SpreadsheetCell.h"

#include <iostream>

#include <sstream>

using namespace std;

void SpreadsheetCell::setValue(double inValue)

{

mValue = inValue;

mString = doubleToString(mValue);

}

double SpreadsheetCell::getValue() const

{

return mValue;

}

void SpreadsheetCell::setString(const string& inString)

{

mString = inString;

mValue = stringToDouble(mString);

}

const string& SpreadsheetCell::getString() const

{

return mString;

}

string SpreadsheetCell::doubleToString(double inValue) const

{

ostringstream ostr;

ostr << inValue;

return ostr.str();

}

double SpreadsheetCell::stringToDouble(const string& inString) const

{

double temp;

istringstream istr(inString);

istr >> temp;

if (istr.fail() || !istr.eof()) {

return 0;

}

return temp;

}

Note that each of the set methods calls a helper method to perform a conversion. With this technique, both mValue and mString are always valid.

The this Pointer

Every normal method call passes a pointer to the object for which it is called as a “hidden” parameter with the name this. You can use this pointer to access data members or call methods, and can pass it to other methods or functions. It is also sometimes useful for disambiguating names. For example, you could have defined the SpreadsheetCell class with a value data member instead of mValue and you could have defined the setValue() method to take a parameter named value instead of inValue. In that case, setValue() would look like this:

void SpreadsheetCell::setValue(double value)

{

value = value; // Ambiguous!

mString = doubleToString(value);

}

That line is confusing. Which value do you mean: the value that was passed as a parameter, or the value that is a member of the object?

NOTE The preceding ambiguous line will typically compile without any warnings or errors, but it will not produce the results that you are expecting.

In order to disambiguate the names you can use the this pointer:

void SpreadsheetCell::setValue(double value)

{

this->value = value;

mString = doubleToString(this->value);

}

However, if you use the naming conventions described in Chapter 3, you will never encounter this type of name collision.

You can also use the this pointer to call a function or method that takes a pointer to an object from within a method of that object. For example, suppose you write a printCell() stand-alone function (not method) like this:

void printCell(const SpreadsheetCell& inCell)

{

cout << inCell.getString() << endl;

}

If you want to call printCell() from the setValue() method, you must pass *this as the argument to give printCell() a reference to the SpreadsheetCell on which setValue() operates:

void SpreadsheetCell::setValue(double value)

{

this->value = value;

mString = doubleToString(this->value);

printCell(*this);

}

NOTE Instead of writing a printCell() function, it would be more convenient to overload the << operator, explained in Chapter 14. You can then use the following line to print a SpreadsheetCell:

cout << *this << endl;

Using Objects

The previous class definition says that a SpreadsheetCell consists of two member variables, four public methods, and two private methods. However, the class definition does not actually create any SpreadsheetCells; it just specifies their shape and behavior. In that sense, a class is similar to architectural blueprints. The blueprints specify what a house should look like, but drawing the blueprints doesn’t build any houses. Houses must be constructed later based on the blueprints.

Similarly, in C++ you can construct a SpreadsheetCell “object” from the SpreadsheetCell class definition by declaring a variable of type SpreadsheetCell. Just as a builder can build more than one house based on a given set of blueprints, a programmer can create more than one SpreadsheetCell object from a SpreadsheetCell class. There are two ways to create and use objects: on the stack and on the heap, and both can be stored in arrays. Arrays are discussed later in this chapter.

Objects on the Stack

Here is some code that creates and uses SpreadsheetCell objects on the stack.

SpreadsheetCell myCell, anotherCell;

myCell.setValue(6);

anotherCell.setString("3.2");

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

cout << "cell 2: " << anotherCell.getValue() << endl;

You create objects just as you declare simple variables, except that the variable type is the class name. The . in lines like myCell.setValue(6); is called the “dot” operator; it allows you to call methods on the object. If there were any public data members in the object, you could access them with the dot operator as well. Remember that public data members are not recommended.

The output of the program is:

cell 1: 6

cell 2: 3.2

Objects on the Heap

You can also dynamically allocate objects by using new:

SpreadsheetCell* myCellp = new SpreadsheetCell();

myCellp->setValue(3.7);

cout << "cell 1: " << myCellp->getValue() <<

" " << myCellp->getString() << endl;

delete myCellp;

myCellp = nullptr;

When you create an object on the heap, you access its members through the “arrow” operator: ->. The arrow combines dereferencing (*) and 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;

myCellp = nullptr;

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. To avoid memory problems, it’s highly recommended to use smart pointers as follows:

auto myCellp = make_unique<SpreadsheetCell>();

// Equivalent to:

// unique_ptr<SpreadsheetCell> myCellp(new SpreadsheetCell());

myCellp->setValue(3.7);

cout << "cell 1: " << myCellp->getValue() <<

" " << myCellp->getString() << endl;

With smart pointers you don’t need to manually free the memory, it will happen automatically.

WARNING If you allocate an object with new, free it with delete when you are finished with it, or use smart pointers to manage the memory automatically.

NOTE If you don’t use smart pointers, it is always a good idea to reset a pointer to the null pointer after deleting the object to which it pointed. You are not required to do this, but it will make debugging easier in case the pointer is accidently used after deleting the object.

OBJECT LIFE CYCLES

The object life cycle involves three activities: creation, destruction, and assignment. 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, new[], or a smart pointer. When an object is created, all its embedded objects are also created. For example:

#include <string>

class MyClass

{

private:

std::string mName;

};

int main()

{

MyClass obj;

return 0;

}

The embedded string object is created at the point where the MyClass object is created in the main() function and is destructed when its containing object is destructed.

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;

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 constructors is executed.

NOTE C++ programmers sometimes call a constructor a ctor.

Writing Constructors

Syntactically, a constructor is specified by a method name that is the same as the class name. A constructor never has a return type and may or may not have parameters. A constructor with no parameters is called the default constructor. There are certain contexts in which you may have to provide a default constructor and you will get compiler errors if you have not provided one. Default constructors are discussed later in the chapter.

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

class SpreadsheetCell

{

public:

SpreadsheetCell(double initialValue);

// Remainder of the class definition omitted for brevity

};

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 member of the SpreadsheetCell class, so C++ requires the normal SpreadsheetCell:: scope resolution before the constructor name. The constructor name itself is also SpreadsheetCell, so the code ends up with the funny lookingSpreadsheetCell::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;

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:

auto smartCellp = make_unique<SpreadsheetCell>(4);

// ... do something with the cell, no need to delete the smart pointer

// Or with naked pointers, without smart pointers (not recommended)

SpreadsheetCell* myCellp = new SpreadsheetCell(5);

SpreadsheetCell* anotherCellp = nullptr;

anotherCellp = new SpreadsheetCell(4);

// ... do something with the cells

delete myCellp; myCellp = nullptr;

delete anotherCellp; anotherCellp = nullptr;

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.

Whenever you declare a pointer on the stack or in a class and don’t immediately initialize the object, then it should be initialized to nullptr like in the previous declaration for anotherCellp. If you don’t assign it to nullptr, the pointer is undefined. Accidentally using an undefined pointer will cause unexpected and difficult-to-diagnose memory corruption. If you initialize it to nullptr, using that pointer will cause a memory access error in most operating environments, instead of producing unexpected results.

Remember to call delete on objects that you dynamically allocate with new or use smart pointers!

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 C++, if you have more than one function with the same name, the compiler will select the one whose parameter types match the types at the call site. This is called overloading and is discussed in detail in Chapter 8.

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 new class definition:

class SpreadsheetCell

{

public:

SpreadsheetCell(double initialValue);

SpreadsheetCell(const std::string& initialValue);

// Remainder of the class definition omitted for brevity

};

Here is the implementation of the second constructor:

SpreadsheetCell::SpreadsheetCell(const 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

auto aThirdCellp = make_unique<SpreadsheetCell>("4.4"); // string-arg ctor

cout << "aThirdCell: " << aThirdCell.getValue() << endl;

cout << "aFourthCell: " << aFourthCell.getValue() << endl;

cout << "aThirdCellp: " << aThirdCellp->getValue() << endl;

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(const 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.

However, C++ supports delegating constructors which allow you to call other constructors from the same class from inside the ctor-initializer. This is discussed later in this chapter.

Default Constructors

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

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 default constructor

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

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

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

SpreadsheetCell(41)};

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. If you haven’t defined your own constructors, the compiler will automatically create a default constructor for you. This compiler-generated constructor is discussed in a next section.

A default constructor is also required for classes that you want to store in an STL container like std::vector.

Default constructors are also useful when you want to create objects of that class inside other classes, which is shown later in this chapter under the section Constructor Initializers.

How To Write a Default Constructor

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

class SpreadsheetCell

{

public:

SpreadsheetCell();

// Remainder of the class definition omitted for brevity

};

Here is a first crack at an implementation of the default constructor. There is no need for the constructor to initialize mString to the empty string, because the default constructor of std::string is automatically called and initializes mString to the empty string.

SpreadsheetCell::SpreadsheetCell()

{

mValue = 0;

}

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!

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

For heap-based object allocation, the default constructor can be used as follows:

auto smartCellp = make_unique<SpreadsheetCell>();

// Or with a naked pointer (not recommended)

SpreadsheetCell* myCellp = new SpreadsheetCell();

// Or

// SpreadsheetCell* myCellp = new SpreadsheetCell;

// ... use myCellp

delete myCellp; myCellp = nullptr;

Compiler-Generated Default Constructor

The first SpreadsheetCell class definition in this chapter looked as follows:

class SpreadsheetCell

{

public:

void setValue(double inValue);

double getValue() const;

private:

double mValue;

};

This definition does not declare a default constructor, but still, the code that follows works perfectly.

SpreadsheetCell myCell;

myCell.setValue(6);

The following definition is the same as the preceding definition except that it adds an explicit constructor, accepting a double. It still does not explicitly declare a default constructor.

class SpreadsheetCell

{

public:

SpreadsheetCell(double initialValue); // No default constructor

// Remainder of the class definition omitted for brevity

};

With this definition, the following code will not compile anymore:

SpreadsheetCell myCell;

myCell.setValue(6);

What’s going on here? 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.

NOTE A default constructor is the same thing as a 0-argument constructor. The term default constructor does not refer only to the constructor automatically generated if you fail to declare any constructors. It refers to the constructor which is defaulted to if no arguments are required.

Explicitly Defaulted Constructors

In C++03 or older if your class required a number of explicit constructors accepting arguments but also a default constructor that does nothing, you had to explicitly write your empty default constructor as follows:

class MyClass

{

public:

MyClass() {}

MyClass(int i);

};

However, it’s recommended that interface files contain only declarations of public methods without any implementations. The preceding class definition violates this. The solution was to define the class as follows:

class MyClass

{

public:

MyClass();

MyClass(int i);

};

The implementation of the empty default constructor in the implementation file would be:

MyClass::MyClass() { }

To avoid having to write empty default constructors manually, C++ now supports the concept of explicitly defaulted constructors. This allows you to write the class definition as follows without the need to implement the default constructor in the implementation file.

class MyClass

{

public:

MyClass() = default;

MyClass(int i);

};

MyClass defines a custom constructor that accepts one integer. However, the compiler will still generate a standard compiler generated default constructor due to the use of the default keyword.

Explicitly Deleted Constructors

C++ also supports the concept of explicitly deleted constructors. For example, you can define a class with only static methods for which you do not want to write any constructors and you also do not want the compiler to generate the default constructor. In that case you need to explicitly delete the default constructor:

class MyClass

{

public:

MyClass() = delete;

};

Constructor Initializers

Up to now, this chapter initialized data members in the body of a constructor, for example:

SpreadsheetCell::SpreadsheetCell()

{

mValue = 0;

}

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

SpreadsheetCell::SpreadsheetCell() : mValue(0)

{

}

As you can see, the ctor-initializer appears syntactically 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 base class constructor (see Chapter 9), or a call to a delegated constructor discussed later.

Initializing data members with a ctor-initializer 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. A ctor-initializer allows you to provide initial values for data members as they are created, which is more efficient than assigning values to them later. The default initialization for strings gives them the empty string; so explicitly initializing mString to the empty string is superfluous.

If your class has as data member an object without a default constructor, you have to use the ctor-initializer to properly construct that object. For example, take the following SpreadsheetCell class:

class SpreadsheetCell

{

public:

SpreadsheetCell(double d);

};

The implementation of the constructor is as follows:

SpreadsheetCell::SpreadsheetCell(double d) { }

This class only has one explicit constructor accepting a double and does not include a default constructor. You can use this class as a data member of another class as follows:

class SomeClass

{

public:

SomeClass();

private:

SpreadsheetCell mCell;

};

And implement the SomeClass constructor as follows:

SomeClass::SomeClass() { }

However, with this implementation, the code will not compile. The compiler does not know how to initialize the mCell data member of SomeClass because it does not have a default constructor.

The solution is to initialize the mCell data member in the ctor-initializer as follows:

SomeClass::SomeClass() : mCell(1.0) { }

NOTE Ctor-initializers allow initialization of data members at the time of their creation.

Some programmers prefer to assign initial values in the body of the constructor. However, several data types must be initialized in a ctor-initializer. 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.

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.

Base classes without default constructors

[Covered in Chapter 9]

There is one important caveat with ctor-initializers: they initialize data members in the order that they appear in the class definition, not their order in the ctor-initializer. Take the following definition for the SpreadsheetCell class:

class SpreadsheetCell

{

public:

// Code omitted for brevity

private:

// Code omitted for brevity

double mValue;

std::string mString;

};

Suppose you write your SpreadsheetCell string constructor to use a ctor-initializer like this:

SpreadsheetCell::SpreadsheetCell(const 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 ctor-initializer. But C++ doesn’t work that way. TheSpreadsheetCell class declares mValue before mString; thus, the ctor-initializer tries 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 initialValueargument instead of mString when initializing mValue. You should also swap their order in the ctor-initializer to avoid confusion:

SpreadsheetCell::SpreadsheetCell(const string& initialValue) :

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

{

}

WARNING Ctor-Initializers initialize data members in their declared order in the class definition, not their order in the ctor-initializer 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 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(const SpreadsheetCell& src);

// Remainder of the class definition omitted for brevity

};

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 copy 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 ctor-initializer.

NOTE Given a set of member variables, called m1, m2, ... mn, the compiler-generated copy constructor can be expressed as:

classname::classname(const classname& src) :

m1(src.m1), m2(src.m2), ... mn(src.mn) { }

Therefore, in most circumstances, there is no need for you to specify a copy constructor yourself. However, under certain conditions, this default copy constructor is not sufficient. These conditions are covered in Chapter 8.

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 value or object. Thus, whenever you pass an object to a function or method the compiler calls the copy constructor of the new object to initialize it. For example, suppose that the definition of the setString() method in the SpreadsheetCell class is as follows:

void SpreadsheetCell::setString(string inString)

{

mString = inString;

mValue = stringToDouble(mString);

}

Recall 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 constructor is the stringyou 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. Chapter 25 explores the impact of temporary objects in more detail. You can avoid the overhead of copy constructors by passing parameters as const references, as has been done throughout this chapter.

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 myCell3(myCell2); // myCell3 has the same values as 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 is discussed in Chapter 8.

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. This is known as passing objects by const reference and has been done in all previous code examples. For example, all methods of the SpreadsheetCell class that require a string are declared with a const string reference parameter. The class definition would look as follows if you would declare those methods without reference parameters.

class SpreadsheetCell

{

public:

SpreadsheetCell();

SpreadsheetCell(double initialValue);

SpreadsheetCell(std::string initialValue);

SpreadsheetCell(const SpreadsheetCell& src);

void setValue(double inValue);

double getValue() const;

void setString(std::string inString);

std::string getString() const;

private:

std::string doubleToString(double inValue) const;

double stringToDouble(std::string inString) const;

double mValue;

std::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(string inString)

{

mString = inString;

mValue = stringToDouble(mString);

}

NOTE For performance reasons, it is best to pass objects by const reference instead of by value.

The doubleToString() method of the SpreadsheetCell class always returns a string by value because the implementation of the method creates a local string object which at the end of the method is returned to the caller. Returning a reference to this string wouldn’t work because the string to which it references will be destroyed when the function exits.

Explicitly Defaulted and Deleted Copy Constructor

You can explicitly default or delete a compiler generated copy constructor as follows:

SpreadsheetCell(const SpreadsheetCell& src) = default;

or

SpreadsheetCell(const SpreadsheetCell& src) = delete;

Initializer-List Constructors

An initializer-list constructor is a constructor with std::initializer_list<T> as first argument, without any additional arguments or with additional arguments having default values. Before you can use the std::initializer_list<T> template you need to include the<initializer_list> header. The following class demonstrates its use.

class EvenSequence

{

public:

EvenSequence(initializer_list<double> args)

{

if (args.size() % 2 != 0) {

throw invalid_argument("initializer_list should "

"contain even number of elements.");

}

mSequence.reserve(args.size());

for (auto value : args) {

mSequence.push_back(value);

}

}

void dump() const

{

for (auto value : mSequence) {

cout << value << ", ";

}

cout << endl;

}

private:

vector<double> mSequence;

};

Inside the initializer-list constructor you can access the elements of the initializer-list with a range-based for loop. You can get the number of elements in the initializer-list with the size() method.

The EvenSequence initializer-list constructor uses a range-based for loop to copy elements from the given initializer_list. In practice, it’s recommended to use STL algorithms as much as possible. STL algorithms are discussed in detail in Chapter 17. As an example, the previous range-based for loop with the push_back() call can be rewritten using an STL algorithm as follows:

mSequence.insert(cend(mSequence), cbegin(args), cend(args));

Objects of EvenSequence can be constructed as follows:

EvenSequence p1 = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0};

p1.dump();

try {

EvenSequence p2 = {1.0, 2.0, 3.0};

} catch (const invalid_argument& e) {

cout << e.what() << endl;

}

The construction of p2 will throw an exception because it has an odd number of elements in the initializer-list. The preceding equal signs are optional and can be left out, for example:

EvenSequence p1{1.0, 2.0, 3.0, 4.0, 5.0, 6.0};

The STL has full support for initializer-list constructors. For example, the std::vector container can be initialized by using an initializer-list:

std::vector<std::string> myVec = {"String 1", "String 2", "String 3"};

Without initializer-list constructors, one way to initialize the vector is by using several push_back() calls:

std::vector<std::string> myVec;

myVec.push_back("String 1");

myVec.push_back("String 2");

myVec.push_back("String 3");

Initializer lists are not limited to constructors and can also be used with normal functions as explained in Chapter 10.

In-Class Member Initializers

Since C++11, member variables can be initialized directly in the class definition. For example:

#include <string>

class MyClass

{

private:

int mInt = 1;

std::string mStr = "test";

};

The only way you could initialize mInt and mStr before C++11 was within a constructor body, or with a ctor-initializer as follows:

#include <string>

class MyClass

{

public:

MyClass() : mInt(1), mStr("test") {}

private:

int mInt;

std::string mStr;

};

Before C++11, only static const integral member variables could be initialized in the class definition. For example:

#include <string>

class MyClass

{

private:

static const int kI1 = 1; // OK

static const std::string kStr = "test"; // Error: not integral type

static int sI2 = 2; // Error: not const

const int kI3 = 3; // Error: not static

};

Delegating Constructors

Delegating constructors allow constructors to call another constructor from the same class. However, this call cannot be placed in the constructor body; it must be in the constructor initializer and it must be the only member-initializer in the list. Following is an example:

SpreadsheetCell::SpreadsheetCell(const string& initialValue)

: SpreadsheetCell(stringToDouble(initialValue))

{

}

When this string constructor (the delegating constructor) is called, it will first delegate the call to the target, double constructor. When the target constructor returns, the body of the delegating constructor will be executed.

Make sure you avoid constructor recursion while using delegate constructors. For example:

class MyClass

{

MyClass(char c) : MyClass(1.2) { }

MyClass(double d) : MyClass('m') { }

};

The first constructor will delegate to the second constructor, which delegates back to the first one. The behavior of such code is undefined by the standard and depends on the compiler.

Summary of Compiler-Generated Constructors

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

IF YOU DEFINE . . .

. . . THEN THE COMPILER GENERATES . . .

. . . AND YOU CAN CREATE AN OBJECT . . .

[no constructors]

A 0-argument constructor
A copy constructor

With no arguments:
SpreadsheetCell cell;
As a copy of another object:
SpreadsheetCell myCell(cell);

A 0-argument constructor only

A copy constructor

With no arguments:
SpreadsheetCell cell;
As a copy of another object:
SpreadsheetCell myCell(cell);

A copy constructor only

No constructors

Theoretically, as a copy of another object. Practically, you can’t create any objects.

A single-argument or multi-argument non-copy constructor only

A copy constructor

With arguments:
SpreadsheetCell cell(6);
As a copy of another object:
SpreadsheetCell myCell(cell);

A 0-argument constructor as well as a single-argument or multi-argument non-copy constructor

A copy constructor

With no arguments:
SpreadsheetCell cell;
With arguments:
SpreadsheetCell myCell(5);
As a copy of another object:
SpreadsheetCell anotherCell(cell);

Note the lack of symmetry between the default constructor and the copy constructor. As long as you don’t define a copy constructor explicitly, the compiler creates one for you. On the other hand, as soon as you define any constructor, the compiler stops generating a default constructor.

As mentioned before in this chapter, the automatic generation of a default constructor and a default copy constructor can be influenced by defining them as explicitly defaulted or explicitly deleted.

NOTE A final type of constructor is called a move constructor, required to implement move semantics. Move semantics can be used to increase performance in certain situations and is discussed in detail in Chapter 10.

Object Destruction

When an object is destroyed, two events occur: the object’s destructor method is called, and the memory it was taking up is freed. The destructor is your chance to perform any cleanup work for the object, such as freeing dynamically allocated memory or closing file handles. If you don’t declare a destructor, the compiler will write one for you that does recursive memberwise destruction and allows the object to be deleted. The section on dynamic memory allocation in Chapter 8 shows you how to write a destructor.

Objects on the stack are destroyed when they go out of scope, which means whenever the current function, method, or other execution block ends. In other words, whenever the code encounters an ending curly brace, any objects created on the stack within those curly braces are destroyed. The following program shows this 'margin-bottom:0cm;margin-bottom:.0001pt;line-height: normal;vertical-align:baseline'>int main()

{

SpreadsheetCell myCell(5);

if (myCell.getValue() == 5) {

SpreadsheetCell anotherCell(6);

} // anotherCell is destroyed as this block ends.

cout << "myCell: " << myCell.getValue() << endl;

return 0;

} // myCell is destroyed as this block ends.

Objects on the stack are destroyed in the reverse order of their declaration (and construction). For example, in the following code fragment, myCell2 is allocated before anotherCell2, so anotherCell2 is destroyed before myCell2 (note that you can start a new code block at any point in your program with an opening curly brace):

{

SpreadsheetCell myCell2(4);

SpreadsheetCell anotherCell2(5); // myCell2 constructed before anotherCell2

} // anotherCell2 destroyed before myCell2

This ordering applies to objects that are data members of other objects. Recall that data members are initialized in the order of their declaration in the class. Thus, following the rule that objects are destroyed in the reverse order of their construction, data member objects are destroyed in the reverse order of their declaration in the class.

Objects allocated on the heap without the help of a smart pointer are not destroyed automatically. You must call delete on the object pointer to call its destructor and free the memory. The following program shows this 'margin-bottom:0cm;margin-bottom:.0001pt;line-height: normal;vertical-align:baseline'>int main()

{

SpreadsheetCell* cellPtr1 = new SpreadsheetCell(5);

SpreadsheetCell* cellPtr2 = new SpreadsheetCell(6);

cout << "cellPtr1: " << cellPtr1->getValue() << endl;

delete cellPtr1; // Destroys cellPtr1

cellPtr1 = nullptr;

return 0;

} // cellPtr2 is NOT destroyed because delete was not called on it.

WARNING Do not write programs like the preceding example where cellPtr2 was not deleted. Make sure you always free dynamically allocated memory by calling delete or delete[] depending on whether the memory was allocated using new ornew[] or better yet, use smart pointers as discussed earlier.

NOTE There are tools that are able to detect unfreed objects. These tools are discussed in Chapter 22.

Assigning to Objects

Just as you can assign the value of one int to another in C++, you can assign the value of one object to another. For example, the following code assigns the value of myCell to anotherCell:

SpreadsheetCell myCell(5), anotherCell;

anotherCell = myCell;

You might be tempted to say that myCell is “copied” to anotherCell. However, in the world of C++, “copying” only occurs when an object is being initialized. If an object already has a value that is being overwritten, the more accurate term is “assigned” to. Note that the facility that C++ provides for copying is the copy constructor. Since it is a constructor, it can only be used for object creation, not for later assignments to the object.

Therefore, C++ provides another method in every class to perform assignment. This method is called the assignment operator. Its name is operator= because it is actually an overloading of the = operator for that class. In the preceding example, the assignment operator for anotherCell is called, with myCell as the argument.

NOTE The assignment operator as explained in this section is sometimes called the copy assignment operator because both the left-hand side and the right-hand side object stay alive after the assignment. This distinction is made because there is also a move assignment operator in which the right-hand side object will be destroyed after the assignment for performance reasons. This move assignment operator is explained in Chapter 10.

As usual, if you don’t write your own assignment operator, C++ writes one for you to allow objects to be assigned to one another. The default C++ assignment behavior is almost identical to its default copying 'margin-top:6.0pt;margin-right:0cm;margin-bottom:6.0pt; margin-left:0cm;line-height:normal;vertical-align:baseline'>Declaring an Assignment Operator

Here is another attempt at the SpreadsheetCell class definition, this time including an assignment operator:

class SpreadsheetCell

{

public:

// Remainder of the class definition omitted for brevity

SpreadsheetCell& operator=(const SpreadsheetCell& rhs);

// Remainder of the class definition omitted for brevity

};

The assignment operator, like the copy constructor, takes a const reference to the source object. In this case, the source object is called rhs which stands for “right-hand side” of the equals sign. The object on which the assignment operator is called is the left-hand side of the equals sign.

Unlike a copy constructor, the assignment operator returns a reference to a SpreadsheetCell object. The reason is that assignments can be chained, as in the following example:

myCell = anotherCell = aThirdCell;

When that line is executed, the first thing that happens is that the assignment operator for anotherCell is called with aThirdCell as its “right-hand side” parameter. Next, the assignment operator for myCell is called. However, its parameter is not anotherCell. Its right-hand side is the result of the assignment of aThirdCell to anotherCell. If that assignment fails to return a result, there is nothing to pass to myCell.

You might be wondering why the assignment operator for myCell can’t just take anotherCell. The reason is that using the equals sign is actually just shorthand for what is really a method call. When you look at the line in its full functional syntax, you can see the problem:

myCell.operator=(anotherCell.operator=(aThirdCell));

Now, you can see that the operator= call from anotherCell must return a value, which is passed to the operator= call for myCell. The correct value to return is anotherCell itself, so it can serve as the source for the assignment to myCell. However, returning anotherCelldirectly would be inefficient, so you can return a reference to anotherCell.

WARNING You could actually declare the assignment operator to return whatever type you wanted, including void. However, you should always return a reference to the object on which it is called because that’s what clients expect.

Defining an Assignment Operator

The implementation of the assignment operator is similar to that of a copy constructor, with several important differences. First, a copy constructor is called only for initialization, so the destination object does not yet have valid values. An assignment operator can overwrite the current values in an object. This consideration doesn’t really come into play until you have dynamically allocated memory in your objects. See Chapter 8 for details.

Second, it’s legal in C++ to assign an object to itself. For example, the following code compiles and runs:

SpreadsheetCell cell(4);

cell = cell; // Self-assignment

Your assignment operator shouldn’t prohibit self-assignment, but also shouldn’t perform a full assignment if it happens. Thus, assignment operators should check for self-assignment at the beginning of the method and return immediately.

Here is the definition of the assignment operator for the SpreadsheetCell class:

SpreadsheetCell& SpreadsheetCell::operator=(const SpreadsheetCell& rhs)

{

if (this == &rhs) {

The previous line checks for self-assignment, but is a bit cryptic. Self-assignment occurs when the left-hand side and the right-hand side of the equals sign are the same. One way to tell if two objects are the same is if they occupy the same memory location — more explicitly, if pointers to them are equal. Recall that this is a pointer to an object accessible from any method called on the object. Thus, this is a pointer to the left-hand side object. Similarly, &rhs is a pointer to the right-hand side object. If these pointers are equal, the assignment must be self-assignment, but because the return type is SpreadsheetCell& a correct value must be returned. All assignment operators return *this, and the self-assignment case is no exception:

return *this;

}

this is a pointer to the object on which the method executes, so *this is the object itself. The compiler will return a reference to the object to match the declared return value. Now, if it is not self-assignment, you have to do an assignment to every member:

mValue = rhs.mValue;

mString = rhs.mString;

Here the method copies the values.

return *this;

}

Finally it returns *this, as explained previously.

Explicitly Defaulted and Deleted Assignment Operator

You can explicitly default or delete a compiler generated assignment operator as follows:

SpreadsheetCell& operator=(const SpreadsheetCell& rhs) = default;

or

SpreadsheetCell& operator=(const SpreadsheetCell& rhs) = delete;

Distinguishing Copying from Assignment

It is sometimes difficult to tell when objects are initialized with a copy constructor rather than assigned to with the assignment operator. Essentially, things that look like a declaration are going to be using copy constructors and things that look like assignment statements will be handled by the assignment operator. Consider the following code:

SpreadsheetCell myCell(5);

SpreadsheetCell anotherCell(myCell);

AnotherCell is constructed with the copy constructor.

SpreadsheetCell aThirdCell = myCell;

aThirdCell is also constructed with the copy constructor, because this is a declaration. This line does not call operator=! This syntax is just another way to write: SpreadsheetCell aThirdCell(myCell);. However:

anotherCell = myCell; // Calls operator= for anotherCell.

Here anotherCell has already been constructed, so the compiler calls operator=.

Objects as Return Values

When you return objects from functions or methods, it is sometimes difficult to see exactly what copying and assignment is happening. Suppose that the code for getString() looks like this:

string SpreadsheetCell::getString() const

{

return mString;

}

Now consider the following code:

SpreadsheetCell myCell2(5);

string s1;

s1 = myCell2.getString();

When getString() returns mString, the compiler actually creates an unnamed temporary string object by calling a string copy constructor. When you assign this result to s1, the assignment operator is called for s1 with the temporary string as a parameter. Then, the temporary string object is destroyed. Thus, the single line of code invokes the copy constructor and the assignment operator (for two different objects). However, compilers are free to implement Return Value Optimization (RVO) to optimize this process.

In case you’re not confused enough, consider this code:

SpreadsheetCell myCell3(5);

string s2 = myCell3.getString();

In this case, getString() still creates a temporary unnamed string object when it returns mString. But now s2 gets its copy constructor called, not its assignment operator.

With move semantics, the compiler can use a move constructor instead of a copy constructor to return mString from getString(). This is more efficient. Move semantics is discussed in Chapter 10.

If you ever forget the order in which these things happen or which constructor or operator is called, you can easily figure it out by temporarily including helpful output in your code or by stepping through it with a debugger.

Copy Constructors and Object Members

You should also note the difference between assignment and copy constructor calls in constructors. If an object contains other objects, the compiler-generated copy constructor calls the copy constructors of each of the contained objects recursively. When you write your own copy constructor, you can provide the same semantics by using a ctor-initializer, as shown previously. If you omit a data member from the ctor-initializer, the compiler performs default initialization on it (a call to the 0-argument constructor for objects) before executing your code in the body of the constructor. Thus, by the time the body of the constructor executes, all object data members have already been initialized.

For example, you could write your copy constructor like this:

SpreadsheetCell::SpreadsheetCell(const SpreadsheetCell& src)

: mString(src.mString)

{

mValue = src.mValue;

}

However, when you assign values to data members in the body of the copy constructor, you are using the assignment operator on them, not the copy constructor, because they have already been initialized, as described previously.

In this example, mString is initialized using the copy constructor, while mValue is assigned to using the assignment operator.

SUMMARY

This chapter covered the fundamental aspects of C++’s facilities for object-oriented programming: classes and objects. It first reviewed the basic syntax for writing classes and using objects, including access control. Then, it covered object life cycles: when objects are constructed, destructed, and assigned, and what methods those actions invoke. The chapter included details of the constructor syntax, including ctor-initializers and initializer-list constructors, and introduced the notion of copy assignment operators. It also specified exactly which constructors the compiler writes for you, and under what circumstances, and explained that default constructors require no arguments.

For some of you, this chapter was mostly review. For others, it hopefully opened your eyes to the world of object-oriented programming in C++. In any case, now that you are proficient with objects and classes, read Chapter 8 to learn more about their tricks and subtleties.