(Public) inheritance is a simple and easy-to-understand concept. Once it is closely examined, it will prove that it is composed of two independent parts: inheritance of function interfaces (Inheritance of function interfaces) and Inheritance of function implementations (Inheritance of function implementation ). The differences between the two inheritance exactly match the differences between function declarations (function declaration) and function definitions (Function Definition) discussed in introduction.
As a class designer, sometimes you want Derived classes to inherit only the interface (Declaration) of a member function ). Sometimes you want Derived classes to inherit both interfaces and implementation, but you need to allow them to replace the implementation they inherit. In other cases, you want Derived classes to inherit the interface and implementation (implementation) of a function, without allowing them to replace anything.
To better understand the differences between these options, consider a graphical ApplicationProgramClass hierarchy (class inheritance system) that represents ry ):
Class shape { Public: Virtual void draw () const = 0;Virtual void error (const STD: string & MSG ); Int objectid () const; ... }; Class rectangle: Public shape {...}; Class ellipse: Public shape {...}; |
Shape is an abstract class. Its pure virtual function (pure virtual function) indicates this. As a result, the customer cannot create an instance of shape class, but can only create an instance of classes inherited from it. However, shape has a very strong impact on all classes inherited from it (public), because
The member functions interfaces are always inherited. As explained in item 32, public inheritance means is-a, so anything that is true for a base class must also be true for its derived classes. Therefore, if a function applies to a class, it must also apply to its derived classes.
The shape class declares three functions. First, draw the current object on a clear display device. Second, error. If member functions needs to report an error, call it. Third, objectid, returns the unique integer identifier of the current object. Each function is declared in a different way: draw is a pure virtual function (pure virtual function); error is a simple (impure ?) Virtual function (simple virtual function), while objectid is a non-virtual function (non-virtual function ). What do these different statements imply?
Consider the first pure virtual function (pure virtual function) draw:
Class shape { Public: Virtual void draw () const = 0; ... }; |
Two of the most notable features of pure virtual functions (pure virtual functions) are that they must be re-declared by any specific class that inherits them, and they are generally not defined in abstract classes. Add these two features together and you should realize that.
The purpose of declaring a pure virtual function is to make Derived classes inherit a function interface only.
This makes the shape: Draw function have a complete meaning, because it requires that all shape objects be properly drawn, however, the shape class itself cannot provide a reasonable default implementation for this function. For example, to draw an ellipticalAlgorithmUnlike the algorithm for drawing a rectangle, the shape: Draw statement tells the designer of the derived classes: "You must provide a draw function, but I have no comments on how you implement it."
By the way, it is possible to provide a definition for a pure virtual function. That is to say, you can provide an implementation for shape: draw, and C ++ will not complain about anything, but the only way to call it is to use the class name to limit this call:
Shape * PS = new shape; // error! Shape is abstract Shape * PS1 = new rectangle; // fine PS1-> draw (); // CILS rectangle: Draw Shape * PS2 = new ellipse; // fine PS2-> draw (); // callellipse: Draw PS1-> shape: Draw (); // callshape: Draw PS2-> shape: Draw (); // callshape: Draw |
In addition to helping you impress fellow programmers at cocktail parties, this feature is usually useless. However, as you will see below, it can be used as a mechanism to "provide a safer-than-usual implementation for simple (impure) virtual functions.
The story behind simple virtual functions is a little different from that of pure functions. Derived classes still inherits the interface of the function as usual, but simple virtual functions provides an implementation that can be replaced by Derived classes. If you think about it for a while, you will realize
The purpose of declaring a simple virtual function is to let Derived classes inherit a function interface as well as a default implementation.
Consider the case of shape: Error:
Class shape { Public: Virtual void error (const STD: string & MSG ); ... }; |
The interface requires that each class must support a function called when an error occurs, but each class can use any method that it feels appropriate to handle the error. If a class does not need to do anything special, it can turn to the default version of error handling provided in the shape class. That is to say, the shape: Error statement tells the designer of derived classes: "You should support an error function, but if you do not want to write it yourself, you can ask for the default version in shape class ."
The result is: it is dangerous to allow simple virtual functions to specify both a function interface and a default implementation. Let's take a look at the reason for considering the hierarchy (inheritance system) of an XYZ Airline plane ). XYZ has only two types of aircraft, model A and model B. Both of them fly exactly in the same way. Therefore, XYZ is designed as follows hierarchy (inheritance system ):
Class airport {...}; // represents airports Class 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 {...}; |
Airplane :: fly is declared as virtual. However, to avoid repeatedCodeThe default flight behavior is provided by the airplane: Fly function body for modela and modelb to inherit.
This is a classic object-oriented design. Because two classes share a common feature (which implements the fly method), this general feature is transferred to a base class and inherited by two classes. This design makes general features clear, avoids code duplication, improves future scalability, and simplifies long-term maintenance-because of the object-oriented technology, all these things are highly sought after. XYZ airlines should be proud of it.
Now, assuming that XYZ's wealth has increased, it is decided to introduce a new model, Model C. Model C is different from model A and model B in some aspects. In particular, its flight is different.
Programmers at XYZ added the class of Model C to hierarchy (inheritance system), but they forgot to redefine the fly function as they rushed to put new models into service:
Class modelc: Public airplane { ... // No Fly function is declared }; |
Therefore, in their code, something similar to this occurs:
Airport PDX (...); // PDX is the airport near my home Airplane * pA = new modelc; ... Pa-> fly (PDX); // callairplane: fly! |
This is a disaster: an attempt to make a modelc object fly like a modela or modelb. This is not an encouraging behavior in the travel crowd.
The problem here is not that airplane: fly has a default behavior, but that modelc is allowed to inherit this line without specifying what it wants to do. Fortunately, it is easy to "provide default behavior for derived classes (derived classes), but they will not be handed over to them unless they make clear requirements. This trick is to cut off the connection between the virtual function interface (Interface) and its default implementation (default implementation. This method is used as follows:
Class airplane { Public: Virtual void fly (const airport & Destination) = 0;... Protected: Void defaultfly (const airport & Destination ); }; Void airplane: defaultfly (const airport & Destination) { Default Code for flying an airplane to the given destination } |
Note airplane: How is fly converted into a pure virtual function (pure virtual function. It provides an interface for flight ). The default implementation will also appear in airplane class, but now it is an independent function, defafly fly. For example, if modela and modelb need to use the default behavior classes, you only need to make an inline call to defaultfly in their fly function bodies (but refer to the inline and virtual Functions) ):
Class modela: Public airplane { Public: Virtual void fly (const airport & Destination) {Destinfly fly (destination );}... }; Class modelb: Public airplane { Public: Virtual void fly (const airport & Destination) {Destinfly fly (destination );} ... }; |
For modelc class, it is impossible to inherit the incorrect fly implementation accidentally, because the pure virtual (pure virtual) in airplane forces modelc to provide its own fly version.
Class modelc: Public airplane { Public: Virtual void fly (const airport & Destination );... }; Void modelc: Fly (const airport & Destination) { Code for flying a modelc airplane to the given destination } |
This solution is not very secure (programmers can still use copy-and-paste to put themselves in trouble), but it is more reliable than the original design. As for airplane: defaultfly, it is protected because it is completely the Implementation Details of airplane and Its Derived classes (derived class. Customers who use airplanes should only care about how they can fly, rather than how they can fly.
Airplane: defaultfly is a non-virtual function (non-virtual function. This is because derived class (derived class) should not redefine this function, which is a principle introduced in item 36. If defafly fly is virtual, you will encounter a loop problem: what if some Derived classes (derived classes) should redefine defaultfly but forget it?
Some people oppose providing functions for interfaces (interfaces) and default implementation (default implementations), just like fly and defaultfly above. First, they noticed that this would cause similar related function names to pollute class namespace. However, they still agree that the interface and default implementation should be separated. How did they solve this apparent conflict? By taking advantage of the fact that pure virtual functions (pure virtual functions) must be redeclared (re-declared) in concrete Derived classes (a specific derived class), they can also have their own implementations. Here is how airplane hierarchy uses this capability to define a pure virtual function ):
class airplane { Public: virtual void fly (const airport & Destination) = 0; ... }; void airplane: Fly (const airport & Destination) // an implementation of {// a pure virtual function default code for flying an airplane to the given destination } class modela: public airplane { Public: virtual void fly (const airport & Destination) {airplane: Fly (destination );} ... }; class modelb: Public airplane { Public: virtual void fly (const airport & Destination) { airplane: Fly (destination) ;} ... }; class modelc: Public airplane { Public: virtual void fly (const airport & Destination ); ... }; void modelc: Fly (const airport & Destination) {< br> code for flying a modelc airplane to the given destination } |
In addition to replacing the independent function airplane: defaultfly with the pure virtual function (pure virtual function) airplane: Fly function, this design is almost identical to the previous one. Essentially, fly can be split into two basic components. Its declaration (Declaration) specifies its interface (Interface) (which is required by Derived classes (derived class), and its definition (Definition) specify its default behavior (this is usable by the derived classes (derived class), but only when they explicitly require this ). Merge fly and defaultfly. In any case, you lose the ability to give these two functions different protection levels: the original protected code (implemented by ultfly) now it becomes public (because it is located in Fly ).
Finally, let's look at the shape non-virtual function (non-virtual function), objectid:
Class shape { Public: Int objectid () const; ... }; |
When a member function (member function) is non-virtual (non-virtual), it should not be expected to behave differently in the derived classes (derived class. In fact, a non-virtual member function (non-virtual member function) specifies an invariant over specialization (beyond the special invariant), because no matter how special a derived class (derived class) becomes, it regards it as an action that does not allow changes. Except as follows,
The purpose of declaring a non-virtual function is to have derived classes inherit a function interface as well as a mandatory implementation (so that the derived class inherits the interface of a function, also inherits a forced implementation ).
You can consider the shape: objectid declaration as follows: "Each shape object generates an object identifier (Object ID code), and this object identifier (Object ID code) this method is always calculated using the same method. This method is determined by the shape: objectid definition, and the derived class (derived class) should not try to change it." Because a non-virtual function (non-virtual function) is considered as an invariant over specialization (beyond the special invariant), it should never be redefined in derived class (derived class, for details, see item 36.
The difference between the declaration of pure virtual, simple virtual, and non-virtual functions allows you to specify exactly what you need to inherit from the derived classes (derived class. Interface only (only interface), interface and a default implementation (interface and a default implementation), and interface and a mandatory implementation (interface and a mandatory implementation ). Because these different declaration types mean that they have different meanings. When you declare your member functions (member functions), you must carefully choose between them. If you do this, you should be able to avoid the two most common errors caused by inexperienced class designers.
The first error is to declare all functions as non-virtual (non-virtual ). This does not leave space for the specialization of derived classes (derived classes); Non-virtual Destructors (non-virtual destructor) is particularly problematic (see item 7 ). Of course, there is a reason to design a class that is not used as a base class (base class. In this case, a set of exclusive non-virtual member functions (non-virtual member functions) is completely reasonable. However, in general, such classes may be ignorant of the differences between virtual and non-virtual functions (non-virtual functions, it may also be the result of no justification for the performance cost of virtual functions. In fact, almost any class used as a base class (base class) will have virtual functions (or refer to item 7 ).
If you are concerned about the cost of virtual functions, please allow me to introduce experience-based 80-20 Rules (see item 30). In a typical program, 80% of the running time is spent on executing 20% of the Code. This rule is very important because it means that, on average, 80% of your function calls can be virtualized without a slight or perceptible impact on the overall performance of your program. Before you step into the shadow of worries about "can you afford a virtual function (virtual function) Cost", you should use some simple preventive measures, to ensure that you are concerned with the 20% that can produce a decisive difference in your program.
Another common error is that all member functions (member functions) are virtual ). Sometimes this is correct-the interface classes (Interface Class) of item 31 can be used as evidence. However, it may also be a sign of a class designer who lacks the determination to express his attitude. Some functions should not be redefined in Derived classes (derived class). In this case, you should declare those functions as non-virtual (non-virtual) and clearly express this. It does not serve those people. They assume that if they only need to spend some time redefining all your functions, your class will be used by all people to do everything, if you have an invariant over specialization (beyond the specialization invariant), simply put, don't be afraid!
Things to remember
· Inheritance of Interface (interface inheritance) is different from inheritance of implementation (Implementation inheritance. In public inheritance (Public inheritance), derived classes (derived class) always inherits base class interfaces (base class interface ).
· Pure virtual functions (pure virtual function) specifies inheritance of interface only (only interfaces are inherited ).
· Simple (impure) virtual functions (simple virtual function) specifies the inheritance of Interface (interface inheritance) plus inheritance of a default implementation (default implementation inheritance ).
· Non-virtual functions (non-virtual function) specifies inheritance of Interface (interface inheritance) plus inheritance of a mandatory implementation (forced inheritance ).