携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第11天,点击查看活动详情
💦 虚函数表
✔ 测试用例一:
#include<iostream>
using namespace std;
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
private:
int _b = 1;
char _ch = 'a';
};
int main()
{
cout << sizeof(Base) << endl;
Base b;
return 0;
}
-
这是常考一道笔试题,遇到这种题时打死也不可能是 8,因为是 8 的话那就是考查结构体的内存对齐,为啥还要搞个类呢。
-
当一个类有虚函数后,这个类会增加 4 个字节在前面,这 4 个字节是一个指针,这个指针叫做虚函数表指针,简称虚表指针 __vfptr (v 是 virtual、f 是 function、ptr 是指针,但是 __vftptr 更准确,就是说这个指针不是指向虚函数,而是指向虚函数表,表里才是虚函数),__vfptr 指向的表是虚函数表,简称虚表,这个表你可以认为它是函数指针数组,表里存储的是虚函数的地址 (注意虚函数存储于虚表中这种说法不完全对,因为虚函数被编译成指令后,跟普通函数一样存储在代码段,只是它的地址放到了虚表中)。注意区分继承中谈的虚基表指针,它所指向的表所存储的是偏移量,用于查找基类。
✔ 测试用例二:
#include<iostream>
using namespace std;
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;
char _ch = 'a';
};
class Drive : public Base
{
public:
virtual void Func1()//重写Func1
{
cout << "Drive::Func1()" << endl;
}
private:
int _d = 2;
};
int main()
{
Base b1;
Base b2;
Base b3;
Drive d1;
Drive d2;
return 0;
}
-
可以看到普通函数并不会放到虚函数表中。
-
可以看到 Base 和 Drive 所创建的对象的虚表指针不同。
如果虚函数 Func2 没有被重写,子类中的虚表中放的依旧是 Func2 的虚函数的地址;如果虚函数 Func1 重写,我们说重写也叫做覆盖,你可以理解为子类的虚表是把父类的虚表拷贝过来 (当然这里没必要做写时拷贝),谁完成了重写,就把重写的位置覆盖成重写的虚函数,所以你可以认为重写是语法层的概念,覆盖是原理层的概念;如果都不完成重写,虽然父子类中虚表的内容是一样的,但是并不代表着它们要共用一张虚表,也没必要,因为空间用的不多。
所以一个类的所有对象共享一张虚表;父子类无论是否完成虚函数重写,都有各自独立的虚表;
💦 动态绑定与静态绑定
- 有些地方也会称普通调用是静态绑定,多态调用称后期绑定等,这可能是由于翻译等其它原因,导致了有不同的术语。
- 静态绑定又称为前期绑定 (早绑定),在程序编译链接期间确定了程序的行为,也称为静态多态,比如函数重载。
- 动态绑定又称为后期绑定 (晚绑定),在程序运行期间,根据具体拿到的类型确定程序具体的行为,调用具体的函数,也称为动态多态,比如多态。