effective c++:virtual函数在构造函数和析构函数中的注意事项

时间:2023-03-08 22:38:16

如不使用自动生成函数要明确拒绝

对于一个类,如果你没有声明,c++会自动生成一个构造函数,一个析构函数,一个copy构造函数和一个copy assignment操作符。

class Empty {
public:
  Empty() { ... } // default constructor
  Empty(const Empty& rhs) { ... } // copy constructor
  ~Empty() { ... } // destructor — see below
  // for whether it’s virtual
  Empty& operator=(const Empty& rhs) { ... } // copy assignment operator
};

一般来讲,如果你不想有某个功能,不定义对应函数即可,但对与自动生成的函数,当某些人试图使用它,编译器会自动的生成。

遇到这种情况我们就需要private帮忙,因为编译器产生的都是public函数,如果我们在private中声明,编译器就不会再次创建相同的函数,同时也阻止了别人的调用。但是当成员函数和friend函数还是可以调用private函数。所以如果要做彻底一点,我们可以不去定义,只是声明,如果不管是谁使用这些函数,都会得到一个链接错误。

class HomeForSale {
public:
  ...
private:
  ...
  HomeForSale(const HomeForSale&); // declarations only
  HomeForSale& operator=(const HomeForSale&);
};

为多态声明virtual析构函数

首先设计一个TimeKeeper base class 和derived class作为不同的计时方法:

class TimeKeeper {
public:
  TimeKeeper();
  ~TimeKeeper();
  ...
};
class AtomicClock: public TimeKeeper { ... };
class WaterClock: public TimeKeeper { ... };
class WristWatch: public TimeKeeper { ... };
TimeKeeper* getTimeKeeper(); // returns a pointer to a dynamic-
                 // ally allocated object of a class
                 // derived from TimeKeeper

返回一个指针指向一个TimeKeeper派生类动态分配对象,因此为了避免内存泄露,应该把创建的对象delete掉:

TimeKeeper *ptk = getTimeKeeper(); // get dynamically allocated object
                    // from TimeKeeper hierarchy
...                  // use it
delete ptk;             // release it to avoid resource leak

但是这样还是出现了内存泄露,问题出在基类的non-virtual析构函数,对象经过基类指针被删除,而实际上派生类的成分还没被销毁,派生类的析构函数没有能够执行起来。

我们可以在基类析构函数前加一个virtual 就会完整的销毁整个对象。

class TimeKeeper {
public:
  TimeKeeper();
  virtual ~TimeKeeper();
  ...
};
TimeKeeper *ptk = getTimeKeeper();
...
delete ptk; // now behaves correctly

虚函数的目的是重新实现基类的功能,任何class带有虚函数都需要一个虚析构函数。

当class中不存在虚函数,使其析构函数为virtual是错误的。一个虚函数的实现往往需要附带一些额外的信息来确定编译器在调用虚函数时找到正确的函数指针,这就使得这个由c++写成的类也不再和其他语言相同的声明有着同样的结构,因此也不再具备可移植性。

所以只有当class内含有至少一个虚函数时,我们才声明虚析构函数,而且当class设计不是为了当基类时(没有虚函数),或不是为了具备多态性,就不该声明虚析构函数。

别让异常逃离析构函数

试想,如果对象出了异常,现在异常处理模块为了维护系统对象数据的一致性,避免资源泄漏,有责任释放这个对象的资源,调用对象的析构函数,可现在假如析构过程又再出现异常,那么请问由谁来保证这个对象的资源释放呢?而且这新出现的异常又由谁来处理呢?不要忘记前面的一个异常目前都还没有处理结束,因此这就陷入了一个矛盾之中,或者说无限的递归嵌套之中。

所以需要在析构函数中做一些额外的工作来避免这一问题,

方法之一在析构函数中抛出异常就abort,例如下面类DBConn调用的析构函数

DBConn::~DBConn()
{
  try { db.close(); }
  catch (...) {
   std::abort();
  }
}

或者吞下调用close而发生的异常。

如果客户需要对某个操作函数运行期间做出反应,那么class应该提供一个普通函数来执行该操作。

class DBConn {
public:
  ...
  void close() // new function for
  { // client use
    db.close();
    closed = true;
  }
  ~DBConn()
  {
    if (!closed) {
      try { // close the connection
        db.close(); // if the client didn’t
      }
      catch (...) { // if closing fails,
      make log entry that call to close failed; // note that and
       ... // terminate or swallow
    }
  }
}
private:
  DBConnection db;
  bool closed;
};

不要在构造和析构过程中调用virtual函数

先来看这样一段代码:

class Transaction { // base class for all
public: // transactions
  Transaction();
  virtual void logTransaction() const = ; // make type-dependent
  // log entry
  ...
};
Transaction::Transaction() // implementation of
{ // base class ctor
  ...
  logTransaction(); // as final action, log this
} // transaction
class BuyTransaction: public Transaction { // derived class
public:
  virtual void logTransaction() const; // how to log trans-
  // actions of this type
  ...
};
class SellTransaction: public Transaction { // derived class
public:
  virtual void logTransaction() const; // how to log trans-
  // actions of this type
  ...
};

当我们执行BuyTransaction b;这条语句时会调用BuyTransaction 构造函数,但在这之前会首先调用Transaction构造函数,而首先调用的logTransaction也是Transaction版本,这个可以直观的理解为,在Transaction构造时BuyTransaction 尚未开始构造,从而BuyTransaction 中的成员变量也还没有初始化,而虚函数logTransaction几乎一定会调用BuyTransaction 中的成员变量,显而易见调用未经初始化的变量是不允许的,所以基类构造函数中的虚函数形同虚设,析构函数同理。而且如果基类虚函数有实体的话,程序运行期间很难发现原本要调用派生类版本的函数变成了基类版本函数。

唯一能解决 这一问题的方法是在构造和析构函数中不要使用虚函数。例如上例,logTransaction改为non-virtual,在BuyTransaction 构造时传递参数给Transaction

class Transaction {
public:
  explicit Transaction(const std::string& logInfo);
  void logTransaction(const std::string& logInfo) const; // now a non-
  // virtual func
  ...
};
Transaction::Transaction(const std::string& logInfo)
{
  ...
  logTransaction(logInfo); // now a non-
} // virtual call
class BuyTransaction: public Transaction {
public:
  BuyTransaction( parameters )
  : Transaction(createLogString( parameters )) // pass log info
  { ... } // to base class
  ... // constructor
private:
  static std::string createLogString( parameters );
};