• Effective C++学习笔记:确定基类有虚析构函数


    设想在一个军事应用程序里,有一个表示敌人目标的类:

    class enemytarget

    {
    public:
      enemytarget() { ++numtargets; }
      enemytarget(const enemytarget&) { ++numtargets; }
      ~enemytarget() { --numtargets; }

      static size_t numberoftargets()
      { return numtargets; }

      virtual bool destroy();         // 摧毁enemytarget对象后
                                                  // 返回成功

    private:
      static size_t numtargets;     // 对象计数器
    };

     

    //类的静态成员要在类外定义;
    // 缺省初始化为0

    size_t enemytarget::numtargets;

    敌人的坦克是一种特殊的敌人目标,所以会很自然地想到将它抽象为一个以公有继承方式从enemytarget派生出来的类。因为不但要关心敌人目标的总数,也要关心敌人坦克的总数,所以和基类一样,在派生类里也采用了上面提到的同样的技巧:

    class enemytank: public enemytarget {
    public:
      enemytank() { ++numtanks; }

      enemytank(const enemytank& rhs)
      : enemytarget(rhs)
      { ++numtanks; }

      ~enemytank() { --numtanks; }

      static size_t numberoftanks()
      { return numtanks; }

      virtual bool destroy();

    private:
      static size_t numtanks;         // 坦克对象计数器
    };

     

    最后,假设程序的其他某处用new动态创建了一个enemytank对象,然后用delete删除掉:

    enemytarget *targetptr = new enemytank;

    ...

    delete targetptr

      这样会发生严重问题,因为c++语言标准关于这个问题的阐述非常清楚:当通过基类的指针去删除派生类的对象,而基类又没有虚析构函数时,结果将是不可确定的。实际运行时经常发生的是,派生类的析构函数永远不会被调用。在本例中,这意味着当targetptr 删除时,enemytank的数量值不会改变,那么,敌人坦克的数量就是错的。

    如果某个类不包含虚函数,那一般是表示它将不作为一个基类来使用。当一个类不准备作为基类使用时,使析构函数为虚一般是个坏主意。因为它会为类增加一个虚函数表,使得对象的体积翻倍,还有可能降低其可移植性。

    所以基本的一条是:无故的声明虚析构函数和永远不去声明一样是错误的。实际上,很多人这样总结:当且仅当类里包含至少一个虚函数的时候才去声明虚析构函数。

    抽象类是准备被用做基类的,基类必须要有一个虚析构函数,纯虚函数会产生抽象类,所以方法很简单:在想要成为抽象类的类里声明一个纯虚析构函数。

    这里是一个例子:

    class awov {                // awov = "abstract w/o
                                       // virtuals"
    public:
      virtual ~awov() = 0;      // 声明一个纯虚析构函数
    };

    这个类有一个纯虚函数,所以它是抽象的,而且它有一个虚析构函数,所以不会产生析构函数问题。但这里还有一件事:必须提供纯虚析构函数的定义:

    awov::~awov() {}           // 纯虚析构函数的定义

    这个定义是必需的,因为虚析构函数工作的方式是:最底层的派生类的析构函数最先被调用,然后各个基类的析构函数被调用。这就是说,即使是抽象类,编译器也要产生对~awov的调用,所以要保证为它提供函数体。如果不这么做,链接器就会检测出来,最后还是得回去把它添上。

    注意:如果声明虚析构函数为inline,将会避免调用它们时产生的开销,但编译器还是必然会在什么地方产生一个此函数的拷贝。

  • 相关阅读:
    LeetCode 404. 左叶子之和
    三年了
    LeetCode 543. 二叉树的直径
    求结点在二叉排序树中层次的算法
    LeetCode 98. 验证二叉搜索树
    LeetCode 236. 二叉树的最近公共祖先
    LeetCode 129. 求根到叶子节点数字之和
    LeetCode 113. 路径总和 II
    LeetCode 107. 二叉树的层次遍历 II
    LeetCode 144. 二叉树的前序遍历 (非递归)
  • 原文地址:https://www.cnblogs.com/8586/p/1248559.html
Copyright © 2020-2023  润新知