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


154

我了解需要虚拟析构函数。但是为什么我们需要一个纯虚拟析构函数?在一篇C ++文章中,作者提到了当我们要使类抽象时,我们使用纯虚拟析构函数。

但是我们可以通过将任何成员函数设为纯虚函数来使类抽象。

所以我的问题是

  1. 我们什么时候才能真正使析构函数成为纯虚拟的?有人可以举一个很好的实时例子吗?

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



14
@ Daniel-提到的链接无法回答我的问题。它回答了为什么纯虚拟析构函数应该有一个定义。我的问题是为什么我们需要一个纯虚拟析构函数。
标记

我试图找出原因,但是您已经在这里提出了问题。
nsivakr 2010年

Answers:


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

  2. 不,普通的旧虚拟就足够了。

如果使用默认实现为其虚拟方法创建对象,并希望使其抽象而不强迫任何人重写任何特定方法,则可以将析构函数设为纯虚拟。我看不出什么要点,但有可能。

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

13
“是的,纯虚拟函数可以有实现”,那么它不是纯虚拟的。
GManNickG

2
如果您想使一个类抽象化,只保护所有的构造函数不是更简单吗?
bdonlan

78
@GMan,您误会了,因为纯虚拟方法意味着派生类必须重写此方法,这与实现是正交的。查看我的代码并注释掉,foof::bar如果您想自己看看。
Motti

15
@GMan:C ++常见问题解答说:“请注意,可以为纯虚函数提供定义,但这通常会使新手感到困惑,最好避免在以后使用。” parashift.com/c++-faq-lite/abcs.html#faq-22.4 Wikipedia(正确性的堡垒)也这样说。我相信ISO / IEC标准使用了类似的术语(不幸的是,目前我正在使用它的副本)...我同意这很令人困惑,在提供定义时,我通常不会在没有明确说明的情况下使用该术语,尤其是周围的新程序员...
leander

9
@Motti:有趣的是,在派生(和实例化)类中不需要显式重写纯虚拟析构函数。在这种情况下,将使用隐式定义:)
kappa

33

您需要的抽象类至少是一个纯虚函数。任何功能都可以;但是碰巧的是,析构函数是任何类都会拥有的东西-因此它始终是候选对象。此外,使析构函数为纯虚拟的(而不是纯虚拟的)除了使类抽象之外,没有行为方面的副作用。因此,许多样式指南建议始终使用纯虚拟析构函数来指示类是抽象的—如果出于其他原因,它提供了一致的位置,那么阅读代码的人可以查看该类是否是抽象的。


1
但是仍然为什么要提供执行纯病毒杀伤剂的方法。如果我将析构函数设为纯虚拟并且不提供其实现,可能会出错。我假设只声明了​​基类指针,因此永远不会调用抽象类的析构函数。
克里希纳(Krishna Oza)2014年

4
@Surfing:因为派生类的析构函数隐式调用其基类的析构函数,即使该析构函数是纯虚拟的也是如此。因此,如果没有实现,则将发生未定义的行为。
a.peganz

19

如果要创建抽象基类:

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

...最简单的方法是,使析构函数成为纯虚函数,为其提供定义(方法主体)。

对于我们假设的ABC:

您保证无法实例化它(即使是在类本身内部,这也就是为什么私有构造函数可能不够用),您可以得到析构函数所需的虚拟行为,而不必查找和标记另一个不需要将虚拟调度作为“虚拟”。


8

从我对您的问题的回答中,我无法得出充分使用纯虚拟析构函数的充分理由。例如,以下原因根本无法说服我:

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

我认为,纯虚拟析构函数可能有用。例如,假设您的代码中有两个类myClassA和myClassB,并且myClassB继承自myClassA。出于斯科特·迈耶斯(Scott Meyers)在他的“更有效的C ++”一书中的第33项“使非叶类抽象化”中提到的原因,更好的做法是实际创建一个抽象类myAbstractClass,myClassA和myClassB继承该抽象类。这样可以提供更好的抽象,并避免某些问题,例如对象副本。

在(创建类myAbstractClass的)抽象过程中,myClassA或myClassB的任何方法都不能成为纯虚拟方法的好候选者(这是myAbstractClass成为抽象的先决条件)。在这种情况下,您可以定义抽象类的析构函数纯虚函数。

此后,我自己编写了一些代码的具体示例。我有两个类,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; 
};

1
我喜欢这种用法,但是“强制”继承的另一种方法是通过声明构造函数为IParam受保护的方法,如其他注释中所述。
rwols

4

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


3

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

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. 当您不需要上面的东西时,只需要安全销毁Derived类对象

    Base * pBase = new Derived(); 删除pBase; 不需要纯虚拟析构函数,只有虚拟析构函数可以完成此工作。


2

您正在使用这些答案来进行假设,因此为了清楚起见,我将尝试做出一个更简单,更扎实的解释。

面向对象设计的基本关系是两个:IS-A和HAS-A。我没有弥补。那就是他们所说的。

IS-A表示某个特定对象在类层次结构中标识为属于其之上的类。如果香蕉对象是水果类的子类,则它是水果对象。这意味着在任何可以使用水果的地方,都可以使用香蕉。但是,它不是自反的。如果需要使用特定类,则不能用基类代替特定类。

Has-a表示对象是组合类的一部分,并且存在所有权关系。这意味着在C ++中它是成员对象,因此在销毁自己之前,有责任将其丢弃或移交所有权。

与像c ++这样的多继承模型相比,在单继承语言中更容易实现这两个概念,但是规则本质上是相同的。当类标识不明确时(例如,将Banana类指针传递到采用Fruit类指针的函数中),就会出现问题。

首先,虚函数是运行时事物。它是多态性的一部分,因为它用于确定在正在运行的程序中调用该函数时要运行哪个函数。

如果对类标识有歧义,则virtual关键字是一个编译器指令,用于按特定顺序绑定函数。虚拟函数始终位于父类中(据我所知),并向编译器指示成员函数与其名称的绑定应首先使用子类函数,然后使用父类函数进行。

Fruit类可以具有虚拟函数color(),该函数默认情况下返回“ NONE”。香蕉类color()函数返回“ YELLOW”或“ BROWN”。

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

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

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

因此,现在出现了“纯虚函数”的想法。这是一个非常不幸的短语,因为纯度与它无关。这意味着永远不要调用基类方法。实际上,不能调用纯虚函数。但是,仍然必须对其进行定义。功能签名必须存在。为了完整起见,许多编码器都会使用一个空的实现{},但是如果没有,编译器将在内部生成一个。在那种情况下,即使指针指向Fruit,调用该函数时,也会调用Banana :: color(),因为它是color()的唯一实现。

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

纯虚拟构造函数完全是非法的。刚出来

但是在您要禁止创建基类实例的情况下,纯虚拟析构函数确实可以工作。如果基类的析构函数是纯虚的,则只能实例化子类。约定是将其分配为0。

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

在这种情况下,您必须创建一个实现。编译器知道这就是您要执行的操作,并确保您做对了,否则,它可能抱怨无法链接到编译所需的所有函数。如果您对类层次结构建模的方式不正确,则错误可能会造成混淆。

因此,在这种情况下,您不能创建Fruit实例,但可以创建Banana实例。

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

这是一个坏模型吗?是的,它在设计阶段比较复杂,但是可以确保在运行时执行正确的链接,并确保在确切地访问哪个子类的情况下执行子类功能。

如果编写C ++以便只传递没有泛型指针或模棱两可的指针的确切类指针,那么实际上就不需要虚函数。但是,如果您需要类型的运行时灵活性(如Apple Banana Orange ==> Fruit),则功能将变得更简单,功能更丰富,并且冗余代码更少。您不再需要为每种类型的水果编写函数,并且您知道每种水果都将使用自己的正确函数对color()做出响应。

我希望这个冗长的解释能够巩固概念,而不是使事情混淆。有很多很好的例子可以看,并且要看得足够多,然后实际运行它们并弄乱它们,您会明白的。


1

这是一个有十年历史的主题:)阅读“有效C ++”书中第7条的最后5个段落,以获取详细信息,从“有时候给类提供纯虚拟析构函数可以很方便……”。


0

您要求提供一个示例,我相信以下内容提供了纯虚拟析构函数的原因。我期待着这是否是一个很好的理由的答复。

我不希望任何人能够抛出的error_base类型,但异常类型error_oh_shucks,并error_oh_blast具有相同的功能,我不想把它写两次。为避免暴露std::string给我的客户,pImpl的复杂性是必需的,并且使用std::auto_ptr必需复制构造函数。

public标头包含异常规范,客户端可以使用该规范来区分由我的库引发的不同类型的异常:

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

0

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

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

我认为这里没有什么大的想法,只是更多的解释,即纯粹的虚拟化实际上是统一工作的,也适用于析构函数。


-2

1)当您需要要求派生类进行清理时。这是罕见的。

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


-2

我们需要将析构函数虚拟化,原因是,如果不对析构函数进行虚拟化,则编译器将只破坏基类的内容,n所有派生类将保持不变,bacuse编译器将不会调用其他任何析构函数类,除了基类。


-1:问题不是关于为什么析构函数应该是虚拟的。
Troubadour

此外,在某些情况下,析构函数不必是虚拟的即可实现正确的销毁。仅当您最终调用delete指向基类的指针而实际上指向它的派生类时,才需要虚拟析构函数。
CygnusX1

您是100%正确的。在过去,这一直是C ++程序中泄漏和崩溃的头号来源之一,仅次于尝试使用空指针和超出数组范围的操作。将在泛型指针上调用非虚拟基类析构函数,如果未将其标记为虚拟,则将完全绕过子类析构函数。如果有任何动态创建的对象属于该子类,则基本析构函数将不会在调用delete时恢复它们。您正在努力,然后布鲁尔!(也很难找到位置。)
克里斯·里德
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.