Inapoi
Inainte
Cuprins
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.
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
#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:
//: 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>
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).
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
want[51];
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 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.
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() {
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.
 |
|