|
Page 1 of 8 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.
|