zoukankan      html  css  js  c++  java
  • [Effective C++ 004]绝不在构造和析构过程中调用virtual函数

    引言:什么是虚函数

    其实从虚函数说起,就得追溯到虚函数的定义:
    简单的来说,虚函数的定义可以表述为以下的概念:
    定义:在某基类中声明为 virtual 并在一个或多个派生类中被重新定义的成员函数
    语法:virtual 函数返回类型 函数名(参数表) { 函数体 }
    用途:实现多态性,通过指向派生类的基类指针,访问派生类中同名覆盖成员函数
       虚函数必须是基类的非静态成员函数,其访问权限可以是protected或public,在基类的类定义中定义虚函数的一般形式:
    形式:

    1 class 基类名{
    2   .......
    3   virtual 返回值类型 将要在派生类中重载的函数名(参数列表);
    4 };

    至于虚函数使用的地方,这个可以根据自己的意愿来定义,毕竟使用场景的规定是有人来决定的。

    一、绝不在构造和析构过程中调用virtual函数

    1、构造函数

     1 class Transaction {                            //所有交易的base class
     2 public:
     3    Transaction();
     4     virtual void logTransaction() const = 0;     //做出一份因类型不同而不同的日志记录(log entry)
     5     ...
     6 };
     7 Transaction::Transaction()                     //base class构造函数之实现
     8 {
     9     ...
    10    logTransaction();                           //最后动作是志记这笔交易
    11 }
    12 class BuyTransaction: public Transaction {     //derived class
    13 public:
    14    virtual void logTransaction() const;        //志记(log)此型交易
    15    ...
    16 };
    17 class SellTransaction: public Transaction {    //derived class
    18 public:
    19    virtual void logTransaction() const;        //志记(log)此型交易
    20    ...
    21 };

    如果我们执行如下的操作:

    BuyTransaction b;
    

    推导它的执行过程:
    会有一个BuyTransaction构造函数被调用,但首先会调用Transaction构造函数(因为派生类对象内的基类成分会在派生类自身成分被构造之前先构造妥当)。Transaction构造函数的最后一行调用virtual函数logTransaction被调用的logTransaction是Transaction(基类)内的版本,不是BuyTransaction内的版本。基类构造期间virtual函数绝不会下降到派生类阶层。
    换句话就是:在base class构造函数执行期间,virtual函数不是virtual函数。

    理由①---直接原因
    由于基类构造函数的执行更早于派生类构造函数,当基类构造函数执行时派生类的成员变量尚未初始化。如果此期间调用的virtual函数下降至派生类阶层,派生类的函数几乎必然取用局部成员变量,而那些成员变量尚未初始化。这明显违反了“要求使用对象内部尚未初始化的成分”!

    理由②---根本原因
    在派生类对象的基类构造期间,对象的类型是基类而不是派生类。不只virtual函数会被编译器解析至基类,若使用运行期类型信息(※附1),也会把对象视为基类类型。本例之中,当Transaction构造函数正执行起来打算初始化“BuyTransaction对象内的基类成分”时,该对象的类型是Transaction。对象在派生类构造函数开始执行前不会成为一个派生类对象。

    对于析构函数,也是同样的,一旦派生类析构函数开始执行,对象内的派生类成员变量便呈现未定义值,所以C++的任何部分包括virtual函数,dynamic_casts等等也就那么看待它。

    二、避免“构造函数或析构函数运行期间调用virtual函数” 

      可能看了上面的示例,就会有一种只要编译出错了我不就发现了这个问题的所在的想法,但是事实上,一个大坑在等着你跳!

     1  class Transaction {
     2  public:
     3    Transaction( )                            //调用non-virtual...
     4    { init( ); }
     5    virtual void logTransaction() const = 0;
     6    ...
     7  private:
     8    void init()
     9    {
    10     ...
    11     logTransaction();                       //这里调用virtual!
    12   }
    13 };

    如果单纯从执行的角度去看,这段代码编译link什么的完全没有问题,但由于logTransaction是Transaction内的一个纯虚函数,当纯虚函数被调用,大多执行系统会终止程序,后对此结果发出一个信息(warning什么的)。

    修正方案1(治标不治本):
    在Transaction内为logTransaction函数弄一份实现代码
    但是比较纠结的是,这个时候的纯虚函数已经实装代码了,就已经不在是纯虚函数了。

    修正方案2(治本的):
    一种做法是在class Transaction内将logTransaction函数改为non-virtual,然后要求派生类构造函数传递必要信息给Transaction构造函数,而后那个构造函数便可安全地调用non-virtual logTransaction。

     1  class Transaction {
     2  public:
     3    explicit Transaction(const std::string& logInfo);
     4    void logTransaction(const std::string& logInfo) const; //如今是个non-virtual函数
     5    ...
     6  };
     7  Transaction::Transaction(const std::string& logInfo)
     8  {
     9    ...
    10    logTransaction(logInfo);                              //如今是个non-virtual调用
    11  }
    12  class BuyTransaction: public Transaction {
    13  public:
    14    BuyTransaction( parameters ) : Transaction(createLogString( parameters ))//将log信息传给base class构造函数
    15    { ... }
    16    ...
    17  private:
    18    static std::string createLogString( parameters );
    19  };

    因为无法使用virtual函数从基类向下调用,在构造期间,你可以借由“令派生类将必要的构造信息向上传递至基类构造函数”替换加以弥补。换种说法,构造函数不能确定类型,所以就让他失去virtual功能就调用自身的函数。

    三、JAVA与C++中,构造函数调用虚函数的区别
       JAVA如果在构造函数中调用虚拟函数的话,是可以编译通过的,也不会出现运行期错误,但他的运行结果也许不是你想要的。
       在JAVA当中,由于是运行期绑定,而构造函数执行的虚拟函数将是衍生类中的函数(假如衍生类对该虚拟函数进行了覆盖的话),但这里我们会有疑问了,构造函数是从父类开始构建的,也就是此时衍生类还未构造出来,所以如果此时执行衍生类的函数无疑是不安全的。所以JAVA中的做法不能说没有问题的。
       java把类型的信息写在class文件里了 其实是可以区分不同的class的,其实是可以区分不同的class的 ;c++没有,只是一段内存结构(c++的虚函数要一个指针的偏移量,基类还是派生类不一样,所以要确定类)

    ※附1 RTTI

    1、定义

       运行期类型信息(runtime type information),其实也可以叫RTTI(Run-Time Type Identification),可以在运行期获得数据类型或类(class)的信息。

    2、RTTI的产生:

            先来看一段代码

    1 Figure *p; 
    2 p = new Circle(); 
    3 Figure &q = *p; 

            在执行时﹐p 指向一个对象﹐但要得知此对象的类信息﹐就有困难了。同样欲得知q 引用对象的信息﹐也无法得到。

            在C++ 环境中﹐头文件含有类和类的定义等。但是﹐这些资料只供编译器使用﹐编译完毕后并未留下来﹐所以在执行时期﹐无法得知对象的类别资料﹐包括类别名称、资料成员名称与型态、函数名称与型态等等,RTTI为了解决这种问题而诞生了。 
            随着应用场合之不同﹐所需支持的RTTI范围也不同。具体的应用场景,可以参考http://wenku.baidu.com/view/d252ffc52cc58bd63186bd39.html,这里不再详述。

    3、简介

         一般在面向对象对象的程序设计中,大多都主张使用虚拟成员函数,而不用 RTTI 机制。但是,在很多情况下,虚拟函数无法克服本身的局限。涉及到处理异类容器和根基类层次(如 MFC)时,不可避免要对对象类型进行动态判断,也就是动态类型的侦测。如何确定对象的动态类型呢?答案是使用RTTI中的运算符:typeiddynamic_cast。 

      (1)typeid操作符,返回指针和引用所指的实际类型;

      (2)dynamic_cast操作符,将基类类型的指针或引用安全地转换为派生类型的指针或引用。

                 这个就要涉及到C++的四种强制数据转换类型了:dynamic_cast,const_cast,static_cast,reinterpret_cast。

                 ①dynamic_cast用于将一个指向派生类的基类指针或引用转换为派生类的指针或引用(只能用于含有虚函数的类)!

     dynamic_cast < ObjectType-ID* > ( ObjectType*)
    

             如果要成功地将ObjectType*转换为ObjectType-ID*,则必须存在这种可能性才可以,也就是说ObjectType*指向的对象要"包含"ObjectType-ID*指向的对象,如此才能够成功.例:

    1 A* pA = new B;
    2 C* pC = new C; 
    3 B* pB  = dynamic_cast<B*>pA;// OK.
    4 C* pC  = dynamic_cast<C*>pA;// Fail.编译可以通过,但是pC为NULL

                ②转换操作符在执行类型转换时首先将检查能否成功转换,如果能成功转换则转换之,如果转换失败,如果是指针则反回一个0值,如果是转换的是引用,则抛出一个bad_cast异常。bad_cast异常可以参照http://technet.microsoft.com/zh-cn/library/82f1eehz

                ③RTTI的常见使用场合有四﹕例外处理(exceptions handling)--JAVA、动态转型态(dynamic casting) 、模块整合、以及对象I/O

  • 相关阅读:
    CSS学习笔记 -- 组合选择符
    CSS学习笔记 -- Position(定位)
    CSS学习笔记 -- CSS 列表
    CSS学习笔记 -- 多重样式优先级深入概念
    CSS学习笔记 -- ID和Class
    HTML学习笔记 -- XHTML
    HTML学习笔记 -- 速查列表
    HTML学习笔记 -- <div> 和<span>
    HTML学习笔记 -- HTML <head>
    HTML学习笔记 -- HTML 中 href、src 区别
  • 原文地址:https://www.cnblogs.com/hustcser/p/2813872.html
Copyright © 2011-2022 走看看