关键字:scalar deleting destructor、虚表
通常我们在写一个基类的时候,若基类成员变量需要动态申请内存空间或基类成员变量引用了系统资源时,需定义类的析构函数来明确在删除对象时需要释放的成员。
1. 析构函数
析构函数在析构时调用。若在堆上申请的内存,则不需要调用delete释放内存,只需要调用析构函数。
1.1 析构函数与delete
在堆上申请的内存需要delete删除对象。在删除对象时既要执行析构又要释放内存。
class A
{
public:
~A(){}
};
int main()
{
A *a = new A;
delete a;
}
上面的代码,delete a的过程是什么样的?其实执行了两步操作:
- 调用析构函数~A()
- 调用全局的::operator delete回收内存
我们直接看看汇编:
但是并没有看到调用析构函数的地方,而是调用了一个A::‘scalar deleting destructor’,看看这个函数:
先调用了A::~A(),然后调用了全局的operator delete。
编译器生成了一个A::'scalar deleting destructor’函数,函数中把调用析构函数和释放内存封装在一起。所有调用delete时,实际是调用了A::'scalar deleting destructor’函数,而且这个函数是由编译器生成的。
A::'scalar deleting destructor’函数是不是一定会生成?
class A
{
public:
~A(){}
};
int main()
{
A a;
}
上一段代码中,在栈上创建的A a对象,不需要delete,并没有看到A::'scalar deleting destructor’函数,不会生成这个函数。只有调用了delete才会生成A::'scalar deleting destructor’函数。
问题:下面这段代码能编译通过吗?为什么?
class A
{
public:
~A(){}
void operator delete(void *p);
};
int main()
{
// 1. 这句能编译通过,未使用delete函数,所以即使没定义也不影响
A a;
// 2. 这句编译失败
A *a1 = new A;
delete a1; // 生成了A::'scalar deleting destructor'函数,而该函数内调用了A::operator delete,delete函数没有实现报错
}
2. 虚析构函数
通常我们在写父类时,若父类有析构函数,通常都会声明为虚析构函数。这是因为若父类的析构函数为非虚函数则在子类对象析构时不会被调用。只有父类析构函数为虚函数时,子类对象析构时才会调用子类析构函数后再调用父类的析构函数。
听到这里,我们肯定会有一个很大的问题:虚析构函数从子类到父类的链式调用是如何实现的?因为我们知道,若子类重写了父类的虚函数,则不管子类对象的静态类型是父类还是子类都只会调用子类的函数。
还是使用上面的代码,只是把析构函数改成虚函数:
class A
{
public:
virtual ~A(){}
};
int main()
{
A a;
}
我们看看class A的结构(32位编译器):
可以看到class A占4byte,其中只有一个虚表指针元素vfptr,在32位中指针占4byte。A的虚表中第一个元素是A::{dtor},看着像是A的析构函数,我们把这个函数在汇编中找出来:
int main()
{
A a;
int dtorAddr = *(int*)(*(int*)&a);
}
dtorAddr值为7737774,转为16进制:0x7611AE,在汇编中找到这个位置:
其实是指向A::‘vector deleting destructor’,但是实际是调用了A::'scalar deleting destructor’函数,并不是A::~A()。
由此可以知道,虚析构函数会在虚表中添加一条指向’scalar deleting destructor’函数的函数指针。
但是上面的A a是在栈上申请的内存,所以在释放时只需要调用析构函数,并不需要释放堆内存,所以a析构时依然调用的是A::~A():
ecx寄存器用于传递this指针
问题:下面的代码是否能编译通过:
class A
{
public:
virtual ~A(){}
void operator delete(void *p);
};
int main()
{
A a;
}
编译不通过,因为编译器会生成A::'scalar deleting destructor’函数,而此函数会调用A::operator delete,但是未定义,所以出错。
2.1 多继承的虚析构函数
通过上面的分析,我们已经知道了编译器会为虚析构函数在虚表中添加一个指向A::'scalar deleting destructor’函数的函数指针,用于delete删除对象。
我们再分析一下下面的代码,看看多继承时,虚函数和delete是如何工作的:
#include <stdio.h>
class Animal {
public:
virtual void eat() = 0;
virtual ~Animal() {
printf("Animal is dead\n");
}
void operator delete(void* p) {
printf("Reclaiming Animal storage from %p\n", p);
::operator delete(p);
}
};
class Sheep : public Animal {
public:
virtual void eat() {
printf("Sheep eat ...\n");
}
virtual ~Sheep() {
printf("Sheep is dead\n");
}
void operator delete(void* p) {
printf("Reclaiming Sheep storage from %p\n", p);
::operator delete(p);
}
};
int main(int argc, char** argv) {
Animal* ap = new Sheep;
ap->eat();
delete ap;
return 0;
}
执行代码,可以知道delete ap时的执行顺序:
- Sheep::~Sheep()
- Animal::~Animal()
- Sheep::operator delete(void *p)
ap的静态类型是Animal,编译器是如何知道:
- 如何知道ap是Sheep类型,并找到~Sheep()
- 调用完~Sheep()又是如何找到 ~Animal()并调用的
- delete ap,为什么调用Sheep::operator delete,而不是Animal的删除函数
要想知道,还是看汇编代码。具体就不详细写了。在delete ap时,其实是调用’scalar deleting destructor’函数,而这个函数是在虚表中,指向了Sheep::‘scalar deleting destructor’,所以调用delete ap就会调用Sheep::~Sheep()和Sheep::operator delete(void*p)。但是Animal:: ~Animal()又是怎么被调用的?其实是编译器在作怪,编译器在Sheep:: ~Sheep()函数之后又调用了Animal:: ~Animal():
编译器通过将父类的析构函数写入子类析构函数之后,实现了从子类到父类的链式析构。
而且,只要父类的析构函数是虚析构函数,则子类的析构函数也是虚析构函数,子类生成的‘deleting destructor’函数会替换虚表中父类的‘deleting destructor’函数。
还有一个知识点:operator new、operator delete都是隐式静态函数。