【c++随笔14】虚函数表
- 一、虚函数表(Virtual Function Table)
- 1、定义
- 2、查看虚函数表
- 2.1、 问题:三种类型,包含一个int类型的class、一个int类型的变量、int类型的指针:这三个大小分别是多少呢?
- 2.2、怎么发现虚函数表存在的?
- 2.3、查看虚函数表里面都有什么?
- 3、继承——虚函数的重写与覆盖
- 4、虚函数为何可以实现多态?
- 5、对象也能切片,为什么不能实现多态?(普通的继承为何不能实现多态?)
- 6、打印虚函数表
- 6.1同一个类型它们的虚表内存地址都是一样的,同一类型的对象共用一份虚表。
- 6.2打印虚函数表
- 二、多态原理
- 1、动态绑定、动态类型、静态类型
- 2、动态绑定和静态绑定对比
- 3、虚函数表的存储位置
- 4、汇编层面看多态实现原理
- 三、多继承中的虚函数表
- 1、多继承会有两张虚表(继承两个时),
- 2、派生类定义的虚函数,存放在第一张虚函数表中
- 四、经典问题
原创作者:郑同学的笔记
原创地址:https://zhengjunxue.blog.csdn.net/article/details/131932164
qq技术交流群:921273910
一、虚函数表(Virtual Function Table)
1、定义
虚函数表是一个由虚函数组成的表格,用于实现动态绑定和多态性。每个包含虚函数的类都有自己的虚函数表,该表列出了该类及其所有基类的虚函数。当一个对象被创建时,它的类虚函数表也被创建,并且可以通过该对象的指针或引用来调用虚函数表中的函数。
虚函数表是一种实现动态多态性的机制。每个包含虚函数的类都有一个虚函数表,其中存储着该类的虚函数地址。当通过基类指针或引用调用虚函数时,程序会根据对象的实际类型查找对应类的虚函数表,并调用正确的虚函数。
2、查看虚函数表
2.1、 问题:三种类型,包含一个int类型的class、一个int类型的变量、int类型的指针:这三个大小分别是多少呢?
#include <iostream>
using namespace std;
class Base {
private:
int _b = 1;
public:
void Func1() {
cout << "Func1()" << endl;
}
void Func2() {
cout << "Func2()" << endl;
}
void Func3() {
cout << "Func3()" << endl;;
}
};
int main(void)
{
Base b;
cout << sizeof(b) << endl;
cout << sizeof(int) << endl;
cout << sizeof(int *) << endl;
return 0;
}
输出
-
答案:(64位系统)
- class 对象实例:占4个字节
- int 变量:占4个字节
- int 指针:占用8个字节
-
其他结论:
- 类class占用内存的大小,就是类calss成员变量占用内存的大小;
- 类class占用内存的大小,和成员函数无关;
- 类也可以作为一种数据类型来看待;
-
Base实例b里面有什么
只有成员变量_b
2.2、怎么发现虚函数表存在的?
- 2.2.1加了virtual后,虚函数Base的大小
#include <iostream>
using namespace std;
class Base {
private:
int _b = 1;
public:
virtual void Func1() {
cout << "Func1()" << endl;
}
virtual void Func2() {
cout << "Func2()" << endl;
}
virtual void Func3() {
cout << "Func3()" << endl;;
}
};
int main(void)
{
Base b;
cout << sizeof(b) << endl;
return 0;
}
输出
结论:
加了virtua后,虚函数Base大小为16;
- 2.2.2、加了虚函数表后为何变成了16字节?
调试查看Base类的实例b里面都有什么
调试截图如下,除了 _b 成员外,还有了一个 _vfptr 在 b1对象中
结论:
- 由于类里面,除了 _b 成员外,还增加了_vfptr,所以由4字节变成了16字节;
- _vfptr就是虚函数表指针(virtual function pointer),指向虚函数表;
扩展:虚函数表指针占用8字节,int类型占用4字节;那8+4应该是12字节,为何总的内存变成了18字节呢,这里面有个内存对齐的问题。打个比方,你int类型占用4字节,double占用8字节,那么会总的便会占用16字节,int类型也会分配8字节的空间。
2.3、查看虚函数表里面都有什么?
结论:
- 虚函数表里面是一个数组;
- 数组里面存储的是每一个虚函数的地址;
扩展
其实,看了我之前写的文章就知道,其实类的实例的每一个成员函数,都有一个单独的地址,存储在代码段.text段。
3、继承——虚函数的重写与覆盖
代码:现在我们增加一个子类 Derive 去继承 Base:
#include <iostream>
using namespace std;
class Base {
private:
int _b = 1;
public:
virtual void Func1() {
cout << "Func1()" << endl;
}
virtual void Func2() {
cout << "Func2()" << endl;
}
virtual void Func3() {
cout << "Func3()" << endl;;
}
};
// 子类 Derive
class Derive : public Base {
public:
virtual void Func1() {
cout << "Derive::Func1()" << endl;
}
private:
int _d = 2;
};
int main(void)
{
Derive d;
cout << sizeof(d) << endl;
return 0;
}
输出
父类 b 对象和子类 b 对象虚表是不一样的,这里看我们发现 Func1 完成了重写,
所以 d 的虚表中存的是重写的 Derive::Func1,所以虚函数的重写也叫做覆盖。
就可以理解为:子类的虚表拷贝了父类的虚表,子类的 Func1 覆盖掉了父类上的 Func1。
(覆盖指的是虚表中虚函数的覆盖)
- 虚函数重写:语法层的概念,子类对继承父类虚函数实现进行了重写。
- 虚函数覆盖:原理层的概念,子类的虚表,拷贝父类虚表进行了修改,覆盖重写那个虚函数。
🔺 总结:虚函数的重写与覆盖,重写是语法层的叫法,覆盖是原理层的叫法。
4、虚函数为何可以实现多态?
多态调用实现是依靠运行时去指向对象的虚表中查,调用函数地址。
#include <iostream>
using namespace std;
class Base {
private:
int _b = 1;
public:
virtual void Func1() {
cout << "Base::Func1()" << endl;
}
virtual void Func2() {
cout << "Base::Func2()" << endl;
}
virtual void Func3() {
cout << "Base::Func3()" << endl;;
}
};
// 子类 Derive
class Derive : public Base {
public:
virtual void Func1() {
cout << "Derive::Func1()" << endl;
}
private:
int _d = 2;
};
int main(void)
{
Base b;
Derive d;
Base* ptr = &b;
ptr->Func1(); // 调用的是父类的虚函数
ptr = &d;
ptr->Func1(); // 调用的是子类的虚函数
return 0;
}
输出
5、对象也能切片,为什么不能实现多态?(普通的继承为何不能实现多态?)
既然指针和引用可以实现多态,那父类赋值给子类对象也可以切片,
根本原因是:对象切片时,子类对象只会拷贝成员给父类对象,并不会拷贝虚表指针。(没有虚函数表)
之前我们讨论过,为何没有静态多态的概念,除了当时说的部符合多态的定义外,本质的原因就在这里。
6、打印虚函数表
6.1同一个类型它们的虚表内存地址都是一样的,同一类型的对象共用一份虚表。
#include <iostream>
using namespace std;
class Base {
public:
int _b = 1;
public:
virtual void Func1() {
cout << "Base::Func1()" << endl;
}
virtual void Func2() {
cout << "Base::Func2()" << endl;
}
};
// 子类 Derive
class Derive : public Base {
public:
virtual void Func1() {
cout << "Derive::Func1()" << endl;
}
virtual void Func3() {
cout << "Derive::Func3()" << endl;;
}
public:
int _d = 2;
};
using pf = void(*)();
//typedef void(*pf)(void); //和上面的写法相等,看不懂的可以看下我的另外一篇博客《函数指针》
int main(void)
{
Base b;
Derive d1;
Derive d2;
return 0;
}
查看局部变量的窗口
可以看到子类继承自父类的虚函数表中Func1函数地址是重写之后的函数地址,已经将父类的func函数地址覆盖掉。
- 如果父类中的虚函数没有被子类重写,那么子类的虚函数表中的地址仍然是父类中虚函数的地址。
- 只有虚函数才会进虚函数表,非虚函数是不进虚函数表的。
- 如果派生类中存在新增加的虚函数,那么就会按照在派生类中的声明顺序依次添加到派生类的虚函数表的最后。
- 虚函数表本质就是一个虚函数指针数组,而虚函数表指针本质就是这个数组的首元素地址。虚函数表的最后一个字段通常置为nullptr。
6.2打印虚函数表
我们例子中,每一个虚函数返回值类型void,参数无,所以,虚函数指针数组中元素的类型为void(*)(void);(不懂的可以查看我的另外一篇博客《【c++随笔09】函数指针》
- 注意:派生类的虚函数,visual并没显示出来,但是我们打印出来了,可见:visual的可视化是有些问题的。
#include <iostream>
using namespace std;
class Base {
public:
int _b = 1;
public:
virtual void Func1() {
cout << "Base::Func1()" << endl;
}
virtual void Func2() {
cout << "Base::Func2()" << endl;
}
};
// 子类 Derive
class Derive : public Base {
public:
virtual void Func1() {
cout << "Derive::Func1()" << endl;
}
virtual void Func3() {
cout << "Derive::Func3()" << endl;;
}
public:
int _d = 2;
};
using pf = void(*)();
//typedef void(*pf)(void); //和上面的写法相等,看不懂的可以看下我的另外一篇博客《函数指针》
int main(void)
{
Base b;
Derive d1;
Derive d2;
Base* ptr = &d1;
//ptr->Func1(); // 调用的是父类的虚函数
//Base* ptr2 = new Derive();
pf* pfun = (pf*)*(long long*)ptr;
//2.pp这个指针是函数指针数组的首元素的地址。
while (*pfun)
{
cout << *pfun << endl;
(*pfun)();
cout << endl;
pfun++;
}
cout << "----------------------------------------" << endl;
ptr = &d2;
pfun = (pf*)*(long long*)ptr;
while (*pfun)
{
cout << *pfun << endl;
(*pfun)();
cout << endl;
pfun++;
}
cout << "----------------------------------------" << endl;
ptr = &b;
pfun = (pf*)*(long long*)ptr;
while (*pfun)
{
cout << *pfun << endl;
(*pfun)();
cout << endl;
pfun++;
}
return 0;
}
输出
二、多态原理
1、动态绑定、动态类型、静态类型
我们依然查看《C++ Primer 第5版》第15章节末尾 术语表中的介绍(p575-576页)
-
动态绑定(dynamic binding) 直到运行时才确定到底执行函数的哪个版本。在C++语言中,动态绑定的意思是在运行时根据引用或指针所绑定对象的实际类型来选择执行虚函数的某一个版本。
-
动态类型(dynamic type) 对象在运行时的类型。引用所引对象或者指针所指对象的动态类型可能与该引用或指针的静态类型不同。基类的指针或引用可以指向一个派生类对象。在这样的情况中,静态类型是基类的引用(或指针),而动态类型是派生类的引用(或指针)。
-
静态类型(static type) 对象被定义的类型或表达式产生的类型。静态类型在编译时是已知的。
《C++ Primer 第5版》第15.2章节(p529页)
- 以动态绑定有时又被称为运行时绑定(run-time binding)。
- 在C++语言中,当我们使用基类的引用(或指针)调用一个虚函数时将发生动态绑定。
2、动态绑定和静态绑定对比
由于《C++ Primer 第5版》并没有给出静态绑定的概念,我们暂时把在程序编译期间确定了程序的行为,也称为静态绑定。比如函数重载。
- 动态绑定
#include <iostream>
using namespace std;
class Base {
private:
int _b = 1;
public:
void Func1() {
cout << "Func1()" << endl;
}
};
int main(void)
{
Base *ptr1 = new Base();
ptr1->Func1();
return 0;
}
- 静态绑定
#include <iostream>
using namespace std;
class Base {
private:
int _b = 1;
public:
virtual void Func1() {
cout << "Func1()" << endl;
}
};
int main(void)
{
Base *ptr1 = new Base();
ptr1->Func1();
return 0;
}
- 汇编层面分析静态绑定和动态绑定的区别
g++ main.cpp
objdump -h -d -x ./a.out
对比反汇编的代码,如下截图
3、虚函数表的存储位置
推断:虚表存储在只读数据段上。
4、汇编层面看多态实现原理
- 多态
#include <iostream>
using namespace std;
class Base {
private:
int _b = 1;
public:
virtual void Func1() {
cout << "Func1()" << endl;
}
};
// 子类 Derive
class Derive : public Base {
public:
virtual void Func1() {
cout << "Derive::Func1()" << endl;
}
private:
int _d = 2;
};
void pf(Base *b)
{
b->Func1();
}
int main(void)
{
Base *ptr1 = new Base();
pf(ptr1);
Base *ptr2 = new Derive();
pf(ptr2);
return 0;
}
- 非多态
#include <iostream>
using namespace std;
class Base {
private:
int _b = 1;
public:
void Func1() {
cout << "Func1()" << endl;
}
};
// 子类 Derive
class Derive : public Base {
public:
void Func1() {
cout << "Derive::Func1()" << endl;
}
private:
int _d = 2;
};
void pf(Base *b)
{
b->Func1();
}
int main(void)
{
Base *ptr1 = new Base();
pf(ptr1);
Base *ptr2 = new Derive();
pf(ptr2);
return 0;
}
g++ main.cpp
objdump -h -d -x ./a.out
对比反汇编的代码,如下截图
三、多继承中的虚函数表
// 基类A
class A {
public:
virtual void func1() {
std::cout << "A::func1()" << std::endl;
}
virtual void func2() {
std::cout << "A::func2()" << std::endl;
}
};
// 基类B
class B {
public:
virtual void func1() {
std::cout << "B::func1()" << std::endl;
}
/*virtual void func2() {
std::cout << "B::func2()" << std::endl;
}*/
};
// 派生类C,多继承自A和B
class C : public A, public B {
public:
virtual void func1() override {
std::cout << "C::func1()" << std::endl;
}
/*virtual void func2() override {
std::cout << "C::func2()" << std::endl;
}*/
virtual void func3() {
std::cout << "C::func3()" << std::endl;
}
};
int main() {
C c;
}
我们先透过监视简单看一下:
2、派生类定义的虚函数,存放在第一张虚函数表中
#include <iostream>
// 基类A
class A {
public:
virtual void func1() {
std::cout << "A::func1()" << std::endl;
}
virtual void func2() {
std::cout << "A::func2()" << std::endl;
}
};
// 基类B
class B {
public:
virtual void func1() {
std::cout << "B::func1()" << std::endl;
}
/*virtual void func2() {
std::cout << "B::func2()" << std::endl;
}*/
};
// 派生类C,多继承自A和B
class C : public A, public B {
public:
virtual void func1() override {
std::cout << "C::func1()" << std::endl;
}
/*virtual void func2() override {
std::cout << "C::func2()" << std::endl;
}*/
virtual void func3() {
std::cout << "C::func3()" << std::endl;
}
};
int main() {
C c;
// 打印A的虚函数表
std::cout << "A's vtable: " << std::endl;
void** aVTable = *(void***)(&c);
for (int i = 0; aVTable[i] != nullptr; i++) {
std::cout << " [" << i << "]" << aVTable[i]<<" -> "<<" 函数执行";
void(*func)() = (void(*)())(aVTable[i]);
func();
}
// 打印B的虚函数表
std::cout << "B's vtable: " << std::endl;
void** bVTable = *(void***)(((char*)&c) + sizeof(A));
for (int i = 0; bVTable[i] != nullptr; i++) {
std::cout << " [" << i << "]" << aVTable[i]<<" -> " << " 函数执行";
void(*func)() = (void(*)())(bVTable[i]);
func();
}
根据虚函数表地址运行虚函数
//std::cout << "Running virtual function using A's vtable: " << std::endl;
//void(*aFunc)() = (void(*)())(aVTable[0]);
//aFunc();
//std::cout << "Running virtual function using B's vtable: " << std::endl;
//void(*bFunc)() = (void(*)())(bVTable[0]);
//bFunc();
return 0;
}
输出
四、经典问题
-
- inline函数可以是虚函数嘛?
inline函数可以是虚函数,但是其内联的特性也就没有了,因为inline只是对编译器的建议。内联函数是在调用的地方展开,没有函数地址,而虚函数的地址是要写入虚函数表的,所以内联函数和虚函数只能为其中的一个,不可兼得。
-
- 静态成员函数可以是虚函数嘛?
不能,因为静态成员函数没有this指针,使用类名::成员函数的调用方式 无法访问虚函数表,所以静态成员函数无法放进虚函数表。
-
- 构造函数可以是虚函数嘛?
不可以,因为虚函数表指针是在构造函数的初始化列表初始化的,但是虚函数又要借助虚函数表指针来调用虚函数,两者矛盾,所以不可以为虚函数。
-
- 析构函数可以是虚函数嘛?
可以,并且建议将析构函数定义为虚函数,因为这样可以避免内存泄漏的问题。如果子类对象是动态开辟的,使用父类指针指向子类对象,在delete时如果构成多态那么就会调用子类析构函数,而调用子类析构函数前系统会默认先调用父类析构函数,这样可以避免内存泄漏。
-
- 对象访问普通函数快还是访问虚函数快?
如果是通过实例化的对象访问那么是一样快的,如果是指针或引用对象访问的话是访问普通函数快的,因为指针或引用去访问虚函数时走的是多态调用是一个晚绑定,需要在运行时去需表中找函数的地址。
-
- 虚函数表是在什么时候形成的?存在哪?
和虚函数相关的字符,字符在只读数据区(.rodata),但是虚函数的实现代码应该是和其他的函数一样,存储在代码段(.text)
-
- 什么是抽象类?
函数纯虚函数的类叫做抽象类,此类不能实例化出对象,这也强制了其派生类如果想要实例化出对象那么就必须重写纯虚函数。
-
- C++菱形继承解决方案和多态原理?
菱形继承具有数据冗余和二义性的问题,解决的方法是通过虚继承的方式,虚继承的派生类中会产生一个虚基表指针,该指针指向虚基表,表中的内容是一个到冗余数据的偏移量,而原本冗余的数据会被放到派生类对象的最后。
多态的原理是通过重写虚函数,达到在派生类的虚函数表中重写的虚函数地址覆盖掉原本的地址,然后通过基类的指针或者引用指向派生类对象时,调用虚函数调用的时子类重写后的虚函数,而执行基类对象时调用的就是基类的虚函数达到多态的行为。
不要将虚基表和虚函数表搞混。