ZeePedia

Inheritance & Composition:The constructor initializer list, Name hiding

<< Dynamic Object Creation:Early examples redesigned, new & delete for arrays
Polymorphism & Virtual Functions:The problem, How C++ implements late binding >>
img
14: Inheritance &
Composition
One of the most compelling features about C++ is
code reuse. But to be revolutionary, you need to be
able to do a lot more than copy code and change it.
613
img
That's the C approach, and it hasn't worked very well. As with
most everything in C++, the solution revolves around the class.
You reuse code by creating new classes, but instead of creating
them from scratch, you use existing classes that someone else has
built and debugged.
The trick is to use the classes without soiling the existing code. In
this chapter you'll see two ways to accomplish this. The first is
quite straightforward: You simply create objects of your existing
class inside the new class. This is called composition because the new
class is composed of objects of existing classes.
The second approach is subtler. You create a new class as a type of
an existing class. You literally take the form of the existing class
and add code to it, without modifying the existing class. This
magical act is called inheritance, and most of the work is done by the
compiler. Inheritance is one of the cornerstones of object-oriented
programming and has additional implications that will be explored
in Chapter 15.
It turns out that much of the syntax and behavior are similar for
both composition and inheritance (which makes sense; they are
both ways of making new types from existing types). In this
chapter, you'll learn about these code reuse mechanisms.
Composition syntax
Actually, you've been using composition all along to create classes.
You've just been composing classes primarily with built-in types
(and sometimes strings). It turns out to be almost as easy to use
composition with user-defined types.
Consider a class that is valuable for some reason:
//: C14:Useful.h
// A class to reuse
#ifndef USEFUL_H
614
Thinking in C++
img
#define USEFUL_H
class X {
int i;
public:
X() { i = 0; }
void set(int ii) { i = ii; }
int read() const { return i; }
int permute() { return i = i * 47; }
};
#endif // USEFUL_H ///:~
The data members are private in this class, so it's completely safe to
embed an object of type X as a public object in a new class, which
makes the interface straightforward:
//: C14:Composition.cpp
// Reuse code with composition
#include "Useful.h"
class Y {
int i;
public:
X x; // Embedded object
Y() { i = 0; }
void f(int ii) { i = ii; }
int g() const { return i; }
};
int main() {
Y y;
y.f(47);
y.x.set(37); // Access the embedded object
} ///:~
Accessing the member functions of the embedded object (referred
to as a subobject) simply requires another member selection.
It's more common to make the embedded objects private, so they
become part of the underlying implementation (which means you
can change the implementation if you want). The public interface
functions for your new class then involve the use of the embedded
object, but they don't necessarily mimic the object's interface:
14: Inheritance & Composition
615
img
//: C14:Composition2.cpp
// Private embedded objects
#include "Useful.h"
class Y {
int i;
X x; // Embedded object
public:
Y() { i = 0; }
void f(int ii) { i = ii; x.set(ii); }
int g() const { return i * x.read(); }
void permute() { x.permute(); }
};
int main() {
Y y;
y.f(47);
y.permute();
} ///:~
Here, the permute( )function is carried through to the new class
interface, but the other member functions of X are used within the
members of Y.
Inheritance syntax
The syntax for composition is obvious, but to perform inheritance
there's a new and different form.
When you inherit, you are saying, "This new class is like that old
class." You state this in code by giving the name of the class as
usual, but before the opening brace of the class body, you put a
colon and the name of the base class (or base classes, separated by
commas, for multiple inheritance). When you do this, you
automatically get all the data members and member functions in
the base class. Here's an example:
//: C14:Inheritance.cpp
// Simple inheritance
#include "Useful.h"
#include <iostream>
616
Thinking in C++
img
using namespace std;
class Y : public X {
int i; // Different from X's i
public:
Y() { i = 0; }
int change() {
i = permute(); // Different name call
return i;
}
void set(int ii) {
i = ii;
X::set(ii); // Same-name function call
}
};
int main() {
cout << "sizeof(X) = " << sizeof(X) << endl;
cout << "sizeof(Y) = "
<< sizeof(Y) << endl;
Y D;
D.change();
// X function interface comes through:
D.read();
D.permute();
// Redefined functions hide base versions:
D.set(12);
} ///:~
You can see Y being inherited from X, which means that Y will
contain all the data elements in X and all the member functions in
X. In fact, Y contains a subobject of X just as if you had created a
member object of X inside Y instead of inheriting from X. Both
member objects and base class storage are referred to as subobjects.
All the private elements of X are still private in Y; that is, just
because Y inherits from X doesn't mean Y can break the protection
mechanism. The private elements of X are still there, they take up
space ­ you just can't access them directly.
In main( ) you can see that Y's data elements are combined with X's
because the sizeof(Y)is twice as big as sizeof(X)
.
14: Inheritance & Composition
617
img
.
You'll notice that the base class is preceded by public. During
inheritance, everything defaults to private. If the base class were
not preceded by public, it would mean that all of the public
members of the base class would be private in the derived class.
This is almost never what you want1; the desired result is to keep
all the public members of the base class public in the derived class.
You do this by using the public keyword during inheritance.
In change( ) the base-class permute( )function is called. The
,
derived class has direct access to all the public base-class functions.
The set( ) function in the derived class redefines the set( ) function in
the base class. That is, if you call the functions read( ) and
permute( )for an object of type Y, you'll get the base-class versions
of those functions (you can see this happen inside main( )). But if
you call set( ) for a Y object, you get the redefined version. This
means that if you don't like the version of a function you get
during inheritance, you can change what it does. (You can also add
completely new functions like change( )
.)
However, when you're redefining a function, you may still want to
call the base-class version. If, inside set( ), you simply call set( )
you'll get the local version of the function ­ a recursive function
call. To call the base-class version, you must explicitly name the
base class using the scope resolution operator.
The constructor initializer list
You've seen how important it is in C++ to guarantee proper
initialization, and it's no different during composition and
inheritance. When an object is created, the compiler guarantees that
constructors for all of its subobjects are called. In the examples so
1 In Java, the compiler won't let you decrease the access of a member during
inheritance.
618
Thinking in C++
img
far, all of the subobjects have default constructors, and that's what
the compiler automatically calls. But what happens if your
subobjects don't have default constructors, or if you want to change
a default argument in a constructor? This is a problem because the
new class constructor doesn't have permission to access the private
data elements of the subobject, so it can't initialize them directly.
The solution is simple: Call the constructor for the subobject. C++
provides a special syntax for this, the constructor initializer list. The
form of the constructor initializer list echoes the act of inheritance.
With inheritance, you put the base classes after a colon and before
the opening brace of the class body. In the constructor initializer
list, you put the calls to subobject constructors after the constructor
argument list and a colon, but before the opening brace of the
function body. For a class MyType, inherited from Bar, this might
look like this:
MyType::MyType(int i) : Bar(i) { // ...
if Bar has a constructor that takes a single int argument.
Member object initialization
It turns out that you use this very same syntax for member object
initialization when using composition. For composition, you give
the names of the objects instead of the class names. If you have
more than one constructor call in the initializer list, you separate
the calls with commas:
MyType2::MyType2(int i) : Bar(i), m(i+1) { // ...
This is the beginning of a constructor for class MyType2, which is
inherited from Bar and contains a member object called m. Note
that while you can see the type of the base class in the constructor
initializer list, you only see the member object identifier.
14: Inheritance & Composition
619
img
Built-in types in the initializer list
The constructor initializer list allows you to explicitly call the
constructors for member objects. In fact, there's no other way to call
those constructors. The idea is that the constructors are all called
before you get into the body of the new class's constructor. That
way, any calls you make to member functions of subobjects will
always go to initialized objects. There's no way to get to the
opening brace of the constructor without some constructor being
called for all the member objects and base-class objects, even if the
compiler must make a hidden call to a default constructor. This is a
further enforcement of the C++ guarantee that no object (or part of
an object) can get out of the starting gate without its constructor
being called.
This idea that all of the member objects are initialized by the time
the opening brace of the constructor is reached is a convenient
programming aid as well. Once you hit the opening brace, you can
assume all subobjects are properly initialized and focus on specific
tasks you want to accomplish in the constructor. However, there's a
hitch: What about member objects of built-in types, which don't
have constructors?
To make the syntax consistent, you are allowed to treat built-in
types as if they have a single constructor, which takes a single
argument: a variable of the same type as the variable you're
initializing. Thus, you can say
//: C14:PseudoConstructor.cpp
class X {
int i;
float f;
char c;
char* s;
public:
X() : i(7), f(1.4), c('x'), s("howdy") {}
};
int main() {
620
Thinking in C++
img
X x;
int i(100);  // Applied to ordinary definition
int* ip = new int(47);
} ///:~
The action of these "pseudo-constructor calls" is to perform a
simple assignment. It's a convenient technique and a good coding
style, so you'll see it used often.
It's even possible to use the pseudo-constructor syntax when
creating a variable of a built-in type outside of a class:
int i(100);
int* ip = new int(47);
This makes built-in types act a little bit more like objects.
Remember, though, that these are not real constructors. In
particular, if you don't explicitly make a pseudo-constructor call,
no initialization is performed.
Combining composition & inheritance
Of course, you can use composition & inheritance together. The
following example shows the creation of a more complex class
using both of them.
//: C14:Combined.cpp
// Inheritance & composition
class A {
int i;
public:
A(int ii) : i(ii) {}
~A() {}
void f() const {}
};
class B {
int i;
public:
B(int ii) : i(ii) {}
14: Inheritance & Composition
621
img
~B() {}
void f() const {}
};
class C : public B {
A a;
public:
C(int ii) : B(ii), a(ii) {}
~C() {} // Calls ~A() and ~B()
void f() const {  // Redefinition
a.f();
B::f();
}
};
int main() {
C c(47);
} ///:~
C inherits from B and has a member object ("is composed of") of
type A. You can see the constructor initializer list contains calls to
both the base-class constructor and the member-object constructor.
The function C::f( ) redefines B::f( ), which it inherits, and also calls
the base-class version. In addition, it calls a.f( ). Notice that the only
time you can talk about redefinition of functions is during
inheritance; with a member object you can only manipulate the
public interface of the object, not redefine it. In addition, calling f( )
for an object of class C would not call a.f( ) if C::f( ) had not been
defined, whereas it would call B::f( ).
Automatic destructor calls
Although you are often required to make explicit constructor calls
in the initializer list, you never need to make explicit destructor
calls because there's only one destructor for any class, and it
doesn't take any arguments. However, the compiler still ensures
that all destructors are called, and that means all of the destructors
in the entire hierarchy, starting with the most-derived destructor
and working back to the root.
622
Thinking in C++
img
It's worth emphasizing that constructors and destructors are quite
unusual in that every one in the hierarchy is called, whereas with a
normal member function only that function is called, but not any of
the base-class versions. If you also want to call the base-class
version of a normal member function that you're overriding, you
must do it explicitly.
Order of constructor & destructor calls
It's interesting to know the order of constructor and destructor calls
when an object has many subobjects. The following example shows
exactly how it works:
//: C14:Order.cpp
// Constructor/destructor order
#include <fstream>
using namespace std;
ofstream out("order.out");
#define CLASS(ID) class ID { \
public: \
ID(int) { out << #ID " constructor\n"; } \
~ID() { out << #ID " destructor\n"; } \
};
CLASS(Base1);
CLASS(Member1);
CLASS(Member2);
CLASS(Member3);
CLASS(Member4);
class Derived1 : public Base1 {
Member1 m1;
Member2 m2;
public:
Derived1(int) : m2(1), m1(2), Base1(3) {
out << "Derived1 constructor\n";
}
~Derived1() {
out << "Derived1 destructor\n";
}
};
14: Inheritance & Composition
623
img
class Derived2 : public Derived1 {
Member3 m3;
Member4 m4;
public:
Derived2() : m3(1), Derived1(2), m4(3) {
out << "Derived2 constructor\n";
}
~Derived2() {
out << "Derived2 destructor\n";
}
};
int main() {
Derived2 d2;
} ///:~
First, an ofstreamobject is created to send all the output to a file.
Then, to save some typing and demonstrate a macro technique that
will be replaced by a much improved technique in Chapter 16, a
macro is created to build some of the classes, which are then used
in inheritance and composition. Each of the constructors and
destructors report themselves to the trace file. Note that the
constructors are not default constructors; they each have an int
argument. The argument itself has no identifier; its only reason for
existence is to force you to explicitly call the constructors in the
initializer list. (Eliminating the identifier prevents compiler
warning messages.)
The output of this program is
Base1 constructor
Member1 constructor
Member2 constructor
Derived1 constructor
Member3 constructor
Member4 constructor
Derived2 constructor
Derived2 destructor
Member4 destructor
Member3 destructor
Derived1 destructor
624
Thinking in C++
img
Member2 destructor
Member1 destructor
Base1 destructor
You can see that construction starts at the very root of the class
hierarchy, and that at each level the base class constructor is called
first, followed by the member object constructors. The destructors
are called in exactly the reverse order of the constructors ­ this is
important because of potential dependencies (in the derived-class
constructor or destructor, you must be able to assume that the base-
class subobject is still available for use, and has already been
constructed ­ or not destroyed yet).
It's also interesting that the order of constructor calls for member
objects is completely unaffected by the order of the calls in the
constructor initializer list. The order is determined by the order that
the member objects are declared in the class. If you could change
the order of constructor calls via the constructor initializer list, you
could have two different call sequences in two different
constructors, but the poor destructor wouldn't know how to
properly reverse the order of the calls for destruction, and you
could end up with a dependency problem.
Name hiding
If you inherit a class and provide a new definition for one of its
member functions, there are two possibilities. The first is that you
provide the exact signature and return type in the derived class
definition as in the base class definition. This is called redefining for
ordinary member functions and overriding when the base class
member function is a virtual function (virtual functions are the
normal case, and will be covered in detail in Chapter 15). But what
happens if you change the member function argument list or return
type in the derived class? Here's an example:
//: C14:NameHiding.cpp
// Hiding overloaded names during inheritance
14: Inheritance & Composition
625
img
#include <iostream>
#include <string>
using namespace std;
class Base {
public:
int f() const {
cout << "Base::f()\n";
return 1;
}
int f(string) const { return 1; }
void g() {}
};
class Derived1 : public Base {
public:
void g() const {}
};
class Derived2 : public Base {
public:
// Redefinition:
int f() const {
cout << "Derived2::f()\n";
return 2;
}
};
class Derived3 : public Base {
public:
// Change return type:
void f() const { cout << "Derived3::f()\n"; }
};
class Derived4 : public Base {
public:
// Change argument list:
int f(int) const {
cout << "Derived4::f()\n";
return 4;
}
};
int main() {
string s("hello");
626
Thinking in C++
img
Derived1 d1;
int x = d1.f();
d1.f(s);
Derived2 d2;
x = d2.f();
//!  d2.f(s); // string version hidden
Derived3 d3;
//!  x = d3.f(); // return int version hidden
Derived4 d4;
//!  x = d4.f(); // f() version hidden
x = d4.f(1);
} ///:~
In Base you see an overloaded function f( ), and Derived1doesn't
make any changes to f( ) but it does redefine g( ). In main( ), you
can see that both overloaded versions of f( ) are available in
Derived1 However, Derived2redefines one overloaded version of
.
f( ) but not the other, and the result is that the second overloaded
form is unavailable. In Derived3 changing the return type hides
,
both the base class versions, and Derived4shows that changing the
argument list also hides both the base class versions. In general, we
can say that anytime you redefine an overloaded function name
from the base class, all the other versions are automatically hidden
in the new class. In Chapter 15, you'll see that the addition of the
virtual keyword affects function overloading a bit more.
If you change the interface of the base class by modifying the
signature and/or return type of a member function from the base
class, then you're using the class in a different way than inheritance
is normally intended to support. It doesn't necessarily mean you're
doing it wrong, it's just that the ultimate goal of inheritance is to
support polymorphism, and if you change the function signature or
return type then you are actually changing the interface of the base
class. If this is what you have intended to do then you are using
inheritance primarily to reuse code, and not to maintain the
common interface of the base class (which is an essential aspect of
polymorphism). In general, when you use inheritance this way it
means you're taking a general-purpose class and specializing it for
14: Inheritance & Composition
627
img
a particular need ­ which is usually, but not always, considered the
realm of composition.
For example, consider the Stack class from Chapter 9. One of the
problems with that class is that you had to perform a cast every
time you fetched a pointer from the container. This is not only
tedious, it's unsafe ­ you could cast the pointer to anything you
want.
An approach that seems better at first glance is to specialize the
general Stack class using inheritance. Here's an example that uses
the class from Chapter 9:
//: C14:InheritStack.cpp
// Specializing the Stack class
#include "../C09/Stack4.h"
#include "../require.h"
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
class StringStack : public Stack {
public:
void push(string* str) {
Stack::push(str);
}
string* peek() const {
return (string*)Stack::peek();
}
string* pop() {
return (string*)Stack::pop();
}
~StringStack() {
string* top = pop();
while(top) {
delete top;
top = pop();
}
}
};
628
Thinking in C++
img
int main() {
ifstream in("InheritStack.cpp");
assure(in, "InheritStack.cpp");
string line;
StringStack textlines;
while(getline(in, line))
textlines.push(new string(line));
string* s;
while((s = textlines.pop()) != 0) { // No cast!
cout << *s << endl;
delete s;
}
} ///:~
Since all of the member functions in Stack4.hare inlines, nothing
needs to be linked.
StringStackspecializes Stack so that push( ) will accept only
String pointers. Before, Stack would accept void pointers, so the
user had no type checking to make sure the proper pointers were
inserted. In addition, peek( ) and pop( ) now return String pointers
instead of void pointers, so no cast is necessary to use the pointer.
Amazingly enough, this extra type-checking safety is free in
push( ), peek( ), and pop( )! The compiler is being given extra type
information that it uses at compile-time, but the functions are
inlined and no extra code is generated.
Name hiding comes into play here because, in particular, the
push( ) function has a different signature: the argument list is
different. If you had two versions of push( ) in the same class, that
would be overloading, but in this case overloading is not what we
want because that would still allow you to pass any kind of pointer
into push( ) as a void*. Fortunately, C++ hides the push(void*)
version in the base class in favor of the new version that's defined
in the derived class, and therefore it only allows us to push( ) string
pointers onto the StringStack
.
14: Inheritance & Composition
629
img
Because we can now guarantee that we know exactly what kind of
objects are in the container, the destructor works correctly and the
ownership problem is solved ­ or at least, one approach to the
ownership problem. Here, if you push( ) a string pointer onto the
StringStack then (according to the semantics of the StringStack
,
)
you're also passing ownership of that pointer to the StringStack If
.
you pop( ) the pointer, you not only get the pointer, but you also
get ownership of that pointer. Any pointers that are left on the
StringStackwhen its destructor is called are then deleted by that
destructor. And since these are always string pointers and the
delete statement is working on string pointers instead of void
pointers, the proper destruction happens and everything works
correctly.
There is a drawback: this class works only for string pointers. If you
want a Stack that works with some other kind of object, you must
write a new version of the class so that it works only with your new
kind of object. This rapidly becomes tedious, and is finally solved
using templates, as you will see in Chapter 16.
We can make an additional observation about this example: it
changes the interface of the Stack in the process of inheritance. If
the interface is different, then a StringStackreally isn't a Stack, and
you will never be able to correctly use a StringStackas a Stack.
This makes the use of inheritance questionable here; if you're not
creating a StringStackthat is-a type of Stack, then why are you
inheriting? A more appropriate version of StringStackwill be
shown later in this chapter.
Functions that don't automatically
inherit
Not all functions are automatically inherited from the base class
into the derived class. Constructors and destructors deal with the
creation and destruction of an object, and they can know what to
630
Thinking in C++
img
do with the aspects of the object only for their particular class, so all
the constructors and destructors in the hierarchy below them must
be called. Thus, constructors and destructors don't inherit and must
be created specially for each derived class.
In addition, the operator=doesn't inherit because it performs a
constructor-like activity. That is, just because you know how to
assign all the members of an object on the left-hand side of the =
from an object on the right-hand side doesn't mean that assignment
will still have the same meaning after inheritance.
In lieu of inheritance, these functions are synthesized by the
compiler if you don't create them yourself. (With constructors, you
can't create any constructors in order for the compiler to synthesize
the default constructor and the copy-constructor.) This was briefly
described in Chapter 6. The synthesized constructors use
memberwise initialization and the synthesized operator=uses
memberwise assignment. Here's an example of the functions that
are synthesized by the compiler:
//: C14:SynthesizedFunctions.cpp
// Functions that are synthesized by the compiler
#include <iostream>
using namespace std;
class GameBoard {
public:
GameBoard() { cout << "GameBoard()\n"; }
GameBoard(const GameBoard&) {
cout << "GameBoard(const GameBoard&)\n";
}
GameBoard& operator=(const GameBoard&) {
cout << "GameBoard::operator=()\n";
return *this;
}
~GameBoard() { cout << "~GameBoard()\n"; }
};
class Game {
GameBoard gb; // Composition
14: Inheritance & Composition
631
img
public:
// Default GameBoard constructor called:
Game() { cout << "Game()\n"; }
// You must explicitly call the GameBoard
// copy-constructor or the default constructor
// is automatically called instead:
Game(const Game& g) : gb(g.gb) {
cout << "Game(const Game&)\n";
}
Game(int) { cout << "Game(int)\n"; }
Game& operator=(const Game& g) {
// You must explicitly call the GameBoard
// assignment operator or no assignment at
// all happens for gb!
gb = g.gb;
cout << "Game::operator=()\n";
return *this;
}
class Other {}; // Nested class
// Automatic type conversion:
operator Other() const {
cout << "Game::operator Other()\n";
return Other();
}
~Game() { cout << "~Game()\n"; }
};
class Chess : public Game {};
void f(Game::Other) {}
class Checkers : public Game {
public:
// Default base-class constructor called:
Checkers() { cout << "Checkers()\n"; }
// You must explicitly call the base-class
// copy constructor or the default constructor
// will be automatically called instead:
Checkers(const Checkers& c) : Game(c) {
cout << "Checkers(const Checkers& c)\n";
}
Checkers& operator=(const Checkers& c) {
// You must explicitly call the base-class
// version of operator=() or no base-class
// assignment will happen:
632
Thinking in C++
img
Game::operator=(c);
cout << "Checkers::operator=()\n";
return *this;
}
};
int main() {
Chess d1;  // Default constructor
Chess d2(d1); // Copy-constructor
//! Chess d3(1); // Error: no int constructor
d1 = d2; // Operator= synthesized
f(d1); // Type-conversion IS inherited
Game::Other go;
//!  d1 = go; // Operator= not synthesized
// for differing types
Checkers c1, c2(c1);
c1 = c2;
} ///:~
The constructors and the operator=for GameBoardand Game
announce themselves so you can see when they're used by the
compiler. In addition, the operator Other( )
performs automatic
type conversion from a Game object to an object of the nested class
Other. The class Chess simply inherits from Game and creates no
functions (to see how the compiler responds). The function f( )
takes an Other object to test the automatic type conversion
function.
In main( ), the synthesized default constructor and copy-
constructor for the derived class Chess are called. The Game
versions of these constructors are called as part of the constructor-
call hierarchy. Even though it looks like inheritance, new
constructors are actually synthesized by the compiler. As you
might expect, no constructors with arguments are automatically
created because that's too much for the compiler to intuit.
The operator=is also synthesized as a new function in Chess using
memberwise assignment (thus, the base-class version is called)
because that function was not explicitly written in the new class.
14: Inheritance & Composition
633
img
And of course the destructor was automatically synthesized by the
compiler.
Because of all these rules about rewriting functions that handle
object creation, it may seem a little strange at first that the
automatic type conversion operator is inherited. But it's not too
unreasonable ­ if there are enough pieces in Game to make an
Other object, those pieces are still there in anything derived from
Game and the type conversion operator is still valid (even though
you may in fact want to redefine it).
operator=is synthesized only for assigning objects of the same type.
If you want to assign one type to another you must always write
that operator=yourself.
If you look more closely at Game, you'll see that the copy-
constructor and assignment operators have explicit calls to the
member object copy-constructor and assignment operator. You will
normally want to do this because otherwise, in the case of the copy-
constructor, the default member object constructor will be used
instead, and in the case of the assignment operator, no assignment
at all will be done for the member objects!
Lastly, look at Checkers which explicitly writes out the default
,
constructor, copy-constructor, and assignment operators. In the
case of the default constructor, the default base-class constructor is
automatically called, and that's typically what you want. But, and
this is an important point, as soon as you decide to write your own
copy-constructor and assignment operator, the compiler assumes
that you know what you're doing and does not automatically call
the base-class versions, as it does in the synthesized functions. If
you want the base class versions called (and you typically do) then
you must explicitly call them yourself. In the Checkerscopy-
constructor, this call appears in the constructor initializer list:
Checkers(const Checkers& c) : Game(c) {
634
Thinking in C++
img
In the Checkersassignment operator, the base class call is the first
line in the function body:
Game::operator=(c);
These calls should be part of the canonical form that you use
whenever you inherit a class.
Inheritance and static member functions
static member functions act the same as non-static member
functions:
1.
They inherit into the derived class.
2.
If you redefine a static member, all the other overloaded
functions in the base class are hidden.
3.
If you change the signature of a function in the base class, all
the base class versions with that function name are hidden
(this is really a variation of the previous point).
However, static member functions cannot be virtual (a topic
covered thoroughly in Chapter 15).
Choosing composition vs. inheritance
Both composition and inheritance place subobjects inside your new
class. Both use the constructor initializer list to construct these
subobjects. You may now be wondering what the difference is
between the two, and when to choose one over the other.
Composition is generally used when you want the features of an
existing class inside your new class, but not its interface. That is,
you embed an object to implement features of your new class, but
the user of your new class sees the interface you've defined rather
than the interface from the original class. To do this, you follow the
14: Inheritance & Composition
635
img
typical path of embedding private objects of existing classes inside
your new class.
Occasionally, however, it makes sense to allow the class user to
directly access the composition of your new class, that is, to make
the member objects public. The member objects use access control
themselves, so this is a safe thing to do and when the user knows
you're assembling a bunch of parts, it makes the interface easier to
understand. A Car class is a good example:
//: C14:Car.cpp
// Public composition
class Engine {
public:
void start() const {}
void rev() const {}
void stop() const {}
};
class Wheel {
public:
void inflate(int psi) const {}
};
class Window {
public:
void rollup() const {}
void rolldown() const {}
};
class Door {
public:
Window window;
void open() const {}
void close() const {}
};
class Car {
public:
Engine engine;
Wheel wheel[4];
Door left, right; // 2-door
636
Thinking in C++
img
};
int main() {
Car car;
car.left.window.rollup();
car.wheel[0].inflate(72);
} ///:~
Because the composition of a Car is part of the analysis of the
problem (and not simply part of the underlying design), making
the members public assists the client programmer's understanding
of how to use the class and requires less code complexity for the
creator of the class.
With a little thought, you'll also see that it would make no sense to
compose a Car using a "vehicle" object ­ a car doesn't contain a
vehicle, it is a vehicle. The is-a relationship is expressed with
inheritance, and the has-a relationship is expressed with
composition.
Subtyping
Now suppose you want to create a type of ifstreamobject that not
only opens a file but also keeps track of the name of the file. You
can use composition and embed both an ifstreamand a string into
the new class:
//: C14:FName1.cpp
// An fstream with a file name
#include "../require.h"
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
class FName1 {
ifstream file;
string fileName;
bool named;
public:
FName1() : named(false) {}
14: Inheritance & Composition
637
img
FName1(const string& fname)
: fileName(fname), file(fname.c_str()) {
assure(file, fileName);
named = true;
}
string name() const { return fileName; }
void name(const string& newName) {
if(named) return; // Don't overwrite
fileName = newName;
named = true;
}
operator ifstream&() { return file; }
};
int main() {
FName1 file("FName1.cpp");
cout << file.name() << endl;
// Error: close() not a member:
//!  file.close();
} ///:~
There's a problem here, however. An attempt is made to allow the
use of the FName1 object anywhere an ifstreamobject is used by
including an automatic type conversion operator from FName1 to
an ifstream& But in main, the line
.
file.close();
will not compile because automatic type conversion happens only
in function calls, not during member selection. So this approach
won't work.
A second approach is to add the definition of close( )to FName1:
void close() { file.close(); }
This will work if there are only a few functions you want to bring
through from the ifstreamclass. In that case you're only using part
of the class, and composition is appropriate.
But what if you want everything in the class to come through? This
is called subtyping because you're making a new type from an
638
Thinking in C++
img
existing type, and you want your new type to have exactly the
same interface as the existing type (plus any other member
functions you want to add), so you can use it everywhere you'd use
the existing type. This is where inheritance is essential. You can see
that subtyping solves the problem in the preceding example
perfectly:
//: C14:FName2.cpp
// Subtyping solves the problem
#include "../require.h"
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
class FName2 : public ifstream {
string fileName;
bool named;
public:
FName2() : named(false) {}
FName2(const string& fname)
: ifstream(fname.c_str()), fileName(fname) {
assure(*this, fileName);
named = true;
}
string name() const { return fileName; }
void name(const string& newName) {
if(named) return; // Don't overwrite
fileName = newName;
named = true;
}
};
int main() {
FName2 file("FName2.cpp");
assure(file, "FName2.cpp");
cout << "name: " << file.name() << endl;
string s;
getline(file, s); // These work too!
file.seekg(-200, ios::end);
file.close();
} ///:~
14: Inheritance & Composition
639
img
Now any member function available for an ifstreamobject is
available for an FName2 object. You can also see that non-member
functions like getline( )that expect an ifstreamcan also work with
an FName2. That's because an FName2 is a type of ifstream it
;
doesn't simply contain one. This is a very important issue that will
be explored at the end of this chapter and in the next one.
private inheritance
You can inherit a base class privately by leaving off the public in
the base-class list, or by explicitly saying private (probably a better
policy because it is clear to the user that you mean it). When you
inherit privately, you're "implementing in terms of;" that is, you're
creating a new class that has all of the data and functionality of the
base class, but that functionality is hidden, so it's only part of the
underlying implementation. The class user has no access to the
underlying functionality, and an object cannot be treated as a
instance of the base class (as it was in FName2.cpp
).
You may wonder what the purpose of private inheritance is,
because the alternative of using composition to create a private
object in the new class seems more appropriate. private inheritance
is included in the language for completeness, but if for no other
reason than to reduce confusion, you'll usually want to use
composition rather than private inheritance. However, there may
occasionally be situations where you want to produce part of the
same interface as the base class and disallow the treatment of the
object as if it were a base-class object. private inheritance provides
this ability.
Publicizing privately inherited members
When you inherit privately, all the public members of the base
class become private. If you want any of them to be visible, just say
their names (no arguments or return values) in the public section of
the derived class:
//: C14:PrivateInheritance.cpp
640
Thinking in C++
img
class Pet {
public:
char eat() const { return 'a'; }
int speak() const { return 2; }
float sleep() const { return 3.0; }
float sleep(int) const { return 4.0; }
};
class Goldfish : Pet { // Private inheritance
public:
Pet::eat; // Name publicizes member
Pet::sleep; // Both overloaded members exposed
};
int main() {
Goldfish bob;
bob.eat();
bob.sleep();
bob.sleep(1);
//! bob.speak();// Error: private member function
} ///:~
Thus, private inheritance is useful if you want to hide part of the
functionality of the base class.
Notice that giving the name of an overloaded function exposes all
the versions of the overloaded function in the base class.
You should think carefully before using private inheritance instead
of composition; private inheritance has particular complications
when combined with runtime type identification (this is the topic of
a chapter in Volume 2 of this book, downloadable from
).
protected
Now that you've been introduced to inheritance, the keyword
protectedfinally has meaning. In an ideal world, private members
would always be hard-and-fast private, but in real projects there
are times when you want to make something hidden from the
14: Inheritance & Composition
641
img
world at large and yet allow access for members of derived classes.
The protectedkeyword is a nod to pragmatism; it says, "This is
private as far as the class user is concerned, but available to anyone
who inherits from this class."
The best approach is to leave the data members private ­ you
should always preserve your right to change the underlying
implementation. You can then allow controlled access to inheritors
of your class through protectedmember functions:
//: C14:Protected.cpp
// The protected keyword
#include <fstream>
using namespace std;
class Base {
int i;
protected:
int read() const
{ return i; }
void set(int ii)
{ i = ii; }
public:
Base(int ii = 0)
: i(ii) {}
int value(int m)
const { return m*i; }
};
class Derived : public Base {
int j;
public:
Derived(int jj = 0) : j(jj) {}
void change(int x) { set(x); }
};
int main() {
Derived d;
d.change(10);
} ///:~
You will find examples of the need for protectedin examples later
in this book, and in Volume 2.
642
Thinking in C++
img
protected inheritance
When you're inheriting, the base class defaults to private, which
means that all of the public member functions are private to the
user of the new class. Normally, you'll make the inheritance public
so the interface of the base class is also the interface of the derived
class. However, you can also use the protectedkeyword during
inheritance.
Protected derivation means "implemented-in-terms-of" to other
classes but "is-a" for derived classes and friends. It's something
you don't use very often, but it's in the language for completeness.
Operator overloading & inheritance
Except for the assignment operator, operators are automatically
inherited into a derived class. This can be demonstrated by
inheriting from C12:Byte.h
:
//: C14:OperatorInheritance.cpp
// Inheriting overloaded operators
#include "../C12/Byte.h"
#include <fstream>
using namespace std;
ofstream out("ByteTest.out");
class Byte2 : public Byte {
public:
// Constructors don't inherit:
Byte2(unsigned char bb = 0) : Byte(bb) {}
// operator= does not inherit, but
// is synthesized for memberwise assignment.
// However, only the SameType = SameType
// operator= is synthesized, so you have to
// make the others explicitly:
Byte2& operator=(const Byte& right) {
Byte::operator=(right);
return *this;
}
Byte2& operator=(int i) {
Byte::operator=(i);
14: Inheritance & Composition
643
img
return *this;
}
};
// Similar test function as in C12:ByteTest.cpp:
void k(Byte2& b1, Byte2& b2) {
b1 = b1 * b2 + b2 % b1;
#define TRY2(OP) \
out << "b1 = "; b1.print(out); \
out << ", b2 = "; b2.print(out); \
out << ";  b1 " #OP " b2 produces "; \
(b1 OP b2).print(out); \
out << endl;
b1 = 9; b2 = 47;
TRY2(+) TRY2(-) TRY2(*) TRY2(/)
TRY2(%) TRY2(^) TRY2(&) TRY2(|)
TRY2(<<) TRY2(>>) TRY2(+=) TRY2(-=)
TRY2(*=) TRY2(/=) TRY2(%=) TRY2(^=)
TRY2(&=) TRY2(|=) TRY2(>>=) TRY2(<<=)
TRY2(=) // Assignment operator
// Conditionals:
#define TRYC2(OP) \
out << "b1 = "; b1.print(out); \
out << ", b2 = "; b2.print(out); \
out << ";  b1 " #OP " b2 produces "; \
out << (b1 OP b2); \
out << endl;
b1 = 9; b2 = 47;
TRYC2(<) TRYC2(>) TRYC2(==) TRYC2(!=) TRYC2(<=)
TRYC2(>=) TRYC2(&&) TRYC2(||)
// Chained assignment:
Byte2 b3 = 92;
b1 = b2 = b3;
}
int main() {
out << "member functions:" << endl;
Byte2 b1(47), b2(9);
k(b1, b2);
} ///:~
644
Thinking in C++
img
The test code is identical to that in C12:ByteTest.cpp
except that
Byte2 is used instead of Byte. This way all the operators are
verified to work with Byte2 via inheritance.
When you examine the class Byte2, you'll see that the constructor
must be explicitly defined, and that only the operator=that assigns
a Byte2 to a Byte2 is synthesized; any other assignment operators
that you need you'll have to synthesize on your own.
Multiple inheritance
You can inherit from one class, so it would seem to make sense to
inherit from more than one class at a time. Indeed you can, but
whether it makes sense as part of a design is a subject of continuing
debate. One thing is generally agreed upon: You shouldn't try this
until you've been programming quite a while and understand the
language thoroughly. By that time, you'll probably realize that no
matter how much you think you absolutely must use multiple
inheritance, you can almost always get away with single
inheritance.
Initially, multiple inheritance seems simple enough: You add more
classes in the base-class list during inheritance, separated by
commas. However, multiple inheritance introduces a number of
possibilities for ambiguity, which is why a chapter in Volume 2 is
devoted to the subject.
Incremental development
One of the advantages of inheritance and composition is that these
support incremental development by allowing you to introduce new
code without causing bugs in existing code. If bugs do appear, they
are isolated within the new code. By inheriting from (or composing
with) an existing, functional class and adding data members and
member functions (and redefining existing member functions
14: Inheritance & Composition
645
img
..
during inheritance) you leave the existing code ­ that someone else
may still be using ­ untouched and unbugged. If a bug happens,
you know it's in your new code, which is much shorter and easier
to read than if you had modified the body of existing code.
It's rather amazing how cleanly the classes are separated. You don't
even need the source code for the member functions in order to
reuse the code, just the header file describing the class and the
object file or library file with the compiled member functions. (This
is true for both inheritance and composition.)
It's important to realize that program development is an
incremental process, just like human learning. You can do as much
analysis as you want, but you still won't know all the answers
when you set out on a project. You'll have much more success ­
and more immediate feedback ­ if you start out to "grow" your
project as an organic, evolutionary creature, rather than
constructing it all at once like a glass-box skyscraper2.
Although inheritance for experimentation is a useful technique, at
some point after things stabilize you need to take a new look at
your class hierarchy with an eye to collapsing it into a sensible
structure3. Remember that underneath it all, inheritance is meant to
express a relationship that says, "This new class is a type of that old
class." Your program should not be concerned with pushing bits
around, but instead with creating and manipulating objects of
various types to express a model in the terms given you from the
problem space.
2 To learn more about this idea, see Extreme Programming Explained, by Kent Beck
(Addison-Wesley 2000).
3 See Refactoring: Improving the Design of Existing Code by Martin Fowler (Addison-
Wesley 1999).
646
Thinking in C++
img
Upcasting
Earlier in the chapter, you saw how an object of a class derived
from ifstreamhas all the characteristics and behaviors of an
ifstreamobject. In FName2.cpp any ifstreammember function
,
could be called for an FName2 object.
The most important aspect of inheritance is not that it provides
member functions for the new class, however. It's the relationship
expressed between the new class and the base class. This
relationship can be summarized by saying, "The new class is a type
of the existing class."
This description is not just a fanciful way of explaining inheritance
­ it's supported directly by the compiler. As an example, consider a
base class called Instrumentthat represents musical instruments
and a derived class called Wind. Because inheritance means that all
the functions in the base class are also available in the derived class,
any message you can send to the base class can also be sent to the
derived class. So if the Instrumentclass has a play( ) member
function, so will Wind instruments. This means we can accurately
say that a Wind object is also a type of Instrument The following
.
example shows how the compiler supports this notion:
//: C14:Instrument.cpp
// Inheritance & upcasting
enum note { middleC, Csharp, Cflat }; // Etc.
class Instrument {
public:
void play(note) const {}
};
// Wind objects are Instruments
// because they have the same interface:
class Wind : public Instrument {};
void tune(Instrument& i) {
// ...
i.play(middleC);
14: Inheritance & Composition
647
img
}
int main() {
Wind flute;
tune(flute); // Upcasting
} ///:~
What's interesting in this example is the tune( ) function, which
accepts an Instrumentreference. However, in main( ) the tune( )
function is called by handing it a reference to a Wind object. Given
that C++ is very particular about type checking, it seems strange
that a function that accepts one type will readily accept another
type, until you realize that a Wind object is also an Instrument
object, and there's no function that tune( ) could call for an
Instrumentthat isn't also in Wind (this is what inheritance
guarantees). Inside tune( ), the code works for Instrumentand
anything derived from Instrument and the act of converting a
,
Wind reference or pointer into an Instrumentreference or pointer
is called upcasting.
Why "upcasting?"
The reason for the term is historical and is based on the way class
inheritance diagrams have traditionally been drawn: with the root
at the top of the page, growing downward. (Of course, you can
draw your diagrams any way you find helpful.) The inheritance
diagram for Instrument.cppis then:
Instrument
Wind
Casting from derived to base moves up on the inheritance diagram,
so it's commonly referred to as upcasting. Upcasting is always safe
because you're going from a more specific type to a more general
type ­ the only thing that can occur to the class interface is that it
can lose member functions, not gain them. This is why the compiler
648
Thinking in C++
img
allows upcasting without any explicit casts or other special
notation.
Upcasting and the copy-constructor
If you allow the compiler to synthesize a copy-constructor for a
derived class, it will automatically call the base-class copy-
constructor, and then the copy-constructors for all the member
objects (or perform a bitcopy on built-in types) so you'll get the
right behavior:
//: C14:CopyConstructor.cpp
// Correctly creating the copy-constructor
#include <iostream>
using namespace std;
class Parent {
int i;
public:
Parent(int ii) : i(ii) {
cout << "Parent(int ii)\n";
}
Parent(const Parent& b) : i(b.i) {
cout << "Parent(const Parent&)\n";
}
Parent() : i(0) { cout << "Parent()\n"; }
friend ostream&
operator<<(ostream& os, const Parent& b) {
return os << "Parent: " << b.i << endl;
}
};
class Member {
int i;
public:
Member(int ii) : i(ii) {
cout << "Member(int ii)\n";
}
Member(const Member& m) : i(m.i) {
cout << "Member(const Member&)\n";
}
friend ostream&
operator<<(ostream& os, const Member& m) {
14: Inheritance & Composition
649
img
return os << "Member: " << m.i << endl;
}
};
class Child : public Parent {
int i;
Member m;
public:
Child(int ii) : Parent(ii), i(ii), m(ii) {
cout << "Child(int ii)\n";
}
friend ostream&
operator<<(ostream& os, const Child& c){
return os << (Parent&)c << c.m
<< "Child: " << c.i << endl;
}
};
int main() {
Child c(2);
cout << "calling copy-constructor: " << endl;
Child c2 = c; // Calls copy-constructor
cout << "values in c2:\n" << c2;
} ///:~
The operator<<for Child is interesting because of the way that it
calls the operator<<for the Parent part within it: by casting the
Child object to a Parent& (if you cast to a base-class object instead
of a reference you will usually get undesirable results):
return os << (Parent&)c << c.m
Since the compiler then sees it as a Parent, it calls the Parent
version of operator<<
.
You can see that Child has no explicitly-defined copy-constructor.
The compiler then synthesizes the copy-constructor (since that is
one of the four functions it will synthesize, along with the default
constructor ­ if you don't create any constructors ­ the operator=
and the destructor) by calling the Parent copy-constructor and the
Member copy-constructor. This is shown in the output
650
Thinking in C++
img
Parent(int ii)
Member(int ii)
Child(int ii)
calling copy-constructor:
Parent(const Parent&)
Member(const Member&)
values in c2:
Parent: 2
Member: 2
Child: 2
However, if you try to write your own copy-constructor for Child
and you make an innocent mistake and do it badly:
Child(const Child& c) : i(c.i), m(c.m) {}
then the default constructor will automatically be called for the
base-class part of Child, since that's what the compiler falls back on
when it has no other choice of constructor to call (remember that
some constructor must always be called for every object, regardless
of whether it's a subobject of another class). The output will then
be:
Parent(int ii)
Member(int ii)
Child(int ii)
calling copy-constructor:
Parent()
Member(const Member&)
values in c2:
Parent: 0
Member: 2
Child: 2
This is probably not what you expect, since generally you'll want
the base-class portion to be copied from the existing object to the
new object as part of copy-construction.
To repair the problem you must remember to properly call the
base-class copy-constructor (as the compiler does) whenever you
write your own copy-constructor. This can seem a little strange-
looking at first but it's another example of upcasting:
14: Inheritance & Composition
651
img
Child(const Child& c)
: Parent(c), i(c.i), m(c.m) {
cout << "Child(Child&)\n";
}
The strange part is where the Parent copy-constructor is called:
Parent(c) What does it mean to pass a Child object to a Parent
.
constructor? But Child is inherited from Parent, so a Child
reference is a Parent reference. The base-class copy-constructor call
upcasts a reference to Child to a reference to Parent and uses it to
perform the copy-construction. When you write your own copy
constructors you'll almost always want to do the same thing.
Composition vs. inheritance (revisited)
One of the clearest ways to determine whether you should be using
composition or inheritance is by asking whether you'll ever need to
upcast from your new class. Earlier in this chapter, the Stack class
was specialized using inheritance. However, chances are the
StringStackobjects will be used only as string containers and
never upcast, so a more appropriate alternative is composition:
//: C14:InheritStack2.cpp
// Composition vs. inheritance
#include "../C09/Stack4.h"
#include "../require.h"
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
class StringStack {
Stack stack; // Embed instead of inherit
public:
void push(string* str) {
stack.push(str);
}
string* peek() const {
return (string*)stack.peek();
}
string* pop() {
return (string*)stack.pop();
652
Thinking in C++
img
}
};
int main() {
ifstream in("InheritStack2.cpp");
assure(in, "InheritStack2.cpp");
string line;
StringStack textlines;
while(getline(in, line))
textlines.push(new string(line));
string* s;
while((s = textlines.pop()) != 0) // No cast!
cout << *s << endl;
} ///:~
The file is identical to InheritStack.cppexcept that a Stack object is
,
embedded in StringStack and member functions are called for the
,
embedded object. There's still no time or space overhead because
the subobject takes up the same amount of space, and all the
additional type checking happens at compile time.
Although it tends to be more confusing, you could also use private
inheritance to express "implemented in terms of." This would also
solve the problem adequately. One place it becomes important,
however, is when multiple inheritance might be warranted. In that
case, if you see a design in which composition can be used instead
of inheritance, you may be able to eliminate the need for multiple
inheritance.
Pointer & reference upcasting
In Instrument.cpp the upcasting occurs during the function call ­ a
,
Wind object outside the function has its reference taken and
becomes an Instrumentreference inside the function. Upcasting
can also occur during a simple assignment to a pointer or reference:
Wind w;
Instrument* ip = &w; // Upcast
Instrument& ir = w; // Upcast
14: Inheritance & Composition
653
img
Like the function call, neither of these cases requires an explicit
cast.
A crisis
Of course, any upcast loses type information about an object. If you
say
Wind w;
Instrument* ip = &w;
the compiler can deal with ip only as an Instrumentpointer and
nothing else. That is, it cannot know that ip actually happens to
point to a Wind object. So when you call the play( ) member
function by saying
ip->play(middleC);
the compiler can know only that it's calling play( ) for an
Instrumentpointer, and call the base-class version of
Instrument::play( )
instead of what it should do, which is call
Wind::play( . Thus, you won't get the correct behavior.
)
This is a significant problem; it is solved in Chapter 15 by
introducing the third cornerstone of object-oriented programming:
polymorphism (implemented in C++ with virtual functions).
Summary
Both inheritance and composition allow you to create a new type
from existing types, and both embed subobjects of the existing
types inside the new type. Typically, however, you use
composition to reuse existing types as part of the underlying
implementation of the new type and inheritance when you want to
force the new type to be the same type as the base class (type
equivalence guarantees interface equivalence). Since the derived
class has the base-class interface, it can be upcast to the base, which
is critical for polymorphism as you'll see in Chapter 15.
654
Thinking in C++
img
Although code reuse through composition and inheritance is very
helpful for rapid project development, you'll generally want to
redesign your class hierarchy before allowing other programmers
to become dependent on it. Your goal is a hierarchy in which each
class has a specific use and is neither too big (encompassing so
much functionality that it's unwieldy to reuse) nor annoyingly
small (you can't use it by itself or without adding functionality).
Exercises
Solutions to selected exercises can be found in the electronic document The Thinking in C++ Annotated
Solution Guide, available for a small fee from .
1.
Modify Car.cpp so that it also inherits from a class called
Vehicle, placing appropriate member functions in
Vehicle (that is, make up some member functions). Add
a nondefault constructor to Vehicle, which you must call
inside Car's constructor.
2.
Create two classes, A and B, with default constructors
that announce themselves. Inherit a new class called C
from A, and create a member object of B in C, but do not
create a constructor for C. Create an object of class C and
observe the results.
3.
Create a three-level hierarchy of classes with default
constructors, along with destructors, both of which
announce themselves to cout. Verify that for an object of
the most derived type, all three constructors and
destructors are automatically called. Explain the order in
which the calls are made.
4.
Modify Combined.cppto add another level of
inheritance and a new member object. Add code to show
when the constructors and destructors are being called.
5.
In Combined.cpp create a class D that inherits from B
,
and has a member object of class C. Add code to show
when the constructors and destructors are being called.
14: Inheritance & Composition
655
img
6.
Modify Order.cppto add another level of inheritance
Derived3with member objects of class Member4 and
Member5. Trace the output of the program.
7.
In NameHiding.cpp verify that in Derived2 Derived3
,
,
,
and Derived4 none of the base-class versions of f( ) are
,
available.
8.
Modify NameHiding.cppby adding three overloaded
functions named h( ) to Base, and show that redefining
one of them in a derived class hides the others.
9.
Inherit a class StringVectorfrom vector<void*>and
redefine the push_back( )and operator[]member
functions to accept and produce string*. What happens if
you try to push_back( )a void*?
10.
Write a class containing a long and use the psuedo-
constructor call syntax in the constructor to initialize the
long.
11.
Create a class called Asteroid Use inheritance to
.
specialize the PStash class in Chapter 13 (PStash.h&
PStash.cpp so that it accepts and returns Asteroid
)
pointers. Also modify PStashTest.cppto test your
classes. Change the class so PStash is a member object.
12.
Repeat Exercise 11 with a vector instead of a PStash.
13.
In SynthesizedFunctions.cppmodify Chess to give it a
,
default constructor, copy-constructor, and assignment
operator. Demonstrate that you've written these
correctly.
14.
Create two classes called Travelerand Pager without
default constructors, but with constructors that take an
argument of type string, which they simply copy to an
internal string variable. For each class, write the correct
copy-constructor and assignment operator. Now inherit a
class BusinessTravelerfrom Travelerand give it a
member object of type Pager. Write the correct default
constructor, a constructor that takes a string argument, a
copy-constructor, and an assignment operator.
656
Thinking in C++
img
15.
Create a class with two static member functions. Inherit
from this class and redefine one of the member functions.
Show that the other is hidden in the derived class.
16.
Look up more of the member functions for ifstream In
.
FName2.cpp try them out on the file object.
,
17.
Use private and protectedinheritance to create two new
classes from a base class. Then attempt to upcast objects
of the derived class to the base class. Explain what
happens.
18.
In Protected.cpp add a member function in Derived that
,
calls the protectedBase member read( ).
19.
Change Protected.cppso that Derived is using protected
inheritance. See if you can call value( )for a Derived
object.
20.
Create a class called SpaceShipwith a fly( ) method.
Inherit Shuttle from SpaceShipand add a land( )
method. Create a new Shuttle, upcast by pointer or
reference to a SpaceShip and try to call the land( )
,
method. Explain the results.
21.
Modify Instrument.cppto add a prepare( )method to
Instrument Call prepare( )inside tune( ).
.
22.
Modify Instrument.cppso that play( ) prints a message
to cout, and Wind redefines play( ) to print a different
message to cout. Run the program and explain why you
probably wouldn't want this behavior. Now put the
virtual keyword (which you will learn about in Chapter
15) in front of the play( ) declaration in Instrumentand
observe the change in the behavior.
23.
In CopyConstructor.cppinherit a new class from Child
,
and give it a Member m. Write a proper constructor
,
copy-constructoroperator= and operator<<for
,
,
ostreams, and test the class in main( ).
24.
Take the example CopyConstructor.cpp
and modify it by
adding your own copy-constructor to Child without
calling the base-class copy-constructor and see what
14: Inheritance & Composition
657
img
happens. Fix the problem by making a proper explicit
call to the base-class copy constructor in the constructor-
initializer list of the Child copy-constructor.
25.
Modify InheritStack2.cpp use a vector<string>
to
instead of a Stack.
26.
Create a class Rock with a default constructor, a copy-
constructor, an assignment operator, and a destructor, all
of which announce to cout that they've been called. In
main( ), create a vector<Rock>(that is, hold Rock objects
by value) and add some Rocks. Run the program and
explain the output you get. Note whether the destructors
are called for the Rock objects in the vector. Now repeat
the exercise with a vector<Rock*> Is it possible to create
.
a vector<Rock&>
?
27.
This exercise creates the design pattern called proxy. Start
with a base class Subject and give it three functions: f( ),
g( ), and h( ). Now inherit a class Proxy and two classes
Implementation1and Implementation2from Subject.
Proxy should contain a pointer to a Subject, and all the
member functions for Proxy should just turn around and
make the same calls through the Subject pointer. The
Proxy constructor takes a pointer to a Subject that is
installed in the Proxy (usually by the constructor). In
main( ), create two different Proxy objects that use the
two different implementations. Now modify Proxy so
that you can dynamically change implementations.
28.
Modify ArrayOperatorNew.cpp
from Chapter 13 to
show that, if you inherit from Widget, the allocation still
works correctly. Explain why inheritance in Framis.cpp
from Chapter 13 would not work correctly.
29.
Modify Framis.cppfrom Chapter 13 by inheriting from
Framis and creating new versions of new and delete for
your derived class. Demonstrate that they work correctly.
658
Thinking in C++