Am avut 371717 vizite de la lansarea siteului.




Inapoi        Inainte       Cuprins

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:

//: 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();
  }
};

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.cpp, except 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 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

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 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.

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 www.BruceEckel.com.

  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.cpp to 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.
  6. Modify Order.cpp to add another level of inheritance Derived3 with 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.cpp by 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 StringVector from 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.cpp to 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.cpp, modify Chess to give it a default constructor, copy-constructor, and assignment operator. Demonstrate that you’ve written these correctly.
  14. Create two classes called Traveler and 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 BusinessTraveler from Traveler and 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.
  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 protected inheritance 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 protected Base member read( ).
  19. Change Protected.cpp so that Derived is using protected inheritance. See if you can call value( ) for a Derived object.
  20. Create a class called SpaceShip with a fly( ) method. Inherit Shuttle from SpaceShip and 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.cpp to add a prepare( ) method to Instrument. Call prepare( ) inside tune( ).
  22. Modify Instrument.cpp so 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 Instrument and observe the change in the behavior.
  23. In CopyConstructor.cpp, inherit a new class from Child and give it a Member m. Write a proper constructor, copy-constructor, operator=, 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 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 to use a vector<string> 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 Implementation1 and Implementation2 from 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.cpp from Chapter 13 by inheriting from Framis and creating new versions of new and delete for your derived class. Demonstrate that they work correctly.


[51] In Java, the compiler won’t let you decrease the access of a member during inheritance.

[52] To learn more about this idea, see Extreme Programming Explained, by Kent Beck (Addison-Wesley 2000).

[53] See Refactoring: Improving the Design of Existing Code by Martin Fowler (Addison-Wesley 1999).

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)