news 2026/6/10 7:51:48

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Effective C++ 条款07:为多态基类声明 virtual 析构函数

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

在 C++ 的多态体系中,基类指针指向派生类对象是一种常见的设计模式。但如果基类的析构函数不是 virtual 的,删除这个指针时可能会引发灾难性的后果。今天我们来深入剖析这个问题。

一、问题的引入

假设我们有一个表示时间记录的基类:

classTimeKeeper{public:TimeKeeper(){}~TimeKeeper(){std::cout<<"TimeKeeper destructor\n";}virtualvoidrecordTime()=0;};classAtomicClock:publicTimeKeeper{public:AtomicClock(){data_=newchar[1024];}~AtomicClock(){delete[]data_;std::cout<<"AtomicClock destructor\n";}voidrecordTime()override{/* ... */}private:char*data_;};

现在,我们通过工厂函数获取一个对象:

TimeKeeper*getTimeKeeper(){returnnewAtomicClock();}

然后在使用完毕后删除它:

TimeKeeper*ptk=getTimeKeeper();// 使用 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的执行过程:

  1. 通过ptk的 vptr 找到 vtable
  2. vtable 中指向~AtomicClock()
  3. 执行~AtomicClock()—— 释放data_
  4. 自动调用~TimeKeeper()—— 释放基类部分

2.3 非 virtual 析构函数的静态绑定

如果析构函数不是 virtual:

  1. 编译器根据指针的静态类型TimeKeeper*)决定调用哪个析构函数
  2. 直接调用TimeKeeper::~TimeKeeper()
  3. AtomicClock::~AtomicClock()永远不会被调用
内存布局示意: [ AtomicClock 对象 ] +------------------+ | TimeKeeper 部分 | <-- ptk 指向这里 +------------------+ | AtomicClock 数据 | <-- 这部分永远不会被析构! | (data_ 等) | +------------------+

三、解决方案:virtual 析构函数

将基类的析构函数声明为 virtual:

classTimeKeeper{public:TimeKeeper(){}virtual~TimeKeeper(){std::cout<<"TimeKeeper destructor\n";}virtualvoidrecordTime()=0;};

现在重新运行:

TimeKeeper*ptk=getTimeKeeper();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*ps=newSpecialString("Hello");deleteps;// 未定义行为!std::string 的析构函数不是 virtual

STL 容器类(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::vector<IPlugin*>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::vector<Component*>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::string&type){if(type=="A")returnnewConcreteProductA();// ...returnnullptr;}};// 使用Product*p=Factory::createProduct("A");// ...deletep;// 安全

七、C++11 及以后的补充

7.1 override 关键字

C++11 引入的override关键字可以帮助我们发现虚函数相关的错误:

classBase{public:virtual~Base()=default;virtualvoidfoo(){}};classDerived:publicBase{public:voidfoo()override{}// 明确标记这是重写// void bar() override; // 编译错误!Base 中没有 bar()};

7.2 final 关键字

如果你不希望某个类被继承,可以使用final

classFinalClassfinal{// 禁止继承public:~FinalClass()=default;// 不需要 virtual};// class Derived : public FinalClass {}; // 编译错误!

这样就不需要担心析构函数是否应该是 virtual 的了。

八、总结

场景析构函数建议原因
类有 virtual 函数,意图作为基类必须 virtual通过基类指针删除时保证完整析构
类没有 virtual 函数,不意图作为基类不要 virtual避免 vptr 开销
纯抽象基类纯虚析构函数既保持抽象性,又保证正确析构
类不希望被继承使用 finalC++11 最佳实践

请记住

  • 带有多态性质的基类应该声明 virtual 析构函数。
  • 如果 class 带有任何 virtual 函数,它就应该拥有一个 virtual 析构函数。
  • 如果 class 不是设计来做基类的,就不要声明 virtual 析构函数。
  • 不要继承没有 virtual 析构函数的类(如 STL 容器)。

一个virtual关键字的缺失,可能导致内存泄漏、资源未释放,甚至程序崩溃。在多态设计中,virtual 析构函数不是可选项,而是必选项。


参考阅读

  • 《Effective C++》第三版,Scott Meyers
  • 《C++ Primer》第五版,关于虚函数和动态绑定的章节
  • C++ Core Guidelines: C.35
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/10 7:51:39

合肥合豚AI硬件方案:专为智能售货柜厂商定制的无人零售接口套件

品牌定位&#xff1a;专注智能售货柜的AI硬件接口套件合肥合豚网络科技有限公司是一家聚焦人工智能行业解决方案的企业&#xff0c;其无人零售硬件服务主要面向智能售货柜、自动售货机等终端生产厂商。品牌不是做整机成品&#xff0c;而是围绕硬件接口集成与AI模块化配套&#…

作者头像 李华
网站建设 2026/6/10 7:48:58

2026年合肥AI产品推广:这些智能工具你还没试过?

2026年&#xff0c;人工智能已不再是科幻电影中的概念&#xff0c;而是合肥中小企业实现降本增效、精准获客的“标配工具”。然而&#xff0c;面对市场上琳琅满目的AI产品&#xff0c;许多本地企业主陷入了另一个困境&#xff1a;工具太多、落地太难、效果无法量化。从AI大模型…

作者头像 李华
网站建设 2026/6/10 7:47:37

2026 免费视频去水印工具实测|手机 / 网页 / 客户端工具选型指南!

2026 年短视频创作、日常存片需求持续上涨&#xff0c;各类平台自带水印、博主浮动字幕、角落 logo 水印成为很多人的困扰。不同使用者需求差异明显&#xff0c;普通用户只想随手保存短视频&#xff0c;不需要复杂操作&#xff1b;自媒体创作者要批量处理素材&#xff0c;对画质…

作者头像 李华
网站建设 2026/6/10 7:43:49

第五篇:物理AI

引言&#xff1a;物理人工智能的自然成果到此为止&#xff0c;我们已经阐述了自动驾驶汽车的进步&#xff0c;以及工业景观在宏观层面的更新。你可能会认为&#xff0c;这些是未来五年里物理人工智能最相关的领域&#xff0c;但在阅读本文之后&#xff0c;这种看法或许就会改变…

作者头像 李华
网站建设 2026/6/10 7:37:50

如何用风扇控制工具让老款Intel Mac重获新生:3个实用场景解析

如何用风扇控制工具让老款Intel Mac重获新生&#xff1a;3个实用场景解析 【免费下载链接】smcFanControl Control the fans of every Intel Mac to make it run cooler 项目地址: https://gitcode.com/gh_mirrors/smc/smcFanControl 你的Intel Mac是不是一跑大程序就烫…

作者头像 李华
网站建设 2026/6/10 7:36:09

SCI 论文机制图、信号通路图怎么做:又快又规范,还不被审稿人挑刺

SCI 论文机制图、信号通路图怎么做&#xff1a;又快又规范&#xff0c;还不被审稿人挑刺机制图大概是科研配图里最磨人的一种。它要你同时过两关&#xff0c;而且两关都不好过。 第一关是科学对不对——谁激活谁、谁抑制谁、在胞膜还是胞核发生&#xff0c;这些定位和方向错一个…

作者头像 李华