C++ 构造函数 & 析构函数

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 的析构函数,这才是正确析构对象的开始。

但是上面的析构是三个析构函数都被调用了,这个顺序是怎么来的,底层怎么工作怎么实现的呢?

关于构造函数和析构函数的实现

首先是构造函数,构造函数的工作过程如下:

  1. 按声明顺序,调用所有基类的构造函数;
  2. 构造虚函数表,指向正确代码段位置;
  3. 按声明顺序,调用每个成员变量的构造函数;
  4. 依次调用构造函数体中的语句。

析构函数工作刚好反过来:

  1. 依次调用析构函数体中的语句;
  2. 按声明逆序,调用每个成员变量的析构函数;
  3. 按声明逆序,调用每个基类的析构函数。

那么问题来了,对于一个继承链很长的子对象,编译器怎么告诉CPU去哪儿找下一个析构函数位置的?
在虚析构函数的末尾加入几个 call 指令就可以了!子类对象逆序去 call 成员变量和基类的析构函数,基类各自也 call 自身的成员变量和基类的析构函数,这样也就形成一个调用链了。
同理,构造函数其实也是编译器自动生成了这一系列 call 指令来工作的。

注意,为了保证析构函数正常调用,所有作为基类的类,其析构函数都必须声明为虚函数;只有必然作为子类的类(比如用了C++11的 final 声明的类),析构函数才不用写虚函数。

参考

  1. C++对象模型(五)构造与析构
posted @ 2021-09-14 23:03  与MPI做斗争  阅读(45)  评论(0编辑  收藏  举报