Administrator
发布于 2025-05-09 / 15 阅读
0
0

C++虚函数实现原理

1.虚函数表

每个包含虚函数的类(或者从包含虚函数的类派生而来的类)都会有一个对应的虚函数表。该类的所有对象共享这个虚函数表。虚函数表中存储了该类虚函数的地址。

class Base {
public:
    virtual void func1() { std::cout << "Base::func1" << std::endl; }
    virtual void func2() { std::cout << "Base::func2" << std::endl; }
    int a;
};

Base a1, a2, a3;  // a1,a2,a3所有对象共享同一个虚函数表

2.虚函数表指针

每个含有虚函数的类的对象中都有一个隐藏的成员变量,即虚函数表指针(vptr),在构造对象时,编译器会设置这个vptr指向对应类的虚函数表。

//内存模型示意图

对象a1: [vptr] → ┌──────────────┐
          ↓     |              |
对象a2: [vptr] → │  Base vtable │ 
          ↓     │              │ → func1()函数地址
对象a3: [vptr] → └──────────────┘ → func2()函数地址

由于虚函数表是类级别的,所以同一类的所有对象共享同一份虚函数表,这样可以节省内存,避免每个对象都存储一份虚函数表。另外:虚函数表的内容在编译时就已经确定,存放在只读数据段(通常)。而每个对象的vptr是在运行时(构造对象时)被赋值的。

class Base {
    // 编译器自动添加的隐藏成员
    vptr* __vptr;  // 每个对象都有这个指针
};

// 虚函数表是静态的,通常存放在只读数据段
// 所有对象通过自己的vptr指向同一个表

继承情况下:Base类有一个虚函数表,其中包含Base::func1和Base::func2的地址。Derived类也有一个虚函数表,其中包含Derived::func1(重写)和Base::func2(继承)的地址。

当创建Base对象b时,b的vptr指向Base的虚函数表。当创建Derived对象d时,d的vptr指向Derived的虚函数表。

class Derived : public Base {
public:
    void func1() override { std::cout << "Derived::func1" << std::endl; }
    // 不重写func2
    virtual void func3() { cout << "Derived::func3" << endl; }
  
    int b;
};

// Derived有自己的虚函数表,是Base表的扩展
// 所有Derived对象共享Derived的虚函数表

//内存布局示例
Base 对象:
+------------------+
| vptr             | -> 指向 Base 的虚函数表
+------------------+
| int a            |
+------------------+

Base 的虚函数表:
+------------------+
| &Base::func1     |
+------------------+
| &Base::func2     |
+------------------+

Derived 对象:
+------------------+
| vptr             | -> 指向 Derived 的虚函数表
+------------------+
| int a (继承)     |
+------------------+
| int b            |
+------------------+

Derived 的虚函数表:
+------------------+
| &Derived::func1  | // 重写 func1
+------------------+
| &Base::func2     | // 未重写,继承 Base 的 func2
+------------------+
| &Derived::func3  | // 新增虚函数
+------------------+

多重继承时,派生类会有多个虚函数表(对应每个基类);派生类对象会有多个 vptr,分别指向不同的虚函数表。

class Base1 { virtual void f1(); int a;};
class Base2 { virtual void f2(); };
class Derived : public Base1, public Base2 {int b;};

// Derived 对象有两个 vptr:
// vptr1 -> Base1 的虚表(包含 Derived 重写的函数)
// vptr2 -> Base2 的虚表

Derived 对象:
+------------------+
| vptr1            | -> 指向 Base1 的虚函数表
+------------------+
| vptr2            | -> 指向 Base2 的虚函数表
+------------------+
| int a (继承Base1) |
+------------------+
| int b            |
+------------------+

多重继承场景下需要处理菱形继承问题:菱形继承是C++多重继承里的一个典型问题。比如有个基类A,类B和类C都继承自A,然后类D又同时继承B和C,这样就形成了一个菱形的继承结构。这会导致一个问题:D的对象里会有两份A的成员变量,一份来自B,一份来自C,不仅浪费内存,还可能造成二义性,比如直接访问A的成员时,编译器不知道该用哪一份。

//菱形继承举例
class Base { virtual void f1(); int a;};
class Derived1 : public Base {int b;};
class Derived2 : public Base {int c;};

class DerivedEx : public Derived1, public Derived2 {int c;};

解决方法主要有两种。第一种是虚继承,在B和C继承A的时候,用virtual关键字声明,比如class B : virtual public A,这样D在继承B和C时,就只会保留一份A的成员,避免了数据冗余和二义性。虚继承的底层原理是,B和C的对象会通过一个虚基类指针,指向共享的A实例,而不是各自保存一份。

//菱形继承解决方案1
class Base { virtual void f1(); int a;};
class Derived1 : virtual public Base {int b;};
class Derived2 : virtual public Base {int c;};

class DerivedEx : public Derived1, public Derived2 {int c;};

第二种方法是用组合代替继承,把A作为B和C的成员变量,而不是基类,这样D继承B和C时,就不会间接继承两份A,而是通过B和C的接口来访问A的功能,这种方式更符合面向对象的设计原则,也能避免复杂的继承问题。

//菱形继承解决方案1
class Base { virtual void f1(); int a;};
class Derived1 : {Base obj;int b;};
class Derived2 : {Base obj;int c;};

class DerivedEx : public Derived1, public Derived2 {int c;};

3.动态绑定

Base* ptr = new Derived();
ptr->func1(); // 调用 Derived::func1

当我们通过基类指针或引用调用虚函数时,会发生动态绑定(也称为晚绑定)。具体过程如下:

  • 通过ptr找到对象的vptr(通常位于对象起始地址)。
  • 通过vptr找到类的虚函数表
  • 在虚函数表中找到 func1 的条目(编译时确定偏移量,如第 0 项)。
  • 通过函数指针调用对应的函数。

所以,通过基类指针调用虚函数时,会根据实际对象的vptr找到对应的虚函数表,然后调用正确的函数。这就是多态的实现机制。另外关于虚函数还有一些需要掌握的点,参见虚函数常见问题


评论