1.虚函数
不要在构造函数和析构函数中调用虚函数
下面说说原理:
假如基类有个虚函数
那么编译器会为其创建虚函数表vtbl
并在对象的内存空间创建虚函数指针vptr
虚函数表的原理是每个类会在里面有自己的所能调用到的虚函数地址
对象的内存空间一般只有两样东西:
虚函数指针vptr 和 数据成员(包括直接基类和间接基类的)
在对象初始化过程中
先构造基类对象再构造派生类对象
也就是说当前正在执行基类的构造函数时
执行完初始化列表之后 执行构造函数体之前
编译器插入了初始化vptr的逻辑
令对象的vptr指向是基类的vtbl
所以此时无论如何也访问不到派生类的vtbl
也就无法调用派生类中覆盖(overrided)了的虚函数
只有执行到派生类的构造函数时
才能更新vptr指向派生类的vtbl
此后调用的虚函数才是派生类中覆盖了的虚函数
析构函数函数是构造函数的逆过程
所以在派生类的析构函数中的vptr
基类析构函数中的vptr是不同的
同样是执行析构函数体之前就被改为了对应当前类的vptr
下面我们做个实验验证一下
struct Base {
void printVPTR() {
void (***pvptr)() = reinterpret_cast<void (***)()>(this);
void (**vptr)() = *pvptr;
printf("%p\n%p\n", this, vptr);
}
Base() {
printVPTR();}
virtual ~Base() {
printVPTR();}
};
struct Derived : Base {
Derived() {
printVPTR();}
~Derived() override {
printVPTR();}
};
int main() {
Derived d;
}
打印结果:
0x7fff5fbff748
0x1000010a8
0x7fff5fbff748
0x100001060
0x7fff5fbff748
0x100001060
0x7fff5fbff748
0x1000010a8
各位可以看到构造/析构过程中this指针是没变的
因为基类子对象和派生类子对象的区域是重叠的
都是从对象的基地址开始偏移为0的内存
如果是非第一直接基类的指针
偏移就不是0了
而是要加上在其之前的基类的成员的size
回到正题
我们看vptr的值在不同的构造函数中是不同的
基类是0x1000010a8
派生类是0x100001060
中间有72bytes也就是9个ptr的间隔
这段空间是派生类vtbl所占用的
主要存储了重载的虚函数和type_info指针
对于每种编译器 vtbl的内容千差万别千奇百怪
一般都会有虚函数和type_info指针
感兴趣的朋友可以用示例代码中得到的vptr解引用得到的函数指针来遍历这段内存
看看你所用的编译器在里面究竟存了什么玩意儿
我在另一个问题里详细地讨论了继承模型的内存布局
问题地址
回到正题
这两个指针说明构造和析构会及时地修改vptr以匹配当前的类
由此证明了开头的论点:
不要在构造函数和析构函数中调用虚函数
因为基类构造函数执行时对象的vptr是指向基类的vtbl
而不是你所要创建的派生类的vtbl
因此你无法获得预期的虚函数多态
以关键字virtual的成员函数称为虚函数,主要是用于运行时多态,也就是动态绑定。
虚函数必须是类的成员函数,不能使友元函数、也不能是构造函数【原因:因为建立一个派生类对象时,必须从类层次的根开始,沿着继承路径逐个调用基类的构造函数,直到自己的构造函数,不能选择性的调用构造函数】
不能将虚函数说明为全局函数,也不能说明为static静态成员函数。因为虚函数的动态绑定必须在类的层次依靠this指针实现。
再添加一点:
虚函数的重载特性:一个派生类中定义基类的虚函数是函数重载的一种特殊形式。
重载一般的函数:函数的返回类型和参数的个数、类型可以不同,仅要求函数名相同;
而重载虚函数:要求函数名、返回类型、参数个数、参数类型和顺序都完全相同。