Functions that don’t automatically inherit Not all functions are automatically inherited from the base class into the derived class.. But, and this is an important point, as soon as yo
Trang 1Because 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
StringStack when 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 StringStack really isn’t a Stack, and
you will never be able to correctly use a StringStack as a Stack
This makes the use of inheritance questionable here; if you’re not
creating a StringStack that is-a type of Stack, then why are you
inheriting? A more appropriate version of StringStack will 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
Trang 2do 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:
Trang 3public:
// 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";
// 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:
Trang 4//! d1 = go; // Operator= not synthesized
// for differing types
Checkers c1, c2(c1);
c1 = c2;
} ///:~
The constructors and the operator= for GameBoard and 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 call hierarchy Even though it looks like inheritance, new
constructor-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
Trang 5And 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 Checkers
copy-constructor, this call appears in the constructor initializer list:
Checkers(const Checkers& c) : Game(c) {
Trang 6In the Checkers assignment 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
Trang 7typical 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 {}
void rollup() const {}
void rolldown() const {}
};
class Door {
public:
Window window;
void open() const {}
void close() const {}
Trang 8Because 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 ifstream object that not
only opens a file but also keeps track of the name of the file You
can use composition and embed both an ifstream and a string into
the new class:
Trang 9FName1(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
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 ifstream object 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 ifstream class 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
Trang 10existing 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
string name() const { return fileName; }
void name(const string& newName) {
if(named) return; // Don't overwrite
Trang 11Now any member function available for an ifstream object is
available for an FName2 object You can also see that non-member
functions like getline( ) that expect an ifstream can 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
Trang 12class 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
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
www.BruceEckel.com)
protected
Now that you’ve been introduced to inheritance, the keyword
protected finally 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
Trang 13world at large and yet allow access for members of derived classes
The protected keyword 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 protected member functions:
int read() const { return i; }
void set(int ii) { i = ii; }
You will find examples of the need for protected in examples later
in this book, and in Volume 2
Trang 14protected 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 protected keyword 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:
// 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) {
Trang 15return *this;
}
};
// Similar test function as in C12:ByteTest.cpp:
void k(Byte2& b1, Byte2& b2) {
TRY2(+) TRY2(-) TRY2(*) TRY2(/)
TRY2(%) TRY2(^) TRY2(&) TRY2(|)
TRY2(<<) TRY2(>>) TRY2(+=) TRY2(-=)
TRY2(*=) TRY2(/=) TRY2(%=) TRY2(^=)
TRY2(&=) TRY2(|=) TRY2(>>=) TRY2(<<=)
TRY2(=) // Assignment operator
TRYC2(<) TRYC2(>) TRYC2(==) TRYC2(!=) TRYC2(<=)
TRYC2(>=) TRYC2(&&) TRYC2(||)
Trang 16The 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
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
Trang 17during 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
Trang 18Upcasting
Earlier in the chapter, you saw how an object of a class derived
from ifstream has all the characteristics and behaviors of an
ifstream object In FName2.cpp, any ifstream member 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 Instrument that 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 Instrument class 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);
Trang 19What’s interesting in this example is the tune( ) function, which
accepts an Instrument reference 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
Instrument that isn’t also in Wind (this is what inheritance
guarantees) Inside tune( ), the code works for Instrument and
anything derived from Instrument, and the act of converting a
Wind reference or pointer into an Instrument reference 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.cpp is 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
Trang 20allows 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:
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;
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) {
Trang 21return os << "Member: " << m.i << endl;
Child(int ii) : Parent(ii), i(ii), m(ii) {
cout << "Child(int ii)\n";
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
Trang 22However, 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:
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:
Trang 23Child(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
StringStack objects will be used only as string containers and
never upcast, so a more appropriate alternative is composition:
Trang 24additional 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 Instrument reference 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
Trang 25Like the function call, neither of these cases requires an explicit
the compiler can deal with ip only as an Instrument pointer 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
Instrument pointer, 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