多态性——vptr和vtable

原文地址:http://www.cppblog.com/fwxjj/archive/2007/01/25/17996.html

        多态性 (polymorphism) 是面向对象编程的基本特征之一。而在C++ 中,多态性通过虚函数 (virtual function) 来实现。我们来看一段简单的代码:
  #include <iostream>
  using namespace std;
  class Base
  {
  int a;
  public:
  virtual void fun1() {cout<<"Base::fun1()"<<endl;}
  virtual void fun2() {cout<<"Base::fun2()"<<endl;}
  virtual void fun3() {cout<<"Base::fun3()"<<endl;}
  };
  class A:public Base
  {
  int a;
  public:
  void fun1() {cout<<"A::fun1()"<<endl;}
  void fun2() {cout<<"A::fun2()"<<endl;}
  };
  void foo (Base& obj)
  {
  obj.fun1();
  obj.fun2();
  obj.fun3();
  }
  int main()
  {
  Base b;
  A a;
  foo(b);
  foo(a);
  }
  运行结果为:
  Base::fun1()
  Base::fun2()
  Base::fun3()
  A::fun1()
  A::fun2()
  Base::fun3()
  仅通过基类的接口,程序调用了正确的函数,它就好像知道我们输入的对象的类型一样!
  那么,编译器是如何知道正确代码的位置的呢?

       其实,编译器在编译时并不知道要调用的函数体的正确位置,但它插入了一段能找到正确的函数体的代码。这称之为 晚捆绑(late binding)  或 运行时捆绑(runtime binding)  技术。
  通过virtual 关键字创建虚函数能引发晚捆绑,编译器在幕后完成了实现晚捆绑的必要机制。它对每个包含虚函数的类创建一个表(称为VTABLE),用于放置虚函数的地址。在每个包含虚函数的类中,编译器秘密地放置了一个称之为vpointer(缩写为VPTR)的指针,指向这个对象的VTABLE。所以无论这个对象包含一个或是多少虚函数,编译器都只放置一个VPTR即可。VPTR由编译器在构造函数中秘密地插入的代码来完成初始化,指向相应的VTABLE,这样对象就“知道”自己是什么类型了。 VPTR都在对象的相同位置,常常是对象的开头。这样,编译器可以容易地找到对象的VTABLE并获取函数体的地址。
  如果我们用sizeof查看前面Base类的长度,我们就会发现,它的长度不仅仅是一个int的长度,而是增加了刚好是一个void指针的长度(在我的机器里面,一个int占4个字节,一个void指针占4个字节,这样正好类Base的长度为8个字节)。
  每当创建一个包含虚函数的类或从包含虚函数的类派生一个类时,编译器就为这个类创建一个唯一的VTABLE。在VTABLE中,放置了这个类中或是它的基类中所有虚函数的地址,这些虚函数的顺序都是一样的,所以通过偏移量可以容易地找到所需的函数体的地址。假如在派生类中没有对在基类中的某个虚函数进行重写(overriding),那么还使用基类的这个虚函数的地址(正如上面的程序结果所示)。
  

<script language="JavaScript" type="text/javascript"></script>

  至今为止,一切顺利。下面,我们的试验开始了。
  就目前得知的,我们可以试探着通过自己的代码来调用虚函数,也就是说我们要找寻一下编译器秘密地插入的那段能找到正确函数体的代码的足迹。
  如果我们有一个Base指针作为接口,它一定指向一个Base或由Base派生的对象,或者是A,或者是其它什么。这无关紧要,因为VPTR的位置都一样,一般都在对象的开头。如果是这样的话,那么包含有虚函数的对象的指针,例如Base指针,指向的位置恰恰是另一个指针——VPTR。VPTR指向的 VTABLE其实就是一个函数指针的数组,现在,VPTR正指向它的第一个元素,那是一个函数指针。如果VPTR向后偏移一个Void指针长度的话,那么它应该指向了VTABLE中的第二个函数指针了。
  这看来就像是一个指针连成的链,我们得从当前指针获取它指向的下一个指针,这样我们才能“顺藤摸瓜”。那么,我来介绍一个函数:
  void *getp (void* p)
  {
  return (void*)*(unsigned long*)p;
  }
  我们不考虑它漂亮与否,我们只是试验。getp() 可以从当前指针获取它指向的下一个指针。如果我们能找到函数体的地址,用什么来存储它呢?我想应该用一个函数指针:
  typedef void (*fun)();
  它与Base中的三个虚函数相似,为了简单我们不要任何输入和返回,我们只要知道它实际上被执行了即可。
  然后,我们负责“摸瓜”的函数登场了:
  fun getfun (Base* obj, unsigned long off)
  {
  void *vptr = getp(obj);
  unsigned char *p = (unsigned char *)vptr;
  p += sizeof(void*) * off;
  return (fun)getp(p);
  }
  第一个参数是Base指针,我们可以输入Base或是Base派生对象的指针。第二个参数是VTABLE偏移量,偏移量如果是0那么对应fun1(),如果是1对应fun2()。getfun() 返回的是fun类型函数指针,我们上面定义的那个。可以看到,函数首先就对Base指针调用了一次getp(),这样得到了vptr这个指针,然后用一个 unsigned char指针运算偏移量,得到的结果再次输入getp(),这次得到的就应该是正确的函数体的位置了。
  那么它到底能不能正确工作呢?我们修改main() 来测试一下:
  int main()
  {
  Base *p = new A;
  fun f = getfun(p, 0);
  (*f)();
  f = getfun(p, 1);
  (*f)();
  f = getfun(p, 2);
  (*f)();
  delete p;
  }
  激动人心的时刻到来了,让我们运行它!
  运行结果为:
  A::fun1()
  A::fun2()
  Base::fun3()
  至此,我们真的成功了。通过我们的方法,我们获取了对象的VPTR,在它的体外执行了它的虚函数。 
<think>好的,我现在需要处理用户关于C++多态性虚函数的问题。用户希望得到详细的解释使用方法。首先,我得回忆一下相关的知识点,并结合提供的引用内容来组织回答。 用户提供的引用内容提到了虚函数的作用、多态性的实现机制,以及虚函数的底层实现。引用[1][2]都强调了虚函数如何实现动态绑定,允许基类指针调用子类方法。引用[3]给出了一个代码示例,说明基类派生类中虚函数的重写以及函数重载的情况。这些内容都是回答用户问题的重要参考。 首先,我需要明确多态性的基本概念,分为编译时多态运行时多态。编译时多态包括函数重载模板,而运行时多态则通过虚函数继承实现。用户的问题重点在运行时多态,也就是虚函数的使用。 接下来,需要解释虚函数的定义方法,即使用virtual关键字,并在派生类中重写。同时,要提到如果派生类没有重写虚函数,则会继承基类的版本。这点在引用[2]中有提到,虚属性可以被继承。 然后,动态绑定的机制是关键。需要说明基类指针或引用如何指向派生类对象,并调用相应的虚函数。这部分可以结合引用[1]中的描述,用简单的例子来说明。例如,基类Animal派生类Dog,通过基类指针调用虚函数speak(),实际执行的是Dog类的实现。 另外,纯虚函数抽象类的概念也需要涵盖。用户可能需要了解如何定义接口,强制派生类实现特定方法。引用中没有直接提到纯虚函数,但这是多态性中的重要部分,需要补充。 虚析构函数的问题也不能忽视。如果基类的析构函数不是虚的,当通过基类指针删除派生类对象时,可能导致资源泄漏。引用中没有提到这一点,但这是使用虚函数时的常见注意事项,应该包含在回答中。 引用[3]中的代码示例展示了函数重载虚函数重写的区别。在派生类中,即使基类的函数是虚函数,重载的函数(如func(int))并不会自动成为虚函数,除非也使用virtual关键字声明。这可能是一个容易混淆的地方,需要解释清楚。 此外,虚函数的底层实现,如虚函数表(vtable虚函数指针(vptr),可以简要提及,以帮助理解动态绑定的机制。这部分在引用[1]的末尾提到,可以适当展开。 用户可能还关心虚函数的使用场景注意事项,比如性能影响(虚函数调用有间接开销)、设计模式中的应用(如工厂模式、策略模式)等。这些内容可以增强回答的实用性。 最后,按照用户要求生成相关问题,需要围绕多态性、虚函数的使用细节、设计考虑等方面提出,例如虚函数与普通成员函数的区别,虚函数表的工作原理,什么情况下必须使用虚函数等。 现在需要将以上思路组织成结构清晰的中文回答,确保符合格式要求:行内公式用$...$,独立公式用$$...$$,正确使用LaTeX语法,引用标识自然添加,并在最后生成相关问题。同时,回答需要分点说明,结合示例代码,帮助用户逐步理解。</think>### C++多态性与虚函数详解 #### 一、多态性基础概念 多态性分为两种类型: 1. **编译时多态**:通过函数重载模板实现,例如: ```cpp int add(int a, int b) { return a+b; } double add(double a, double b) { return a+b; } ``` 2. **运行时多态**:通过虚函数继承实现,满足$Liskov$替换原则[^1] #### 二、虚函数核心机制 1. **定义方式** ```cpp class Animal { public: virtual void speak() { // 基类虚函数 cout << "Animal sound" << endl; } }; class Dog : public Animal { public: void speak() override { // 派生类重写 cout << "Woof!" << endl; } }; ``` 2. **动态绑定示例** ```cpp Animal* animal = new Dog(); animal->speak(); // 输出"Woof!" [^2] ``` 3. **虚函数表(vtable)** - 每个包含虚函数的类自动生成虚函数表 - 对象内部包含隐藏指针`vptr`指向vtable - 调用过程:`obj->vptr[n]()`(n为函数索引) #### 三、高级特性 1. **纯虚函数与抽象类** ```cpp class Shape { public: virtual double area() = 0; // 纯虚函数 }; ``` 2. **虚析构函数必要性** ```cpp class Base { public: virtual ~Base() {} // 必须声明为虚函数 }; ``` 3. **override/final关键字** ```cpp class Derived : public Base { public: void func() override {} // 显式声明重写 virtual void finalFunc() final {} }; ``` #### 四、关键应用场景 1. 插件架构设计 2. GUI事件处理系统 3. 游戏实体组件系统 4. 框架扩展点实现 ```cpp // 工厂模式示例 class Creator { public: virtual Product* create() = 0; }; class CarFactory : public Creator { public: Product* create() override { return new Car(); } }; ``` #### 五、性能与注意事项 1. 虚函数调用比普通函数多一次指针间接访问 2. 虚函数不应声明为`inline` 3. 构造函数中虚函数机制未生效 4. 虚函数表占用额外内存空间 $$ \text{内存开销} = \text{sizeof(vptr)} + \text{vtable大小} $$
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值