大学IT网 - 最懂大学生的IT学习网站! QQ资料交流群:367606806
当前位置:大学IT网 > C++技巧 > C++箴言:多态基类中将析构函数声明为虚拟

C++箴言:多态基类中将析构函数声明为虚拟

关键词:箴言函数类中声明虚拟  阅读(642) 赞(12)

[摘要]本文是对C++箴言:多态基类中将析构函数声明为虚拟函数的讲解,希望对您学习C++编程有所帮助。

  有很多办法可以跟踪工夫的轨迹,所以有必要树立一个 TimeKeeper 基类,并为不同的计时办法树立派生类:

class TimeKeeper {
 public:
  TimeKeeper();
  ~TimeKeeper();
 ...
};

class AtomicClock: public TimeKeeper { ... };
class WaterClock: public TimeKeeper { ... };
class WristWatch: public TimeKeeper { ... };

  很多客户只是想复杂地获得工夫而不关怀如何计算的细节,所以一个 factory 函数——前往一个指向新建派生类对象的基类指针的函数——被用来前往一个指向计时对象的指针:

TimeKeeper* getTimeKeeper(); // returns a pointer to a dynamic-
// ally allocated object of a class
// derived from TimeKeeper

  依照 factory 函数的常规,getTimeKeeper 前往的对象是树立在堆上的,所以为了防止走漏内存和其他资源,最重要的就是要让每一个前往的对象都可以被完全删除。

TimeKeeper *ptk = getTimeKeeper(); // get dynamically allocated object
// from TimeKeeper hierarchy

... // use it

delete ptk; // release it to avoid resource leak

  如今我们肉体集中于下面的代码中一个更根本的缺陷:即便客户做对了每一件事,也无法预知顺序将如何运转。

  效果在于 getTimeKeeper 前往一个指向派生类对象的指针(比方 AtomicClock),那个对象经过一个基类指针(也就是一个 TimeKeeper* 指针)被删除,而且这个基类(TimeKeeper)有一个非虚的析构函数。祸端就在这里,由于 C++ 指出:当一个派生类对象经过运用一个基类指针删除,而这个基类有一个非虚的析构函数,则后果是未定义的。运转时比拟有代表性的结果是对象的派生局部不会被销毁。假定 getTimeKeeper 前往一个指向 AtomicClock 对象的指针,则对象的 AtomicClock 局部(也就是在 AtomicClock 类中声明的数据成员)很可以不会被销毁,AtomicClock 的析构函数也不会运转。但是,基类局部(也就是 TimeKeeper 局部)很可以已被销毁,这就招致了一个乖僻的“局部析构”对象。这是一个走漏资源,毁坏数据构造以及耗费大批调试工夫的绝妙办法。 扫除这个效果十分复杂:给基类一个虚析构函数。于是,删除一个派生类对象的时分就有了你所希冀的正确行为。将销毁整个对象,包括全部的派生类局部:

class TimeKeeper {
 public:
  TimeKeeper();
  virtual ~TimeKeeper();
  ...
};

TimeKeeper *ptk = getTimeKeeper();
...
delete ptk; // now behaves correctly

  相似 TimeKeeper 的基类普通都包括除了析构函数以外的其它虚函数,由于虚函数的目的就是允许派生类定制完成(参见 Item 34)。例如,TimeKeeper 可以有一个虚函数 getCurrentTime,在各种不同的派生类中有不同的完成。简直一切拥有虚函数的类差不多都应该有虚析构函数。

  假定一个类不包括虚函数,这常常预示不方案将它作为基类运用。当一个类不方案作为基类时,将析构函数声明为虚拟通常是个坏主见。思索一个表现二维空间中的点的类:

class Point { // a 2D point
 public:
  Point(int xCoord, int yCoord);
  ~Point();
 private:
  int x, y;
};

  假定一个 int 占 32 位,一个 Point 对象正好适用于 64 位的存放器。而且,这样一个 Point 对象可以被作为一个 64 位的量传递给其它言语写的函数,比方 C 或许 FORTRAN。假定 Point 的析构函数是虚拟的,状况就完全不一样了。

  虚函数的完成要求对象携带额定的信息,这些信息用于在运转时确定该对象应该调用哪一个虚函数。典型状况下,这一信息具有一种被称为 vptr(virtual table pointer,虚函数表指针)的指针的方式。vptr 指向一个被称为 vtbl(virtual table,虚函数表)的函数指针数组,每一个包括虚函数的类都关联到 vtbl。当一个对象调用了虚函数,实践的被调用函数经过上面的步骤确定:找到对象的 vptr 指向的 vtbl,然后在 vtbl 中寻觅适宜的函数指针。

  虚函数如何被完成的细节是不重要的。重要的是假定 Point 类包括一个虚函数,这个类型的对象的大小就会添加。在一个 32 位架构中,它们将从 64 位(相当于两个 int)长到 96 位(两个 int 加上 vptr);在一个 64 位架构中,他们可以从 64 位长到 128 位,由于在这样的架构中指针的大小是 64 位的。为 Point 加上 vptr 将会使它的大小增长 50-100%!Point 对象不再合适 64 位存放器。而且,Point 对象在 C++ 和其他言语(比方 C)中,看起来不再具有相反的构造,由于其它言语缺乏 vptr 的对应物。后果,Points 不再可以传入其它言语写成的函数或从其中传出,除非你为 vptr 做出明白的对应,而这是它本人的完成细节并因而失掉可移植性。

  这里的基准就是不加选择地将一切析构函数声明为虚拟,和从不把它们声明为虚拟一样是错误的。实践上,很多人总结过这条规则:当且仅当类中至多包括一个虚拟函数时,则声明一个虚析构函数。

  但是,当完全没有虚函数时,就可以和非虚析构函数效果发作撕咬。例如,规范 string 类型不包括虚函数,但是被误导的顺序员有时将它当作基类运用:

class SpecialString: public std::string { // bad idea! std::string has a
... // non-virtual destructor
};

  一眼看上去,这可以无伤大雅,但是,假定在顺序的某个中央由于某种缘由,你将一个指向 SpecialString 的指针转型为一个指向 string 的指针,然后你将 delete 施加于这个 string 指针,你就立即被送入未定义行为的领地。

SpecialString *pss = new SpecialString("Impending Doom");

std::string *ps;
...
ps = pss; // SpecialString* => std::string*
...
delete ps; // undefined! In practice,
// *ps’s SpecialString resources
// will be leaked, because the
// SpecialString destructor won’t
// be called.

  异常的剖析可以适用于任何短少虚析构函数的类,包括全部的 STL 容器类型(例如,vector,list,set,tr1::unordered_map。假定你遭到从规范容器类或任何其他带有非虚析构函数的类派生的引诱,一定要挺住!(不幸的是,C++ 不提供相似 Java 的 final 类或 C# 的 sealed 类的防派活力制。) 偶然地,给一个类提供一个纯虚析构函数能提供一些便当。回想一下,纯虚函数招致笼统类——不能被实例化的类(也就是说你不能创立这个类型的对象)。有时分,你有一个类,你希望它是笼统的,但没有任何纯虚函数。怎样办呢?由于一个笼统类注定要被用作基类,又由于一个基类应该有一个虚析构函数,又由于一个纯虚函数发作一个笼统类,好了,处置方案很复杂:在你希望成为笼统类的类中声明一个纯虚析构函数。这是一个例子:

class AWOV { // AWOV = "Abstract w/o Virtuals"
public:
 virtual ~AWOV() = 0; // declare pure virtual destructor
};

  这个类有一个纯虚函数,所以它是笼统的,又由于它有一个虚析构函数,所以你不用担忧析构函数效果。这是一个螺旋。你必需为纯虚析构函数提供一个定义:

AWOV::~AWOV() {} // definition of pure virtual dtor

  析构函数的义务方式是:最底层的派生类(most derived class)的析构函数最先被调用,然后调用每一个基类的析构函数。编译器会发作一个从派生类的析构函数对 ~AWOV 的调用,所以你不得不的确为函数提供一个函数体。假定你不这样做,衔接顺序会提出抗议。

  为基类提供虚析构函数的规则仅仅适用于多态基类——基类被设计用来允许派生类型经过基类的接口中止操作。TimeKeeper 就是一个多态基类,由于我们希冀能操作 AtomicClock 和 WaterClock 对象,甚至当我们仅有指向他们的类型为 TimeKeeper 的指针的时分。

  并非一切的基类都被设计用于多态。例如,无论是规范 string 类型,还是 STL 容器类型都被完全设计成基类,可没有哪个是多态的。一些类虽然被设计用于基类,但并非被设计用于多态。这样的类——例如Uncopyable 和规范库中的 input_iterator_tag——没有被设计成允许经过基类的接口操作派生类对象。所以它们就不需求虚析构函数。

  Things to Remember

  ·多态基类应该声明虚析构函数。假定一个类有任何虚函数,它就应该有一个虚析构函数。

  ·假定不是设计用于做基类或不是设计用于多态,这样的类就不应该声明虚析构函数。



相关评论