FAQ:Inheritance篇 — virtual functions

文章目录

1、What is a “virtual member function”?(什么是虚成员函数)

Virtual member functions are key to the object-oriented paradigm, such as making it easy for old code to call new code.

虚成员函数是面向对象范式的关键,比如可以让旧代码更容易调用新代码。

A virtual function allows derived classes to replace the implementation provided by the base class. The compiler makes sure the replacement is always called whenever the object in question is actually of the derived class, even if the object is accessed by a base pointer rather than a derived pointer. This allows algorithms in the base class to be replaced in the derived class, even if users don’t know about the derived class.

虚函数允许派生类取代基类提供的实现。编译器确保只要对象实际上是派生类的,就一定会调用替换方法,即使访问对象的是基指针而不是派生类指针。这允许在派生类中替换基类中的算法,即使用户不知道派生类。

The derived class can either fully replace (“override”) the base class member function, or the derived class can partially replace (“augment”) the base class member function. The latter is accomplished by having the derived class member function call the base class member function, if desired.

派生类可以完全替换(“覆盖”)基类成员函数,或部分替换(“扩充”)基类成员函数。如果需要,后者可以通过让派生类成员函数调用基类成员函数来实现。

2、Why are member functions not virtual by default?(为什么成员函数不是默认为虚函数?)

Because many classes are not designed to be used as base classes. For example, see class complex.

因为很多类并不是设计来作为基类使用的。比如类 complex

Also, objects of a class with a virtual function require space needed by the virtual function call mechanism - typically one word per object. This overhead can be significant, and can get in the way of layout compatibility with data from other languages (e.g. C and Fortran).

此外,具有虚函数的类对象需要虚函数调用机制所需的空间——通常每个对象一个字。这种开销可能是巨大的,并且可能妨碍与其他语言(例如C和Fortran)数据的布局兼容性。

See The Design and Evolution of C++ for more design rationale.

请参阅C++的设计和演变以了解更多的设计原理。

3、How can C++ achieve dynamic binding yet also static typing? (C++是如何既实现动态绑定又实现静态绑定的?)

When you have a pointer to an object, the object may actually be of a class that is derived from the class of the pointer (e.g., a Vehicle* that is actually pointing to a Car object; this is called “polymorphism”). Thus there are two types: the (static) type of the pointer (Vehicle, in this case), and the (dynamic) type of the pointed-to object (Car, in this case).

当你有一个指向某个对象的指针时,这个对象可能实际上是这个指针类的派生类(如,Vehicle* 实际上指向的是 Car 对象,这被称为“多态性”)。因此,有两种指针类型:指针的(静态)类型(本例中的 Vehicle),和所指向对象的(动态)类型(本例中的 Car)。

Static typing means that the legality of a member function invocation is checked at the earliest possible moment: by the compiler at compile time. The compiler uses the static type of the pointer to determine whether the member function invocation is legal. If the type of the pointer can handle the member function, certainly the pointed-to object can handle it as well. E.g., if Vehicle has a certain member function, certainly Car also has that member function since Car is a kind-of Vehicle.

静态类型意味着尽早检查成员函数调用的合法性:由编译器在编译时检查。编译器通过指针的静态类型来确定成员函数调用是否合法。如果指针的类型可以处理成员函数,那么所指向的对象当然也可以处理它。例如,Vehicle有某个成员函数,当然 Car 也有那个成员函数,因为 CarVehicle 的一种。

Dynamic binding means that the address of the code in a member function invocation is determined at the last possible moment: based on the dynamic type of the object at run time. It is called “dynamic binding” because the binding to the code that actually gets called is accomplished dynamically (at run time). Dynamic binding is a result of virtual functions.

动态绑定意味着成员函数调用中的代码地址是在最后可能的时刻确定的:基于运行时对象的动态类型。之所以称为 “动态绑定”,因为与实际调用代码的绑定是动态完成的(即运行时)。动态绑定是虚函数的结果。

4、What is a pure virtual function?

A pure virtual function is a function that must be overridden in a derived class and need not be defined. A virtual function is declared to be “pure” using the curious =0 syntax. For example:

纯虚函数是一种必须在派生类中重写且不需要定义的函数。通过使用 =0 语法,虚函数被定义为“纯”虚函数。 例如:

class Base {
   
public:
	void f1();      // not virtual
    virtual void f2();  // virtual, not pure
    virtual void f3() = 0;  // pure virtual
};
    
Base b; // error: pure virtual f3 not overridden

Here, Base is an abstract class (because it has a pure virtual function), so no objects of class Base can be directly created: Base is (explicitly) meant to be a base class. For example:

这里,Base 是一个抽象类(因为它有一个纯虚函数),所以不能直接创建Base类的对象:Base(显式地)是一个基类。例如:

class Derived : public Base {
   
	// no f1: fine
	// no f2: fine, we inherit Base::f2
	void f3();
};

Derived d;  // ok: Derived::f3 overrides Base::f3

Abstract classes are immensely useful for defining interfaces. In fact, a class with no data and where all functions are pure virtual functions is often called an interface.

抽象类在定义接口时非常有用。事实上,没有数据且所有函数都是纯虚函数的类通常被称为接口。

You can provide a definition for a pure virtual function:

你可以提供一个纯虚函数的定义:

Base::f3() {
    /* ... */ }

This is very occasionally useful (to provide some simple common implementation detail for derived classes), but Base::f3() must still be overridden in some derived class. If you don’t override a pure virtual function in a derived class, that derived class becomes abstract:

这偶尔很有用(为派生类提供一些简单的通用实现细节),但是 Base::f3() 仍然必须在某些派生类中重写。如果你不在派生类中重写纯虚函数,那么派生类就会变成抽象类:

class D2 : public Base {
   
	// no f1: fine
	// no f2: fine, we inherit Base::f2
	// no f3: fine, but D2 is therefore still abstract
};

D2 d;   // error: pure virtual Base::f3 not overridden

5、What’s the difference between how virtual and non-virtual member functions are called?(调用虚成员函数和非虚成员函数有什么区别?)

Non-virtual member functions are resolved statically. That is, the member function is selected statically (at compile-time) based on the type of the pointer (or reference) to the object.

非虚成员函数是静态解析的。即是说,成员函数是根据指向对象的指针(或引用)的类型静态地(在编译时)被选择的。

In contrast, virtual member functions are resolved dynamically (at run-time). That is, the member function is selected dynamically (at run-time) based on the type of the object, not the type of the pointer/reference to that object. This is called “dynamic binding.” Most compilers use some variant of the following technique: if the object has one or more virtual functions, the compiler puts a hidden pointer in the object called a “virtual-pointer” or “v-pointer.” This v-pointer points to a global table called the “virtual-table” or “v-table.”

相反,虚成员函数是动态解析的(在运行时)。即是说,成员函数是基于对象类型被动态选择的(在运行时),而不是指向该对象的指针/引用。 这被称为“动态绑定”。大多数编译器使用以下技术的某种变体:如果对象有一个或多个虚函数,编译器会在对象中放入一个隐藏指针,称为“虚指针”或“v指针”。这个 “v” 指针指向一个称为“虚拟表”或“v表”的全局表。

The compiler creates a v-table for each class that has at least one virtual function. For example, if class Circle has virtual functions for draw() and move() and resize(), there would be exactly one v-table associated with class Circle, even if there were a gazillion Circle objects, and the v-pointer of each of those Circle objects would point to the Circle v-table. The v-table itself has pointers to each of the virtual functions in the class. For example, the Circle v-table would have three pointers: a pointer to Circle::draw(), a pointer to Circle::move(), and a pointer to Circle::resize().

对于每个至少有一个虚函数的类,编译器都会被它创建一个v-table。例如,如果类 Circledraw()move()resize() 这些虚函数,那么即使有无数个 Circle 对象,Circle 类也只会关联一个 v-table,而且每个 Circle 对象的 v-pointer 都会指向 Circle 的 v-table。v-table本身有指向类中每个虚函数的指针。例如,Circle 的 v-table 有三个指针:一个指向 Circle::draw() 的指针、一个指向 Circle::move() 的指针和一个指向 Circle::resize() 的指针。

During a dispatch of a virtual function, the run-time system follows the object’s v-pointer to the class’s v-table, then follows the appropriate slot in the v-table to the method code.

在虚函数的分发过程中,运行系统追踪对象的指向类的v-table的v-pointer,然后沿着v-table的适当槽找到方法代码。

The space-cost overhead of the above technique is nominal: an extra pointer per object (but only for objects that will need to do dynamic binding), plus an extra pointer per method (but only for virtual methods). The time-cost overhead is also fairly nominal: compared to a normal function call, a virtual function call requires two extra fetches (one to get the value of the v-pointer, a second to get the address of the method). None of this runtime activity happens with non-virtual functions, since the compiler resolves non-virtual functions exclusively at compile-time based on the type of the pointer.

上述技术的空间开销是具有象征性的:每个对象一个额外的指针(但只有需要进行动态绑定的对象),每个方法一个额外的指针(但只有虚方法)。这种时间开销也是具有象征性的:与普通的函数调用相比,虚函数调用需要两次额外的获取(一次是获取v指针的值,一次是获取方法的地址)。这种运行活动不会发生在非虚函数上,因为编译器在编译时根据指针的类型专门解析非虚函数。

Note: the above discussion is simplified considerably, since it doesn’t account for extra structural things like multiple inheritance, virtual inheritance, RTTI, etc., nor does it account for space/speed issues such as page faults, calling a function via a pointer-to-function, etc. If you want to know about those other things, please ask comp.lang.c++; PLEASE DO NOT SEND E-MAIL TO ME!

注意:上面的讨论大大简化了,因为它没有考虑额外的结构问题,如多重继承、虚拟继承、RTTI等,也没有考虑空间/速度问题,如缺页异常、通过指向函数的指针调用函数等。如果你想知道其他的事情,请问comp.lang.c++;请不要给我发电子邮件!

6、What happens in the hardware when I call a virtual function? How many layers of indirection are there? How much overhead is there?(当我调用虚函数时,硬件会发生什么?一共有多少层间接层?有多少开销?)

This is a drill-down of the previous FAQ. The answer is entirely compiler-dependent, so your mileage may vary, but most C++ compilers use a scheme similar to the one presented here.

这是对之前常见问题的深入研究。答案完全取决于编译器,因此具体情况可能有所不同,但大多数C++编译器都使用与这里介绍的方案类似的方案。

Let’s work an example. Suppose class Base has 5 virtual functions: virt0() through virt4().

让我们来看一个例子。假设类基有5个虚函数:virt0()virt4()

// Your original C++ source code

class Base {
   
public:
	virtual arbitrary_return_type virt0( /*...arbitrary params...*/ );
  	virtual arbitrary_return_type virt1( /*...arbitrary params...*/ );
  	virtual arbitrary_return_type virt2( /*...arbitrary params...*/ );
  	virtual arbitrary_return_type virt3( /*...arbitrary params...*/ );
  	virtual arbitrary_return_type virt4( /*...arbitrary params...*/ );
  	// ...
};

Step #1: the compiler builds a static table containing 5 function-pointers, burying that table into static memory somewhere. Many (not all) compilers define this table while compiling the .cpp that defines Base’s first non-inline virtual function. We call that table the v-table; let’s pretend its technical name is Base::__vtable. If a function pointer fits into one machine word on the target hardware platform, Base::__vtable will end up consuming 5 hidden words of memory. Not 5 per instance, not 5 per function; just 5. It might look something like the following pseudo-code:

步骤 #1:编译器生成一个包含 5 个函数指针的静态表,将该表嵌入到静态内存的某个地方。 许多(不是全部)编译器在编译定义 Base 的第一个非内联虚函数的 .cpp 时定义这个表。我们称这个表为 v-table;假设它的技术名称是 Base::__vtable。如果一个函数指针在目标硬件平台上适合一个机器字,那么 Base::__vtable 最终将消耗 5 个隐藏字的内存。不是每个实例和每个函数都是5;只是5。它可能看起来像下面的伪代码:

// Pseudo-code (not C++, not C) for a static table defined within file Base.cpp

// Pretend FunctionPtr is a generic pointer to a generic member function
// (Remember: this is pseudo-code, not C++ code)
FunctionPtr Base::__vtable[5] = {
   
	&Base::virt0, &Base::virt1, &Base::virt2, &Base::virt3, &Base::virt4
};

Step #2: the compiler adds a hidden pointer (typically also a machine-word) to each object of class Base. This is called the v-pointer. Think of this hidden pointer as a hidden data member, as if the compiler rewrites your class to something like this:

步骤 #2:编译器给Base类的每个对象都添加一个隐藏指针(通常也是一个机器字)。这被称为 v-pointer。把这个隐藏的指针想象成一个隐藏的数据成员,就好像编译器把你的类重写成这样:

// Your original C++ source code

class Base {
   
public:
	// ...
  	FunctionPtr* __vptr;  // Supplied by the compiler, hidden from the programmer
  	// ...
};

Step #3: the compiler initializes this->__vptr within each constructor. The idea is to cause each object’s v-pointer to point at its class’s v-table, as if it adds the following instruction in each constructor’s init-list:
步骤 #3:编译器在每个构造函数中初始化 this->__vptr。这个做法是让每个对象的 v-pointer 指向其类的 v-table,就像在每个构造函数的 init-list 中添加以下指令一样:

Base::Base( /*...arbitrary params...*/ )
	: __vptr(&Base::__vtable[0])  // Supplied by the compiler, hidden from the programmer
  	// ...
{
   
  	// ...
}

Now let’s work out a derived class. Suppose your C++ code defines class Der that inherits from class Base. The compiler repeats steps #1 and #3 (but not #2). In step #1, the compiler creates a hidden v-table, keeping the same function-pointers as in Base::__vtable but replacing those slots that correspond to overrides. For instance, if Der overrides virt0() through virt2() and inherits the others as-is, Der’s v-table might look something like this (pretend Der doesn’t add any new virtuals):

现在让我们来创建一个派生类。假设你的C++代码定义了一个继承自 Base 类的 Der类。编译器重复步骤 #1和 #3(而不是 #2)。在步骤 #1中,编译器创建了一个隐藏的v-table,保留了与Base::__vtable 相同的函数指针,但替换了那些对应于重写函数的槽。例如,如果 Der 重写了virt0()virt2(),并原样继承了其他的函数,那么 Derv 表可能看起来像这样(假设 Der 没有添加任何新的虚函数):

// Pseudo-code (not C++, not C) for a static table defined within file Der.cpp
// Pretend FunctionPtr is a generic pointer to a generic member function
// (Remember: this is pseudo-code, not C++ code)
FunctionPtr Der::__vtable[5] = {
   
	&Der::virt0, &Der::virt1, &Der::virt2, &Base::virt3, &Base::virt4
                                          	↑↑↑↑          ↑↑↑↑ // Inherited as-is
};

In step #3, the compiler adds a similar pointer-assignment at the beginning of each of Der’s constructors. The idea is to change each Der object’s v-pointer so it points at its class’s v-table. (This is not a second v-pointer; it’s the same v-pointer that was defined in the base class, Base; remember, the compiler does not repeat step #2 in class Der.)

在步骤 #3中,编译器在每个 Der 构造函数的开头添加了类似的指针赋值。这个想法是改变每个 Der对象的 v 指针,使其指向其类的 v 表。(这不是第二个 v 指针;它和在基类 Base中定义的 v 指针是同一个;记住,编译器不会在 Der 类中重复步骤 #2。)

Finally, let’s see how the compiler implements a call to a virtual function. Your code might look like this:

最后,让我们看看编译器如何实现对虚函数的调用。你的代码可能看起来像这样:

/ Your original C++ code

void mycode(Base* p)
{
   
  p->virt3();
}

The compiler has no idea whether this is going to call Base::virt3() or Der::virt3() or perhaps the virt3() method of another derived class that doesn’t even exist yet. It only knows for sure that you are calling virt3() which happens to be the function in slot #3 of the v-table. It rewrites that call into something like this:

编译器不知道是要调用 Base::virt3() 还是 Der::virt3(),或者是另一个派生类的 virt3() 方法,而这个派生类甚至还不存在。它只知道你正在调用 virt3(),而 virt3() 恰好是 v 表的 3 号槽中的函数。它把这个调用重写成这样:

/ Pseudo-code that the compiler generates from your C++

void mycode(Base* p)
{
   
	p->__vptr[3](p);
}

On typical hardware, the machine-code is two ‘load’s plus a call:

在典型的硬件上,机器代码是两个 ‘load’ 加上一个调用:

  1. The first load gets the v-pointer, storing it into a register, say r1. (第一次 load 得到了 v 指针,并将其存储到寄存器 r1 中。)
  2. The second load gets the word at r1 + 3*4 (pretend function-pointers are 4-bytes long, so r1 + 12 is the pointer to the right class’s virt3() function). Pretend it puts that word into register r2 (or r1 for that matter). (第二次 load 获取了位于 r1 + 3*4 的字(假设函数指针长度为 4 字节,因此 r1 + 12 是指向正确类的 virt3() 函数的指针)。假设它将该单词放入寄存器 r2(或 r1)。)
  3. The third instruction calls the code at location r2. (第三条指令调用 r2 位置的代码。)

Conclusions: 结论

  • Objects of classes with virtual functions have only a small space-overhead compared to those that don’t have virtual functions. (与那些没有虚函数的类对象相比,具有虚函数类的对象只有很小的空间开销。)
  • Calling a virtual function is fast — almost as fast as calling a non-virtual function.(调用虚函数非常快——几乎与调用非虚函数一样快。)
  • You don’t get any additional per-call overhead no matter how deep the inheritance gets. You could have 10 levels of inheritance, but there is no “chaining” — it’s always the same — fetch, fetch, call.(无论继承有多深,每次调用都不会有任何额外的开销。你可以有10层继承,但没有“链”——它总是一样的——获取,获取,调用。)

Caveat: I’ve intentionally ignored multiple inheritance, virtual inheritance and RTTI. Depending on the compiler, these can make things a little more complicated. If you want to know about these things, DO NOT EMAIL ME, but instead ask comp.lang.c++.

警告:我故意忽略了多重继承、虚拟继承和RTTI。根据编译器的不同,可能会使事情变得稍微复杂一些。如果你想知道这些事情,不要给我发电子邮件,而是问comp.lang.c++。

Caveat: Everything in this FAQ is compiler-dependent. Your mileage may vary.

警告:本FAQ中的所有内容都依赖于编译器。你的可能有所不同。

6、How can a member function in my derived class call the same function from its base class? (如何在我的派生类中的成员函数中调用它的基类的相同的函数?)

Use Base::f();

使用 Base::f();

Let’s start with a simple case. When you call a non-virtual function, the compiler obviously doesn’t use the virtual-function mechanism. Instead it calls the function by name, using the fully qualified name of the member function. For instance, the following C++ code…

调用非虚函数时,编译器显然不会使用虚函数机制。相反,它使用成员函数的完全限定名来调用函数。例如,下面的C++代码:

void mycode(Fred* p)
{
   
	p->goBowling();  // Pretend Fred::goBowling() is non-virtual
}

…might get compiled into something like this C-like code (the p parameter becomes the this object within the member function):

…可能会编译成类似于C的代码( 形参p变成成员函数中的 this 对象):

void mycode(Fred* p)
{
   
	__Fred__goBowling(p);  // Pseudo-code only; not real
}

The actual name-mangling scheme is more involved than the simple one implied above, but you get the idea. The point is that there is nothing strange about this particular case — it resolves to a normal function more-or-less like printf().

实际的名称更改方案比上面所暗示的简单方案要复杂得多,但您可以理解其中的意思。关键是这个特殊的情况没有什么奇怪的——它或多或少地解析为一个普通的函数,类似于 printf()

Now for the case being addressed in the question above: When you call a virtual function using its fully-qualified name (the class-name followed by “::”), the compiler does not use the virtual call mechanism, but instead uses the same mechanism as if you called a non-virtual function. Said another way, it calls the function by name rather than by slot-number. So if you want code within derived class Der to call Base::f(), that is, the version of f() defined in its base class Base, you should write:

现在,对于上面问题中要解决的情况:当使用虚函数的全限定名称(类名后跟“::”)调用虚函数时,编译器不会使用虚函数调用机制,而是使用与调用非虚函数相同的机制。换句话说,它通过函数名而不是 slot-number 调用函数。因此,如果你希望派生类 Der 中的代码调用 Base::f(),即在基类 Base 中定义的 f() 版本,你应该这样写:

void Der::f()
{
   
	Base::f();  // Or, if you prefer, this->Base::f();
}

The complier will turn that into something vaguely like the following (again using an overly simplistic name-mangling scheme):

编译器会把它变成类似下面这样的东西(再次使用过于简单的名称转换方案):

void __Der__f(Der* this)  // Pseudo-code only; not real
{
   
	__Base__f(this);        // Pseudo-code only; not real
}

7、I have a heterogeneous list of objects, and my code needs to do class-specific things to the objects. Seems like this ought to use dynamic binding but can’t figure it out. What should I do? (我有一个异构的对象列表,代码中对象要做特定类的事。似乎这应该使用动态绑定,但弄不清楚。我该怎么办?)

It’s surprisingly easy.

非常简单。

Suppose there is a base class Vehicle with derived classes Car and Truck. The code traverses a list of Vehicle objects and does different things depending on the type of Vehicle. For example it might weigh the Truck objects (to make sure they’re not carrying too heavy of a load) but it might do something different with a Car object — check the registration, for example.

假设有一个基类 Vehicle,它有两个派生类 CarTruck。这段代码遍历了一个 Vehicle 对象的列表,并根据 Vehicle 类型执行不同的操作。例如,它可能会对 Truck 对象称重(以确保没有超重),但它可能会对 Car 对象做些不同的事——如检查注册。

The initial solution for this, at least with most people, is to use an if statement. E.g., “if the object is a Truck, do this, else if it is a Car, do that, else do a third thing”:

对于大多数人来说,最初的解决方案是使用 if 语句。例如,“如果对象是卡车,就做这个,否则如果对象是汽车,就做那个,否则做第三件事”:

typedef std::vector<Vehicle*>  VehicleList;
void myCode(VehicleList& v)
{
   
	for (VehicleList::iterator p = v.begin(); p != v.end(); ++p) {
   
    	Vehicle& v = **p;  // just for shorthand
    	
    	// generic code that works for any vehicle...
    	// ...
    	
    	// perform the "foo-bar" operation.
    	// note: the details of the "foo-bar" operation depend
    	// on whether we're working with a car or a truck.
    	if (v is a Car) {
   
      		// car-specific code that does "foo-bar" on car v
      		// ...
    	} else if (v is a Truck) {
   
      		// truck-specific code that does "foo-bar" on truck v
      		// ...
    	} else {
   
	      	// semi-generic code that does "foo-bar" on something else
	      	// ...
    	}
    	
    	// generic code that works for any vehicle...
    	// ...
  	}
}

The problem with this is what I call “else-if-heimer’s disease” (say it fast and you’ll understand). The above code gives you else-if-heimer’s disease because eventually you’ll forget to add an else if when you add a new derived class, and you’ll probably have a bug that won’t be detected until run-time, or worse, when the product is in the field.

这其中的问题就是我所说的 “else-if海默症”。上面的代码会让你患上else-if-heimer病,因为最终当你添加一个新的派生类时,你会忘记添加一个 else if,并且你可能会有一个bug,直到运行时,或者更糟的是,当产品在现场时才被检测到。

The solution is to use dynamic binding rather than dynamic typing. Instead of having (what I call) the live-code dead-data metaphor (where the code is alive and the car/truck objects are relatively dead), we move the code into the data. This is a slight variation of Bertrand Meyer’s Law of Inversion.

解决方案是使用动态绑定而不是动态类型。我们没有使用(我称之为)活代码死数据的比喻(代码是活的,而car/truck对象相对来说是死的),而是将代码移动到数据中。这是贝特朗·迈耶反演定律的一个微小变化。

The idea is simple: use the description of the code within the {...} blocks of each if (in this case it is “the foo-bar operation”; obviously your name will be different). Just pick up this descriptive name and use it as the name of a new virtual member function in the base class (in this case we’ll add a fooBar() member function to class Vehicle).

这个想法很简单:使用每个 if 的 {…} 块 (在这种情况下,它是" foo-bar操作";显然你的名字会不同)。只需取这个描述性名称,并将其用作基类中新的虚成员函数的名称(在本例中,我们将向 Vehicle 类添加一个 fooBar() 成员函数)。

class Vehicle {
   
public:
	// performs the "foo-bar" operation
  	virtual void fooBar() = 0;
};

Then you remove the whole if...else if… block and replace it with a simple call to this virtual function:

然后你移除整个 if…Else if…块并将其替换为对这个虚函数的简单调用:

typedef std::vector<Vehicle*>  VehicleList;

void myCode(VehicleList& v)
{
   
	for (VehicleList::iterator p = v.begin(); p != v.end(); ++p) {
   
    	Vehicle& v = **p;  // just for shorthand
    	
    	// generic code that works for any vehicle...
    	// ...
    	
    	// perform the "foo-bar" operation.
    	v.fooBar();
    	
    	// generic code that works for any vehicle...
    	// ...
  	}
}

Finally you move the code that used to be in the {...} block of each if into the fooBar() member function of the appropriate derived class:

最后,移动每个 if 语句中原来在 {…} 块的代码,将其将放入相应派生类的 fooBar() 成员函数中:

class Car : public Vehicle {
   
public:
	virtual void fooBar();
};

void Car::fooBar()
{
   
	// car-specific code that does "foo-bar" on 'this'
  	// this is the code that was in {...} of if (v is a Car)
}

class Truck : public Vehicle {
   
public:
	virtual void fooBar();
};

void Truck::fooBar()
{
   
	// truck-specific code that does "foo-bar" on 'this'
	// this is the code that was in {...} of if (v is a Truck)
}

If you actually have an else block in the original myCode() function (see above for the “semi-generic code that does the ‘foo-bar’ operation on something other than a Car or Truck”), change Vehicle’s fooBar() from pure virtual to plain virtual and move the code into that member function:

如果在原来的 myCode() 函数中确实有一个 else 块( 见上文“在 Car 或 Truck 以外的其他东西上执行foo-bar操作的半通用代码”),将 VehiclefooBar() 从纯虚改为普通虚,并将代码移动到该成员函数中:

class Vehicle {
   
public:
	// performs the "foo-bar" operation
  	virtual void fooBar();
};

void Vehicle::fooBar()
{
   
	// semi-generic code that does "foo-bar" on something else
	// this is the code that was in {...} of the else
	// you can think of this as "default" code...
}

That’s it!

就是这样!

The point, of course, is that we try to avoid decision logic with decisions based on the kind-of derived class you’re dealing with. In other words, you’re trying to avoid if the object is a car do xyz, else if it's a truck do pqr, etc., because that leads to else-if-heimer’s disease.

当然,关键是我们要尽量避免基于派生类的决策逻辑。换句话说,避免if-else的使用,因为这会导致 else if-heimer病。

8、When should my destructor be virtual?(什么时候析构函数应该是虚的?)

When someone will delete a derived-class object via a base-class pointer.

当有人通过基类指针删除派生类对象时。

In particular, here’s when you need to make your destructor virtual:

特别地,这里是你需要将你的虚构函数设为虚函数的情况:

  • if someone will derive from your class, (如果有人想从你的类派生)
  • and if someone will say new Derived, where Derived is derived from your class, (如果 Derived 类是从你的类派生的,且有人使用了 new Derived
  • and if someone will say delete p, where the actual object’s type is Derived but the pointer p’s type is your class. (如果有人使用了 delete p,实际对象类型是 Derived,但指针 p 的类型是你的类)

Confused? Here’s a simplified rule of thumb that usually protects you and usually doesn’t cost you anything: make your destructor virtual if your class has any virtual functions. Rationale:

困惑吗?这里有一个简化的经验法则,可以保护你,而且通常不会造成任何损失:如果类有虚函数,就把析构函数设为虚函数。 理由是:

  • that usually protects you because most base classes have at least one virtual function. (可以保护你,是因为大多数基类至少有一个虚函数)
  • that usually doesn’t cost you anything because there is no added per-object space-cost for the second or subsequent virtual in your class. In other words, you’ve already paid all the per-object space-cost that you’ll ever pay once you add the first virtual function, so the virtual destructor doesn’t add any additional per-object space cost. (Everything in this bullet is theoretically compiler-specific, but in practice it will be valid on almost all compilers.)
    通常不会造成任何损失,是因为类中的第二个或后续虚函数不会增加每个对象的空间开销。换句话说,你已经在添加第一个虚函数的时候一次性完成了的所有的每个对象的空间开销,因此虚析构函数不会增加任何额外的每个对象的空间开销。(本文中的所有内容理论上都是特定于编译器的,但实际上它几乎适用于所有编译器。)

Note: in a derived class, if your base class has a virtual destructor, your own destructor is automatically virtual. You might need an explicitly defined destructor for other reasons, but there’s no need to redeclare a destructor simply to make sure it is virtual. No matter whether you declare it with the virtual keyword, declare it without the virtual keyword, or don’t declare it at all, it’s still virtual.

注意:如果基类有虚析构函数,则派生类中的析构函数自动为虚函数。 你可能因为其他原因需要显式定义析构函数,但没有必要仅仅为了确保它是虚的而重新声明析构函数。无论你是使用 virtual 关键字声明它,还是不使用virtual关键字声明它,或者根本不声明它,它仍然是虚的。

By the way, if you’re interested, here are the mechanical details of why you need a virtual destructor when someone says delete using a Base pointer that’s pointing at a Derived object. When you say delete p, and the class of p has a virtual destructor, the destructor that gets invoked is the one associated with the type of the object *p, not necessarily the one associated with the type of the pointer. This is A Good Thing. In fact, violating that rule makes your program undefined. The technical term for that is, “Yuck.”

顺便说一下,如果你感兴趣,这里有一些机制细节关于当有人使用指向派生对象的基指针进行delete 时,为什么需要虚析构函数。当你使用 delete p 时,p 的类有一个虚析构函数,被调用的析构函数是与对象 *p 的类型相关联的,而不一定是与指针的类型相关联的。这是一件好事。事实上,违反这个规则会让你的程序没有定义。用专业术语来说就是"恶心"

9、Why are destructors not virtual by default?(为什么析构函数在默认情况下不是虚函数?)

Because many classes are not designed to be used as base classes. Virtual functions make sense only in classes meant to act as interfaces to objects of derived classes (typically allocated on a heap and accessed through pointers or references).

因为很多类都不是设计来作为基类的。虚函数仅在作为派生类对象接口的类中有意义(通常在堆上分配,并通过指针或引用访问)。

So when should I declare a destructor virtual? Whenever the class has at least one virtual function. Having virtual functions indicate that a class is meant to act as an interface to derived classes, and when it is, an object of a derived class may be destroyed through a pointer to the base. For example:

那么什么时候应该将析构函数声明为虚函数呢?当类至少有一个虚函数时。拥有虚函数表明一个类旨在充当派生类的接口,当它是这样时,派生类的对象可以通过指向基类的指针来销毁。例如:

class Base {
   
	// ...
	virtual ~Base();
};

class Derived : public Base {
   
	// ...
	~Derived();
};

void f()
{
   
	Base* p = new Derived;
    delete p;   // virtual destructor used to ensure that ~Derived is called
}

Had Base’s destructor not been virtual, Derived’s destructor would not have been called – with likely bad effects, such as resources owned by Derived not being freed.

如果 Base 类的析构函数不是虚函数,Derived 类的析构函数就不会被调用——这可能会带来不好的影响,例如派生类拥有的资源不会被释放。

10、What is a “virtual constructor”?(什么是“虚构造函数”?)

An idiom that allows you to do something that C++ doesn’t directly support.

一种允许您执行C++不直接支持的操作的习惯用法。

You can get the effect of a virtual constructor by a virtual clone() member function (for copy constructing), or a virtual create() member function (for the default constructor).

你可以通过 virtual clone() 成员函数(用于复制构造)或 virtual create() 成员函数(用于默认构造)来达到虚析构函数的效果。

class Shape {
   
public:
	virtual ~Shape() {
    }                 // A virtual destructor
  	virtual void draw() = 0;             // A pure virtual function
  	virtual void move() = 0;
  	// ...
  	virtual Shape* clone()  const = 0;   // Uses the copy constructor
  	virtual Shape* create() const = 0;   // Uses the default constructor
};

class Circle : public Shape {
   
public:
	Circle* clone()  const;   // Covariant Return Types; see below
  	Circle* create() const;   // Covariant Return Types; see below
  	// ...
};

Circle* Circle::clone()  const {
    return new Circle(*this); }
Circle* Circle::create() const {
    return new Circle();      }

In the clone() member function, the new Circle(*this) code calls Circle’s copy constructor to copy the state of this into the newly created Circle object. (Note: unless Circle is known to be final (AKA a leaf), you can reduce the chance of slicing by making its copy constructor protected.) In the create() member function, the new Circle() code calls Circle’s default constructor.

clone() 成员函数中,new Circle(*this) 代码调用 Circle 的复制构造函数将 this 的状态复制到新创建的 Circle 对象中。(注意:除非已知 Circlefinal(又名叶节点),否则可以通过将其复制构造函数设置为 protected 来减少切片的机会。)在成员函数 create() 中,new Circle() 代码调用了 Circle 的默认构造函数。

Users use these as if they were “virtual constructors”:

用户就像使用“虚构造函数”一样使用它们:

void userCode(Shape& s)
{
   
	Shape* s2 = s.clone();
  	Shape* s3 = s.create();
  	// ...
  	delete s2;    // You need a virtual destructor here
  	delete s3;
}

This function will work correctly regardless of whether the Shape is a Circle, Square, or some other kind-of Shape that doesn’t even exist yet.

无论形状是圆、方还是其他不存在的形状,这个函数都能正确地工作。

Note: The return type of Circle’s clone() member function is intentionally different from the return type of Shape’s clone() member function. This is called Covariant Return Types, a feature that was not originally part of the language. If your compiler complains at the declaration of Circle* clone() const within class Circle (e.g., saying “The return type is different” or “The member function’s type differs from the base class virtual function by return type alone”), you have an old compiler and you’ll have to change the return type to Shape*.

注意:Circleclone() 成员函数的返回类型故意与 Shapeclone() 成员函数的返回类型不同。这被称为协变返回类型(Covariant Return Types),这一特性原本并不是语言的一部分。如果编译器提示在Circle 类中声明了 Circle* clone() const(例如,“返回类型不同” 或 “成员函数的类型与基类虚函数仅因返回类型不同而不同”),那么你的编译器版本比较旧,必须将返回类型更改为 Shape*

11、Why don’t we have virtual constructors?(为什么没有虚构造函数呢?)

A virtual call is a mechanism to get work done given partial information. In particular, virtual allows us to call a function knowing only an interfaces and not the exact type of the object. To create an object you need complete information. In particular, you need to know the exact type of what you want to create. Consequently, a “call to a constructor” cannot be virtual.

虚拟调用是一种在给定部分信息的情况下完成工作的机制。特别地,虚函数允许我们调用一个只知道接口而不知道对象确切类型的函数。要创建对象,您需要完整的信息。特别是,你需要知道你想要创建的对象的确切类型。因此,“调用构造函数”不能是虚的。

Techniques for using an indirection when you ask to create an object are often referred to as “Virtual constructors”. For example, see TC++PL3 15.6.2.

在请求创建对象时使用间接方法的技术通常被称为“虚构造函数”。

For example, here is a technique for generating an object of an appropriate type using an abstract class:

例如,下面是一种使用抽象类生成适当类型对象的技术:

struct F {
     // interface to object creation functions
	virtual A* make_an_A() const = 0;
	virtual B* make_a_B() const = 0;
};

void user(const F& fac)
{
   
	A* p = fac.make_an_A(); // make an A of the appropriate type
	B* q = fac.make_a_B();  // make a B of the appropriate type
	// ...
}
    
struct FX : F {
   
	A* make_an_A() const {
    return new AX(); } // AX is derived from A
	B* make_a_B() const {
    return new BX();  } // BX is derived from B
};

struct FY : F {
   
	A* make_an_A() const {
    return new AY(); } // AY is derived from A
	B* make_a_B() const {
    return new BY();  } // BY is derived from B
};

int main()
{
   
	FX x;
	FY y;
	user(x);    // this user makes AXs and BXs
	user(y);    // this user makes AYs and BYs
	
	user(FX()); // this user makes AXs and BXs
	user(FY()); // this user makes AYs and BYs
	// ...
}

This is a variant of what is often called “the factory pattern”. The point is that user() is completely isolated from knowledge of classes such as AX and AY.

这是通常被称为“工厂模式”的变体。关键是 user()AXAY 等类的内容是完全隔离的。

相关推荐

  1. Redis<span style='color:red;'>篇</span>

    Redis

    2023-12-14 06:18:02      52 阅读
  2. 模 块

    2023-12-14 06:18:02       37 阅读
  3. 函 数

    2023-12-14 06:18:02       38 阅读
  4. MySql<span style='color:red;'>篇</span>

    MySql

    2023-12-14 06:18:02      24 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2023-12-14 06:18:02       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2023-12-14 06:18:02       100 阅读
  3. 在Django里面运行非项目文件

    2023-12-14 06:18:02       82 阅读
  4. Python语言-面向对象

    2023-12-14 06:18:02       91 阅读

热门阅读

  1. 数据分析用哪个系统

    2023-12-14 06:18:02       54 阅读
  2. lua脚本的基本语法,以及Redis中简单使用

    2023-12-14 06:18:02       66 阅读
  3. ChatGPT 技术架构设计与实践

    2023-12-14 06:18:02       63 阅读
  4. mac切换node版本

    2023-12-14 06:18:02       59 阅读
  5. 力扣120. 三角形最小路径和

    2023-12-14 06:18:02       56 阅读
  6. 工作中 docker 的使用积累

    2023-12-14 06:18:02       60 阅读
  7. uniapp 页面通信

    2023-12-14 06:18:02       68 阅读
  8. 华为实训课笔记

    2023-12-14 06:18:02       52 阅读
  9. 回调地狱Axios

    2023-12-14 06:18:02       52 阅读
  10. 编写一个简易的 Axios 函数

    2023-12-14 06:18:02       57 阅读