C++之虚函数与多态的实现原理分析

 更新时间:2025年09月18日 08:50:14   作者:一枝小雨  
文章讲解了C++多态原理,通过虚函数表(虚表)实现动态绑定,探讨了单继承、多继承及菱形继承中的虚表结构,并指出虚函数不能是内联或静态函数,因需函数地址存储于虚表

1.多态的原理

1.1 虚函数表(简称虚表)

class Base
{
public:
        virtual void Func1()
        {
                cout << "Func1()" << endl;
        }
private:
        int _b = 1;
};

问:sizeof(Base) 是多少?

  • 32位系统下,他是 8。
int main()
{
        Base a;
        cout << sizeof(a) << endl;    // 8
        return 0;
}

观察监视窗口,我们发现除了_b成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(一般是__vftptr,即virtual function table ptr)。

一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。

针对上面的代码我们做出以下改造:

  • 我们增加一个派生类Derive去继承Base
  • Derive中重写Func1
  • Base再增加一个虚函数Func2和一个普通函数Func3
class Base
{
public:
        virtual void Func1(){cout << "Base::Func1()" << endl;}
        virtual void Func2(){cout << "Base::Func2()" << endl;}
        void Func3(){cout << "Base::Func3()" << endl;}

private:
        int _b = 1;
};
class Derive : public Base
{
public:
        virtual void Func1(){cout << "Derive::Func1()" << endl;}

private:
        int _d = 2;
};

int main()
{
        Base b;
        Derive d;
        return 0;
}

观察监控窗口的信息,我们可以发现:

  • 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员(Base 部分),一类是自己的成员(_d)。虚表指针就是图中的__vfptr。
  • 基类b对象和派生类d对象虚表是不一样的,Func1 在 d 类中完成了重写,所以d的虚表中存的是重写的Derive::Func1,通过对比可以发现,d对象中的Func1的虚表指针和b对象中的Func1虚表指针指向地址不一样。所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
  • Func2继承下来后是虚函数,所以放进了虚表,但是因为Func2没有重写覆盖,所以b对象中的Func2的虚表地址和d对象的Func2虚表地址一致。父类中的Func3也继承下来了,但是不是虚函数,所以不会放进虚表。

1.2 虚函数表本质

虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个 nullptr 标志数组结束。

派生类的虚表生成:

  • 先将基类中的虚表内容拷贝一份到派生类虚表中。
  • 如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数。
  • 派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
  • 注意:对象中存的是虚表指针,不是虚表本身虚表中存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,vs下虚表本身是存在代码段(常量区)的。
  • 同类型的对象共用一个虚表。

1.3 多态的原理

多态是如何实现指向谁就调用谁的虚函数的?

在运行时,多态会到指向对象的虚表中查找要调用的虚函数的地址,父类对象的虚表中的虚函数指针指向的是父类虚函数,而子类对象的虚表中的虚函数指针指向的是子类重写后的虚函数。

class Person {
public:
        virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
        virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p)
{
        p.BuyTicket();
}
int main()
{
        Person Mike;
        Func(Mike);
        Student Johnson;
        Func(Johnson);
        return 0;
}

  • 当 p 是指向mike对象时,p->BuyTicket在mike的虚表中找到虚函数是Person::BuyTicket。
  • 当 p 是指向johnson对象时,p->BuyTicket在johson的虚表中找到虚函数是Student::BuyTicket。

也就是说,调用函数时,他其实并不知道自己要调用的是子类还是父类的虚函数,他只需要到这个对象的虚表里面找就行了,找到是哪个就是哪个。如果是父类对象,那就直接通过虚表指针到虚表里找。

如果是子类对象,我们知道:

派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。

子类对象中的父类部分会被切割赋给 p,p 还是按照父类对象找虚函数的方式去虚表里找。这样就实现了不同对象去完成同一行为时,展现出不同的形态。

PS:满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到指向对象的虚函数表中查找对应的虚函数的地址。不满足多态的函数调用是编译时直接确定的,通过 p 的类型确定要调用函数的地址

1.4 动态绑定与静态绑定

  1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为也称为静态多态,比如:函数重载
  2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态
int i = 0;
double d = 1.1;
// 静态绑定 静态的多态(静态:编译时确定函数)
f1(i);
f1(d);

// 动态绑定 动态的多态(一般的多态指的就是动态多态)(动态:运行时去虚表找函数)
Base* p = new Base;
p->Func1();
p = new Derive;
p->Func1();

2. 单继承和多继承关系的虚函数表

需要注意的是在单继承和多继承关系中,下面我们去关注的是派生类对象的虚表模型,因为基类的虚表模型前面我们已经看过了,没什么需要特别研究的

2.1 单继承中的虚函数表

class Base {
public:
        virtual void func1() { cout << "Base::func1" << endl; }
        virtual void func2() { cout << "Base::func2" << endl; }
private:
        int a;
};
class Derive :public Base {
public:
        virtual void func1() { cout << "Derive::func1" << endl; }
        virtual void func3() { cout << "Derive::func3" << endl; }
        virtual void func4() { cout << "Derive::func4" << endl; }
private:
        int b;
};

int main()
{
        Base b;
        Derive d;

        return 0;
}

从监视窗口我们可以看到虚表的内容,但是我们 d 对象的 func3 和 func4 呢?监视窗口好像没有,实际上不是虚表中没有,而是监视窗口没有展示,它认为不需要展示,就没有显示出来。

我们可以自己打印虚表,看看到底有没有 func3 和 func4 :

typedef void(*VF_PTR)();   // 函数指针类型重定义
                           // 定义完成后可以使用 VF_PTR p;来创建一个函数指针对象

void PrintVFTable(VF_PTR* pTable)
{
        for (size_t i = 0; pTable[i] != 0; ++i)
        {
                printf("vfTable[%d]:%p->", i, pTable[i]);
                VF_PTR f = pTable[i];
                f();        // 调用函数指针指向的这个函数
        }
        cout << endl;
}

int main()
{
        Base b;
        Derive d;
        // 取对象中前四个字节存的虚表指针打印虚表
        PrintVFTable((VF_PTR*)(*(int*)&b));
        PrintVFTable((VF_PTR*)(*(int*)&d));

        return 0;
}

事实证明 func3 和 func4 确实存在于虚表中,我们成功打印并且调用了。

2.2 多继承中的虚函数表

typedef void(*VF_PTR)();        // 函数指针类型重定义
                                                        // 定义完成后可以使用 VF_PTR p;来创建一个函数指针对象

void PrintVFTable(VF_PTR pTable[])
{
        for (size_t i = 0; pTable[i] != 0; ++i)
        {
                printf("vfTable[%d]:%p->", i, pTable[i]);
                VF_PTR f = pTable[i];
                f();        // 调用函数指针指向的这个函数
        }
        cout << endl;
}

class Base1 {
public:
        virtual void func1() { cout << "Base1::func1" << endl; }
        virtual void func2() { cout << "Base1::func2" << endl; }
private:
        int b1;
};
class Base2 {
public:
        virtual void func1() { cout << "Base2::func1" << endl; }
        virtual void func2() { cout << "Base2::func2" << endl; }
private:
        int b2;
};
class Derive : public Base1, public Base2 {
public:
        virtual void func1() { cout << "Derive::func1" << endl; }
        virtual void func3() { cout << "Derive::func3" << endl; }
private:
        int d1;
};

int main()
{
        // base1虚表4 + int4 + base2虚表4 + int4 + int4 = 20字节
        cout << sizeof(Derive) << endl;    // 20
        
        Derive d;

        // base1 的虚表
        PrintVFTable((VF_PTR*)(*(int*)&d));
        // base2 的虚表
        PrintVFTable((VF_PTR*)(*(int*)((char*)&d + sizeof(Base1))));

        return 0;
}

  • 这说明子类 d 对象的虚函数 func3 是往第一个继承的父类 base1 的虚表里放的。
  • 另外也说明了先继承的父类,它的虚表放在前面(即低地址处)。

2.3 菱形继承、菱形虚拟继承

实际中不建议设计出菱形继承及菱形虚拟继承,一方面太复杂容易出问题,另一方面这样的模型,访问基类成员有一定得性能损耗。

3. Q&A

3.1 内联函数为什么不能是虚函数?

内联函数会在调用位置直接展开,所以内联函数没有地址,也不需要地址,没有函数地址就无法放入虚表,所以内联函数不能是虚函数。

3.2 静态函数为什么不能是虚函数?

1. 调用方式与对象绑定的根本差异

静态函数:

  • 不依赖于任何类的实例(对象)
  • 可以直接通过类名调用(ClassName::StaticFunction()
  • 没有 this 指针,无法访问对象的非静态成员

虚函数:

  • 完全依赖于类的实例(对象)
  • 必须通过对象或对象指针调用
  • this 指针,可以访问对象的非静态成员
  • 通过虚函数表(vTable)实现动态绑定,而vTable是每个对象实例的一部分

2. 虚函数机制依赖于对象实例

虚函数的实现依赖于:

  • 每个对象内部的虚函数表指针(vPtr)
  • 通过vPtr在运行时查找正确的函数实现

静态函数没有this指针,因此无法访问对象的vPtr,也就无法实现动态绑定。如果静态函数是虚的,编译器无法知道应该使用哪个类的虚函数表。

3.3 虚表?虚基表?虚基类?

  • 虚表是虚函数表,存储的是虚函数指针,是一个函数指针数组。
  • 虚基表存储的是偏移量,是解决菱形继承的数据冗余和二义性问题的。
  • 虚基类是在继承中给父类前面加 virtual 关键字,是为了解决菱形继承的数据冗余和二义性问题而存在的。

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • C++实现雷霆战机可视化小游戏

    C++实现雷霆战机可视化小游戏

    这篇文章主要为大家详细介绍了C++实现雷霆战机可视化小游戏,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2020-11-11
  • 深入解析C++编程中线程池的使用

    深入解析C++编程中线程池的使用

    这篇文章主要介绍了深入解析C++编程中线程池的使用,包括线程池的封装实现等内容,需要的朋友可以参考下
    2015-11-11
  • C语言算法金手指摩尔投票法手撕绝大多数问题

    C语言算法金手指摩尔投票法手撕绝大多数问题

    这篇文章主要为大家介绍了C语言算法之金手指摩尔投票法手撕绝大多数问题的示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助
    2022-02-02
  • C++ 进程间通信IPC的实现示例

    C++ 进程间通信IPC的实现示例

    本文主要介绍了C++ 进程间通信IPC的实现示例,包括管道、消息队列、共享内存等基础与高级方法,具有一定的参考价值,感兴趣的可以了解一下
    2025-06-06
  • C++中实现调试日志输出

    C++中实现调试日志输出

    在 C++ 编程中,调试日志对于定位问题和优化代码至关重要,本文将介绍几种常用的调试日志输出方法,并教你如何在日志中添加时间戳,希望对大家有所帮助
    2025-01-01
  • C语言中指针的加减运算方法示例

    C语言中指针的加减运算方法示例

    这篇文章主要给大家介绍了关于C语言中指针的加减运算的相关资料,文中通过示例代码介绍的非常详细,对大家学习或者使用C语言具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧
    2019-08-08
  • 数据结构之数组翻转的实现方法

    数据结构之数组翻转的实现方法

    这篇文章主要介绍了数据结构之数组翻转的实现方法的相关资料,这里用几种实现方法来实现这样的功能,需要的朋友可以参考下
    2017-10-10
  • 通过代码实例解析c++ vector常用方法

    通过代码实例解析c++ vector常用方法

    这篇文章主要介绍了通过代码实例解析c++ vector常用方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-07-07
  • 基于c++强制类型转换的(总结)详解

    基于c++强制类型转换的(总结)详解

    本篇文章对C++中的强制类型转换进行了详细的分析介绍。需要的朋友参考下
    2013-05-05
  • Qt实现简单五子棋小游戏

    Qt实现简单五子棋小游戏

    这篇文章主要为大家详细介绍了Qt实现简单五子棋小游戏,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2020-08-08

最新评论