C++ 构造函数 & 析构函数
美团二面被问了一个问题,没有答上来,今天整理一下相关知识。
问:为什么析构函数要声明成虚函数?
涉及到虚函数,表明这个问题实际上和多态有关系,具体来讲是用基类指针指向的子类对象如何虚构的问题。
举个例子,以下函数的输出是什么?
典型代码
#include <bits/stdc++.h>
using namespace std;
class A {
public:
A() { cout << "A::A()" << endl; }
virtual ~A() {cout << "A::~A()" << endl; }
};
class B {
public:
B() { cout << "B::B()" << endl; }
virtual ~B() {cout << "B::~B()" << endl; }
};
class C : public A {
public:
C() { cout << "C::C()" << endl; }
virtual ~C() {cout << "C::~C()" << endl; }
private:
B b;
};
int main() {
// C c;
A* p = new C;
delete p;
}
问题解答
问题揭秘,输出为:
A::A()
B::B()
C::C()
C::~C()
B::~B()
A::~A()
这里首先回答原来的问题,为什么需要虚函数,因为 A *p = new C;
这种方式声明的对象,如果不用虚函数的话,用的是 class A
的析构函数来析构,会造成资源泄漏。可以尝试把上面代码中的 virtual
全部删除,输出为:
A::A()
B::B()
C::C()
A::~A()
使用虚函数的话,析构时调用的是从对象的虚函数表中查找到的析构函数,也就是 class C
的析构函数,这才是正确析构对象的开始。
但是上面的析构是三个析构函数都被调用了,这个顺序是怎么来的,底层怎么工作怎么实现的呢?
关于构造函数和析构函数的实现
首先是构造函数,构造函数的工作过程如下:
- 按声明顺序,调用所有基类的构造函数;
- 构造虚函数表,指向正确代码段位置;
- 按声明顺序,调用每个成员变量的构造函数;
- 依次调用构造函数体中的语句。
析构函数工作刚好反过来:
- 依次调用析构函数体中的语句;
- 按声明逆序,调用每个成员变量的析构函数;
- 按声明逆序,调用每个基类的析构函数。
那么问题来了,对于一个继承链很长的子对象,编译器怎么告诉CPU去哪儿找下一个析构函数位置的?
在虚析构函数的末尾加入几个 call 指令就可以了!子类对象逆序去 call 成员变量和基类的析构函数,基类各自也 call 自身的成员变量和基类的析构函数,这样也就形成一个调用链了。
同理,构造函数其实也是编译器自动生成了这一系列 call 指令来工作的。
注意,为了保证析构函数正常调用,所有作为基类的类,其析构函数都必须声明为虚函数;只有必然作为子类的类(比如用了C++11的 final 声明的类),析构函数才不用写虚函数。