为什么我们需要一个纯粹的C ++虚拟析构函数?

我明白需要一个虚拟析构函数。 但为什么我们需要一个纯粹的虚拟析构函数呢? 在其中一篇C ++文章中,作者提到当我们想要做一个类抽象的时候,我们使用了纯粹的虚拟析构函数。

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

所以我的问题是

  1. 我们什么时候才真正构build一个纯虚拟的析构函数? 任何人都可以提供一个很好的实时示例吗?

  2. 当我们创build抽象类时,使析构函数也是纯粹的虚拟是一个好的做法吗? 如果是的话,为什么?

  1. 纯虚拟析构函数被允许的真正原因可能是禁止它们意味着增加另一个规则到语言中,并且不需要这个规则,因为不允许纯虚拟析构函数产生不良影响。

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

如果您为其虚拟方法创build一个具有默认实现的对象,并希望在不强制任何人重写任何特定方法的情况下将其抽象化,则可以使析构函数为纯虚拟。 我没有看到太多的意思,但是这是可能的。

请注意,由于编译器将为派生类生成隐式析构函数,如果类的作者不这样做,则任何派生类都不会是抽象的。 因此,在基类中使用纯虚析构函数对派生类没有任何影响。 它只会使基类抽象(感谢@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. }; 

所有你需要的抽象类至less有一个纯虚函数。 任何function都可以做到; 但正如它所发生的那样,析构函数是任何类的东西 – 所以它总是作为一个候选者。 此外,使析构函数纯粹为虚拟(而不是仅仅是虚拟的)除了使类抽象外,没有任何行为副作用。 因此,许多风格指南build议纯粹使用虚拟分离器来表明一个类是抽象的 – 如果没有其他的理由,除了提供一个一致的地方,读取代码的人可以看看这个类是否抽象。

如果你想创build一个抽象基类:

  • 不能被实例化 (是的,这与“抽象”这个词是多余的!)
  • 需要虚拟析构函数的行为 (你打算把指针指向ABC,而不是指向派生types的指针,并通过它们删除)
  • 不需要其他方法的其他方法的虚拟调度行为(也许没有其他方法?考虑一个简单的受保护的“资源”容器,需要一个构造/析构/分配,但没有其他许多)

…通过使析构函数纯粹为虚拟为其提供一个定义(方法体)来使类抽象变得容易。

对于我们假设的ABC:

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

从我读到的问题的答案,我不能推断出实际使用纯粹的虚拟析构函数的一个好理由。 例如,下面的原因根本不能说服我:

纯虚拟析构函数被允许的真正原因可能是禁止它们意味着增加另一个规则到语言中,并且不需要这个规则,因为不允许纯虚拟析构函数产生不良影响。

在我看来,纯虚拟析构函数可能是有用的。 例如,假设你的代码中有两个类myClassA和myClassB,myClassBinheritance自myClassA。 由于Scott Meyers在他的书“更有效的C ++”,第33项“使非叶类抽象”中提到的理由,最好实践创build一个myClassA和myClassBinheritance的抽象类myAbstractClass。 这提供了更好的抽象,并防止了例如对象副本产生的一些问题。

在创build类myAbstractClass的抽象过程中,myClassA或myClassB的方法可能不是一个纯粹的虚拟方法(这是myAbstractClass抽象的先决条件)的好方法。 在这种情况下,您可以定义抽象类的析构函数pure virtual。

下面从我自己编写的代码的一个具体的例子。 我有两个类,分别为共同属性的Numerics / PhysicsParams。 因此,我让他们inheritance了抽象类IParams。 在这种情况下,我绝对没有办法可以纯粹是虚拟的。 例如,setParameter方法必须对每个子类具有相同的主体。 我唯一的select是让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; }; 

如果你想停止实例化基类而不改变已经实现和testing过的派生类,你可以在你的基类中实现一个纯粹的虚析构函数。

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

 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. 当你希望没有人能够直接创buildBase类的对象时,使用纯虚拟析构函数virtual ~Base() = 0 。 通常至less需要一个纯虚函数,让我们把virtual ~Base() = 0作为这个函数。

  2. 当你不需要上面的东西时,只有你需要安全地销毁Derived类对象

    Base * pBase = new Derived(); 删除pBase; 纯粹的虚拟析构函数不是必需的,只有虚拟析构函数才能完成这个工作。

你问了一个例子,我相信以下提供了一个纯粹的虚拟析构函数的原因。 我期待着回答这是否是一个很好的理由。

我不希望任何人能够抛出error_basetypes,但exceptiontypeserror_oh_shuckserror_oh_blast具有相同的function,我不想写两次。 pImpl复杂性是必要的,以避免暴露std::string到我的客户端,并且使用std::auto_ptr需要拷贝构造函数。

公共头文件包含客户端可以使用的exception规范来区分我的库引发的不同types的exception:

 // 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>&) { } 

你正在考虑这些答案的假设,所以为了清晰起见,我会尽量做出一个更简单,更实际的解释。

面向对象devise的基本关系有两个:IS-A和HAS-A。 我没有弄明白 这就是他们所称的。

IS-A表示一个特定的对象在一个类层次结构中被识别为它上面的类。 香蕉物体是水果物体,如果它是水果类的一个子类的话。 这意味着在任何地方都可以使用水果类,可以使用香蕉。 不过,这并不是反身性的。 如果要调用特定的类,则不能将基类replace为特定的类。

has-a表示对象是组合类的一部分,并且存在所有权关系。 这意味着在C ++中它是一个成员对象,因此它有责任在自己的类上处理它,或者在破坏它之前把所有权closures。

这两个概念在单inheritance语言中比在c ++这样的多inheritance模型中更容易实现,但是规则基本相同。 当类标识不明确时,比如将一个Banana类指针传递给一个带有Fruit类指针的函数,就会出现复杂性。

虚拟function首先是一个运行时间的事物。 它是多态的一部分,它用于决定在运行程序中调用哪个函数。

virtual关键字是一个编译器指令,如果对类标识有歧义,则按照某个顺序绑定函数。 虚函数总是在父类中(就我所知),并向编译器指出,成员函数与其名称的绑定应该在子类函数第一个和父类函数之后进行。

一个Fruit类可以有一个虚函数color(),默认返回“NONE”。 Banana类的color()函数返回“YELLOW”或“BROWN”。

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

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

这与重写子类中的函数是不同的。 在这种情况下,Fruit指针将会调用Fruit :: color(),如果它知道的话就是它是一个指向Fruit的指针。

所以现在到了“纯虚函数”的概念出现了。 这是一个相当不幸的短语,因为纯度与它无关。 这意味着它的目的是永远不会调用基类的方法。 事实上纯虚函数是不能被调用的。 但它仍然必须定义。 函数签名必须存在。 许多编码器为了完整性而做出一个空的实现,但是如果不是,编译器将会在内部生成一个。 在这种情况下,即使指针指向Fruit,函数也会被调用,所以Banana :: color()将被调用,因为它是color()的唯一实现。

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

纯粹的虚拟构造是完全非法的。 这只是出来。

但是,如果您想禁止创build基类实例,纯虚拟析构函数可以工作。 如果基类的析构函数是纯虚拟的,则只能实例化子类。 约定是分配给0。

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

在这种情况下你必须创build一个实现。 编译器知道这是你正在做的事情,并确保你做的是正确的,或者它抱怨,它不能链接到所有需要编译的function。 如果你不正确的轨道如何build模你的类层次结构的错误可能会混淆。

因此,在这种情况下禁止创buildFruit实例,但允许创buildBanana实例。

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

这是一个不好的模式? 是的,在devise阶段是比较复杂的,但是它可以确保在运行时执行正确的链接,并且在哪个子类正在访问时存在歧义的情况下执行子类函数。

如果您编写C ++,以便您只传递精确的类指针,而没有generics指针,则虚拟函数并不是真正需要的。 但是如果你需要types的运行时间灵活性(如在苹果香蕉橙==水果)function变得更容易,更多function,less冗余的代码。 你不再需要为每种水果写一个函数,而且你知道每一个水果都会用自己的正确函数来响应颜色()。

我希望这个啰嗦的解释巩固了这个概念,而不是混淆事物。 有很多很好的例子可以看,看得足够多,实际上运行它们,并与他们混乱,你会得到它。

也许还有另一个真正的使用案例纯粹的虚拟析构函数,我实际上无法在其他答案中看到:)

起初,我完全同意明显的答案:这是因为禁止纯虚拟析构函数在语言规范中需要额外的规则。 但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() 。 但是,它是纯虚拟的原因仍然是相同的 – 该方法没有默认的代码。 有点混淆可能是因为必须有一些析构函数有效,编译器实际上为它生成一个空的代码。 但是从程序员的angular度来看,纯虚拟仍然意味着:“我没有任何默认代码,它必须由派生类提供”。

我认为在这里没有什么大的想法,只是更多的解释,纯粹的虚拟工作真正统一 – 也为破坏者。

这是一个十年的话题:)阅读“Effective C ++”书中第7项最后5段的细节,从“偶尔给一个纯虚拟析构函数可以方便…”

1)当你想要求派生类进行清理。 这很less见。

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

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