Am avut 371724 vizite de la lansarea siteului.




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.

Home   |   Web Faq   |   Radio Online   |   About   |   Products   |   Webmaster Login

The quality software developer.™
© 2003-2004 ruben|labs corp. All Rights Reserved.
Timp de generare a paginii: 17583 secunde
Versiune site: 1.8 SP3 (build 2305-rtm.88542-10.2004)