💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
# [20] 继承 — 虚函数 ## FAQs in section [20]: * [20.1] 什么是“虚成员函数”? * [20.2] C++ 怎样同时实现动态绑定和静态类型? * [20.3] 虚成员函数和非虚成员函数调用方式有什么不同? * [20.4] 析构函数何时该时虚拟的? * [20.5] 什么是“虚构造函数(`virtual` constructor)”? ## 20.1 什么是“虚成员函数”? 从面向对象观点来看,它是 C++ 最重要的特征:[6.8], [6.9]. 虚函数允许派生类取代基类所提供的实现。编译器确保当对象为派生类时,取代者(译注:即派生类的实现)总是被调用,即使对象是使用基类指针访问而不是派生类的指针。这样就允许基类的算法被派生类取代,即使用户不知道派生类的细节。 派生类可以完全地取代基类成员函数(覆盖(override)),也可以部分地取代基类成员函数(增大(augment))。如果愿意的话,后者由派生类成员函数调用基类成员函数来完成。 ## 20.2 C++ 怎样同时实现动态绑定和静态类型? 当你有一个对象的指针,而对象实际是该指针类型的派生类(例如:一个 `Vehicle*`指针实际指向一个Car 对象)。由此有两种类型:指针的(静态)类型(在此是`Verhicle`),和指向的对象的(动态)类型(在此是Car)。 _静态类型_意味着成员函数调用的合法性被尽可能早地检查:编译器在编译时。编译器用指针的静态类型决定成员函数调用是否合法。如果指针类型能够处理成员函数,那么指针所指对象当然能很好的处理它。例如,如果 `Vehicle` 有某个成员函数,则由于`Car`是一种`Vehicle`,那么`Car` 当然也有该成员函数。 _动态绑定_意味着成员函数调用的代码地址在最终时刻才被决定:基于运行时的对象动态类型。因为绑定到实际被调用的代码这个过程是动态完成的(在运行时),所以被称为“动态绑定”。动态绑定是虚函数导致的结果之一。 ## 20.3 虚成员函数和非虚成员函数调用方式有什么不同? 非虚成员函数是静态确定的。也就是说,该成员函数(在编译时)被静态地选择,该选择基于指象对象的指针(或引用)的类型。 相比而言,虚成员函数是动态确定的(在运行时)。也就是说,成员函数(在运行时)被动态地选择,该选择基于对象的类型,而不是指向该对象的指针/引用的类型。这被称作“动态绑定”。大多数的编译器使用以下的一些的技术:如果对象有一个或多个虚函数,编译器将一个 隐藏的指针放入对象,该指针称为“virtual-pointor”或“v-pointer”。这个v-pointer指向一个全局表,该表称为“虚函数表(virtural-table)”或“v-table”。 编译器为每个含有至少一个虚函数的类创建一个v-table。例如,如果`Cirle`类有虚函数d`draw()`、`move()` 和 `resize()`,那么将有且只有一个和Cricle类相关的v-table,即使有一大堆Circle对象。并且每个 `Circle`对象的 v-poiner将指向 `Circle`的这个 v-table。该 v-table自己有指向类的各个虚函数的指针。例如,`Circle` 的v-table 会有三个指针:一个指向`Circle::draw()`,一个指向 `Circle::move()`,还有一个指向`Circle::resize()`。 在分发一个虚函数时,运行时系统跟随对象的 v-pointer找到类的 v-table,然后跟随v-table中适当的项找到方法的代码。 以上技术的空间开销是存在的:每个对象一个额外的指针(仅仅对于需要动态绑定的对象),加上每个方法一个额外的指针(仅仅对于虚方法)。时间开销也是有的:和普通函数调用比较,虚函数调用需要两个额外的步骤(得到v-pointer的值,得到方法的地址)。由于编译器在编译时就通过指针类型解决了非虚函数的调用,所以这些开销不会发生在非虚函数上。 注意:由于没有涉及诸如多继承,虚继承,RTTI等内容,也没有涉及诸如page fault,通过指向函数的指针调用函数等空间/时间论的内容,所以以上讨论是相当简单的。如果你想知道其他的内容,请询问 _[`comp.lang.c++`](news:comp.lang.c++)_;而不要给我发E-MAIL! ## 20.4 析构函数何时该时虚拟的? 当你可能通过基类指针删除派生类对象时。 虚函数绑定到对象的类的代码,而不是指针/引用的类。如果基类有虚析构函数,`delete basePtr`时(译注:即基类指针),`*basePtr` 的对象类型的析构函数被调用,而不是该指针的类型的析构函数。这通常是一件好事情。 _TECHNO-GEEK WARNING; PUT YOUR PROPELLER HAT ON._ 从技术上来说,如果你打算允许其他人通过基类指针调用对象的析构函数(通过`delete`这样做是正常的),并且被析构的对象是有重要的析构函数的派生类的对象,就需要让基类的析构函数成为虚拟的。如果一个类有显式的析构函数,或者有成员对象,该成员对象或基类有重要的析构函数,那么这个类就有重要的析构函数。(注意这是一个递归的定义(例如,某个具有重要析构函数的类,它有一个成员对象(它有基类(该基类有成员对象(它有基类(该基类有显式的析构函数)))))) _END TECHNO-GEEK WARNING; REMOVE YOUR PROPELLER HAT_ 如果你对以上的规则理解有困难,试试这个简单的:类应该有虚析构函数,除非这个类没有虚函数。原理:如果有虚函数,说明你想通过基类指针来使用派生对象,并且你所可能做的事情之中,可能包含了调用析构函数(通常通过`delete`隐含完成)。一旦你在类中加上了一个虚函数,你就已经需要为每一个对象支付空间代价(每个对象一个指针;注意这是理论上的编译器特性;实际上每个编译器都是这样做的),所以这时使析构函数成为虚拟的通常不会额外付出什么。 ## 20.5 什么是“虚构造函数(`virtual` constructor)”? 一种允许你做一些 C++ 不直接支持的事情的用法。 你可能通过虚函数 `virtual` `clone()`(对于拷贝构造函数)或虚函数 `virtual` `create()`(对于默认构造函数),得到虚构造函数产生的效果。 ```  class Shape {  public:    virtual ~Shape() { }                 // 虚析构函数    virtual void draw() = 0;             // 纯虚函数    virtual void move() = 0;    // ...    virtual Shape* clone()  const = 0;   // 使用拷贝构造函数_    virtual Shape* create() const = 0;   // 使用默认构造函数  };  class Circle : public Shape {  public:    Circle* clone()  const { return new Circle(*this); }    Circle* create() const { return new Circle();      }    // ...  }; ``` 在 `clone()` 成员函数中,代码 `new Circle(*this)` 调用 `Circle` 的拷贝构造函数来复制`this`的状态到新创建的`Circle`对象。在 `create()`成员函数中,代码 `new Circle()` 调用`Circle`的默认构造函数。 用户将它们看作“虚构造函数”来使用它们: ```  void userCode(Shape& s)  {    Shape* s2 = s.clone();    Shape* s3 = s.create();    // ...    delete s2;    // 在此处,你可能需要虚析构函数    delete s3;  } ``` 这个函数将正确工作,而不管 `Shape` 是一个`Circle`,`Square`,或是其他种类的 `Shape`,甚至它们还并不存在。 注意:成员函数`Circle`'s `clone()`的返回值类型故意与成员函数`Shape`'s `clone()`的不同。这种特征被称为“协变的返回类型”,该特征最初并不是语言的一部分。如果你的编译器不允许在`Circle`类中这样声明`Circle* clone() const`(如,提示“The return type is different”或“The member function's type differs from the base class virtual function by return type alone”),说明你的编译器陈旧了,那么你必须改变返回类型为`Shape*。`