当前位置: 首页 > backend >正文

pure virtual function call--虚函数调用的错误

前几天我们项目刚刚解决了一个pure virtual function call引起的stopship的bug,乘热打铁,学习总结一下。

一、理论上case

当一个纯虚函数被调用到时,vc++的debug模式下会弹出这么一个对话框:

这里没拷贝到。

然后就是crash了。

在网上找了一下,发现已经有人对此作了详细的介绍:"Pure Virtual Function Called": An Explanation. 这是一篇相当全面的文章,从纯虚函数抽象基类讲起,介绍了对象模型中vptr及vtable的概念以及他们的构造析构过程。有了这些基础,作者然后列出了5中可能出现"pure virtual function call"的情况,其实可以总结为两种:

  • 在基类的构造函数或析构函数中直接或间接的调用纯虚函数
    举个在基类构造函数中间接调用纯虚函数的例子:
    class Base
    {
    public:Base(){callVirtual();}void callVirtual(){virtualFunc();}virtual void virtualFunc() = 0;
    };
    class Derived: public Base
    {
    public:virtual void virtualFunc(){}
    };Derived d; //构造过程中调用到纯虚函数
  • 通过野指针调用到虚函数 
      还是上面那个例子,但是不在基类构造函数中调用callVirtual:
    Derived* pD = new Derived;
    Base* pB = pD;
    delete pD;
    pB->virtualFunc();

其实对于第一种情况,如果你在基类构造函数或析构函数中直接调用纯虚函数,编译器应该能捕捉到这个错误;间接的调用虽然编译器无法检测到,但是由于Scott同学在<Effective C++>中的大力宣传:Item 9: Never call virtual functions during construction or destruction,这种情况发生的概率应该比较小,况且即使发生了,排起错来相对比较简单。

而对于第二种情况,虽然野指针的行为是未定义的,但就我所了解的,我们一般会得到一个"access violation",而不是"pure virutal function call" :

二、现实中的case

我们在现实中遇到的情况会比上面提到的复杂一些:一个子类对象在析构的过程中遇到异常而未完全销毁,从而遗留下一个"次品"对象,程序继续使用此次品对象而调用到纯虚函数:

class Base
{
public:~Base(){throw 0;}  // . . . a)virtual void virtualFunc() = 0;
};
class Derived: public Base
{
public:virtual void virtualFunc(){}
};Base* pB = new Derived;__try
{delete pB;         // . . . b)pB = NULL;
}
__except(EXCEPTION_EXECUTE_HANDLER){
}pB->virtualFunc();       // . . . c) 

在b)处析构Derived对象的时候,在其基类析构函数中a)处抛出了异常,而此时,因为Derived的析构函数已经调用完毕,该对象中的vptr已经指向基类的vtable,从而形成了一个按照正常流程无法构造出来的"次品"对象,当你使用该对象在c)处来调用virtualFunc时,自然导致 "pure virtual function call"的错误。

需要注意的是,这里,你遇到  "pure virtual function call""的时候,可能离真正的出错点,也就是析构函数中抛出异常的点已经很远了。所以这种情况相对来讲比较难调试。

三、析构函数与异常

好吧,我知道你忍了好久了,你早想喊出来:"你本来就不该在析构函数中抛出异常!",就像Scott同学说的:Item 11:  Prevent exceptions from leaving destructors;就像C++ FAQ中说的:Never throw an exception from a destructor.

虽然,也有人站出来说,there is nothing wrong with throwing destructors,但我还是支持你的观点,我们的确不应该在析构函数中抛出异常,不然,我们不得不面对以下两个严重的问题:

  • 二次异常导致程序退出;
  • 遗留下来的未完全销毁的对象与未完成的工作导致的后续问题
    pure virtual function call就是这种情况。

但是理想与现实总是有差距的,有些事情,你总得面对:

  1. 10多年,几百万行代码,无数人维护过的code base,谁都不敢保证是否某个析构函数会直接或间接的抛出异常。
  2. 或许我们应该对第1种情况,也就是我们自己的代码负责,对原有代码做一次全面的检查,并保证之后的代码不会在析构函数中抛出异常。可是即使如此,如果我们在析构函数中调用了第三方的库函数,而该函数会抛出异常呢?
  3. 即使我们调用到的函数( 包括自己的和第三方的)不会显式的抛出异常,当我们用SEH处理异常时,如果代码中出现除0操作,access violation等,都还是会被当做异常捕获的。
  4. 那么如果在每个non-trivial的析构函数中都加上异常处理呢?这样代码未免也太ugly了。况且在保证不主动抛出异常的前提下,这样的代码只是以防万一,意义不是很大。

所以,要在析构函数中完全避免异常还是蛮纠结的。Herb Sutter曾就C++语言提出过一个提议:让析构函数无法抛出异常,从语言级别上去解决,但被Bjarne Stroustrup,  Andy Koenig等人否决了。因为这会导致原有程序的行为不一致,况且在极少数的情况下,我们还是希望能抛出异常来的。

这里我的结论就是:不主动,不拒绝。不主动是指原则上杜绝在析构函数中抛出异常;不拒绝是指不强制在析构函数中去swallow莫须有的异常,而是把可能的问题暴露出来才便于解决。比如上面提到的那个现实中的例子,通过抛出异常,然后调用pure virtual function call把问题暴露出来,我们就可以着手研究为什么会抛出异常,如何处理不让其抛出异常~~~


http://www.xdnf.cn/news/11189.html

相关文章:

  • MPP文件怎么打开?五种方法总有最合适!
  • 搭建免费的ftp服务器
  • 别人总结的英语网站大全
  • uploadify 火狐 php,ThinkPHP 解决使用uploadify 在Firefox浏览器上传图片出现HTTP 500报错...
  • C++使用CriticalSection前需要先初始化
  • ResultSetMetaData用法介绍
  • mysql2005中文版_SQL Server 2005 sp4 简体中文完整免费版下载
  • 去AV片马赛克,开发者被抓了
  • 轻论坛StartBBS、YouBBS、Xiuno对比
  • C++表达式括号匹配
  • 安卓(Android) 刷机教程(任何机型、小米、华为等等)
  • #Linux存储的基本管理
  • Android定时器:实现定时的几种方法
  • 一刀999?怎样让你的 iPhone 手游也拥有开挂般的体验
  • 这是我见过最全的权限系统设计方案!
  • 一文读懂AlphaGo Zero算法
  • JRTPLIB使用实例
  • 颜色代码大全
  • 反向链接是什么(反向链接有什么用)
  • Android App开发基础
  • Windows安装包制作指南——Advanced Installer的使用
  • 指令系统超全知识点详解
  • 计算机专业的N种就业方向!条条大路通高薪!(非常详细)零基础入门到精通,收藏这一篇就够了
  • 质量管理体系之缺陷管理
  • 上海市四金及税后工资计算器
  • 程序员接私活一定要知道的事情,我走的弯路你们都别走了
  • Filter及FilterChain的使用详解
  • npm : 无法将“npm”项识别为 cmdlet、函数、脚本文件或可运行程序的名称
  • 全面了解苹果WWDC2020,一篇文章足矣
  • 数据库题库