新闻详情

新闻详情

首页 / 资讯中心 / 详情

Effective C++ 条款09:绝不在构造和析构过程中调用 virtual 函数

发布时间:2026/9/2 19:42:00来源:尧图网络
Effective C++ 条款09:绝不在构造和析构过程中调用 virtual 函数
Effective C 条款09绝不在构造和析构过程中调用 virtual 函数多态是 C 面向对象编程的核心特性之一但有一个场景会让多态失效——那就是在构造函数和析构函数中调用 virtual 函数。这个看似反直觉的行为背后有着深刻的语言设计原理。一、一个令人困惑的例子假设我们正在设计一个股票交易记录系统classTransaction{public:Transaction(){logTransaction();// 调用 virtual 函数}virtualvoidlogTransaction()const{std::coutTransaction base log\n;}};classBuyTransaction:publicTransaction{public:virtualvoidlogTransaction()constoverride{std::coutBuyTransaction log\n;}};classSellTransaction:publicTransaction{public:virtualvoidlogTransaction()constoverride{std::coutSellTransaction log\n;}};现在当我们创建一个BuyTransaction对象时BuyTransaction bt;你期望的输出是什么BuyTransaction log但实际输出是Transaction base log发生了什么为什么调用的是基类版本的logTransaction而不是派生类重写的版本二、原理分析构造期间的类型变化2.1 对象的构造顺序在 C 中对象的构造遵循严格的顺序1. 分配内存 2. 调用基类构造函数 3. 设置 vptr 指向基类的 vtable 4. 执行基类构造函数体 5. 调用成员变量构造函数 6. 设置 vptr 指向派生类的 vtable 7. 执行派生类构造函数体关键洞察在基类构造函数执行期间对象的类型是基类而不是派生类。2.2 vptr 的切换过程阶段vptr 指向对象的动态类型进入Transaction()Transaction的 vtableTransaction执行Transaction()函数体Transaction的 vtableTransaction进入BuyTransaction()BuyTransaction的 vtableBuyTransaction执行BuyTransaction()函数体BuyTransaction的 vtableBuyTransaction因此当我们在Transaction()中调用logTransaction()时通过 vptr 查找 vtablevptr 指向的是Transaction的 vtablevtable 中logTransaction的条目指向Transaction::logTransaction调用的是基类版本2.3 为什么语言要这样设计这个设计不是 bug而是必要的选择。考虑如果允许调用派生类版本会发生什么classBuyTransaction:publicTransaction{public:BuyTransaction():price_(fetchPrice()){}virtualvoidlogTransaction()constoverride{std::coutBuy price: price_std::endl;}private:doubleprice_;};如果在Transaction()中调用的logTransaction()下降到BuyTransaction::logTransaction()price_还没有被初始化访问未初始化的成员 未定义行为C 的设计哲学是安全优先在构造期间对象被视为其当前正在构造的类型以避免访问未初始化的派生类成员。2.4 析构函数中的同样问题析构过程是构造的逆过程1. 执行派生类析构函数体 2. 设置 vptr 指向基类的 vtable 3. 析构成员变量 4. 执行基类析构函数体 5. 设置 vptr 继续指向基类的 vtable 6. 调用基类析构函数 7. 释放内存classTransaction{public:~Transaction(){logTransaction();// 同样调用的是 Transaction::logTransaction}virtualvoidlogTransaction()const{std::coutTransaction base log\n;}};在基类析构函数中派生类部分已经被销毁了此时如果调用派生类的 virtual 函数同样会访问已销毁的成员。三、更危险的场景间接调用有时候virtual 函数的调用不是直接的而是通过另一个函数间接发生的classTransaction{public:Transaction(){init();// 看起来安全}voidinit(){// ... 一些初始化代码 ...logTransaction();// 间接调用了 virtual 函数}virtualvoidlogTransaction()const0;// 纯虚函数};这种情况下如果logTransaction是纯虚函数某些编译器可能会在运行时检测到并终止程序。但更多情况下这会导致未定义行为。四、正确的解决方案4.1 方案一使用非 virtual 函数 参数传递将需要的信息通过参数传递给基类构造函数classTransaction{public:explicitTransaction(conststd::stringlogInfo){logTransaction(logInfo);// 调用非 virtual 函数}voidlogTransaction(conststd::stringlogInfo)const{// 记录日志Logger::log(Transaction: %s,logInfo.c_str());}};classBuyTransaction:publicTransaction{public:BuyTransaction(conststd::stringsymbol,intquantity,doubleprice):Transaction(createLogInfo(symbol,quantity,price)),symbol_(symbol),quantity_(quantity),price_(price){}private:staticstd::stringcreateLogInfo(conststd::stringsymbol,intquantity,doubleprice){returnBuy std::to_string(quantity) shares of symbol at std::to_string(price);}std::string symbol_;intquantity_;doubleprice_;};4.2 方案二延后初始化如果必须在构造后执行某些操作可以使用工厂方法或两阶段构造classTransaction{public:// 构造函数不做日志记录Transaction()default;// 提供一个显式的初始化方法virtualvoidinitialize(){logTransaction();}virtualvoidlogTransaction()const{std::coutTransaction base log\n;}};classBuyTransaction:publicTransaction{public:voidinitialize()override{// 先完成 BuyTransaction 特有的初始化price_fetchPrice();// 然后调用基类的初始化如果需要Transaction::initialize();}voidlogTransaction()constoverride{std::coutBuyTransaction log, priceprice_std::endl;}private:doubleprice_0.0;};// 使用工厂方法确保正确的初始化顺序std::unique_ptrTransactioncreateBuyTransaction(){autoptrstd::make_uniqueBuyTransaction();ptr-initialize();// 现在可以安全地调用 virtual 函数了returnptr;}4.3 方案三使用辅助函数推荐将日志逻辑提取到独立的、非 virtual 的辅助函数中classTransaction{public:Transaction(){logTransactionImpl();// 非 virtual 辅助函数}virtualvoidlogTransaction()const{logTransactionImpl();}protected:// 派生类可以重写这个来提供自定义日志信息virtualstd::stringgetLogInfo()const{returnBase transaction;}private:voidlogTransactionImpl()const{Logger::log(getLogInfo());}};classBuyTransaction:publicTransaction{public:BuyTransaction(conststd::stringsymbol,intqty):symbol_(symbol),quantity_(qty){}protected:std::stringgetLogInfo()constoverride{returnBuy std::to_string(quantity_) symbol_;}private:std::string symbol_;intquantity_;};注意这里getLogInfo()虽然也是 virtual 的但它是在派生类构造函数之后才被调用的通过logTransaction()所以是安全的。如果在基类构造函数中直接调用getLogInfo()仍然会有同样的问题。五、实际应用场景5.1 GUI 框架中的窗口初始化classWidget{public:Widget(){// 错误在构造函数中调用 virtual 函数// paint(); // 不要这样做}virtualvoidpaint()const0;};classButton:publicWidget{public:Button(conststd::stringlabel):label_(label){}voidpaint()constoverride{// 使用 label_ 绘制按钮drawRect();drawText(label_);}private:std::string label_;};正确做法classWidget{public:Widget()default;// 显式的初始化方法voidshow(){paint();// 现在安全了因为对象已完全构造}virtualvoidpaint()const0;};// 使用Buttonbtn(Click me);btn.show();// 在对象完全构造后调用 paint()5.2 数据库连接池中的连接初始化classDBConnection{public:DBConnection(conststd::stringconnStr):connStr_(connStr){// 不要在这里调用 virtual 的 onConnect()doConnect();// 非 virtual 的基础连接逻辑}virtualvoidonConnect(){// 派生类可以重写但在构造函数中不会下降到派生类}protected:voiddoConnect(){// 实际的数据库连接逻辑}std::string connStr_;};classMySQLConnection:publicDBConnection{public:MySQLConnection(conststd::stringhost,intport):DBConnection(buildConnStr(host,port)){}voidonConnect()override{// MySQL 特有的连接后初始化setCharacterSet(utf8mb4);}private:staticstd::stringbuildConnStr(conststd::stringhost,intport){returnhost:std::to_string(port);}};5.3 游戏开发中的角色创建classCharacter{public:explicitCharacter(conststd::stringname):name_(name){// 不要在这里调用 virtual 的 onSpawn()}// 在游戏循环中对象完全构造后调用voidspawn(){onSpawn();// 现在安全}virtualvoidonSpawn(){std::coutname_ spawned\n;}protected:std::string name_;};classWarrior:publicCharacter{public:Warrior(conststd::stringname):Character(name),weapon_(Sword){}voidonSpawn()override{Character::onSpawn();std::coutEquipped with weapon_std::endl;}private:std::string weapon_;};六、编译器的帮助现代编译器通常会对在构造函数/析构函数中调用 virtual 函数发出警告classBase{public:Base(){foo();// GCC/Clang 可能警告// call to pure virtual function during construction}virtualvoidfoo()0;};建议开启编译器的所有警告-Wall -Wextra并视警告为错误-Werror。七、总结场景行为风险构造函数中调用 virtual 函数调用当前正在构造的类的版本不调用派生类版本逻辑错误析构函数中调用 virtual 函数调用当前正在析构的类的版本可能访问已销毁的成员间接调用通过非 virtual 函数同样不会下降更隐蔽更难发现请记住在构造和析构期间不要调用 virtual 函数因为这类调用从不下降至 derived class。如果需要派生类提供信息给基类构造函数使用辅助函数并将信息作为参数传递。考虑使用工厂方法或两阶段构造来确保 virtual 函数在对象完全构造后被调用。开启编译器警告帮助发现这类问题。理解构造函数和析构函数中 vptr 的变化规律是掌握 C 对象模型的关键一步。这个规则看似限制了灵活性实则是语言为了保护你免受未定义行为的伤害而设置的安全网。参考阅读《Effective C》第三版Scott Meyers《Inside the C Object Model》Stanley B. LippmanC Core Guidelines: C.82
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

现在口碑好的AI写作辅助软件有哪些品牌?选对工具少走弯路 2026/9/2 19:41:46

现在口碑好的AI写作辅助软件有哪些品牌?选对工具少走弯路

每到期末、毕业答辩、课题申报阶段,很多学生都会陷入论文写作的困境:选题无从下手、大纲逻辑混乱、正文撰写耗时费力、参考文献格式错误、查重率居高不下、AIGC检测风险频发、学校排版标准复杂多变。依靠纯人工从零开始撰写、反复修改格式和降重&#xf…

阅读更多 →
谨慎选择!不是每款 AI 都能用来写学术论文,2026 导师信赖工具清单 2026/9/2 19:41:46

谨慎选择!不是每款 AI 都能用来写学术论文,2026 导师信赖工具清单

每年毕业季,无数同学深陷论文难题:开题毫无思路、搭建框架耗费数日、初稿逻辑松散、查重标红泛滥、AI检测超标、格式反复被导师驳回。面对海量的AI工具,不少学生抱着试一试的心态选择通用型大模型,却往往发现其存在诸多致命短板。…

阅读更多 →
物料领取流程详解:如何高效完成物料领取? 2026/9/2 19:41:46

物料领取流程详解:如何高效完成物料领取?

一、为什么要重视物料领取流程物料领取看似只是“领一下东西”,但在生产、仓储、行政等场景中,它往往是连接计划、库存和现场作业的关键环节。流程不清晰,容易出现领错料、重复领用、登记缺失、库存账实不符等问题;流程过于繁琐&a…

阅读更多 →
EBNF可视化解析:让文法结构一目了然 2026/9/2 19:41:46

EBNF可视化解析:让文法结构一目了然

简介:扩展巴科斯范式可视化工具(EBNF Visualizer)是一款面向编译原理学习者、编程语言研究者与工具开发者的开源程序,能够解析 EBNF 规则,将其转化为直观的语法图,辅助理解复杂的文法结构与推导关系。资源包…

阅读更多 →
2026蓝牙耳机选购指南:场景、参数与形态全解析 2026/9/2 19:41:46

2026蓝牙耳机选购指南:场景、参数与形态全解析

写这篇测评之前,先解决一个核心矛盾:2026 年的蓝牙耳机市场已经不是“预算定型号”,而是“场景定技术”。同样的 500 元预算,你可以买到支持 LDAC 高解析的降噪入耳,也可以买到佩戴无感的开放式耳挂,还可以…

阅读更多 →
NexArm ROS分拣沙盘联动OpenClaw:机械臂具身智能闭环实践 2026/9/2 19:38:46

NexArm ROS分拣沙盘联动OpenClaw:机械臂具身智能闭环实践

这次我们来看一套把桌面机械臂、ROS 和 AI 智能体真正串起来的分拣沙盘方案:NexArm ROS 分拣沙盘联动 OpenClaw AI 智能体。这套方案的价值不在硬件多贵,而在“感知 → 决策 → 执行”这条链路完整闭环。视觉负责看,OpenClaw 负责想&#xff…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞