避免构造/析构函数调用虚函数(转)

不要在类的构造或者析构函数中调用虚函数,因为这种调用不会如你所愿,即使成功一点,最后还会使你沮丧不已。如果你以前是一个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++干脆不让你这样做。
        
事实上还有比这更具基本的要求。在派生类对象的基类对象构造过程中,该类的类型是基类类型。不仅虚函数依赖于基类,而且使用运行时刻信息的语言的相应部分(例如,dynamic_cast和typeid)也把该对象当基类类型对待。在我们的示例中,当Transaction的构造函数正运行以初始化BuyTransaction对象的基类部分时,该对象是Transaction类型。

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

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

  在上面的示例代码中,Transaction构造函数直接调用了一个虚函数,这明显地破坏了本文所强调的原则。这种破坏性非常容易觉察,一些编译器对此发出警告(注意:另外一些编译器并不给出警告,即使没有给出警告,该问题在代码运行时刻也是相当明显的,因为函数logTransaction是类Transaction中的纯虚函数。除非该函数被定义了(可能性不太大,但确实存在这种情况),否则程序不会进行链接:链接器没法找到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对象的尚未初始化的数据成员是没有危险的。这一点很重要,因为那些数据成员还处于一种未定义的状态中,这一事实解释了为什么在基类的构造或者析构函数中对于虚函数的调用不能首先传递到派生子类中去。

结论

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

时间: 2024-10-02 14:48:22

避免构造/析构函数调用虚函数(转)的相关文章

构造函数、析构函数调用虚函数

昨天笔试的时候碰到一个很有意思的题目,大体如下: class Parent { public:     Parent()     {         doit();     }     ~Parent()     {         doit();     }     virtual void doit()     {         cout << "I'm Parent!" << endl;     } }; class Child: public Pare

c++ 类内部函数调用虚函数

做项目的过程中,碰到一个问题. 问题可以抽象为下面的问题: 普通人吃饭拿筷子,小孩吃饭拿勺子. class People { public: void eat() { get_util_to_eat(); } virtual void get_util_to_eat() { std::cout << "People get chopsticks" << std::endl; } }; class Children : public People { public

继承中的构造析构函数调用顺序

子类构造函数必须堆继承的成员进行初始化: 1. 通过初始化列表或则赋值的方式进行初始化(子类无法访问父类私有成员) 2. 调用父类构造函数进行初始化 2.1  隐式调用:子类在被创建时自动调用父类构造函数(只能调用父类的无参构造函数和使用默认参数的构造函数) 2.2  显示调用:在初始化列表调用父类构造函数(适用所有的父类构造函数) #include <iostream> #include <string> using namespace std; class PParent //

【核心基础】虚函数

本节研究虚函数的相关问题: 虚函数表 无继承 代码片段 class Animal { public: Animal(int age) : _age(age) { } virtual void f() { cout << "Animal::f " << _age << endl; } virtual void g() { cout << "Animal::g " << _age << endl;

C++ Primer 学习笔记33_面向对象编程(4)--虚函数与多态(一):多态、派生类重定义、虚函数的访问、 . 和-&gt;的区别、虚析构函数、object slicing与虚函数

C++ Primer学习笔记33_面向对象编程(4)--虚函数与多态(一):多态.派生类重定义.虚函数的访问. . 和->的区别.虚析构函数.object slicing与虚函数 一.多态 多态可以简单地概括为"一个接口,多种方法",前面讲过的重载就是一种简单的多态,一个函数名(调用接口)对应着几个不同的函数原型(方法). 更通俗的说,多态行是指同一个操作作用于不同的对象就会产生不同的响应.或者说,多态性是指发出同样的消息被不同类型的对象接收时有可能导致完全不同的行为. 多态行分

c++笔记:const、初始化、copy构造/析构/赋值函数

构造函数 Default构造函数:可被调用而不带任何实参的构造函数,没有参数或每个参数都有缺省值.如: class A { public: A(); }; 将构造函数声明为explicit,可阻止它们被用来执行隐式类型转换,但仍可用来进行显示类型转换.如: class B { public: explicit B(int x = 0, bool b = ture); }; copy构造函数:用于以同型对象初始化自我对象,以passed by value的方式传递对象:· copy assignm

条款9:绝不要在构造以及析构函数中调用虚函数

在构造以及析构函数期间不要调用virtual函数,因为这类调用从不下降到derived class中.例如说下面这个例子: 1 class Transaction{ 2 public: 3 Transaction(); 4 virtual void logTransactions()s const = 0; 5 //... 6 }; 7 Transaction::Transaction() 8 { 9 //... 10 logTransaction(); 11 } 12 class BuyTra

【Effective C++ 笔记】构造/析构/赋值

编译器的自动机能 编译器可以暗自为 class 创建 default 构造函数.copy 构造函数.copy assignment 操作符,以及析构函数. 为驳回编译器自动提供的机能,可将成员函数声明为 private 并且不予实现. 例如,如果你打算在一个内含 reference 成员或者 const 成员的 class 内支持赋值操作,必须自己定义 copy assignment 操作符,因为 reference 和 const 变量不可修改. 另外,如果某个 base classes 将

Effective C++笔记:构造/析构/赋值运算

条款05:了解C++默默编写并调用哪些函数 默认构造函数.拷贝构造函数.拷贝赋值函数.析构函数构成了一个类的脊梁,只有良好的处理这些函数的定义才能保证类的设计良好性. 当我们没有人为的定义上面的几个函数时,编译器会给我们构造默认的. 当成员变量里有const对象或引用类型时,编译器会不能合成默认的拷贝赋值函数:当一个基类把它的拷贝赋值函数定义为private时,它的派生类也不无生成默认的拷贝赋值函数,因为它无法完成基类成份的赋值. 条款06:若不想使用编译器自动生成的函数,就该明确拒绝 将拷贝构