1、C++中,给多态基类声明虚析构函数(virtual destructor)
- 是为了确保在使用基类指针删除派生类对象时,能够正确调用派生类的析构函数,从而避免资源泄漏或未定义行为。
1.1、多态场景下的析构问题
当基类指针指向派生类对象时,如果基类的析构函数不是虚函数,那么通过基类指针删除对象时,只会调用基类的析构函数,而不会调用派生类的析构函数。这会导致派生类中的资源无法正确释放。
class Base{
public:
~Base() {std::cout << "Base destructor" << std::endl; } // 基类非虚析构函数
}
class Derived : public Base{
public:
~Derived(){std::cout << "Derived destructor" << std::endl;} // 派生类析构函数
}
int main(){
Base *prt = new Derived();
delete ptr;
return 0;
}
输出:
- Derived 的析构函数没有被调用,可能导致派生类中的资源泄漏
Base destructor
1.2、虚析构函数的作用
将上面的基类析构函数设置为虚函数后,通过基类指针删除派生类对象时,会先调用派生类的析构函数,再调用基类的析构函数,确保所有资源正确释放。
class Base{
public:
virtual ~Base() {std::cout << "Base destructor" << std::endl; } // 基类虚析构函数
}
class Derived : public Base{
public:
~Derived(){std::cout << "Derived destructor" << std::endl;} // 派生类析构函数
}
int main(){
Base *prt = new Derived();
delete ptr;
return 0;
}
输出:
- 派生类和基类的析构函数都被正确调用
Derived destructor
Base destructor
2、为什么将基类的析构函数设置为虚函数(virtual)后,通过基类指针删除派生类对象时,会正确调用派生类的析构函数?
- 这种行为是由C++的多态机制和虚函数表(vtable)的实现机制决定的。
2.1、多态与虚函数表(vtable)
当类中声明了虚函数(包括虚析构函数)时,编译器会为该类生成一个虚函数表(vtable)。虚函数表示一个存储虚函数指针的数组,每个虚函数在表中有一个对应的条目。
- 虚函数表的作用:在运行时,通过虚函数表动态决定调用哪个函数,从而实现多态
- 虚函数表的内容:包括类的虚函数地址(包括虚析构函数、其他虚函数等)
- 每个对象在内存中会包含一个指向其虚函数表的指针(通常称为vptr),这个指针在对象构造时被初始化。
2.2、虚析构函数的实现原理
当基类的析构函数是虚函数时,派生类的析构函数也会自动改为虚函数(即使没有显示声明为virtual)。编译器会为基类和派生类分别生成虚函数表,并将虚函数的地址放入表中。
class Base {
public:
virtual ~Base() { std::cout << "Base destructor\n"; } // 虚析构函数
};
class Derived : public Base {
public:
~Derived() { std::cout << "Derived destructor\n"; } // 自动成为虚函数
};
- Base的虚函数表:包含~Base()函数地址
- Derived的虚函数表:包含~Derived()的函数地址
2.3、delete 通过基类指针删除派生类对象时的行为
通过基类指针删除派生类对象时,编译器会根据对象的动态类型(即实际指向的派生类类型)调用正确的析构函数。具体过程如下:
①、查找虚函数表:
- 编译器通过对象的vptr找到派生类的虚函数表(此时的对象为派生类对象指针)
- 在派生类的虚函数表中查找析构函数的地址
②、调用派生类的析构函数
- 首先调用派生类的析构函数(~Derived()),释放派生类的资源
③、调用基类的析构函数
- 派生类的析构函数执行完毕后,编译器会自动调用基类的析构函数(~Base()),释放基类的资源
暂时无法在飞书文档外展示此内容
3、没有将基类的析构函数设置为虚函数时,为什么不行?
如果基类的析构函数不是虚函数,编译器会直接调用基类的析构函数,而不会通过虚函数表查找派生类的析构函数。因为:
- 非虚函数的调用是静态绑定的,在编译时就已经确定了调用哪个函数
- 虚函数的调用是动态绑定的,在运行时根据对象的实际类型决定调用哪个函数
4、什么时候需要使用虚函数?
- 基类是多态的: 如果基类中有至少一个虚函数(用于多态),则应将析构函数声明为虚函数
- 基类可能被继承:即使当前没有派生类,如果基类可能被继承,也应声明为虚析构函数,以防未来扩展
- 基类不打算被继承:如果基类不会被继承(final类),则不需要虚析构函数
思维导图笔记: