C++虚表&多态的实现原理分析

 更新时间:2025年09月09日 15:42:57   作者:爱上小公举  
文章介绍了C++虚函数的实现机制,通过虚函数指针(vfptr)和虚函数表(vftable)实现多态,对象包含vfptr指针,虚表存函数地址并以空指针结尾,继承时虚表继承并扩展,多态需基类指针引用和虚函数重写,利用虚表实现动态绑定

C++虚表&多态实现原理

这里我们只看继承中的多态 ,本文程序调试在VS2017

先看一个小问题,下面的类A实例化出的对象占几个字节呢?

#include<iostream>
using namespace std;
class A {
	int m_a;
public:
	void func(){
		cout << "类A的func" << endl;
	}
};
int main() {
        A a;
	cout << sizeof(a) << endl;
	system("pause");
	return 0;
}

答案是4字节,这是因为成员函数存放在公共的代码段, 所以只计算成员变量m_a所占字节的大小

调试一下来看, 

我们如果将成员函数定义成虚函数又会如何呢?

我们来看:

#include<iostream>
using namespace std;
class A {
	int m_a;
public:
	virtual void func(){
		cout << "类A的func" << endl;
	}
	virtual int func1() {
		cout << "类A的func1" << endl;
		return 0;
	}
};
int main() {
	A a,b;
	cout << sizeof(A) << endl;
	system("pause");
	return 0;
}

可以看到结果是8字节,emm.... 事出反常必有妖,我们调试一下,看看到底多出来了个什么东西

我们可以看到,在实例化出的对象a和b中多了一个_vfptr,它的类型时void**,是一个二级指针,指针在32位平台中占4字节,所以这里的结果是8(m_a的4字节+_vfptr的4字节),那么_vfptr到底是个什么东西? 

类中有了虚函数之后才有了_vfptr,它们之间到底有着什么关系?

其实,_vfptr,其实就是虚函数指针 (virtual function pointer)

可以看到_vfptr 指向了一个 vftable(virtual function table) 虚函数表(也叫虚表),虚表中元素是void*类型 ,第一个元素是指向了虚函数func(),第二个元素指向了fun1()

虚函数指针(_vfptr) 和 虚函数表(vftable)

虚函数指针和虚表是什么

通过上面的调试,我们已经看到了,_vfptr是指向虚表的一个指针,那么我们也可以叫 _vfptr 为虚函数表指针

  • 当一个类中有虚函数时,编译期间,就会为这个类分配一片连续的内存 (这就是虚表vftable),来存放虚函数的地址,类中只保存着
  • 指向虚表的指针 (也就是虚函数表指针_vfptr)  ,(虚函数其实和普通函数一样,存放在代码段) ,当这个类实例出对象时,每个对象
  • 都会有一个虚函数表指针_vfptr    (VS中虚表内存分配在代码段)

虚表本质上是一个在编译时就已经确定好了的void* 类型的指针数组 .

注意 :  虚函数表为了标志结尾,会在虚表最后一个元素位置保存一个空指针.所以看到的虚表元素个数比实际虚函数个数多一个

C++中的虚函数的实现一般是通过虚函数表 (C++规范并没有规定具体用哪种方法,但大部分的编译器都用虚函数表的方法)  大多数编译器(如本文用的VS)中虚函数表指针都在对象的最前面位置,意味着能通过对象的地址就能遍历虚函数表(能够在多层继承或多重继承中保持较高性能)

虚函数是为了继承时的多态才有的概念,上面简单了解了一下虚表,我们再来看继承关系中的虚表

继承中的虚表

在有虚函数的类(有虚表的类)被继承后,  虚表也会被拷贝给派生类. 注意,编译器会给派生类新分配一片空间来拷贝基类的虚表,将这个虚表的指针给派生类, 而并不是沿用基类的虚表,在发生虚函数的重写时,重写的是派生类为了拷贝基类虚表新创建的这虚表中的虚函数地址

虚表为所有这个类的对象所共享.  注意,是通过给每个对象一个虚表指针_vfptr共享到的虚表.

单继承中的虚表

1. 单继承未重写虚函数: 会继承派生类的虚表,如果派生类中新增了虚函数,则会加继承的虚表后面

2. 单继承重写虚函数: 继承的虚表中被重写的虚函数地址会在继承虚表时被修改为派生类函数的地址(如下面例子中把A::func()修改成了B::func()的地址)(注意: 此时基类的虚表并没有被修改,修改的是派生类自己的虚表)

所以, 重写实际上就是在继承基类虚表时,把基类的虚函数地址修改为派生类虚函数的地址

举个栗子

#include<iostream>
using namespace std;
class A {
	int m_a;
public:
	virtual void func(){
		cout << "类A的func" << endl;
	}
	virtual int func1() {
		cout << "类A的func1" << endl;
		return 0;
	}
};
class B :public A {
public:
	virtual void func() {
		cout << "类B的func" << endl;
	}
	virtual void func2() {
		cout << "类B的func2" << endl;
	}
};
int main() {
        A a1;
        A a2;
	B b;
	system("pause");
	return 0;
}

调试如下:      

可以看到对象b中的_vfptr所指向的虚表继承了类A的虚表 ,但是地址却和a1,a2的_vfptr不一样,也印证了前面所说,新分配了一片空间来拷贝基类的虚表. 

还可以看到a1和a2的虚表地址相同,也印证了前面所说,类的虚表被所有对象所共享. 

但我们却发现,B中也有虚函数,怎么没有了,讲道理这是不科学的,那么B类的虚函数的地址放到底哪去了呢?

思考一下,它肯定是存在的,要么另外建一张虚表放里面,要么放在继承A的虚表里.

实际上,在单继承中,派生类的虚函数地址会放在继承来的基类的虚表后面,只是VS这里没有显示出来.

我们也可以看到图中红色圈出来的vftable[4],虚表中也已经有了四个元素,既然VS不给力,只能自己想办法,可以在监视窗口,通过地址看到,B类中的虚函数指针指向的虚表,其实是这样的,如下 

还可以看到,继承的A中的虚函数func()被重写之后,虚表中就放的是重写后的B中的func()的地址 

多继承中的虚表

  • 多继承不重写虚函数: 继承的多个基类中有多张虚表,派生类会全部拷贝下来,成为派生类的多张虚表,如果派生类有新的虚函数,会加在派生类拷贝的第一张虚表的后面(拷贝的第一张虚表是继承的第一个有虚函数(或虚表)的基类的)
  • 多继承重写虚函数 : 规则与 不重写虚函数 相同,但需要注意的是,如果多个基类中含有相同的虚函数,例如func. 当派生类重写func这个虚函数后,所有含有这个函数的基类虚表都会被重写 (改的是派生类自己拷贝的基类虚表,并不是基类自己的虚表)

举个栗子

#include<iostream>
using namespace std;
class A {
	int m_a;
public:
	virtual void funcA() {
		cout << "类A的funcA" << endl;
	}
	virtual void func() {
		cout << "类A的func" << endl;
	}
};
class B {
public:
	virtual void funcB() {
		cout << "类B的funcB" << endl;
	}
	virtual void func() {
		cout << "类B的func" << endl;
	}
};
class C :public A,public B {
public:
	virtual void func() {
		cout << "类C的func" << endl;
	}
	virtual void funcC() {
		cout << "类C的funcC" << endl;
	}
};
int main() {
	C c;
	A a;
	system("pause");
	return 0;
}

调试如下 :

可以看到,派生类继承了两张虚表,A::func()和B::func()的地址修改为了C:::func()的地址

我们再次通过_vfptr的地址,在监视窗口可以看到,派生类中新增的虚函数,虚函数地址被加在了派生类拷贝基类的第一张虚表的后面. 

多态的原理

我们回忆一下多态的两个构成条件

  • 1.通过指向派生类对象的基类的指针或引用调用虚函数
  • 2. 被调用的函数必须是被派生类重写过的虚函数

简单来说就是,利用了虚函数可以重写的特性,当一个有虚函数的基类有多个派生类时,通过各个派生类对基类虚函数的不同重写,实现通过指向派生类对象基类指针基类引用调用同一个虚函数,去实现不同功能的特性. 抽象来说就是,为了完成某个行为,不同的对象去完成时会产生多种不同的状态

总结

  • 一个有虚函数的类,它实例出的所有对象通过虚表指针vfptr共享类的虚表
  • 对象中存放的是虚函数(表)指针vfptr,不是虚表. vfptr是虚表的首地址,指向虚表
  • 虚表中存放的时虚函数地址,不是虚函数,虚函数和普通函数一样,存放在代码段
  • 虚表是在编译阶段生成的,一般分配在在代码段(常量区),例如VS中

派生类的虚表生成:

  • a.先将基类中的虚表内容拷贝一份到派生类虚表中
  • b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
  • c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
  • d.如是多继承,则派生类新增加的虚函数地址添加在派生类拷贝(继承)的第一张虚表后面

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

相关文章

  • Qt实现画笔功能

    Qt实现画笔功能

    这篇文章主要为大家详细介绍了Qt实现画笔功能,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-08-08
  • C++实现LeetCode(155.最小栈)

    C++实现LeetCode(155.最小栈)

    这篇文章主要介绍了C++实现LeetCode(155.最小栈),本篇文章通过简要的案例,讲解了该项技术的了解与使用,以下就是详细内容,需要的朋友可以参考下
    2021-07-07
  • C语言的循环小练习详解

    C语言的循环小练习详解

    这篇文章主要为大家介绍了C语言的循环小练习,具有一定的参考价值,感兴趣的小伙伴们可以参考一下,希望能够给你带来帮助
    2022-01-01
  • 深入linux下遍历目录树的方法总结分析

    深入linux下遍历目录树的方法总结分析

    本篇文章是对linux下遍历目录树的方法进行了详细的总结与分析,需要的朋友参考下
    2013-05-05
  • C++实现简单的计算器功能

    C++实现简单的计算器功能

    这篇文章主要为大家详细介绍了C++实现简单的计算器功能,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-01-01
  • C++实现封装的顺序表的操作与实践

    C++实现封装的顺序表的操作与实践

    在程序设计中,顺序表是一种常见的线性数据结构,通常用于存储具有固定顺序的元素,与链表不同,顺序表中的元素是连续存储的,因此访问速度较快,但插入和删除操作的效率可能较低,本文将详细介绍如何用 C++ 语言实现一个封装的顺序表类,深入探讨顺序表的核心操作
    2025-02-02
  • C/C++可变参数的使用

    C/C++可变参数的使用

    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    2013-09-09
  • C++ 析构函数与变量的生存周期实例详解

    C++ 析构函数与变量的生存周期实例详解

    这篇文章主要介绍了C++ 析构函数与变量的生存周期实例详解的相关资料
    2017-06-06
  • C语言中%zu的用法解读

    C语言中%zu的用法解读

    size_t是无符号整数类型,用于表示对象大小或内存操作结果,%zu是C99标准中专为size_t设计的printf占位符,避免因类型不匹配导致错误,使用%u或%d可能引发跨平台兼容性问题,尤其在64位系统中
    2025-08-08
  • C语言可变参数函数详解示例

    C语言可变参数函数详解示例

    一般我们编程的时候,函数中形式参数的数目通常是确定的,在调用时要依次给出与形式参数对应的实际参数。但在某些情况下我们希望函数的参数个数可以根据需要确定,因此c语言引入可变参数函数。典型的可变参数函数的例子有printf()、scanf()等,下面我就开始讲解
    2013-11-11

最新评论