ChatGPT解决这个技术问题 Extra ChatGPT

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

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

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

所以我的问题是

我们什么时候才能真正使析构函数成为纯虚拟的?任何人都可以举一个很好的实时例子吗?当我们创建抽象类时,使析构函数也成为纯虚拟是一种好习惯吗?如果是的话..那为什么?

@Daniel- 提到的链接没有回答我的问题。它回答了为什么纯虚析构函数应该有定义。我的问题是为什么我们需要一个纯虚拟析构函数。
我试图找出原因,但你已经在这里问了这个问题。

C
Community

可能允许纯虚拟析构函数的真正原因是禁止它们意味着向语言添加另一条规则,并且不需要这条规则,因为允许纯虚拟析构函数不会产生不良影响。不,普通的旧虚拟就足够了。

如果您为其虚拟方法创建一个具有默认实现的对象,并希望使其抽象而不强制任何人覆盖任何特定方法,您可以使析构函数成为纯虚拟的。我认为这没什么意义,但这是可能的。

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

“是的,纯虚函数可以有实现”然后它不是纯虚函数。
如果你想让一个类抽象,那么让所有的构造函数都受到保护不是更简单吗?
@GMan,您错了,纯虚拟意味着派生类必须覆盖此方法,这与实现是正交的。如果您想亲自查看,请查看我的代码并注释掉 foof::bar
@GMan:C++ FAQ 精简版说“请注意,可以为纯虚函数提供定义,但这通常会使新手感到困惑,最好在以后避免。” parashift.com/c++-faq-lite/abcs.html#faq-22.4 维基百科(正确性的堡垒)也说同样的话。我相信 ISO/IEC 标准使用了类似的术语(不幸的是,我的副本目前正在使用)......我同意它令人困惑,并且在我提供定义时,我通常不会在没有澄清的情况下使用该术语,尤其是围绕新程序员...
@Motti:这里有趣并提供更多混淆的是纯虚拟析构函数不需要在派生(和实例化)类中显式覆盖。在这种情况下,使用隐式定义:)
B
Braden

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


但是为什么要提供纯virtaul析构函数的实现。什么可能会出错我将析构函数设为纯虚拟并且不提供它的实现。我假设只声明了基类指针,因此从不调用抽象类的析构函数。
@Surfing:因为派生类的析构函数隐式调用其基类的析构函数,即使该析构函数是纯虚拟的。因此,如果没有实现它,未定义的行为将会发生。
l
leander

如果要创建抽象基类:

无法实例化(是的,这与“抽象”一词是多余的!)

但需要虚拟析构函数行为(您打算携带指向 ABC 的指针而不是指向派生类型的指针,并通过它们删除)

但不需要其他方法的任何其他虚拟调度行为(也许没有其他方法?考虑一个简单的受保护“资源”容器,它需要构造函数/析构函数/赋值,但仅此而已)

...通过使析构函数纯虚拟并为其提供定义(方法体)来使类抽象是最简单的。

对于我们假设的 ABC:

您保证它不能被实例化(即使在类本身内部,这就是私有构造函数可能不够的原因),您可以获得析构函数所需的虚拟行为,并且您不必找到并标记另一个不不需要将虚拟调度作为“虚拟”。


按分数记下答案,这是第一个 1) 正确,2) 以综合语气写成的答案(而不是依赖于例子和附言),3) 问题的答案,因为它写在标题和 4) 展示了一个非常常见的用例(即具有可变大小且没有方法的“纯结构”)。点赞+点赞
L
Laurent Michel

从我读到的问题的答案中,我无法推断出实际使用纯虚拟析构函数的充分理由。例如,以下原因根本无法说服我:

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

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

在抽象过程(创建类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; 
};

我喜欢这种用法,但另一种“强制”继承的方法是声明 IParam 的构造函数受到保护,正如其他评论中所述。
A
Anil8753

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

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
}

当你希望没有人可以直接创建 Base 类的对象时,使用纯虚析构函数 virtual ~Base() = 0。通常至少需要一个纯虚函数,我们取 virtual ~Base() = 0 , 作为这个函数。当你不需要上面的东西时,只需要安全销毁 Derived 类对象 Base* pBase = new Derived();删除 pBase;不需要纯虚拟析构函数,只有虚拟析构函数才能完成这项工作。


s
sukumar

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


C
Chris Reid

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

面向对象设计的基本关系有两种:IS-A和HAS-A。这些不是我编的。他们就是这样称呼的。

IS-A 表示特定对象标识为属于在类层次结构中高于它的类。如果香蕉对象是水果类的子类,那么它就是水果对象。这意味着在任何可以使用水果类的地方,都可以使用香蕉。不过,它不是反身的。如果需要特定类,则不能用基类替换特定类。

has-a 表示一个对象是复合类的一部分并且存在所有权关系。这意味着在 C++ 中它是一个成员对象,因此拥有类的责任在于在销毁自身之前处置它或移交所有权。

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

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

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

Fruit 类可以有一个默认返回“NONE”的虚函数 color()。 Banana 类 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 实例。

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

这是一个坏模型吗?是的,它在设计阶段更复杂,但它可以确保在运行时执行正确的链接,并且在确切访问哪个子类存在歧义的情况下执行子类函数。

如果您编写 C++ 以便只传递精确的类指针,而没有通用或不明确的指针,那么就不需要虚函数。但是,如果您需要类型的运行时灵活性(如在 Apple Banana Orange ==> Fruit 中),则函数会变得更容易、更通用,并且冗余代码更少。您不再需要为每种类型的水果编写函数,并且您知道每种水果都会以自己的正确函数响应 color()。

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


R
Rai

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

我不希望任何人能够抛出 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>&) {
}

J
Jarek C

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

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

我认为这没什么大不了的,只是更多地解释了纯虚拟的工作方式非常一致——也适用于析构函数。


J
J-Q

这是一个十年前的话题 :) 阅读“Effective C++”一书第 7 条的最后 5 段了解详细信息,从“偶尔给一个类一个纯虚拟析构函数......”开始。


B
Brad Larson

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


-1:问题不在于为什么析构函数应该是虚拟的。
此外,在某些情况下,析构函数不必是虚拟的即可实现正确的销毁。仅当您最终在指向基类的指针上调用 delete 时才需要虚拟析构函数,而实际上它指向其派生类。
你是 100% 正确的。这是并且在过去一直是 C++ 程序中泄漏和崩溃的第一大来源之一,仅次于尝试使用空指针和超出数组边界的操作。非虚拟基类析构函数将在通用指针上调用,如果子类析构函数未标记为虚拟,则完全绕过子类析构函数。如果有任何动态创建的对象属于子类,它们将不会被基析构函数在调用删除时恢复。 BLUURRK 你玩得很好! (也很难找到在哪里。)