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

我知道需要一个虚拟毁灭器。但是为什么我们需要一个 纯洁虚拟析构函数?在 C + + 的一篇文章中,作者提到当我们想要使类抽象时,我们使用纯虚析构函数。

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

所以我的问题是

  1. 什么时候我们真正使一个析构函数纯虚拟? 有人能给出一个很好的实时例子吗?

  2. 当我们创建抽象类的时候,使析构函数也是纯虚的是一个好的实践吗?如果是的话。.那为什么?

106754 次浏览
  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.
};

如果要创建抽象基类:

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

... 通过使析构函数为其提供一个定义(方法体) ,最容易使类抽象化。

对于我们假设的 ABC:

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

对于抽象类,您所需要的至少是一个纯虚函数。任何函数都可以; 但是碰巧的是,析构函数是 任何类将拥有的函数,所以它总是作为候选函数存在。此外,使析构函数纯虚(而不仅仅是虚)除了使类抽象之外没有其他行为副作用。因此,许多样式指南建议一致地使用纯虚构函数来表示类是抽象的 & mash; 如果没有其他原因,只是提供了一个一致的位置,读代码的人可以查看该类是否是抽象的。

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

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

您需要一个示例,我相信下面提供了使用纯虚析构函数的理由。我期待着答复,这是否是一个 很好的原因..。

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

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

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. 当您不需要以上内容时,只需要安全地销毁派生类对象即可

    基数 * pBase = 新派生() ; 删除 pBase; 不需要纯粹的虚拟析构函数,只需要虚拟析构函数就可以了

根据我对你问题的回答,我无法推断出使用纯虚析构函数的充分理由。例如,下面的理由根本不能说服我:

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

在我看来,纯虚析构函数是有用的。例如,假设您的代码中有两个类 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;
};

你的这些答案都是假设性的,所以为了清楚起见,我会试着做一个更简单,更实际的解释。

面向对象设计的基本关系有两个: IS-A 和 HAS-A。这两个名字不是我编的。它们就是这么叫的。

IS-A 表示某个特定对象标识为在类层次结构中位于该对象之上的类的对象。如果香蕉对象是水果类的子类,那么它就是水果对象。这意味着在任何可以使用水果类的地方,都可以使用香蕉。不过,这不是条件反射。如果调用特定类,则不能用基类替换该特定类。

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

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

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

Virtual 关键字是一个编译器指令,用于在类标识不明确时按一定顺序绑定函数。虚函数总是位于父类中(据我所知) ,并向编译器指示成员函数的名称绑定应该先与子类函数绑定,然后再与父类函数绑定。

Fruitclass 可以有一个默认返回“ NONE”的虚函数 color ()。 香蕉类 color ()函数返回“黄色”或“棕色”。

但是,如果带有 Fruit 指针的函数调用发送给它的 Banana 类的 color ()——那么会调用哪个 color ()函数呢? 这个函数通常会为一个 Fruit 对象调用 Fruit: : color ()。

99% 的情况都不是我们想要的。 但是,如果 Fruit: : color ()声明为 Virtual,那么将为对象调用 Banana: color () ,因为正确的 color ()函数将在调用时绑定到 Fruit 指针。 运行时将检查指针指向哪个对象,因为它在 Fruit 类定义中被标记为 Virtual。

这与重写子类中的函数不同 如果它只知道它是——一个水果指针,那么它将调用 Fruit: : color ()。

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

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

纯粹的虚拟构造函数是非法的,完全不合法。

但是如果您希望禁止创建基类实例,那么纯虚析构函数确实可以工作。如果基类的析构函数是纯虚的,则只能实例化子类。 约定是将它赋值为0。

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

在这种情况下,您必须创建一个实现。编译器知道这就是您正在做的事情,并确保您做得正确,否则它会强烈抱怨无法链接到它需要编译的所有函数。如果在如何建模类层次结构的问题上没有走在正确的轨道上,那么这些错误可能会令人困惑。

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

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

这个型号不好吗?它在设计阶段更加复杂,是的,但是它可以确保在运行时执行正确的链接,并且在访问哪个子类的确切位置不明确的情况下执行子类函数。

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

我希望这个冗长的解释能够巩固这个概念,而不是混淆视听。有很多很好的例子可以参考, 看够了,真正地运行它们,把它们搞乱,你就会得到它们。

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

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

我认为这里没有什么大的想法,只是更多的解释,纯粹的虚拟工作真的一致-也为析构函数。

这是一个十年前的话题:) 阅读关于“有效的 C + +”一书的第7项的最后5段,了解详细信息,从“有时候给一个类提供一个纯粹的虚析构函数是很方便的... ...”开始