Effective C 条款07为多态基类声明 virtual 析构函数在 C 的多态体系中基类指针指向派生类对象是一种常见的设计模式。但如果基类的析构函数不是 virtual 的删除这个指针时可能会引发灾难性的后果。今天我们来深入剖析这个问题。一、问题的引入假设我们有一个表示时间记录的基类classTimeKeeper{public:TimeKeeper(){}~TimeKeeper(){std::coutTimeKeeper destructor\n;}virtualvoidrecordTime()0;};classAtomicClock:publicTimeKeeper{public:AtomicClock(){data_newchar[1024];}~AtomicClock(){delete[]data_;std::coutAtomicClock destructor\n;}voidrecordTime()override{/* ... */}private:char*data_;};现在我们通过工厂函数获取一个对象TimeKeeper*getTimeKeeper(){returnnewAtomicClock();}然后在使用完毕后删除它TimeKeeper*ptkgetTimeKeeper();// 使用 ptk...deleteptk;// 危险会发生什么输出TimeKeeper destructor注意只有基类的析构函数被调用了派生类AtomicClock的析构函数完全没有执行导致data_指向的内存泄漏了。二、原理分析为什么非 virtual 析构函数会导致局部销毁2.1 虚函数与动态绑定C 的多态性依赖于**虚函数表vtable**机制。当一个类声明了 virtual 函数时特性说明虚函数表编译器为该类生成一个 vtable存储所有虚函数的地址虚指针每个对象包含一个隐藏的 vptr 指针指向对应的 vtable动态绑定通过 vptr 在运行时确定调用哪个函数版本2.2 析构函数的调用链当析构函数是 virtual 时classTimeKeeper{public:virtual~TimeKeeper(){/* ... */}// 注意 virtual};delete ptk的执行过程通过ptk的 vptr 找到 vtablevtable 中指向~AtomicClock()执行~AtomicClock()—— 释放data_自动调用~TimeKeeper()—— 释放基类部分2.3 非 virtual 析构函数的静态绑定如果析构函数不是 virtual编译器根据指针的静态类型TimeKeeper*决定调用哪个析构函数直接调用TimeKeeper::~TimeKeeper()AtomicClock::~AtomicClock()永远不会被调用内存布局示意 [ AtomicClock 对象 ] ------------------ | TimeKeeper 部分 | -- ptk 指向这里 ------------------ | AtomicClock 数据 | -- 这部分永远不会被析构 | (data_ 等) | ------------------三、解决方案virtual 析构函数将基类的析构函数声明为 virtualclassTimeKeeper{public:TimeKeeper(){}virtual~TimeKeeper(){std::coutTimeKeeper destructor\n;}virtualvoidrecordTime()0;};现在重新运行TimeKeeper*ptkgetTimeKeeper();deleteptk;输出AtomicClock destructor TimeKeeper destructor完美派生类的资源被正确释放然后基类部分也被正确释放。四、规则与例外4.1 核心规则如果 class 带有任何 virtual 函数它就应该拥有一个 virtual 析构函数。原因很直接带有 virtual 函数的类设计意图就是被当作基类使用被当作基类使用就意味着可能通过基类指针删除派生类对象因此必须保证析构时的多态行为4.2 反面规则如果 class 不含 virtual 函数通常并不意图被用来做 base class那么就不要声明 virtual 析构函数。为什么因为 virtual 析构函数有代价代价说明额外内存开销每个对象需要存储 vptr通常 4 或 8 字节无法内联优化析构调用需要通过 vtable 间接寻址无法与其他语言互操作如 C 语言无法直接使用带 vptr 的对象4.3 一个常见的陷阱std::string 和 STL 容器classSpecialString:publicstd::string{// ...};std::string*psnewSpecialString(Hello);deleteps;// 未定义行为std::string 的析构函数不是 virtualSTL 容器类string、vector、list 等的析构函数都不是 virtual 的因此绝不应该继承它们如果你需要扩展 STL 容器的功能应该使用**组合composition**而不是继承classSpecialString{private:std::string data_;// 组合而非继承public:// 提供你需要的额外接口};五、纯虚析构函数抽象基类的技巧有时候你需要一个纯抽象基类所有函数都是纯虚函数但仍然希望它有 virtual 析构函数。这时可以使用纯虚析构函数classAWOV{// Abstract WithOut Virtual (non-pure virtual functions)public:virtualvoidinterface()0;virtual~AWOV()0;// 纯虚析构函数};// 必须提供定义AWOV::~AWOV(){}为什么纯虚析构函数需要定义因为析构函数的调用链中派生类析构完成后会自动调用基类析构函数。如果基类析构函数没有定义链接器会报错。classDerived:publicAWOV{public:voidinterface()override{}~Derived(){/* ... */}};// Derived 析构时// 1. 执行 ~Derived()// 2. 自动调用 ~AWOV() -- 必须有定义六、实际应用场景6.1 插件系统中的接口基类classIPlugin{public:virtualvoidinitialize()0;virtualvoidexecute()0;virtualvoidshutdown()0;virtual~IPlugin()default;// virtual 析构函数};classImageProcessor:publicIPlugin{public:voidinitialize()override{buffer_newchar[4096];}voidexecute()override{/* ... */}voidshutdown()override{/* ... */}~ImageProcessor(){delete[]buffer_;}private:char*buffer_;};// 插件管理器classPluginManager{std::vectorIPlugin*plugins_;public:voidunloadAll(){for(auto*p:plugins_){deletep;// 安全会正确调用派生类析构函数}plugins_.clear();}};6.2 游戏引擎中的组件系统classComponent{public:virtualvoidupdate(floatdeltaTime)0;virtualvoidrender()0;virtual~Component()default;};classPhysicsComponent:publicComponent{public:voidupdate(floatdeltaTime)override{/* ... */}voidrender()override{}~PhysicsComponent(){// 清理物理引擎中的刚体引用PhysicsEngine::removeBody(body_);}private:Body*body_;};classGameObject{std::vectorComponent*components_;public:~GameObject(){for(auto*c:components_){deletec;// 正确析构每个组件}}};6.3 工厂模式中的产品基类classProduct{public:virtualvoiduse()0;virtual~Product()default;};classConcreteProductA:publicProduct{public:voiduse()override{/* ... */}~ConcreteProductA(){/* 清理资源 A */}};classFactory{public:staticProduct*createProduct(conststd::stringtype){if(typeA)returnnewConcreteProductA();// ...returnnullptr;}};// 使用Product*pFactory::createProduct(A);// ...deletep;// 安全七、C11 及以后的补充7.1 override 关键字C11 引入的override关键字可以帮助我们发现虚函数相关的错误classBase{public:virtual~Base()default;virtualvoidfoo(){}};classDerived:publicBase{public:voidfoo()override{}// 明确标记这是重写// void bar() override; // 编译错误Base 中没有 bar()};7.2 final 关键字如果你不希望某个类被继承可以使用finalclassFinalClassfinal{// 禁止继承public:~FinalClass()default;// 不需要 virtual};// class Derived : public FinalClass {}; // 编译错误这样就不需要担心析构函数是否应该是 virtual 的了。八、总结场景析构函数建议原因类有 virtual 函数意图作为基类必须 virtual通过基类指针删除时保证完整析构类没有 virtual 函数不意图作为基类不要 virtual避免 vptr 开销纯抽象基类纯虚析构函数既保持抽象性又保证正确析构类不希望被继承使用 finalC11 最佳实践请记住带有多态性质的基类应该声明 virtual 析构函数。如果 class 带有任何 virtual 函数它就应该拥有一个 virtual 析构函数。如果 class 不是设计来做基类的就不要声明 virtual 析构函数。不要继承没有 virtual 析构函数的类如 STL 容器。一个virtual关键字的缺失可能导致内存泄漏、资源未释放甚至程序崩溃。在多态设计中virtual 析构函数不是可选项而是必选项。参考阅读《Effective C》第三版Scott Meyers《C Primer》第五版关于虚函数和动态绑定的章节C Core Guidelines: C.35