当前位置: 首页 > 文档资料 > C++ FAQ Lite >

[20] 继承 — 虚函数

优质
小牛编辑
130浏览
2023-12-01

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类有虚函数ddraw()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++;而不要给我发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 是一个CircleSquare,或是其他种类的 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*。