📄 ei36.htm
字号:
default code for flying an airplane to the given destination}Notice how Airplane::fly has been turned into a pure virtual function. That provides the interface for flying. The default implementation is also present in the Airplane class, but now it's in the form of an independent function, defaultFly. Classes like ModelA and ModelB that want to use the default behavior simply make an inline call to defaultFly inside their body of fly (but see Item 33 for information on the interaction of inlining and virtual functions): class ModelA: public Airplane {public: virtual void fly(const Airport& destination) { defaultFly(destination); } ...};class ModelB: public Airplane {public: virtual void fly(const Airport& destination) { defaultFly(destination); } ...};For the ModelC class, there is no possibility of accidentally inheriting the incorrect implementation of fly, because the pure virtual in Airplane forces ModelC to provide its own version of fly. 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 scheme isn't foolproof (programmers can still copy-and-paste themselves into trouble), but it's more reliable than the original design. As for Airplane::defaultFly, it's protected because it's truly an implementation detail of Airplane and its derived classes. Clients using airplanes should care only that they can be flown, not how the flying is implemented.It's also important that Airplane::defaultFly is a nonvirtual function. This is because no subclass should redefine this function, a truth to which Item 37 is devoted. If defaultFly were virtual, you'd have a circular problem: what if some subclass forgets to redefine defaultFly when it's supposed to?Some people object to the idea of having separate functions for providing interface and default implementation, such as fly and defaultFly above. For one thing, they note, it pollutes the class namespace with a proliferation of closely-related function names. Yet they still agree that interface and default implementation should be separated. How do they resolve this seeming contradiction? By taking advantage of the fact that pure virtual functions must be redeclared in subclasses, but they may also have implementations of their own. Here's how the Airplane hierarchy could take advantage of the ability to define a pure virtual function: class Airplane {public: virtual void fly(const Airport& destination) = 0; ...};void Airplane::fly(const Airport& destination){ 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){ code for flying a ModelC airplane to the given destination}This is almost exactly the same design as before, except that the body of the pure virtual function Airplane::fly takes the place of the independent function Airplane::defaultFly. In essence, fly has been broken into its two fundamental components. Its declaration specifies its interface (which derived classes must use), while its definition specifies its default behavior (which derived classes may use, but only if they explicitly request it). In merging fly and defaultFly, however, you've lost the ability to give the two functions different protection levels: the code that used to be protected (by being in defaultFly) is now public (because it's in fly).Finally, we come to Shape's nonvirtual function, objectID. When a member function is nonvirtual, it's not supposed to behave differently in derived classes. In fact, a nonvirtual member function specifies an invariant over specialization, because it identifies behavior that is not supposed to change, no matter how specialized a derived class becomes. As such,The purpose of declaring a nonvirtual function is to have derived classes inherit a function interface as well as a mandatory implementation.You can think of the declaration for Shape::objectID as saying, "Every Shape object has a function that yields an object identifier, and that object identifier is always computed in the same way. That way is determined by the definition of Shape::objectID, and no derived class should try to change how it's done." Because a nonvirtual function identifies an invariant over specialization, it should never be redefined in a subclass, a point that is discussed in detail in Item 37.The differences in declarations for pure virtual, simple virtual, and nonvirtual functions allow you to specify with precision what you want derived classes to inherit: interface only, interface and a default implementation, or interface and a mandatory implementation, respectively. Because these different types of declarations mean fundamentally different things, you must choose carefully among them when you declare your member functions. If you do, you should avoid the two most common mistakes made by inexperienced class designers.The first mistake is to declare all functions nonvirtual. That leaves no room for specialization in derived classes; nonvirtual destructors are particularly problematic (see Item 14). Of course, it's perfectly reasonable to design a class that is not intended to be used as a base class. Item M34 gives an example of a case where you might want to. In that case, a set of exclusively nonvirtual member functions is appropriate. Too often, however, such classes are declared either out of ignorance of the differences between virtual and nonvirtual functions or as a result of an unsubstantiated concern over the performance cost of virtual functions (see Item M24). The fact of the matter is that almost any class that's to be used as a base class will have virtual functions (again, see Item 14).If you're concerned about the cost of virtual functions, allow me to bring up the rule of 80-20 (see Item M16), which states that in a typical program, 80 percent of the runtime will be spent executing just 20 percent of the code. This rule is important, because it means that, on average, 80 percent of your function calls can be virtual without having the slightest detectable impact on your program's overall performance. Before you go gray worrying about whether you can afford the cost of a virtual function, then, take the simple precaution of making sure that you're focusing on the 20 percent of your program where the decision might really make a difference.The other common problem is to declare all member functions virtual. Sometimes this is the right thing to do witness Protocol classes (see Item 34), for example. However, it can also be a sign of a class designer who lacks the backbone to take a firm stand. Some functions should not be redefinable in derived classes, and whenever that's the case, you've got to say so by making those functions nonvirtual. It serves no one to pretend that your class can be all things to all people if they'll just take the time to redefine all your functions. Remember that if you have a base class B, a derived class D, and a member function mf, then each of the following calls to mf must work properly: D *pd = new D;B *pb = pd;pb->mf(); // call mf through a // pointer-to-basepd->mf(); // call mf through a // pointer-to-derivedSometimes, you must make mf a nonvirtual function to ensure that everything behaves the way it's supposed to (see Item 37). If you have an invariant over specialization, don't be afraid to say so! Back to Item 35: Make sure public inheritance models "isa."Continue to Item 37: Never redefine an inherited nonvirtual function.
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -