- •Contents
- •Introduction
- •Who This Book Is For
- •What This Book Covers
- •How This Book Is Structured
- •What You Need to Use This Book
- •Conventions
- •Source Code
- •Errata
- •p2p.wrox.com
- •The Basics of C++
- •The Obligatory Hello, World
- •Namespaces
- •Variables
- •Operators
- •Types
- •Conditionals
- •Loops
- •Arrays
- •Functions
- •Those Are the Basics
- •Diving Deeper into C++
- •Pointers and Dynamic Memory
- •Strings in C++
- •References
- •Exceptions
- •The Many Uses of const
- •C++ as an Object-Oriented Language
- •Declaring a Class
- •Your First Useful C++ Program
- •An Employee Records System
- •The Employee Class
- •The Database Class
- •The User Interface
- •Evaluating the Program
- •What Is Programming Design?
- •The Importance of Programming Design
- •Two Rules for C++ Design
- •Abstraction
- •Reuse
- •Designing a Chess Program
- •Requirements
- •Design Steps
- •An Object-Oriented View of the World
- •Am I Thinking Procedurally?
- •The Object-Oriented Philosophy
- •Living in a World of Objects
- •Object Relationships
- •Abstraction
- •Reusing Code
- •A Note on Terminology
- •Deciding Whether or Not to Reuse Code
- •Strategies for Reusing Code
- •Bundling Third-Party Applications
- •Open-Source Libraries
- •The C++ Standard Library
- •Designing with Patterns and Techniques
- •Design Techniques
- •Design Patterns
- •The Reuse Philosophy
- •How to Design Reusable Code
- •Use Abstraction
- •Structure Your Code for Optimal Reuse
- •Design Usable Interfaces
- •Reconciling Generality and Ease of Use
- •The Need for Process
- •Software Life-Cycle Models
- •The Stagewise and Waterfall Models
- •The Spiral Method
- •The Rational Unified Process
- •Software-Engineering Methodologies
- •Extreme Programming (XP)
- •Software Triage
- •Be Open to New Ideas
- •Bring New Ideas to the Table
- •Thinking Ahead
- •Keeping It Clear
- •Elements of Good Style
- •Documenting Your Code
- •Reasons to Write Comments
- •Commenting Styles
- •Comments in This Book
- •Decomposition
- •Decomposition through Refactoring
- •Decomposition by Design
- •Decomposition in This Book
- •Naming
- •Choosing a Good Name
- •Naming Conventions
- •Using Language Features with Style
- •Use Constants
- •Take Advantage of const Variables
- •Use References Instead of Pointers
- •Use Custom Exceptions
- •Formatting
- •The Curly Brace Alignment Debate
- •Coming to Blows over Spaces and Parentheses
- •Spaces and Tabs
- •Stylistic Challenges
- •Introducing the Spreadsheet Example
- •Writing Classes
- •Class Definitions
- •Defining Methods
- •Using Objects
- •Object Life Cycles
- •Object Creation
- •Object Destruction
- •Assigning to Objects
- •Distinguishing Copying from Assignment
- •The Spreadsheet Class
- •Freeing Memory with Destructors
- •Handling Copying and Assignment
- •Different Kinds of Data Members
- •Static Data Members
- •Const Data Members
- •Reference Data Members
- •Const Reference Data Members
- •More about Methods
- •Static Methods
- •Const Methods
- •Method Overloading
- •Default Parameters
- •Inline Methods
- •Nested Classes
- •Friends
- •Operator Overloading
- •Implementing Addition
- •Overloading Arithmetic Operators
- •Overloading Comparison Operators
- •Building Types with Operator Overloading
- •Pointers to Methods and Members
- •Building Abstract Classes
- •Using Interface and Implementation Classes
- •Building Classes with Inheritance
- •Extending Classes
- •Overriding Methods
- •Inheritance for Reuse
- •The WeatherPrediction Class
- •Adding Functionality in a Subclass
- •Replacing Functionality in a Subclass
- •Respect Your Parents
- •Parent Constructors
- •Parent Destructors
- •Referring to Parent Data
- •Casting Up and Down
- •Inheritance for Polymorphism
- •Return of the Spreadsheet
- •Designing the Polymorphic Spreadsheet Cell
- •The Spreadsheet Cell Base Class
- •The Individual Subclasses
- •Leveraging Polymorphism
- •Future Considerations
- •Multiple Inheritance
- •Inheriting from Multiple Classes
- •Naming Collisions and Ambiguous Base Classes
- •Interesting and Obscure Inheritance Issues
- •Special Cases in Overriding Methods
- •Copy Constructors and the Equals Operator
- •The Truth about Virtual
- •Runtime Type Facilities
- •Non-Public Inheritance
- •Virtual Base Classes
- •Class Templates
- •Writing a Class Template
- •How the Compiler Processes Templates
- •Distributing Template Code between Files
- •Template Parameters
- •Method Templates
- •Template Class Specialization
- •Subclassing Template Classes
- •Inheritance versus Specialization
- •Function Templates
- •Function Template Specialization
- •Function Template Overloading
- •Friend Function Templates of Class Templates
- •Advanced Templates
- •More about Template Parameters
- •Template Class Partial Specialization
- •Emulating Function Partial Specialization with Overloading
- •Template Recursion
- •References
- •Reference Variables
- •Reference Data Members
- •Reference Parameters
- •Reference Return Values
- •Deciding between References and Pointers
- •Keyword Confusion
- •The const Keyword
- •The static Keyword
- •Order of Initialization of Nonlocal Variables
- •Types and Casts
- •typedefs
- •Casts
- •Scope Resolution
- •Header Files
- •C Utilities
- •Variable-Length Argument Lists
- •Preprocessor Macros
- •How to Picture Memory
- •Allocation and Deallocation
- •Arrays
- •Working with Pointers
- •Array-Pointer Duality
- •Arrays Are Pointers!
- •Not All Pointers Are Arrays!
- •Dynamic Strings
- •C-Style Strings
- •String Literals
- •The C++ string Class
- •Pointer Arithmetic
- •Custom Memory Management
- •Garbage Collection
- •Object Pools
- •Function Pointers
- •Underallocating Strings
- •Memory Leaks
- •Double-Deleting and Invalid Pointers
- •Accessing Out-of-Bounds Memory
- •Using Streams
- •What Is a Stream, Anyway?
- •Stream Sources and Destinations
- •Output with Streams
- •Input with Streams
- •Input and Output with Objects
- •String Streams
- •File Streams
- •Jumping around with seek() and tell()
- •Linking Streams Together
- •Bidirectional I/O
- •Internationalization
- •Wide Characters
- •Non-Western Character Sets
- •Locales and Facets
- •Errors and Exceptions
- •What Are Exceptions, Anyway?
- •Why Exceptions in C++ Are a Good Thing
- •Why Exceptions in C++ Are a Bad Thing
- •Our Recommendation
- •Exception Mechanics
- •Throwing and Catching Exceptions
- •Exception Types
- •Throwing and Catching Multiple Exceptions
- •Uncaught Exceptions
- •Throw Lists
- •Exceptions and Polymorphism
- •The Standard Exception Hierarchy
- •Catching Exceptions in a Class Hierarchy
- •Writing Your Own Exception Classes
- •Stack Unwinding and Cleanup
- •Catch, Cleanup, and Rethrow
- •Use Smart Pointers
- •Common Error-Handling Issues
- •Memory Allocation Errors
- •Errors in Constructors
- •Errors in Destructors
- •Putting It All Together
- •Why Overload Operators?
- •Limitations to Operator Overloading
- •Choices in Operator Overloading
- •Summary of Overloadable Operators
- •Overloading the Arithmetic Operators
- •Overloading Unary Minus and Unary Plus
- •Overloading Increment and Decrement
- •Overloading the Subscripting Operator
- •Providing Read-Only Access with operator[]
- •Non-Integral Array Indices
- •Overloading the Function Call Operator
- •Overloading the Dereferencing Operators
- •Implementing operator*
- •Implementing operator->
- •What in the World Is operator->* ?
- •Writing Conversion Operators
- •Ambiguity Problems with Conversion Operators
- •Conversions for Boolean Expressions
- •How new and delete Really Work
- •Overloading operator new and operator delete
- •Overloading operator new and operator delete with Extra Parameters
- •Two Approaches to Efficiency
- •Two Kinds of Programs
- •Is C++ an Inefficient Language?
- •Language-Level Efficiency
- •Handle Objects Efficiently
- •Use Inline Methods and Functions
- •Design-Level Efficiency
- •Cache as Much as Possible
- •Use Object Pools
- •Use Thread Pools
- •Profiling
- •Profiling Example with gprof
- •Cross-Platform Development
- •Architecture Issues
- •Implementation Issues
- •Platform-Specific Features
- •Cross-Language Development
- •Mixing C and C++
- •Shifting Paradigms
- •Linking with C Code
- •Mixing Java and C++ with JNI
- •Mixing C++ with Perl and Shell Scripts
- •Mixing C++ with Assembly Code
- •Quality Control
- •Whose Responsibility Is Testing?
- •The Life Cycle of a Bug
- •Bug-Tracking Tools
- •Unit Testing
- •Approaches to Unit Testing
- •The Unit Testing Process
- •Unit Testing in Action
- •Higher-Level Testing
- •Integration Tests
- •System Tests
- •Regression Tests
- •Tips for Successful Testing
- •The Fundamental Law of Debugging
- •Bug Taxonomies
- •Avoiding Bugs
- •Planning for Bugs
- •Error Logging
- •Debug Traces
- •Asserts
- •Debugging Techniques
- •Reproducing Bugs
- •Debugging Reproducible Bugs
- •Debugging Nonreproducible Bugs
- •Debugging Memory Problems
- •Debugging Multithreaded Programs
- •Debugging Example: Article Citations
- •Lessons from the ArticleCitations Example
- •Requirements on Elements
- •Exceptions and Error Checking
- •Iterators
- •Sequential Containers
- •Vector
- •The vector<bool> Specialization
- •deque
- •list
- •Container Adapters
- •queue
- •priority_queue
- •stack
- •Associative Containers
- •The pair Utility Class
- •multimap
- •multiset
- •Other Containers
- •Arrays as STL Containers
- •Strings as STL Containers
- •Streams as STL Containers
- •bitset
- •The find() and find_if() Algorithms
- •The accumulate() Algorithms
- •Function Objects
- •Arithmetic Function Objects
- •Comparison Function Objects
- •Logical Function Objects
- •Function Object Adapters
- •Writing Your Own Function Objects
- •Algorithm Details
- •Utility Algorithms
- •Nonmodifying Algorithms
- •Modifying Algorithms
- •Sorting Algorithms
- •Set Algorithms
- •The Voter Registration Audit Problem Statement
- •The auditVoterRolls() Function
- •The getDuplicates() Function
- •The RemoveNames Functor
- •The NameInList Functor
- •Testing the auditVoterRolls() Function
- •Allocators
- •Iterator Adapters
- •Reverse Iterators
- •Stream Iterators
- •Insert Iterators
- •Extending the STL
- •Why Extend the STL?
- •Writing an STL Algorithm
- •Writing an STL Container
- •The Appeal of Distributed Computing
- •Distribution for Scalability
- •Distribution for Reliability
- •Distribution for Centrality
- •Distributed Content
- •Distributed versus Networked
- •Distributed Objects
- •Serialization and Marshalling
- •Remote Procedure Calls
- •CORBA
- •Interface Definition Language
- •Implementing the Class
- •Using the Objects
- •A Crash Course in XML
- •XML as a Distributed Object Technology
- •Generating and Parsing XML in C++
- •XML Validation
- •Building a Distributed Object with XML
- •SOAP (Simple Object Access Protocol)
- •. . . Write a Class
- •. . . Subclass an Existing Class
- •. . . Throw and Catch Exceptions
- •. . . Read from a File
- •. . . Write to a File
- •. . . Write a Template Class
- •There Must Be a Better Way
- •Smart Pointers with Reference Counting
- •Double Dispatch
- •Mix-In Classes
- •Object-Oriented Frameworks
- •Working with Frameworks
- •The Model-View-Controller Paradigm
- •The Singleton Pattern
- •Example: A Logging Mechanism
- •Implementation of a Singleton
- •Using a Singleton
- •Example: A Car Factory Simulation
- •Implementation of a Factory
- •Using a Factory
- •Other Uses of Factories
- •The Proxy Pattern
- •Example: Hiding Network Connectivity Issues
- •Implementation of a Proxy
- •Using a Proxy
- •The Adapter Pattern
- •Example: Adapting an XML Library
- •Implementation of an Adapter
- •Using an Adapter
- •The Decorator Pattern
- •Example: Defining Styles in Web Pages
- •Implementation of a Decorator
- •Using a Decorator
- •The Chain of Responsibility Pattern
- •Example: Event Handling
- •Implementation of a Chain of Responsibility
- •Using a Chain of Responsibility
- •Example: Event Handling
- •Implementation of an Observer
- •Using an Observer
- •Chapter 1: A Crash Course in C++
- •Chapter 3: Designing with Objects
- •Chapter 4: Designing with Libraries and Patterns
- •Chapter 5: Designing for Reuse
- •Chapter 7: Coding with Style
- •Chapters 8 and 9: Classes and Objects
- •Chapter 11: Writing Generic Code with Templates
- •Chapter 14: Demystifying C++ I/O
- •Chapter 15: Handling Errors
- •Chapter 16: Overloading C++ Operators
- •Chapter 17: Writing Efficient C++
- •Chapter 19: Becoming Adept at Testing
- •Chapter 20: Conquering Debugging
- •Chapter 24: Exploring Distributed Objects
- •Chapter 26: Applying Design Patterns
- •Beginning C++
- •General C++
- •I/O Streams
- •The C++ Standard Library
- •C++ Templates
- •Integrating C++ and Other Languages
- •Algorithms and Data Structures
- •Open-Source Software
- •Software-Engineering Methodology
- •Programming Style
- •Computer Architecture
- •Efficiency
- •Testing
- •Debugging
- •Distributed Objects
- •CORBA
- •XML and SOAP
- •Design Patterns
- •Index
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