我了解需要虚拟析构函数。但是为什么我们需要一个纯虚拟析构函数?在一篇C ++文章中,作者提到了当我们要使类抽象时,我们使用纯虚拟析构函数。
但是我们可以通过将任何成员函数设为纯虚函数来使类抽象。
所以我的问题是
我们什么时候才能真正使析构函数成为纯虚拟的?有人可以举一个很好的实时例子吗?
当我们创建抽象类时,将析构函数也设置成纯虚拟的是一种好习惯吗?如果是..那为什么呢?
我了解需要虚拟析构函数。但是为什么我们需要一个纯虚拟析构函数?在一篇C ++文章中,作者提到了当我们要使类抽象时,我们使用纯虚拟析构函数。
但是我们可以通过将任何成员函数设为纯虚函数来使类抽象。
所以我的问题是
我们什么时候才能真正使析构函数成为纯虚拟的?有人可以举一个很好的实时例子吗?
当我们创建抽象类时,将析构函数也设置成纯虚拟的是一种好习惯吗?如果是..那为什么呢?
Answers:
允许纯虚拟析构函数的真正原因可能是,禁止它们意味着将另一种规则添加到语言中,并且不需要此规则,因为允许纯虚拟析构函数不会带来任何不良影响。
不,普通的旧虚拟就足够了。
如果使用默认实现为其虚拟方法创建对象,并希望使其抽象而不强迫任何人重写任何特定方法,则可以将析构函数设为纯虚拟。我看不出什么要点,但有可能。
请注意,由于编译器将为派生类生成隐式析构函数,因此,如果类的作者不这样做,则任何派生类都不会是抽象的。因此,在基类中具有纯虚拟析构函数不会对派生类产生任何影响。它只会使基类成为抽象类(感谢@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.
};
foof::bar
如果您想自己看看。
您需要的抽象类至少是一个纯虚函数。任何功能都可以;但是碰巧的是,析构函数是任何类都会拥有的东西-因此它始终是候选对象。此外,使析构函数为纯虚拟的(而不是纯虚拟的)除了使类抽象之外,没有行为方面的副作用。因此,许多样式指南建议始终使用纯虚拟析构函数来指示类是抽象的—如果出于其他原因,它提供了一致的位置,那么阅读代码的人可以查看该类是否是抽象的。
如果要创建抽象基类:
...最简单的方法是,使析构函数成为纯虚函数,并为其提供定义(方法主体)。
对于我们假设的ABC:
您保证无法实例化它(即使是在类本身内部,这也就是为什么私有构造函数可能不够用),您可以得到析构函数所需的虚拟行为,而不必查找和标记另一个不需要将虚拟调度作为“虚拟”。
从我对您的问题的回答中,我无法得出充分使用纯虚拟析构函数的充分理由。例如,以下原因根本无法说服我:
允许纯虚拟析构函数的真正原因可能是,禁止它们意味着将另一种规则添加到语言中,并且不需要此规则,因为允许纯虚拟析构函数不会带来任何不良影响。
我认为,纯虚拟析构函数可能有用。例如,假设您的代码中有两个类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;
};
IParam
受保护的方法,如其他注释中所述。
在这里我想告诉我们什么时候需要虚拟析构函数,什么时候需要纯虚拟析构函数
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; 不需要纯虚拟析构函数,只有虚拟析构函数可以完成此工作。
您正在使用这些答案来进行假设,因此为了清楚起见,我将尝试做出一个更简单,更扎实的解释。
面向对象设计的基本关系是两个:IS-A和HAS-A。我没有弥补。那就是他们所说的。
IS-A表示某个特定对象在类层次结构中标识为属于其之上的类。如果香蕉对象是水果类的子类,则它是水果对象。这意味着在任何可以使用水果的地方,都可以使用香蕉。但是,它不是自反的。如果需要使用特定类,则不能用基类代替特定类。
Has-a表示对象是组合类的一部分,并且存在所有权关系。这意味着在C ++中它是成员对象,因此在销毁自己之前,有责任将其丢弃或移交所有权。
与像c ++这样的多继承模型相比,在单继承语言中更容易实现这两个概念,但是规则本质上是相同的。当类标识不明确时(例如,将Banana类指针传递到采用Fruit类指针的函数中),就会出现问题。
首先,虚函数是运行时事物。它是多态性的一部分,因为它用于确定在正在运行的程序中调用该函数时要运行哪个函数。
如果对类标识有歧义,则virtual关键字是一个编译器指令,用于按特定顺序绑定函数。虚拟函数始终位于父类中(据我所知),并向编译器指示成员函数与其名称的绑定应首先使用子类函数,然后使用父类函数进行。
Fruit类可以具有虚拟函数color(),该函数默认情况下返回“ NONE”。香蕉类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实例。
调用删除指向香蕉实例的Fruit指针将首先调用Banana ::〜Banana(),然后始终调用Fuit ::〜Fruit()。因为无论如何,当您调用子类析构函数时,必须遵循基类析构函数。
这是一个坏模型吗?是的,它在设计阶段比较复杂,但是可以确保在运行时执行正确的链接,并确保在确切地访问哪个子类的情况下执行子类功能。
如果编写C ++以便只传递没有泛型指针或模棱两可的指针的确切类指针,那么实际上就不需要虚函数。但是,如果您需要类型的运行时灵活性(如Apple Banana Orange ==> Fruit),则功能将变得更简单,功能更丰富,并且冗余代码更少。您不再需要为每种类型的水果编写函数,并且您知道每种水果都将使用自己的正确函数对color()做出响应。
我希望这个冗长的解释能够巩固概念,而不是使事情混淆。有很多很好的例子可以看,并且要看得足够多,然后实际运行它们并弄乱它们,您会明白的。
您要求提供一个示例,我相信以下内容提供了纯虚拟析构函数的原因。我期待着这是否是一个很好的理由的答复。
我不希望任何人能够抛出的error_base
类型,但异常类型error_oh_shucks
,并error_oh_blast
具有相同的功能,我不想把它写两次。为避免暴露std::string
给我的客户,pImpl的复杂性是必需的,并且使用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>&) {
}
也许还有另一个纯虚拟析构函数的真实使用案例,我实际上在其他答案中看不到:)
首先,我完全同意标记的答案:这是因为禁止纯虚拟析构函数在语言规范中需要额外的规则。但这并不是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()
。但是纯虚拟的原因仍然相同-该方法没有默认代码。可能有些混乱的事实是,必须有效地存在一些析构函数,而编译器实际上会为其生成一个空代码。但是从程序员的角度来看,纯虚拟性仍然意味着:“我没有任何默认代码,它必须由派生类提供。”
我认为这里没有什么大的想法,只是更多的解释,即纯粹的虚拟化实际上是统一工作的,也适用于析构函数。
我们需要将析构函数虚拟化,原因是,如果不对析构函数进行虚拟化,则编译器将只破坏基类的内容,n所有派生类将保持不变,bacuse编译器将不会调用其他任何析构函数类,除了基类。
delete
指向基类的指针而实际上指向它的派生类时,才需要虚拟析构函数。