- •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
Chapter 10
Building Classes with Inheritance
In Chapter 3, you learned that an “is-a” relationship recognizes the pattern that real-world objects tend to exist in hierarchies. In programming, that pattern becomes relevant when you need to write a class that builds on, or slightly changes, another class. One way to accomplish this aim is to copy code from one class and paste it into the other. By changing the relevant parts or amending the code, you can achieve the goal of creating a new class that is slightly different from the original. This approach, however, leaves an OOP programmer feeling sullen and slightly annoyed for the following reasons:
A bug fix to the original class will not be reflected in the new class because the two classes contain completely separate code.
The compiler does not know about any relationship between the two classes, so they are not polymorphic — they are not just different variations on the same thing.
This approach does not build a true is-a relationship. The new class is very similar to the original because it shares code, not because it really is the same type of object.
The original code might not be obtainable. It may exist only in a precompiled binary format, so copying and pasting the code might be impossible.
Not surprisingly, C++ provides built-in support for defining a true is-a relationship. The characteristics of C++ is-a relationships are described in the following section.
Extending Classes
When you write a class definition in C++, you can tell the compiler that your class is inheriting from, or extending, an existing class. By doing so, your class will automatically contain the data members and methods of the original class, which is called the parent class or superclass. Extending an existing class gives your class (which is now called a derived class or a subclass) the ability to describe only the ways in which it is different from the parent class.
To extend a class in C++, you specify the class you are extending when you write the class definition. To show the syntax for inheritance, we use two classes called Super and Sub. Don’t worry — more interesting examples are coming later. To begin, consider the following definition for the Super class.
class Super
{
public:
Super();
void someMethod();
protected:
int mProtectedInt;
private:
int mPrivateInt;
};
If you wanted to build a new class, called Sub, which inherits from Super, you would tell the compiler that Sub derives from Super with the following syntax:
224
Discovering Inheritance Techniques
class Sub : public Super
{
public:
Sub();
void someOtherMethod();
};
Sub itself is a full-fledged class that just happens to share the characteristics of the Super class. Don’t worry about the word public for now — its meaning is explained later in this chapter. Figure10-1 shows the simple relationship between Sub and Super. You can declare objects of type Sub just like any other object. You could even define a third class that subclasses Sub, forming a chain of classes, as shown in Figure 10-2.
Super
Sub
Figure 10-1
Super
Sub
SubSub
Figure 10-2
Sub doesn’t have to be the only subclass of Super. Additional classes can also subclass Super, effectively becoming siblings to Sub, as shown in Figure 10-3.
Super
Sub Foo
Figure 10-3
Clients’ View of Inheritance
To a client, or another part of your code, an object of type Sub is also an object of type Super because Sub inherits from Super. This means that all the public methods and data members of Super and all the public methods and data members of Sub are available.
225
Chapter 10
Code that uses the subclass does not need to know which class in your inheritance chain has defined a method in order to call it. For example, the following code calls two methods of a Sub object even though one of the methods was defined by the Super class.
Sub mySub;
mySub.someMethod();
mySub.someOtherMethod();
It is important to understand that inheritance only works in one direction. The Sub class has a very clearly defined relationship to the Super class, but the Super class, as written, doesn’t know anything about the Sub class. That means that objects of type Super do not support public methods and data members of Sub because Super is not a Sub.
The following code will not compile because the Super class does not contain a public method called someOtherMethod().
Super mySuper;
mySuper.someOtherMethod(); // BUG! Super doesn’t have a someOtherMethod().
From the perspective of other code, an object belongs to its defined class as well as to any superclasses.
A pointer or reference to an object can refer to an object of the declared class or any of its subclasses. This tricky subject is explained in detail later in this chapter. The concept to understand at this point is that a pointer to a Super can actually be pointing a Sub object. The same is true for a reference. The client can still access only the methods and data members that exist in Super, but through this mechanism, any code that operates on a Super can also operate on a Sub.
For example, the following code compiles and works just fine even though it initially appears that there is a type mismatch:
Super* superPointer = new Sub(); // Create a sub, and store it in a super pointer.
Subclass’s View of Inheritance
To the subclass itself, nothing much has changed in terms of how it is written or how it behaves. You can still define methods and data members on a subclass just as you would on a regular class. The previous definition of Sub declares a method called someOtherMethod(). Thus, the Sub class augments the Super class by adding an additional method.
A subclass can access public and protected methods and data members declared in its superclass as though they were its own, because technically, they are. For example, the implementation of someOtherMethod() on Sub could make use of the data member mProtectedInt, which was declared as part of Super. The following code shows this implementation. Accessing a superclass data member or method is no different than if the data member of method were declared as part of the subclass.
void Sub::someOtherMethod()
{
226
Discovering Inheritance Techniques
cout << “I can access the superclass data member mProtectedInt.” << endl; cout << “Its value is “ << mProtectedInt << endl;
}
When we introduced access specifiers (public, private, and protected) in Chapter 8, the difference between private and protected may have been confusing. Now that you understand subclasses, the difference should be clearer. If a class declares methods or data members as protected, subclasses have access to them. If they are declared as private, subclasses do not have access.
The following implementation of someOtherMethod() will not compile because the subclass attempts to access a private data member from the superclass.
void Sub::someOtherMethod()
{
cout << “I can access the superclass data member mProtectedInt.” << endl; cout << “Its value is “ << mProtectedInt << endl;
cout << “The value of mPrivateInt is “ << mPrivateInt << endl; // BUG!
}
The private access specifier gives you control over how a potential subclass could interact with your class. In practice, most data members are declared as protected, and most methods are either public or protected. The reason is that most of the time, you or someone you work with will be extending the class so you don’t want to shut out any potential uses by making methods or members private. Occasionally, the private specifier is useful to block subclasses from accessing potentially dangerous methods. It is also useful when writing classes that external or unknown parties will extend because you can block access to prevent misuse.
From the perspective of a subclass, all public and protected data members and methods from the superclass are available for use.
Overriding Methods
As you read in Chapter 3, the main reasons to inherit from a class are to add or replace functionality. The definition of Sub adds functionality to its parent class by providing an additional method, someOtherMethod(). The other method, someMethod(), is inherited from Super and behaves in the subclass exactly as it does in the superclass. In many cases, you will want to modify the behavior of a class by replacing, or overriding, a method.
How I Learned to Stop Worrying and Make Everything virtual
There is one small twist to overriding methods in C++ and it has to do with the keyword virtual. Only methods that are declared as virtual in the superclass can be overridden properly by subclasses. The keyword goes at the beginning of a method declaration as shown in the modified version of Super that follows.
class Super
{
public:
Super();
227
Chapter 10
virtual void someMethod();
protected:
int mProtectedInt;
private:
int mPrivateInt;
};
The virtual keyword has a few subtleties and is often cited as a poorly designed part of the language. A good rule of thumb is to just make all of your methods virtual. That way, you won’t have to worry about whether or not overriding the method will work. The only drawback is a small performance hit. The subtleties of the virtual keyword are covered toward the end of this chapter, and performance is discussed further in Chapter 17.
Even though it is unlikely that the Sub class will be extended, it is a good idea to make its methods virtual as well, just in case.
class Sub : public Super
{
public:
Sub();
virtual void someOtherMethod();
};
As a rule of thumb, make all your methods virtual (including the destructor, but not constructors) to avoid problems associated with omission of the virtual keyword.
Syntax for Overriding a Method
To override a method, you simply redeclare it in the subclass class definition exactly as it was declared in the superclass. In the subclass’s implementation file, you provide the new definition.
For example, the Super class contains a method called someMethod(). The definition of someMethod() is provided in Super.cpp and shown here:
void Super::someMethod()
{
cout << “This is Super’s version of someMethod().” << endl;
}
Note that you do not repeat the virtual keyword in front of the method definition.
If you wish to provide a new definition for someMethod() in the Sub class, you must first add it to the class definition for Sub, as follows:
class Sub : public Super
{
public:
Sub();
228
Discovering Inheritance Techniques
virtual void someMethod(); // Overrides Super’s someMethod() virtual void someOtherMethod();
};
The new definition of someMethod() is specified along with the rest of Sub’s methods.
void Sub::someMethod()
{
cout << “This is Sub’s version of someMethod().” << endl;
}
Clients’ View of Overridden Methods
With the preceding changes, other code would still call someMethod() the same way it did before. Just as before, the method could be called on an object of class Super or an object of class Sub. Now, however, the behavior of someMethod() will vary based on the class of the object.
For example, the following code works just as it did before, calling Super’s version of someMethod():
Super mySuper;
mySuper.someMethod(); // Calls Super’s version of someMethod().
The output of this code is:
This is Super’s version of someMethod().
If the code declares an object of class Sub, the other version will automatically be called.
Sub mySub;
mySub.someMethod(); // Calls Sub’s version of someMethod()
The output this time is:
This is Sub’s version of someMethod().
Everything else about objects of class Sub remains the same. Other methods that might have been inherited from Super will still have the definition provided by Super unless they are explicitly overridden in Sub.
As you learned earlier, a pointer or reference can refer to an object of a class or any of its subclasses. The object itself “knows” the class of which it is actually a member, so the appropriate method is called as long as it was declared virtual. For example, if you have a Super reference that refers to an object that is really a Sub, calling someMethod() will actually call the subclass’s version, as shown next. This aspect of overriding will not work properly if you omit the virtual keyword in the superclass.
Sub mySub;
Super& ref = mySub;
ref.someMethod(); |
// Calls Sub’s version of someMethod() |
229