`
totoxian
  • 浏览: 1074406 次
  • 性别: Icon_minigender_2
  • 来自: 西安
文章分类
社区版块
存档分类
最新评论

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

阅读更多

原文地址:http://blog.csdn.net/pdiy/archive/2005/12/14/551983.aspx

如果你已经从另外一种语言如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++干脆不让你这样做。
事实上还有比这更具基本的要求。在派生类对象的基类对象构造过程中,该类的类型是基类类型。不仅虚函数依赖于基类,而且使用运行时刻信息的语言的相应部分(例如,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++箴言:避免析构函数调用虚函数[归类].pdf

    在C++编程中,有一条重要的原则是避免在构造函数或析构函数中调用虚函数。这条箴言尤其对那些从C#或Java转向C++的开发者来说可能显得有些反直觉,因为在这些语言中,这样的调用通常是允许的。然而,在C++中,这样做...

    C++箴言:防止异常离开析构函数

    ### C++箴言:防止异常离开析构函数 #### 一、引言 在C++编程中,析构函数是一个非常重要的概念,它用于释放对象生命周期结束时所占用的资源。然而,在某些情况下,如果析构函数抛出了异常,可能会导致程序出现...

    C++中析构函数定义成虚函数的原因.doc

    1. **避免内存泄漏**:如前所述,如果不将基类的析构函数声明为虚函数,则通过基类指针删除派生类对象时,只会调用基类的析构函数,派生类的析构函数不会被调用。这可能导致派生类中的资源未被正确释放,引发内存...

    虚析构函数示例

    总结起来,虚析构函数是C++多态性的重要组成部分,它确保在对象删除时,无论通过基类还是派生类的指针,都能正确调用到每个层级的析构函数,从而保证资源的有效回收。在设计面向对象的程序时,正确理解和使用虚析构...

    构造函数不能声明为虚函数,析构函数可以声明为虚函数

    1. **避免内存泄漏**:如果基类指针指向派生类对象,并且基类的析构函数不是虚函数,那么通过基类指针删除派生类对象时,只会调用基类的析构函数。这样就会导致派生类中额外分配的资源没有得到释放,从而引发内存...

    为何要虚析构函数.docx

    虚析构函数的必要性 ...虚析构函数是非常必要的,它可以确保正确的析构函数序列被调用,避免内存泄漏等问题。而构造函数不能是虚函数,是因为它的作用是在初始化对象,而不是通过父类的指针或者引用来调用。

    派生的析构函数被调用

    基类的析构函数默认是虚函数(virtual),这样在派生类的对象指针指向基类时,如果通过指针调用析构函数,会执行相应的派生类析构函数,确保所有资源都能被正确释放。这是因为C++支持动态绑定或称为运行时多态。 ...

    c语言析构函数

    3. **唯一性**:一个类只能有一个析构函数,这是因为析构函数的调用是由C++运行时系统自动管理的,当对象生命周期结束时(如离开作用域或显式删除动态分配的对象),析构函数将被自动调用。 4. **自动调用**:析构...

    C++析构函数使用virtual的原因

    如此一来,无论通过何种类型的指针删除对象,C++的运行时系统都能确保从最派生的类开始,按层次结构逆序调用所有析构函数,从而正确地释放所有资源,避免了内存泄漏等问题的发生。 总之,当一个类预期作为其他类的...

    C++ 析构函数 参考源代码

    在有继承层次结构的类中,基类的析构函数通常是虚函数(`virtual`),以确保即使通过基类指针或引用删除派生类对象时,也能调用正确的析构函数。例如: ```cpp class Base { public: virtual ~Base() { // 基类析...

    虚基类 虚函数成员 虚析构函数

    在C++编程语言中,虚基类、虚函数成员和虚析构函数是面向对象编程中的关键概念,它们对于理解和实现多态性至关重要。多态性允许我们编写更灵活、可扩展的代码,使得程序能处理多种不同类型的对象。下面将详细解释这...

    析构函数应用

    6. **可以是虚函数**:如果希望在派生类中覆盖基类的析构函数,则可以在基类中声明析构函数为虚函数。 #### 三、示例代码解析 下面通过给出的代码示例来详细了解析构函数的应用: ```cpp #include using ...

    C++析构函数 快速学习

    在C++编程中,析构函数(Destructor)是类的一个特殊成员函数,它在对象生命周期结束时被自动调用,用于执行清理工作。这通常包括释放动态分配的内存和其他资源。学习并掌握析构函数是深入理解C++内存管理的关键。 ...

    C++析构函数调用时间及分配对象堆与栈区别demo

    描述了C++析构函数调用时间及分配对象堆与栈区别,一个理清C++析构函数和默认系统析构函数,C++堆栈分配的原则。

    C++构造函数_析构函数和赋值操作符学习小结

    C++构造函数、析构函数和赋值操作符学习小结 C++ 构造函数、析构函数和赋值操作符是 C++ 编程语言中的基本组件,它们提供了对象的初始化、销毁和赋值操作。这些函数的正确性对整个类的正确性至关重要。 构造函数 ...

    C++构造函数与析构函数

    ### C++构造函数与析构函数详解 在学习面向对象编程的过程中,构造函数与析构函数是两个非常重要的概念。本文将围绕一个简单的股票信息类`Stock`来深入讲解这两个概念,并通过具体代码实例帮助理解。 #### 构造...

    析构函数不能抛出异常的原因

    因为析构函数经常被调用,特别是在程序结束时,大量的析构函数调用可能会触发大量的异常处理机制。这些额外的操作会增加程序的运行时间,降低程序的整体性能。通过避免在析构函数中抛出异常,可以减少不必要的开销,...

Global site tag (gtag.js) - Google Analytics