Why do we need a pure virtual destructor in C++?
我理解虚拟析构函数的必要性。但是为什么我们需要一个纯粹的虚拟析构函数呢?在一篇C++文章中,作者提到,当我们想要抽象类时,我们使用纯虚析构函数。
但是我们可以通过将任何成员函数设置为纯虚拟的方式来抽象类。
所以我的问题是
我们什么时候真正使析构函数成为纯虚拟的?有人能给出一个很好的实时例子吗?
当我们创建抽象类时,让析构函数也是纯虚拟的是一个好的实践吗?如果是……那为什么?
可能允许纯虚拟析构函数的真正原因是,禁止它们将意味着向语言中添加另一个规则,并且不需要此规则,因为允许纯虚拟析构函数不会产生任何不良影响。
不,简单的老虚拟就足够了。
如果您使用虚拟方法的默认实现创建一个对象,并且希望在不强制任何人重写任何特定方法的情况下使其抽象,那么您可以使析构函数纯虚拟。我看不出有什么意义,但这是可能的。
注意,由于编译器将为派生类生成隐式析构函数,因此如果该类的作者不这样做,则任何派生类都不会是抽象的。因此,在基类中使用纯虚拟析构函数不会对派生类产生任何影响。它只会使基类抽象(感谢@kappa的评论)。
我们还可以假设每个派生类可能都需要特定的清理代码,并使用纯虚拟析构函数作为编写代码的提醒,但这似乎是人为的(并且是非强制的)。
注意:析构函数是唯一的方法,即使它是纯虚拟的,也必须有一个实现才能实例化派生类(是的,纯虚拟函数可以有实现)。
1 2 3 4 5 6 7 8 9 | 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. }; |
抽象类所需要的至少是一个纯虚拟函数。任何函数都可以,但正如它所发生的那样,析构函数是任何类都可以拥有的东西,因此它总是作为候选者存在的。此外,将析构函数设为纯虚拟的(而不是仅仅是虚拟的)除了使类抽象之外,没有任何行为副作用。因此,许多样式指南建议一致地使用纯虚拟destructor来表示类是抽象的,如果没有其他原因,而是因为它提供了一个一致的位置,那么阅读代码的人可以查看类是否是抽象的。
如果要创建抽象基类:
- 无法实例化(是的,这与术语"abstract"重复!)
- 但需要虚拟析构函数行为(您打算携带指向ABC的指针,而不是指向派生类型的指针,并通过它们进行删除)
- 但对于其他方法(可能没有其他方法)不需要任何其他虚拟分派行为吗?考虑一个简单的受保护的"资源"容器,它需要一个构造函数/析构函数/赋值,但不需要太多其他内容)
…通过使析构函数纯虚拟化并为其提供定义(方法体),最容易将类抽象化。
对于我们假设的ABC:
您保证它不能被实例化(甚至在类本身内部,这也是私有构造函数可能不够的原因),您得到了您想要的析构函数的虚拟行为,并且您不必找到另一个不需要虚拟调度的方法并将其标记为"virtual"。
从我读到的答案到你的问题,我无法推断出实际使用纯虚拟析构函数的一个好理由。例如,以下原因根本无法说服我:
Probably the real reason that pure virtual destructors are allowed is that to prohibit them would mean adding another rule to the language and there's no need for this rule since no ill-effects can come from allowing a pure virtual destructor.
在我看来,纯虚拟析构函数是有用的。例如,假设代码中有两个类MyClassA和MyClassB,并且MyClassB继承自MyClassA。由于Scott Meyers在《更有效的C++》一书中提到的"非叶类抽象"项目33的原因,实际创建一个MyCyclass和MyCasb继承的抽象类MyAcExct类是更好的实践。这提供了更好的抽象,并防止了一些问题的发生,例如,对象副本。
在(创建类MyAbstractClass)的抽象过程中,可能没有MyClassA或MyClassB的方法是纯虚拟方法(这是MyAbstractClass抽象的先决条件)的好候选。在本例中,定义抽象类的析构函数pure-virtual。
下面是我自己编写的一些代码的一个具体示例。我有两个类,数值类/物理类,它们共享公共属性。因此,我让他们从抽象类iparams继承。在这种情况下,我手边绝对没有一种方法可以是纯虚拟的。例如,setParameter方法对于每个子类必须具有相同的主体。我唯一的选择就是让iparams的析构函数完全虚拟化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | 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++中意味着它是一个成员对象,因此ONU属于拥有的类,在销毁自身之前把它处理掉或者手的所有权。好的。
这两个概念在单继承语言中比在C++之类的多重继承模型中更容易实现,但是这些规则本质上是相同的。当类标识不明确时(例如将香蕉类指针传递到接受水果类指针的函数中),就会出现复杂的情况。好的。
虚拟函数首先是运行时的东西。它是多态性的一部分,因为它用于决定在运行程序中调用函数时运行哪个函数。好的。
虚关键字是一个编译器指令,用于在类标识不明确时按一定顺序绑定函数。虚函数总是在父类中(据我所知),并向编译器指示成员函数与其名称之间的绑定应该首先使用子类函数,然后使用父类函数。好的。
水果类可以有一个虚拟函数color(),默认情况下它返回"none"。函数的作用是:返回"黄色"或"棕色"。好的。
但是,如果接受水果指针的函数调用发送给它的香蕉类的color(),那么调用哪个color()函数?函数通常会为水果对象调用fruit::color()。好的。
那将是99%的时间不是预期的。但是,如果将fruit::color()声明为虚拟的,那么将为该对象调用banana:color(),因为在调用时,正确的color()函数将绑定到fruit指针。运行时将检查指针指向的对象,因为它在水果类定义中被标记为虚拟对象。好的。
这与重写子类中的函数不同。在那种情况下水果指针将调用水果::color(),如果它只知道它是一个指向水果的指针。好的。
因此,现在出现了"纯虚函数"的概念。这是一个相当不幸的短语,因为纯洁与此无关。这意味着它的目的是永远不调用基类方法。实际上,不能调用纯虚拟函数。然而,它仍然必须被定义。函数签名必须存在。为了完整性,许多编码人员会生成一个空的实现,但如果不完整,编译器将在内部生成一个。在这种情况下,当调用函数时,即使指针指向水果,也会调用banana::color(),因为它是color()的唯一实现。好的。
现在是最后一个难题:构造器和析构函数。好的。
纯虚拟构造函数完全是非法的。刚刚好。好的。
但是纯虚拟析构函数确实可以在您希望禁止创建基类实例的情况下工作。如果基类的析构函数是纯虚拟的,则只能实例化子类。约定将其分配给0。好的。
1 2 | virtual ~Fruit() = 0; // pure virtual Fruit::~Fruit(){} // destructor implementation |
在这种情况下,您必须创建一个实现。编译器知道这就是您正在做的,并确保您正确地执行它,或者它强烈地抱怨它不能链接到它需要编译的所有函数。如果您在如何建模类层次结构方面不处于正确的轨道上,那么这些错误可能会令人困惑。好的。
因此,在本例中,禁止创建水果实例,但允许创建香蕉实例。好的。
删除指向香蕉实例的水果指针的调用将首先调用banana::~banana(),然后始终调用fuit::~fruit()。因为不管怎样,当调用子类析构函数时,基类析构函数必须跟随其后。好的。
这是个坏模型吗?它在设计阶段更复杂,是的,但是它可以确保在运行时执行正确的链接,并且在不明确访问哪个子类的情况下执行子类函数。好的。
如果你写C++,这样你只会绕过没有泛型或不明确指针的精确类指针,那么虚拟函数就不需要了。但是,如果您需要类型的运行时灵活性(如Apple Banana Orange==>Fruit),那么使用更少的冗余代码,函数就变得更简单、更通用。您不再需要为每种水果类型编写函数,并且您知道每种水果都将使用自己正确的函数响应color()。好的。
我希望这个冗长的解释巩固了概念,而不是混淆了事情。有很多很好的例子要看,看看足够多的东西,然后实际地运行它们,和它们混在一起,你就会得到它。好的。好啊。
这里我想告诉你什么时候需要虚拟析构函数,什么时候需要纯虚拟析构函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | 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*pbase=new derived();删除pBASE;不需要纯虚拟析构函数,只有虚拟析构函数才能完成此任务。
这是一个十年前的话题:)从"有效C++"一书中阅读第5条第7段的细节,开始从"偶尔给一个类纯虚拟析构函数可以很方便……"
也许还有另一个纯虚拟析构函数的真实用例,我在其他答案中看不到它:)
首先,我完全同意标记的答案:这是因为禁止纯虚拟析构函数在语言规范中需要一个额外的规则。但Mark所要求的用例仍然不是:)
首先想象一下:
1 2 3 4 | class Printable { virtual void print() const = 0; // virtual destructor should be here, but not to confuse with another problem }; |
像是:
1 2 3 4 | class Printer { void queDocument(unique_ptr<Printable> doc); void printAll(); }; |
简单地说,我们有接口
现在想象完全一样,除了印刷而不是毁灭:
1 2 3 | class Destroyable { virtual ~Destroyable() = 0; }; |
还有一个类似的容器:
1 2 3 4 5 6 | class PostponedDestructor { // Queues an object to be destroyed later. void queObjectForDestruction(unique_ptr<Destroyable> obj); // Destroys all already queued objects. void destroyAll(); }; |
它是我实际应用程序中的简化用例。这里唯一的区别是使用了"特殊"方法(析构函数)而不是"普通"
我认为这里没有什么大的想法,只是更多的解释纯虚拟性工作非常一致——也适用于析构函数。
您询问了一个示例,我相信下面提供了一个纯虚拟析构函数的原因。我期待着回答这是否是一个好的理由…
我不希望任何人能够抛出
公共头包含异常规范,客户机可以使用这些规范来区分我的库引发的不同类型的异常:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | // 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; } |
下面是共享的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | // 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:
1 2 3 4 5 6 7 8 9 10 11 12 | // 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_; }; |
然后,我的代码引发一个错误,原因是:
1 2 3 | #include"error.h" throw error<error_oh_shucks>("That didn't work"); |
为
1 2 3 4 5 6 7 8 | // client.cpp #include <error.h> try { } catch (const error<error_oh_shucks>&) { } catch (const error<error_oh_blast>&) { } |
我们需要使析构函数虚化,因为如果我们不使析构函数虚化,那么编译器将只破坏基类的内容,n所有派生类将保持不变,而bacuse编译器将不调用除基类以外的任何其他类的析构函数。
1)当需要派生类进行清理时。这是罕见的。
2)没有,但是您希望它是虚拟的。