为什么我们需要在C ++中使用纯虚拟析构函数?

时间:2009-08-02 19:27:29

标签: c++ destructor pure-virtual

我理解虚拟析构函数的必要性。但为什么我们需要纯虚拟析构函数?在其中一篇C ++文章中,作者提到我们在创建类抽象时使用纯虚析构函数。

但是我们可以通过将任何成员函数作为纯虚拟来创建类抽象。

所以我的问题是

  1. 我们什么时候才能真正使析构函数变为虚拟?任何人都能给出一个很好的实时例子吗?

  2. 当我们创建抽象类时,将析构函数设置为纯虚拟是一种很好的做法吗?如果是..那为什么?

12 个答案:

答案 0 :(得分:109)

  1. 允许纯虚拟析构函数的真正原因可能是禁止它们意味着在语言中添加另一条规则,并且不需要此规则,因为允许纯虚拟析构函数不会产生任何不良影响。

  2. 不,简单的老虚拟就足够了。

  3. 如果使用其虚拟方法的默认实现创建一个对象,并希望在不强制任何人覆盖任何特定方法的情况下使其成为抽象,则可以使析构函数为纯虚拟。我没有看到太多的意义,但它是可能的。

    请注意,由于编译器将为派生类生成隐式析构函数,如果类的作者不这样做,则任何派生类都是抽象的。因此,在基类中使用纯虚析构函数不会对派生类产生任何影响。它只会使基类抽象(感谢@kappa的注释)。

    还可以假设每个派生类可能需要具有特定的清理代码,并使用纯虚拟析构函数作为提示来编写一个,但这似乎是人为的(并且没有执行)。

    注意:析构函数是唯一的方法,即使 纯虚拟 具有实现以实例化派生类(是的纯虚函数可以有实现)。

    struct foo {
        virtual void bar() = 0;
    };
    
    void foo::bar() { /* default implementation */ }
    
    class foof : public foo {
        void bar() { foo::bar(); } // have to explicitly call default implementation.
    };
    

答案 1 :(得分:30)

抽象类所需要的只是至少一个纯虚函数。任何功能都可以;但实际上,析构函数是任何类所具有的东西 - 所以它总是作为候选者存在。此外,使析构函数纯虚拟(而不仅仅是虚拟)除了使类抽象之外没有任何行为副作用。因此,许多样式指南建议始终使用纯虚拟目标来指示一个类是抽象的 - 如果没有其他原因,它提供了一个一致的位置,有人阅读代码可以查看该类是否是抽象的。

答案 2 :(得分:18)

如果要创建抽象基类:

  • 无法实例化(是的,这与术语“抽象”是多余的!)
  • 需要虚拟析构函数行为(您打算携带指向ABC的指针,而不是指向派生类型的指针,并通过它们删除)
  • 对其他方法不需要任何其他虚拟调度行为(可能 没有其他方法?考虑一个需要构造函数的简单受保护“资源”容器/析构函数/赋值,但其他并不多)

...通过使析构函数纯虚拟为它提供定义(方法体),使类抽象化是最容易的。

对于我们假设的ABC:

你保证它不能被实例化(即使是类本身内部,这就是私有构造函数可能不够的原因),你得到了析构函数所需的虚拟行为,而你不必查找和标记另一个不需要虚拟调度的方法为“虚拟”。

答案 3 :(得分:7)

从我已经阅读过你的问题的答案中,我无法推断出一个真正使用纯虚拟析构函数的理由。例如,以下原因并不能说服我:

  

允许纯虚拟析构函数的真正原因可能是禁止它们意味着在语言中添加另一条规则,因此不需要这条规则,因为允许纯虚拟析构函数不会产生任何不良影响

在我看来,纯虚拟析构函数可能很有用。例如,假设代码中有两个类myClassA和myClassB,myClassB继承自myClassA。由于Scott Meyers在他的书中提到的原因"更有效的C ++",项目33"使非叶类抽象化",更好的做法是实际创建一个抽象类myCbstractA和myClassB继承的myAbstractClass。这提供了更好的抽象,并防止了例如对象副本引起的一些问题。

在抽象过程中(创建类myAbstractClass),可能没有myClassA或myClassB的方法是一个很好的候选者,因为它是一个纯虚方法(这是myAbstractClass抽象的先决条件)。在这种情况下,您可以定义抽象类的析构函数pure virtual。

以下是我自己写的一些代码的具体例子。我有两个类,Numerics / PhysicsParams,它们共享共同的属性。因此,我让他们继承自抽象类IParams。在这种情况下,我绝对没有可以纯粹是虚拟的方法。例如,setParameter方法必须为每个子类具有相同的主体。我唯一的选择就是制作IParams'析构函数纯虚拟。

struct IParams
{
    IParams(const ModelConfiguration& aModelConf);
    virtual ~IParams() = 0;

    void setParameter(const N_Configuration::Parameter& aParam);

    std::map<std::string, std::string> m_Parameters;
};

struct NumericsParams : IParams
{
    NumericsParams(const ModelConfiguration& aNumericsConf);
    virtual ~NumericsParams();

    double dt() const;
    double ti() const;
    double tf() const;
};

struct PhysicsParams : IParams
{
    PhysicsParams(const N_Configuration::ModelConfiguration& aPhysicsConf);
    virtual ~PhysicsParams();

    double g()     const; 
    double rho_i() const; 
    double rho_w() const; 
};

答案 4 :(得分:4)

如果要在不对已经实现和测试的派生类进行任何更改的情况下停止实例化基类,则需要在基类中实现纯虚析构函数。

答案 5 :(得分:2)

在这里,我想告诉我们何时需要虚拟析构函数以及何时需要纯虚析构函数

class Base
{
public:
    Base();
    virtual ~Base() = 0; // Pure virtual, now no one can create the Base Object directly 
};

Base::Base() { cout << "Base Constructor" << endl; }
Base::~Base() { cout << "Base Destructor" << endl; }


class Derived : public Base
{
public:
    Derived();
    ~Derived();
};

Derived::Derived() { cout << "Derived Constructor" << endl; }
Derived::~Derived() {   cout << "Derived Destructor" << endl; }


int _tmain(int argc, _TCHAR* argv[])
{
    Base* pBase = new Derived();
    delete pBase;

    Base* pBase2 = new Base(); // Error 1   error C2259: 'Base' : cannot instantiate abstract class
}
  1. 如果您希望没有人能够直接创建Base类的对象,请使用纯虚析构函数virtual ~Base() = 0。通常至少需要一个纯虚函数,让virtual ~Base() = 0作为此函数。

  2. 当您不需要上述内容时,只需要安全销毁派生类对象

    Base * pBase = new Derived(); 删除pBase; 不需要纯虚析构函数,只有虚析构函数才能完成这项工作。

答案 6 :(得分:2)

你正在接受这些答案的假设,所以为了清楚起见,我会尝试做一个更简单,更实际的解释。

面向对象设计的基本关系是两个:  IS-A和HAS-A。我没有把它们搞定。这就是所谓的。

IS-A表示特定对象在类层次结构中标识为其上方的类。如果banana对象是fruit类的子类,则它是一个fruit对象。这意味着在任何可以使用水果类的地方都可以使用香蕉。但这并不是反身。如果要调用特定的类,则不能将基类替换为特定的类。

Has-a表示对象是复合类的一部分,并且存在所有权关系。它意味着在C ++中它是一个成员对象,因此onus是在拥有的类上处理它或在破坏自己之前关闭所有权。

这两个概念在单继承语言中比在c ++等多继承模型中更容易实现,但规则基本相同。当类标识不明确时,例如将Banana类指针传递给采用Fruit类指针的函数,就会出现复杂情况。

虚拟功能首先是运行时的事情。它是多态性的一部分,因为它用于决定在正在运行的程序中调用哪个函数。

virtual关键字是一个编译器指令,用于在类标识存在歧义的情况下以特定顺序绑定函数。虚函数总是在父类中(据我所知)并向编译器指示成员函数与其名称的绑定应该首先使用子类函数和之后的父类函数。

Fruit类可以有一个虚函数color()返回&#34; NONE&#34;默认情况下。 Banana类的color()函数返回&#34; YELLOW&#34;或&#34;布朗&#34;。

但是如果使用Fruit指针的函数调用发送给它的Banana类上的color() - 调用哪个color()函数? 该函数通常会为Fruit对象调用Fruit :: color()。

那99%的时间都不是预期的。 但是如果Fruit :: color()被声明为virtual,那么将为该对象调用Banana:color(),因为正确的color()函数将在调用时绑定到Fruit指针。 运行时将检查指针指向哪个对象,因为它在Fruit类定义中标记为虚拟。

这与覆盖子类中的函数不同。在这种情况下 如果它只知道它是IS-A指向Fruit的指针,那么Fruit指针将调用Fruit :: color()。

所以现在想到一个纯粹的虚拟功能&#34;过来。 这是一个相当不幸的短语,因为纯洁与它无关。这意味着永远不会调用基类方法。 实际上,无法调用纯虚函数。但是,它仍然必须定义。必须存在函数签名。许多编码器为了完整性而制作一个空的实现{},但如果没有,编译器将在内部生成一个。在这种情况下,即使指针指向Fruit也调用该函数,将调用Banana :: color(),因为它是color()的唯一实现。

现在是拼图的最后一部分:构造函数和析构函数。

纯虚拟构造函数完全是非法的。那就是结束了。

但是纯虚拟析构函数在你想要禁止创建基类实例的情况下工作。如果基类的析构函数是纯虚拟的,则只能实例化子类。 惯例是将其分配给0。

 virtual ~Fruit() = 0;  // pure virtual 
 Fruit::~Fruit(){}      // destructor implementation

在这种情况下,您必须创建一个实现。编译器知道你正在做的事情,并确保你做得对,或者它大肆抱怨它无法链接到它需要编译的所有函数。如果您没有在如何建模类层次结构方面走上正轨,则错误可能会令人困惑。

因此,在这种情况下禁止创建Fruit实例,但允许创建Banana实例。

调用删除指向Banana实例的Fruit指针 将首先调用Banana :: ~Banana()然后调用Fuit :: ~Fruit()。 因为无论如何,当你调用子类析构函数时,基类析构函数必须遵循。

这是一个糟糕的模特吗?它在设计阶段更复杂,是的,但是它可以确保在运行时执行正确的链接,并且执行子类函数,其中存在关于确切访问哪个子类的歧义。

如果你编写C ++以便只传递没有通用指针或模糊指针的确切类指针,那么实际上并不需要虚函数。 但是如果您需要类型的运行时灵活性(如在Apple Banana Orange ==&gt; Fruit中),则功能变得更容易,功能更多,冗余代码更少。 你不再需要为每种类型的水果编写一个函数,而且你知道每个水果都会对color()做出正确的反应。

我希望这个冗长的解释能够巩固这个概念而不是混淆事物。有很多很好的例子可以看, 并且看得够,实际上运行它们并弄乱它们,你就会得到它。

答案 7 :(得分:0)

您问了一个示例,我相信以下内容提供了纯虚拟析构函数的原因。我期待回答这是否是的原因......

我不希望任何人能够抛出error_base类型,但异常类型error_oh_shuckserror_oh_blast具有相同的功能,我不想写它两次。 pImpl的复杂性对于避免向我的客户端公开std::string是必要的,并且使用std::auto_ptr需要复制构造函数。

公共标头包含客户端可用的异常规范,以区分我的库抛出的不同类型的异常:

// error.h

#include <exception>
#include <memory>

class exception_string;

class error_base : public std::exception {
 public:
  error_base(const char* error_message);
  error_base(const error_base& other);
  virtual ~error_base() = 0; // Not directly usable

  virtual const char* what() const;
 private:
  std::auto_ptr<exception_string> error_message_;
};

template<class error_type>
class error : public error_base {
 public:
   error(const char* error_message) : error_base(error_message) {}
   error(const error& other) : error_base(other) {}
   ~error() {}
};

// Neither should these classes be usable
class error_oh_shucks { virtual ~error_oh_shucks() = 0; }
class error_oh_blast { virtual ~error_oh_blast() = 0; }

这是共享实现:

// error.cpp

#include "error.h"
#include "exception_string.h"

error_base::error_base(const char* error_message)
  : error_message_(new exception_string(error_message)) {}

error_base::error_base(const error_base& other)
  : error_message_(new exception_string(other.error_message_->get())) {}

error_base::~error_base() {}

const char* error_base::what() const {
  return error_message_->get();
}

exception_string类保持私有,从我的公共接口隐藏std :: string:

// exception_string.h

#include <string>

class exception_string {
 public:
  exception_string(const char* message) : message_(message) {}

  const char* get() const { return message_.c_str(); }
 private:
  std::string message_;
};

我的代码然后抛出一个错误:

#include "error.h"

throw error<error_oh_shucks>("That didn't work");

使用error模板有点无偿。它节省了一些代码,代价是要求客户端捕获错误:

// client.cpp

#include <error.h>

try {
} catch (const error<error_oh_shucks>&) {
} catch (const error<error_oh_blast>&) {
}

答案 8 :(得分:0)

也许还有另一个 REAL USE-CASE 的纯虚析构函数,我实际上在其他答案中看不到:)

首先,我完全同意明确的答案:这是因为禁止纯虚拟析构函数需要在语言规范中有一个额外的规则。但它仍然不是Mark要求的用例:)

首先想象一下:

class Printable {
  virtual void print() const = 0;
  // virtual destructor should be here, but not to confuse with another problem
};

等等:

class Printer {
  void queDocument(unique_ptr<Printable> doc);
  void printAll();
};

简单 - 我们有接口Printable和一些“容器”用这个接口保存任何东西。我认为这里很清楚为什么print()方法是纯虚拟的。它可能有一些正文但是如果没有默认实现,纯虚拟是一个理想的“实现”(=“必须由后代类提供”)。

现在想象完全相同,除了它不是用于打印而是用于销毁:

class Destroyable {
  virtual ~Destroyable() = 0;
};

也可能有类似的容器:

class PostponedDestructor {
  // Queues an object to be destroyed later.
  void queObjectForDestruction(unique_ptr<Destroyable> obj);
  // Destroys all already queued objects.
  void destroyAll();
};

这是我真实应用程序中简化的用例。这里唯一的区别是使用“特殊”方法(析构函数)而不是“普通”print()。但它是纯虚拟的原因仍然是相同的 - 该方法没有默认代码。 有点令人困惑的事实是,必须有效地使用一些析构函数,编译器实际上会为它生成一个空代码。但是从程序员的角度看,纯虚拟性仍然意味着:“我没有任何默认代码,它必须由派生类提供。”

我认为这里没有任何重要的想法,只是更多解释纯粹的虚拟性真正统一 - 也适用于析构函数。

答案 9 :(得分:0)

这是一个十年的老话题:) 阅读#34;有效C ++&#34;的第7项的最后5段。有关详细信息的书,从&#34开始;偶尔为类提供纯虚拟析构函数很方便....&#34;

答案 10 :(得分:-2)

1)当您想要派生类进行清理时。这很少见。

2)不,但你希望它是虚拟的。

答案 11 :(得分:-2)

我们需要使析构函数虚拟化,因为如果我们不使析构函数为虚拟,那么编译器只会破坏基类的内容,所有派生类都将保持不变,bacuse编译器不会调用析构函数除基类之外的任何其他类。