分享
 
 
 

C++箴言:避免析构函数调用虚函数

王朝c/c++·作者佚名  2008-06-01
窄屏简体版  字體: |||超大  

假如你已经从另外一种语言如C#或者java转向了C++,你会觉得,避免在类的构造函数或者析构函数中调用虚函数这一原则有点违反直觉。但是在C++中,违反这个原则会给你带来难以预料的后果和无尽的烦恼。

正文

我想以重复本文的主题开篇:不要在类的构造或者析构函数中调用虚函数,因为这种调用不会如你所愿,即使成功一点,最后还会使你沮丧不已。假如你以前是一个Java或者C#程序员,请密切注重本节的内容-这正是C++与其它语言的大区别之一。

假设你有一个为股票交易建模的类层次结构,例如买单,卖单,等等。为该类交易建立审计系统是非常重要的,这样的话,每当创建一个交易对象,在审计登录项上就生成一个适当的入口项。这看上去不失为一种解决该问题的合理方法:

class Transaction {// 所有交易的基类

public:

Transaction();

virtual void logTransaction() const = 0;//建立依靠于具体交易类型的登录项

...

};

Transaction::Transaction() //实现基类的构造函数

{

...

logTransaction(); //最后,登录该交易

}

class BuyTransaction: public Transaction {

// 派生类

public:

virtual void logTransaction() const; //怎样实现这种类型交易的登录?

...

};

class SellTransaction: public Transaction {

//派生类

public:

virtual void logTransaction() const; //怎样实现这种类型交易的登录?

...

};

现在,请分析执行下列代码调用时所发生的事情:

BuyTransaction b;

很明显,一个BuyTransaction类构造器被调用。但是,首先调用的是Transaction类的构造器-派生类对象的基类部分是在派生类部分之前被构造的。Transaction构造器的最后一行调用了虚函数logTransaction,但是希奇的事情正是在此发生的。被调用函数logTransaction的版本是Transaction中的那个,而不是BuyTransaction中的那个-即使现在产生的对象的类型是BuyTransaction,情况也是如此。在基类的构造过程中,虚函数调用从不会被传递到派生类中。代之的是,派生类对象表现出来的行为好象其本身就是基类型。不规范地说,在基类的构造过程中,虚函数并没有被"构造"。

对上面这种看上去有点违反直觉的行为可以用一个理由来解释-因为基类构造器是在派生类之前执行的,所以在基类构造器运行的时候派生类的数据成员还没有被初始化。假如在基类的构造过程中对虚函数的调用传递到了派生类,派生类对象当然可以参照引用局部的数据成员,但是这些数据成员其时尚未被初始化。这将会导致无休止的未定义行为和彻夜的代码调试。沿类层次往下调用尚未初始化的对象的某些部分本来就是危险的,所以C++干脆不让你这样做。

更多内容请看C/C++技术专题专题,或

事实上还有比这更具基本的要求。在派生类对象的基类对象构造过程中,该类的类型是基类类型。不仅虚函数依靠于基类,而且使用运行时刻信息的语言的相应部分(例如,dynamic_cast(参见Item 27)和typeid)也把该对象当基类类型对待。

在我们的示例中,当Transaction的构造器正运行以初始化BuyTransaction对象的基类部分时,该对象是Transaction类型。

在C++编程中处处都这样处理,这样做很有意义:在基类对象的初始化中,派生类对象BuyTransaction相关部分并未被初始化,所以其时把这些部分当作根本不存在是最安全的。 在一个派生类对象的构造器开始执行之前,它不会成为一个派生类对象的。

在对象的析构期间,存在与上面同样的逻辑。一旦一个派生类的析构器运行起来,该对象的派生类数据成员就被假设为是未定义的值,这样以来,C++就把它们当做是不存在一样。一旦进入到基类的析构器中,该对象即变为一个基类对象,C++中各个部分(虚函数,dynamic_cast运算符等等)都这样处理。

在上面的示例代码中,Transaction构造器直接调用了一个虚函数-这明显地破坏了本文所强调的原则。这种破坏性非常轻易觉察,一些编译器对此发出警告(注重:另外一些编译器并不给出警告,请参考Item 53有关警告的讨论)。即使没有给出警告,该问题在代码运行时刻也是相当明显的,因为函数logTransaction是类Transaction中的纯虚函数。除非该函数被定义了(可能性不太大,但确实存在这种情况-参见Item 34),否则程序不会进行链接:链接器没法找到Transaction::logTransaction的必需的实现代码。

在类的构造或者析构函数中进行虚函数调用并非总是那么轻易被发现。假如Transaction类有多个构造器且其中每个必须执行一些相同的任务,也许只有优秀的软件工程师才能够避免代码的重复,这可以通过把相同的初始化代码(包括调用logTransaction)放到一个私有的且非虚的初始化函数中实现,譬如下面的init:

class Transaction {

public:

Transaction()

{ init(); } //调用非虚函数...

virtual void logTransaction() const = 0;

...

PRivate:

void init()

{

...

logTransaction(); //注重这里调用了虚函数

}

};

这段代码从概念上看与前面的版本一样,但是却更具有潜在的危险性,因为典型情况下,该代码会被成功地编译与链接。在这种情况下,因为logTransaction是Transaction类中的纯虚函数,绝大多数的运行时刻系统会在该纯虚函数被调用时(典型地是通过发送一个带有调用该函数意义的消息实现)流产掉程序。然而,假如logTransaction是一个"正常的"虚函数(也就是,不是纯虚的),并在Transaction中有它的实现部分,该代码段将被调用而且程序会顺利地运行一段时间,这让你考虑为什么在一个派生类对象被创建时调用了logTransaction的错误版本。唯一避免该问题的办法是确保没有任何一个构造器或者析构器在正被产生或毁坏的对象上调用了虚函数,而且所有其调用的函数都要遵循同样的约束。

但是,每当有一个对象在Transaction类层次结构中产生时,如何保证调用的是logTransaction的正确版本呢?很明显,从Transaction的构造器中调用对象上的虚函数是错误的做法。

有几种不同的办法可以解决这个问题。一种办法就是在Transaction中把函数logTransaction改变为一个非虚函数,然后要求派生子类的构造器要把必要的登录信息传递给Transaction的构造器。如此以来,上面的函数就能够安全地调用非虚函数logTransaction了。如下所示:

class Transaction {

public:

eXPlicit Transaction(const std::string& logInfo);

void logTransaction(const std::string& logInfo) const;//现在是一个非虚函数

...

};

Transaction::Transaction(const std::string& logInfo)

{

...

logTransaction(logInfo);// 现在调用的是一个非虚函数

}

class BuyTransaction: public Transaction {

public:

BuyTransaction( parameters )

:Transaction(createLogString(parameters)) { ... } //把登录信息传送给基类的构造函数

...

private:

static std::string createLogString( parameters );

};

换句话说,既然在基类的构造函数中不能沿着类的继续层次往下调用虚函数,你可以通过在派生类中沿着类的层次结构把必要的构造信息传递到基类的构造器中来补偿这一点。

在这个例子中,请注重BuyTransaction中私有静态函数createLogString的使用方法。通过使用帮助函数来创建一个值并把它传递到基类构造器中,这种方式比起在成员初始化列表中实现基类所需的操作要更方便和更具有可读性。这里我们把该函数创建为static型,这对于偶然参照引用一下刚产生的BuyTransaction对象的尚未初始化的数据成员是没有危险的。这一点很重要,因为那些数据成员还处于一种未定义的状态中,这一事实解释了为什么在基类的构造或者析构函数中对于虚函数的调用不能首先传递到派生子类中去。

结论

不要在类的构造或者析构过程中调用虚函数,因为这样的调用永远不会沿类继续树往下传递到子类中去。

更多内容请看C/C++技术专题专题,或

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有