📄 ei36.htm
字号:
Effective C++, 2E | Item 36: Differentiate between inheritance of interface and inheritance of implementation Back to Item 35: Make sure public inheritance models "isa."Continue to Item 37: Never redefine an inherited nonvirtual function.Item 36: Differentiate between inheritance of interface and inheritance of implementation.The seemingly straightforward notion of (public) inheritance turns out, upon closer examination, to be composed of two separable parts: inheritance of function interfaces and inheritance of function implementations. The difference between these two kinds of inheritance corresponds exactly to the difference between function declarations and function definitions discussed in the Introduction to this book.As a class designer, you sometimes want derived classes to inherit only the interface (declaration) of a member function; sometimes you want derived classes to inherit both the interface and the implementation for a function, but you want to allow them to override the implementation you provide; and sometimes you want them to inherit both interface and implementation without allowing them to override anything.To get a better feel for the differences among these options, consider a class hierarchy for representing geometric shapes in a graphics application: class Shape {public: virtual void draw() const = 0; virtual void error(const string& msg); int objectID() const; ...};class Rectangle: public Shape { ... };class Ellipse: public Shape { ... };Shape is an abstract class; its pure virtual function draw marks it as such. As a result, clients cannot create instances of the Shape class, only of the classes derived from it. Nonetheless, Shape exerts a strong influence on all classes that (publicly) inherit from it, becauseMember function interfaces are always inherited. As explained in Item 35, public inheritance means isa, so anything that is true of a base class must also be true of its derived classes. Hence, if a function applies to a class, it must also apply to its subclasses.Three functions are declared in the Shape class. The first, draw, draws the current object on an implicit display. The second, error, is called by member functions if they need to report an error. The third, objectID, returns a unique integer identifier for the current object; Item 17 gives an example of how such a function might be used. Each function is declared in a different way: draw is a pure virtual function; error is a simple (impure?) virtual function; and objectID is a nonvirtual function. What are the implications of these different declarations?Consider first the pure virtual function draw. The two most salient features of pure virtual functions are that they must be redeclared by any concrete class that inherits them, and they typically have no definition in abstract classes. Put these two traits together, and you realize thatThe purpose of declaring a pure virtual function is to have derived classes inherit a function interface only.This makes perfect sense for the Shape::draw function, because it is a reasonable demand that all Shape objects must be drawable, but the Shape class can provide no reasonable default implementation for that function. The algorithm for drawing an ellipse is very different from the algorithm for drawing a rectangle, for example. A good way to interpret the declaration of Shape::draw is as saying to designers of subclasses, "You must provide a draw function, but I have no idea how you're going to implement it."Incidentally, it is possible to provide a definition for a pure virtual function. That is, you could provide an implementation for Shape::draw, and C++ wouldn't complain, but the only way to call it would be to fully specify the call with the class name: Shape *ps = new Shape; // error! Shape is abstractShape *ps1 = new Rectangle; // fineps1->draw(); // calls Rectangle::drawShape *ps2 = new Ellipse; // fineps2->draw(); // calls Ellipse::drawps1->Shape::draw(); // calls Shape::drawps2->Shape::draw(); // calls Shape::drawAside from helping impress fellow programmers at cocktail parties, knowledge of this feature is generally of limited utility. As you'll see below, however, it can be employed as a mechanism for providing a safer-than-usual default implementation for simple (impure) virtual functions.Sometimes it's useful to declare a class containing nothing but pure virtual functions. Such a Protocol class can provide only function interfaces for derived classes, never implementations. Protocol classes are described in Item 34 and are mentioned again in Item 43.The story behind simple virtual functions is a bit different from that behind pure virtuals. As usual, derived classes inherit the interface of the function, but simple virtual functions traditionally provide an implementation that derived classes may or may not choose to override. If you think about this for a minute, you'll realize thatThe purpose of declaring a simple virtual function is to have derived classes inherit a function interface as well as a default implementation.In the case of Shape::error, the interface says that every class must support a function to be called when an error is encountered, but each class is free to handle errors in whatever way it sees fit. If a class doesn't want to do anything special, it can just fall back on the default error-handling provided in the Shape class. That is, the declaration of Shape::error says to designers of subclasses, "You've got to support an error function, but if you don't want to write your own, you can fall back on the default version in the Shape class."It turns out that it can be dangerous to allow simple virtual functions to specify both a function declaration and a default implementation. To see why, consider a hierarchy of airplanes for XYZ Airlines. XYZ has only two kinds of planes, the Model A and the Model B, and both are flown in exactly the same way. Hence, XYZ designs the following hierarchy: class Airport { ... }; // represents airportsclass Airplane {public: virtual void fly(const Airport& destination); ...};void Airplane::fly(const Airport& destination){ default code for flying an airplane to the given destination}class ModelA: public Airplane { ... };class ModelB: public Airplane { ... };To express that all planes have to support a fly function, and in recognition of the fact that different models of plane could, in principle, require different implementations for fly, Airplane::fly is declared virtual. However, in order to avoid writing identical code in the ModelA and ModelB classes, the default flying behavior is provided as the body of Airplane::fly, which both ModelA and ModelB inherit.This is a classic object-oriented design. Two classes share a common feature (the way they implement fly), so the common feature is moved into a base class, and the feature is inherited by the two classes. This design makes common features explicit, avoids code duplication, facilitates future enhancements, and eases long-term maintenance all the things for which object-oriented technology is so highly touted. XYZ Airlines should be proud.Now suppose that XYZ, its fortunes on the rise, decides to acquire a new type of airplane, the Model C. The Model C differs from the Model A and the Model B. In particular, it is flown differently.XYZ's programmers add the class for Model C to the hierarchy, but in their haste to get the new model into service, they forget to redefine the fly function: class ModelC: public Airplane { ... // no fly function is // declared};In their code, then, they have something akin to the following: Airport JFK(...); // JFK is an airport in // New York CityAirplane *pa = new ModelC;...pa->fly(JFK); // calls Airplane::fly!This is a disaster: an attempt is being made to fly a ModelC object as if it were a ModelA or a ModelB. That's not the kind of behavior that inspires confidence in the traveling public.The problem here is not that Airplane::fly has default behavior, but that ModelC was allowed to inherit that behavior without explicitly saying that it wanted to. Fortunately, it's easy to offer default behavior to subclasses, but not give it to them unless they ask for it. The trick is to sever the connection between the interface of the virtual function and its default implementation. Here's one way to do it: class Airplane {public: virtual void fly(const Airport& destination) = 0; ...protected: void defaultFly(const Airport& destination);};void Airplane::defaultFly(const Airport& destination){
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -