首页 文章

为什么我们需要在C中使用纯虚拟析构函数?

提问于
浏览
136

我理解虚拟析构函数的必要性 . 但为什么我们需要纯虚拟析构函数?在其中一篇C文章中,作者提到我们在创建类抽象时使用纯虚析构函数 .

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

所以我的问题是

  • 我们什么时候才能真正使析构函数变为虚拟?任何人都能给出一个很好的实时例子吗?

  • 当我们创建抽象类时,将析构函数设置为纯虚拟是一种很好的做法吗?如果是..那为什么?

12 回答

  • -2

    在这里,我想告诉我们何时需要 virtual destructor 以及何时需要 pure virtual destructor

    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;不需要纯虚析构函数,只有虚析构函数才能完成这项工作 .

  • 0

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

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

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

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

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

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

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

    Fruit类可以具有虚函数color(),默认情况下返回“NONE” . Banana类color()函数返回“YELLOW”或“BROWN” .

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

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

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

    所以现在出现了“纯虚函数”的想法 . 这是一个相当不幸的短语,因为纯洁与它无关 . 这意味着永远不会调用基类方法 . 实际上,无法调用纯虚函数 . 但是,它仍然必须定义 . 必须存在函数签名 . 许多程序员都是空的实现{}表示完整性,但如果没有,编译器将在内部生成一个 . 在那种情况下,即使指针是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 ==> Fruit中),则功能变得更容易,更通用,冗余代码更少 . 您不再需要为每种类型的水果编写一个函数,并且您知道每个水果都会以自己正确的函数响应color() .

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

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

    • 不,简单的老虚拟就足够了 .

    如果您为其虚拟方法创建一个具有默认实现的对象,并希望在不强制任何人覆盖任何 specific 方法的情况下使其成为抽象,则可以使析构函数为纯虚拟 . 我不可能_1159472 .

    请注意,由于编译器将为派生类生成隐式析构函数,如果类的作者不这样做,则任何派生类都将是抽象的 not . 因此,在基类中使用纯虚析构函数不会对派生类产生任何影响 . 它只会使基类抽象(感谢@kappa的评论) .

    也可以假设每个派生类可能需要具有特定的清理代码并使用纯虚拟析构函数作为提示来编写一个,但这似乎是人为的(并且是非强制的) .

    Note: 析构函数是唯一的方法,即使它是纯虚拟的 has 也有一个实现来实例化派生类(是的纯虚函数可以有实现) .

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

    抽象类所需要的只是至少一个纯虚函数 . 任何功能都可以;但是当它发生时,析构函数是任何类都会有的 - 所以它总是作为候选者存在 . 此外,使析构函数纯虚拟(而不仅仅是虚拟)除了使类抽象之外没有任何行为副作用 . 因此,许多样式指南建议始终使用纯虚拟目标来指示一个类是抽象的 - 如果没有其他原因,它提供一致的位置,有人阅读代码可以查看该类是否是抽象的 .

  • 1

    如果要创建抽象基类:

    • 那个 can't be instantiated (是的,这与术语"abstract"多余了!)

    • needs virtual destructor behavior (你打算携带指向ABC的指针,而不是指向派生类型的指针,并通过它们删除)

    • 但是 does not need any other virtual dispatch 其他方法的行为(也许没有其他方法?考虑一个简单的受保护的"resource"容器需要构造函数/析构函数/赋值但不是很多其他方法)

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

    对于我们假设的ABC:

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

  • 28

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

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

    在我看来,纯虚拟析构函数可能很有用 . 例如,假设代码中有两个类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; 
    };
    
  • 0

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

  • 1

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

    我不希望任何人能够抛出 error_base 类型,但异常类型 error_oh_shuckserror_oh_blast 具有相同的功能,我不想写两次 . pImpl的复杂性对于避免将 std::string 暴露给我的客户端是必要的,并且使用 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

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

    首先,我完全同意明确的答案:这是因为禁止纯虚拟析构函数需要在语言规范中有一个额外的规则 . 但它仍然不是马克要求的用例:)

    首先想象一下:

    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 和一些"container"用这个接口保存任何东西 . 我想在这里很明显为什么 print() 方法是纯虚拟的 . 它可能有一些正文,但如果没有默认实现,纯虚拟是一个理想的"implementation"(= "must be provided by a descendant class") .

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

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

    它是我真实应用程序中简化的用例 . 这里唯一的区别是使用了"special"方法(析构函数)而不是"normal" print() . 但它是纯虚拟的原因仍然是相同的 - 该方法没有默认代码 . 有点令人困惑的事实是,必须有效地使用一些析构函数,编译器实际上会生成一个空代码为了它 . 但从程序员的角度看,纯虚拟仍然意味着:"I don't have any default code, it must be provided by derived classes."

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

  • 7

    这是一个十年之久的话题:)详细阅读“Effective C”一书中关于第7项的最后5段,从“偶尔为一个类提供一个纯粹的虚拟析构函数......”开始 .

  • 3

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

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

  • 18

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

相关问题