我理解对虚拟析构函数的需求。但是为什么我们需要一个纯虚析构函数呢?在其中一篇 C++ 文章中,作者提到当我们想要使类抽象时,我们使用纯虚析构函数。
但是我们可以通过将任何成员函数设为纯虚拟来使类抽象。
所以我的问题是
我们什么时候才能真正使析构函数成为纯虚拟的?任何人都可以举一个很好的实时例子吗?
当我们创建抽象类时,使析构函数也成为纯虚拟是一种好习惯吗?如果是的话..那为什么?
我理解对虚拟析构函数的需求。但是为什么我们需要一个纯虚析构函数呢?在其中一篇 C++ 文章中,作者提到当我们想要使类抽象时,我们使用纯虚析构函数。
但是我们可以通过将任何成员函数设为纯虚拟来使类抽象。
所以我的问题是
我们什么时候才能真正使析构函数成为纯虚拟的?任何人都可以举一个很好的实时例子吗?
当我们创建抽象类时,使析构函数也成为纯虚拟是一种好习惯吗?如果是的话..那为什么?
可能允许纯虚拟析构函数的真正原因是禁止它们意味着向语言添加另一条规则,并且不需要这条规则,因为允许纯虚拟析构函数不会产生不良影响。
不,普通的旧虚拟就足够了。
如果您为其虚拟方法创建一个具有默认实现的对象,并希望使其抽象而不强制任何人覆盖任何特定方法,您可以使析构函数成为纯虚拟的。我认为这没什么意义,但这是可能的。
请注意,由于编译器将为派生类生成隐式析构函数,如果类的作者不这样做,任何派生类都不会是抽象的。因此,在基类中拥有纯虚析构函数不会对派生类产生任何影响。它只会使基类抽象(感谢@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:
您保证它不能被实例化(即使在类本身内部,这就是私有构造函数可能不够的原因),您可以获得析构函数所需的虚拟行为,并且您不必找到并标记另一个不不需要将虚拟调度作为“虚拟”。
从我读到的问题的答案中,我无法推断出实际使用纯虚拟析构函数的充分理由。例如,以下原因根本无法说服我:
可能允许纯虚拟析构函数的真正原因是禁止它们意味着向语言添加另一条规则,并且不需要这条规则,因为允许纯虚拟析构函数不会产生不良影响。
在我看来,纯虚拟析构函数可能很有用。例如,假设您的代码中有两个类 myClassA 和 myClassB,并且 myClassB 继承自 myClassA。由于 Scott Meyers 在他的“更有效的 C++”一书中第 33 条“使非叶类抽象化”中提到的原因,更好的做法是实际创建一个抽象类 myAbstractClass,myClassA 和 myClassB 继承自该抽象类。这提供了更好的抽象并防止了一些问题,例如对象副本。
在抽象过程(创建类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
}
当您希望没有人可以直接创建 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 类可以有一个默认返回“NONE”的虚函数 color()。Banana 类 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 实例。
对指向 Banana 实例的 Fruit 指针的 delete 调用将首先调用 Banana::~Banana(),然后总是调用 Fuit::~Fruit()。因为无论如何,当你调用子类析构函数时,基类析构函数必须跟随。
这是一个坏模型吗?是的,它在设计阶段更复杂,但它可以确保在运行时执行正确的链接,并且在确切访问哪个子类存在歧义的情况下执行子类函数。
如果您编写 C++ 以便只传递精确的类指针,而没有通用或不明确的指针,那么就不需要虚函数。但是,如果您需要类型的运行时灵活性(如在 Apple Banana Orange ==> Fruit 中),则函数会变得更容易、更通用,并且冗余代码更少。您不再需要为每种类型的水果编写函数,并且您知道每种水果都会以自己的正确函数响应 color()。
我希望这个冗长的解释能够巩固这个概念,而不是混淆事物。有很多很好的例子可以看,看够了,实际运行它们,弄乱它们,你就会明白的。
你问了一个例子,我相信下面提供了一个纯虚拟析构函数的原因。我期待着回答这是否是一个很好的理由......
我不希望任何人能够抛出error_base
类型,但异常类型error_oh_shucks
和error_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>&) {
}
也许还有另一个纯虚拟析构函数的真实用例,我实际上在其他答案中看不到:)
起初,我完全同意标记的答案:这是因为禁止纯虚拟析构函数需要语言规范中的额外规则。但这仍然不是 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()
。但它是纯虚的原因还是一样的——方法没有默认代码。有点令人困惑的是,必须有一些有效的析构函数,编译器实际上会为它生成一个空代码。但从程序员的角度来看,纯虚拟仍然意味着:“我没有任何默认代码,它必须由派生类提供。”
我认为这没什么大不了的,只是更多地解释了纯虚拟的工作方式非常一致——也适用于析构函数。
这是一个十年前的话题 :) 阅读“Effective C++”一书第 7 条的最后 5 段了解详细信息,从“偶尔给一个类一个纯虚拟析构函数......”开始。
我们需要将析构函数设为虚拟,因为如果我们不将析构函数设为虚拟,那么编译器只会破坏基类的内容,所有派生类都将保持不变,bacuse编译器不会调用任何其他的析构函数。除了基类之外的类。