[Effective C++]条款07:为多态基类声明virtual析构函数

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类),则不需要虚析构函数

思维导图笔记:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值