目录一、基类析构函数需要加 virtual二、虚函数和虚析构函数的关系三、基类和虚析构的关系四、不要继承没有虚析构的 STL 容器五、纯虚析构函数阻止实例化1. 为什么需要纯虚析构函数2. 问题派生类也无法实例化3. 解决方案提供纯虚析构函数的定义一、基类析构函数需要加 virtual如果基类的析构函数不是虚函数用基类指针指向派生类对象后delete该指针只会调用基类的析构函数而不会调用派生类的析构函数导致派生类的资源无法释放。class shape { public: shape() {} virtual ~shape() {} // 基类析构函数声明为虚函数 }; class circle : public shape { public: circle(int area 0) { _area area; } ~circle() {} // 派生类析构函数自动成为虚函数 int _area; };正确用法shape* ps new circle(10); delete ps; // 正确先调用 circle 析构再调用 shape 析构错误用法没有 virtualclass shape { public: shape() {} ~shape() {} // 非虚析构函数 }; shape* ps new circle(10); delete ps; // 错误只调用 shape 的析构函数circle 的资源没有被释放二、虚函数和虚析构函数的关系基本规则如果一个类有虚函数那么它大概率是要作为基类使用的因此它的析构函数也应该声明为虚函数。也就是说有虚函数 → 通常是基类 → 需要虚析构函数虚函数和虚析构函数通常是成对出现的三、基类和虚析构的关系问题如果一个类不作为基类可以声明虚析构函数吗答案可以但会有性能损耗。struct num_pair { int _x; int _y; num_pair(int x, int y) : _x(x), _y(y) {} ~num_pair() {} };分析原本num_pair只有两个int共 64 位8 字节可以整个放入一个 64 位寄存器中非常高效。如果给析构函数加上virtual编译器会为类添加一个vptr指针指向虚函数表对象体积会增大。可能从 64 位8 字节变为 96 位12 字节或 128 位16 字节取决于指针大小和对齐要求。对象不能再放入寄存器只能放在内存中效率降低。结论只有当类确实作为基类使用时才应该声明虚析构函数。否则不需要为虚析构付出额外的代价。四、不要继承没有虚析构的 STL 容器标准库容器如string、vector、list等的析构函数都不是虚函数。如果把它们作为基类派生类对象通过基类指针删除时会导致未定义行为。class specialString : public string { // 危险string 没有虚析构 public: specialString(const string str) : _str(str) {} string _str; }; int main() { specialString* pss new specialString(hello); string* ps pss; // 基类指针指向派生类对象 delete ps; // 未定义行为只调用 string 的析构specialString 的资源没有释放 return 0; }问题delete ps只会调用string的析构函数specialString中新增的成员_str不会被析构可能导致资源泄漏或程序崩溃教训不要继承没有虚析构函数的类包括所有 STL 容器。五、纯虚析构函数阻止实例化1. 为什么需要纯虚析构函数有些基类不适合被实例化。例如shape是一个抽象概念不应该有具体的shape对象但circle、rectangle等派生类应该可以实例化最优雅的方式是让析构函数成为纯虚函数因为基类析构函数本来就应该是虚函数纯虚函数让类成为抽象类无法实例化class shape { public: shape() {} virtual ~shape() 0; // 纯虚析构函数 };这样直接创建shape对象就会报错2. 问题派生类也无法实例化但上面的代码会导致派生类也无法实例化circle c1(2); // 编译错误原因派生类对象析构时最后会调用基类的析构函数但基类的纯虚析构函数只有声明没有定义链接器找不到shape::~shape()的定义3. 解决方案提供纯虚析构函数的定义纯虚析构函数可以且应该提供定义class shape { public: shape() {} virtual ~shape() 0; // 声明为纯虚 }; // 提供定义必须在类外 shape::~shape() {} // 空实现即可完整示例class shape { public: shape() {} virtual ~shape() 0; // 纯虚析构函数 }; shape::~shape() {} // 必须提供定义 class circle : public shape { public: circle(int area 0) { _area area; } ~circle() {} int _area; }; int main() { // shape s; // 错误抽象类不能实例化 circle c1(2); // 正确派生类可以实例化 shape* ps new circle(3); delete ps; // 正确调用 circle 析构然后调用 shape 析构 return 0; }原理纯虚析构函数让shape成为抽象类阻止实例化提供定义后派生类析构时可以正常调用基类析构函数完美实现基类不能实例化派生类可以实例化且多态删除正确
【读书笔记】Effective C++ 条款7:为多态基类声明 virtual 析构函数
目录一、基类析构函数需要加 virtual二、虚函数和虚析构函数的关系三、基类和虚析构的关系四、不要继承没有虚析构的 STL 容器五、纯虚析构函数阻止实例化1. 为什么需要纯虚析构函数2. 问题派生类也无法实例化3. 解决方案提供纯虚析构函数的定义一、基类析构函数需要加 virtual如果基类的析构函数不是虚函数用基类指针指向派生类对象后delete该指针只会调用基类的析构函数而不会调用派生类的析构函数导致派生类的资源无法释放。class shape { public: shape() {} virtual ~shape() {} // 基类析构函数声明为虚函数 }; class circle : public shape { public: circle(int area 0) { _area area; } ~circle() {} // 派生类析构函数自动成为虚函数 int _area; };正确用法shape* ps new circle(10); delete ps; // 正确先调用 circle 析构再调用 shape 析构错误用法没有 virtualclass shape { public: shape() {} ~shape() {} // 非虚析构函数 }; shape* ps new circle(10); delete ps; // 错误只调用 shape 的析构函数circle 的资源没有被释放二、虚函数和虚析构函数的关系基本规则如果一个类有虚函数那么它大概率是要作为基类使用的因此它的析构函数也应该声明为虚函数。也就是说有虚函数 → 通常是基类 → 需要虚析构函数虚函数和虚析构函数通常是成对出现的三、基类和虚析构的关系问题如果一个类不作为基类可以声明虚析构函数吗答案可以但会有性能损耗。struct num_pair { int _x; int _y; num_pair(int x, int y) : _x(x), _y(y) {} ~num_pair() {} };分析原本num_pair只有两个int共 64 位8 字节可以整个放入一个 64 位寄存器中非常高效。如果给析构函数加上virtual编译器会为类添加一个vptr指针指向虚函数表对象体积会增大。可能从 64 位8 字节变为 96 位12 字节或 128 位16 字节取决于指针大小和对齐要求。对象不能再放入寄存器只能放在内存中效率降低。结论只有当类确实作为基类使用时才应该声明虚析构函数。否则不需要为虚析构付出额外的代价。四、不要继承没有虚析构的 STL 容器标准库容器如string、vector、list等的析构函数都不是虚函数。如果把它们作为基类派生类对象通过基类指针删除时会导致未定义行为。class specialString : public string { // 危险string 没有虚析构 public: specialString(const string str) : _str(str) {} string _str; }; int main() { specialString* pss new specialString(hello); string* ps pss; // 基类指针指向派生类对象 delete ps; // 未定义行为只调用 string 的析构specialString 的资源没有释放 return 0; }问题delete ps只会调用string的析构函数specialString中新增的成员_str不会被析构可能导致资源泄漏或程序崩溃教训不要继承没有虚析构函数的类包括所有 STL 容器。五、纯虚析构函数阻止实例化1. 为什么需要纯虚析构函数有些基类不适合被实例化。例如shape是一个抽象概念不应该有具体的shape对象但circle、rectangle等派生类应该可以实例化最优雅的方式是让析构函数成为纯虚函数因为基类析构函数本来就应该是虚函数纯虚函数让类成为抽象类无法实例化class shape { public: shape() {} virtual ~shape() 0; // 纯虚析构函数 };这样直接创建shape对象就会报错2. 问题派生类也无法实例化但上面的代码会导致派生类也无法实例化circle c1(2); // 编译错误原因派生类对象析构时最后会调用基类的析构函数但基类的纯虚析构函数只有声明没有定义链接器找不到shape::~shape()的定义3. 解决方案提供纯虚析构函数的定义纯虚析构函数可以且应该提供定义class shape { public: shape() {} virtual ~shape() 0; // 声明为纯虚 }; // 提供定义必须在类外 shape::~shape() {} // 空实现即可完整示例class shape { public: shape() {} virtual ~shape() 0; // 纯虚析构函数 }; shape::~shape() {} // 必须提供定义 class circle : public shape { public: circle(int area 0) { _area area; } ~circle() {} int _area; }; int main() { // shape s; // 错误抽象类不能实例化 circle c1(2); // 正确派生类可以实例化 shape* ps new circle(3); delete ps; // 正确调用 circle 析构然后调用 shape 析构 return 0; }原理纯虚析构函数让shape成为抽象类阻止实例化提供定义后派生类析构时可以正常调用基类析构函数完美实现基类不能实例化派生类可以实例化且多态删除正确