析构函数的重写(面试常见题)
基类的析构函数为虚函数,此时派生类析构函数只要定义,⽆论是否加virtual关键字,都与基类的析构函数构成重写。
虽然基类与派⽣类析构函数名字不同看起来不符合重写的规则,实际上编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统⼀处理成destructor,所以基类的析构函数加了 vialtual修饰,派⽣类的析构函数就构成重写。
下⾯的代码我们可以看到,如果~A(),不加virtual,那么delete p2时只调⽤的A的析构函数,没有调⽤ B的析构函数,就会导致内存泄漏问题,因为~B()中在释放资源。
class A
{
public:
virtual ~A()
{
cout << "~A()" << endl;
}
};
class B : public A {
public:
virtual ~B()
{
cout << "~B()->delete:" << _p << endl;
delete _p;
}
protected:
int* _p = new int[10];
};
// 只有派⽣类Student的析构函数重写了Person的析构函数,下⾯的delete对象调⽤析构函数,才能
//构成多态,才能保证p1和p2指向的对象正确的调⽤析构函数。
int main()
{
A* p1 = new A;
A* p2 = new B;
delete p1;
delete p2;
return 0;
}
对于这段代码,传统意义上来说我们认为不构成重写(派⽣类虚函数与基类虚函数的返回值类型、函数名字、参数类型完全相同),但实际上它构成重写。
为什么要设计成这样呢?是为了解决这样的一些场景,我们在实践场景有这样的坑:
我们父类的指针可能指向父类对象,也可能指向子类对象。delete p1;
没有问题,调用父类的析构函数。但是delete p2;
我们是希望它调用父类的析构函数,还是调用子类的析构函数呢?我们是希望它调用子类的析构函数。
delete由两部分构成, 第一部分是去调用析构函数,然后再去调用operator delete(相当于free)。如果这个地方不构成多态(我们把~A()前面的virtual去掉):
我们可以看到的是,它只调用到了A的析构。而B里面是有资源的,如果没有调用B的析构,就导致了内存泄漏。
所以这里达成多态才能解决问题。达成多态后指针调用就和A* p1 = new A; A* p2 = new B;
没有关系了,不再delete后都调用A的析构,而是什么类型的对象就调用什么类型的析构。
那么要达成这个多态我们已经达成其中一个条件了:父类指针/引用去调用。第二个条件是虚函数的重写。我们可以只给基类加virtual。
给基类加了virtual后,编译器就把析构函数的名字特殊处理成了destructor。
我们可以看到给~A()加了virtual变为虚函数后,现在delete p2;
就调用了一次 ~B() 和一次~A()。
这是因为在将继承的时候我们说过,子类对象由两部分构成,一部分是父类的部分一部分是子类的部分。子类的几个默认成员函数是这样规定的:子类处理子类的,要处理父类就去调用父类的那一部分函数(构造、拷贝构造、析构、赋值重载都一样)。为了保证先子后父,所以在调用子类的析构函数后会自动调用父类的析构函数(不同于其他的构造、拷贝构造、赋值重载,都是需要我们主动去调用)。
这种设计也解释另一个问题:
实际是不需要显式地调用的,如果想显式地调用,需要写成A::~A;
因为子类的析构和父类的析构在不是多态的情况下又构成了隐藏关系。因为被处理成了destructor,所以同名了,所以构成了隐藏。
我们重新再缕一下:为了正常释放子类中申请的资源,我们需要父子类构成多态,所以析构函数的名字被处理成了destructor,于是父子类的析构函数构成隐藏。
注意:这个问题面试中经常考察,⼀定要结合类似上面的样例才能讲清楚,为什么基类中的析构函数建议设计为虚函数。
本文到此结束=_=