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

但我们可以做一个类抽象的通过使得任何成员的职能作为纯粹的虚拟的。

所以我的问题是

  1. 当我们真的做出析构纯粹的虚拟?任何人都可以提供一个良好实例的时间?

  2. 当我们正在创造抽象的类是一个好的实践做析构也纯粹的虚拟?如果是的话..那么为什么?

有帮助吗?

解决方案

  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.
    };
    

其他提示

抽象类所需要的只是至少一个纯虚函数。任何功能都可以;但是当它发生时,析构函数是任何类将具有<!>#8212;因此它始终作为候选者存在。此外,使析构函数纯虚拟(而不仅仅是虚拟)除了使类抽象之外没有任何行为副作用。因此,很多样式指南都建议使用纯虚拟目标来表示一个类是抽象的<!>#8212;如果没有其它原因提供一个一致的地方,那么阅读代码的人可以查看是否这个班是抽象的。

如果你想要创建一个抽象的基类:

  • 不能以实例 (是的,这是多余的词"抽象"!)
  • 需要虚拟析构的行为 (你打算携带周围的指针指向ABC而不是指向源的种类,并删除过他们)
  • 不需要任何其他虚拟调度 行为的其他方法(也许有 没有其他的方法?考虑一个简单的保护"资源"容器,需要一个构造/析构/转让,但没有多少人)

...这是最简单的使类抽象的通过使得析构纯粹的虚拟 提供一个定义(法体)。

我们假设ABC:

你保证,它无法实例(即使是内部的类本身,这就是为什么私人的构造可能是不够的),则获得虚拟的行为你想要为析构,而你没有找到并标记的另一种方法,不需要虚拟派遣的"虚拟"。

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

  

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

在我看来,纯虚拟析构函数可能很有用。例如,假设代码中有两个类myClassA和myClassB,myClassB继承自myClassA。由于Scott Meyers在他的书中提到的原因<!>“更有效的C ++ <!>”,项目33 <!>“使非叶类抽象<!>”,最好实际创建myClassA和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; 
};

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

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

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。通常至少需要一个纯虚函数,让我们把<=>作为这个函数。

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

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

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

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

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

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

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

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

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

Fruit类可以有一个虚函数color()返回<!> quot; NONE <!> quot;默认情况下。 Banana类color()函数返回<!>“; YELLOW <!>”;或<!> quot; BROWN <!>“。

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

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

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

所以现在想到一个<!>“纯虚函数<!>”;过来。 这是一个相当不幸的短语,因为纯洁与它无关。这意味着永远不会调用基类方法。 实际上,无法调用纯虚函数。但是,它仍然必须定义。必须存在函数签名。许多编码器为了完整性而制作一个空的实现{},但如果没有,编译器将在内部生成一个。在这种情况下,即使指针指向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()做出正确的反应。

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

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

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

也许还有另一个 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()方法是纯虚拟的。它可能有一些正文,但如果没有默认实现,纯虚拟是一个理想的<!> quot; implementation <!> quot; (= <!> quot;必须由后代类<!>提供;)。

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

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();
};

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

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

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

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

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

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

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top