我相信我搜索了许多有关虚拟析构函数的内容,其中大多数提到虚拟析构函数的目的以及为什么需要虚拟析构函数。我还认为,在大多数情况下,析构函数必须是虚拟的。
然后的问题是:为什么c ++默认不将所有析构函数设置为虚拟?或其他问题:
什么时候不需要使用虚拟析构函数?
在这种情况下,我不应该使用虚拟析构函数?
即使我不需要虚拟析构函数,使用虚拟析构函数的成本是多少?
我相信我搜索了许多有关虚拟析构函数的内容,其中大多数提到虚拟析构函数的目的以及为什么需要虚拟析构函数。我还认为,在大多数情况下,析构函数必须是虚拟的。
然后的问题是:为什么c ++默认不将所有析构函数设置为虚拟?或其他问题:
什么时候不需要使用虚拟析构函数?
在这种情况下,我不应该使用虚拟析构函数?
即使我不需要虚拟析构函数,使用虚拟析构函数的成本是多少?
Answers:
如果将虚拟析构函数添加到类中:
在大多数(所有)当前C ++实现中,该类的每个对象实例都需要为运行时类型存储指向虚拟调度表的指针,并且该虚拟调度表本身已添加到可执行映像中
虚拟调度表的地址不一定跨进程有效,这可能会阻止在共享内存中安全地共享此类对象
具有一个嵌入式虚拟指针使创建类的内存布局与某些已知的输入或输出格式相匹配(例如,因此a Price_Tick*
可以直接针对传入的UDP数据包中适当对齐的内存,并用于解析/访问或更改数据),或者放置new
此类以将数据写入输出数据包)
在某些条件下,销毁者自己调用可能必须虚拟地调度,因此是脱机的;而非虚拟销毁者可能对于调用者而言琐碎或无关紧要地进行内联或优化
如果“以非继承的方式进行设计”并非如上述那样在实际情况下也较差,那么它也不是始终不具有虚拟析构函数的实际原因。但更糟糕的是,这是何时支付费用的主要标准:如果您的类打算用作基类,则默认为使用虚拟析构函数。这并非总是必要的,但是如果使用基类指针或引用调用派生类析构函数,则可以确保继承体系中的类可以更自由地使用,而不会发生意外的未定义行为。
“在大多数情况下,析构函数必须是虚拟的”
并非如此……许多班级都没有这种需要。有太多的例子说明了在不必要的地方枚举它们是很愚蠢的,但是只要浏览一下标准库或说boost,就会发现大多数类没有虚拟析构函数。在提升1.53中,我计算了494个虚拟析构函数中的72个。
在这种情况下,我不应该使用虚拟析构函数?
顺便说一句,
在哪种情况下应使用虚拟析构函数?
对于具有多态删除的基类。
即使我不需要虚拟析构函数,使用虚拟析构函数的成本是多少?
将任何虚函数引入类(继承或类定义的一部分)的成本对于每个对象存储的虚拟指针而言,其初始成本可能非常高(或不取决于对象),如下所示:
struct Integer
{
virtual ~Integer() {}
int value;
};
在这种情况下,存储器成本相对较大。现在,在64位体系结构上,类实例的实际内存大小通常看起来像这样:
struct Integer
{
// 8 byte vptr overhead
int value; // 4 bytes
// typically 4 more bytes of padding for alignment of vptr
};
此类的总数为16个字节,Integer
而仅有4个字节。如果将一百万个这样的内存存储在一个阵列中,最终将占用16兆字节的内存:这是典型的8 MB L3 CPU高速缓存大小的两倍,而反复循环访问此类数组可能会比4兆字节慢很多倍。由于额外的高速缓存未命中和页面错误而导致没有虚拟指针。
但是,随着更多的虚拟功能,每个对象的虚拟指针成本不会增加。一个类中可以有100个虚拟成员函数,每个实例的开销仍然是单个虚拟指针。
从开销的角度来看,虚拟指针通常是更直接的关注点。但是,除了虚拟指针之外,每个实例还需要支付每个类的费用。每个具有虚拟函数的类都会vtable
在内存中存储一个内存,该地址将在进行虚拟函数调用时实际应调用的函数(虚拟/动态调度)。然后,vptr
每个实例存储的指向该特定于类的vtable
。通常,这种开销较少,但是如果不必要地为复杂代码库中的一千个类支付了这种开销,则可能会增加二进制文件的大小并增加一点运行时成本,例如,这vtable
方面的开销实际上会随着更多和更多部分成比例地增加。混合了更多虚拟功能。
在性能至关重要的领域工作的Java开发人员非常了解这种开销(尽管通常在装箱说明中进行了描述),因为Java用户定义的类型隐式继承自中央object
基类并且Java中的所有函数都是隐式虚拟的(可重写的) ),除非另有说明。结果,由于每个实例相关联Integer
的这种vptr
样式元数据,Java 同样倾向于在64位平台上需要16个字节的内存,并且在Java中通常不可能在int
不付出运行时的情况下将单个对象包装到类中性能成本。
然后的问题是:为什么c ++默认不将所有析构函数设置为虚拟?
C ++确实以“随用随付”的心态来支持性能,并且仍然从C继承了许多裸机硬件驱动的设计。它不想不必要地包括vtable生成和动态分配所需的开销。每个涉及的类/实例。如果性能不是使用C ++这样的语言的关键原因之一,那么您可能会从其他编程语言中受益更多,因为许多C ++语言的安全性和难度要比理想情况下的理想状态要差得多。支持这种设计的关键原因。
什么时候不需要使用虚拟析构函数?
经常。如果一个类不被设计为继承的,那么它就不需要虚拟的析构函数,而最终只会为不需要的东西付出可能的大笔开销。同样,即使将一个类设计为继承的,但您绝不会通过基指针删除子类型实例,那么它也不需要虚拟析构函数。在这种情况下,一种安全的做法是定义一个受保护的非虚拟析构函数,如下所示:
class BaseClass
{
protected:
// Disallow deleting/destroying subclass objects through `BaseClass*`.
~BaseClass() {}
};
在这种情况下,我不应该使用虚拟析构函数?
实际上,当您应该使用虚拟析构函数时,它更容易涵盖。通常,代码库中的更多类都不是为继承而设计的。
std::vector
,例如,不是设计为可以继承的,并且通常不应该被继承(非常不稳定的设计),否则std::vector
,除了笨拙的对象切片问题之外,这还会导致此基本指针删除问题(故意避免使用虚拟析构函数)派生类会添加任何新状态。
通常,继承的类应该具有公共虚拟析构函数或受保护的非虚拟析构函数。从C++ Coding Standards
第50章开始:
50.使基类析构函数成为公共的和虚拟的,或受保护的和非虚拟的。删除,不删除 问题是:如果应该允许通过指向基本Base的指针进行删除,则Base的析构函数必须是公共的和虚拟的。否则,应该对其进行保护并且是非虚拟的。
C ++倾向于隐含地强调的一件事(因为设计往往变得非常脆弱,笨拙,甚至可能不安全),这是这样的想法,即继承不是旨在用作事后思考的机制。这是一种考虑了多态性的可扩展性机制,但它需要对需要扩展性的地方具有远见。因此,您的基类应被设计为预先继承层次结构的根,而不是您事后从事后继承的东西,而无需事先进行任何此类预见。
在那些您只想继承以重用现有代码的情况下,通常强烈建议进行组合(复合重用原理)。
为什么C ++默认不将所有析构函数设置为虚拟? 额外存储和调用虚拟方法表的成本。C ++用于可能会造成负担的系统,低延迟,rt编程。
这是何时不使用虚拟析构函数的一个很好的例子:来自Scott Meyers:
如果一个类不包含任何虚函数,则通常表明它不打算用作基类。当不打算将一个类用作基类时,将析构函数虚拟化通常不是一个好主意。考虑以下示例,基于ARM中的讨论:
// class for representing 2D points
class Point {
public:
Point(short int xCoord, short int yCoord);
~Point();
private:
short int x, y;
};
如果short int占用16位,则Point对象可以放入32位寄存器中。此外,Point对象可以作为32位量传递给用其他语言(例如C或FORTRAN)编写的函数。但是,如果将Point的析构函数设为虚拟,情况将发生变化。
添加虚拟成员后,虚拟指针即被添加到您的类中,该指针指向该类的虚拟表。
If a class does not contain any virtual functions, that is often an indication that it is not meant to be used as a base class.
t 还有其他人还记得“旧时光”吗?在那儿,我们被允许使用类和继承来构建可重复使用的成员和行为的连续层,而根本不必关心虚拟方法?来吧,斯科特。我明白了核心要点,但是“常”确实可以实现。
虚拟析构函数会增加运行时成本。如果该类没有任何其他虚拟方法,则代价尤其巨大。仅在一种特定情况下才需要虚拟析构函数,在该特定情况下,通过指向基类的指针删除或破坏对象。在这种情况下,基类的析构函数必须是虚的,并且任何派生类的析构函数都将是隐式虚的。在某些情况下,使用多态基类的方式不需要析构函数是虚拟的:
std::unique_ptr<Derived>
,并且多态性仅通过非拥有的指针和引用发生。另一个示例是使用分配对象时std::make_shared<Derived>()
。std::shared_ptr<Base>
只要初始指针是a ,就可以使用它std::shared_ptr<Derived>
。这是因为共享指针对析构函数(删除程序)具有自己的动态分配,这些动态分配不一定依赖于虚拟基类的析构函数。当然,仅以上述方式使用对象的任何约定都可以轻易打破。因此,赫伯·萨特(Herb Sutter)的建议仍然一如既往:“基类析构函数应该是公共的和虚拟的,或者应该是受保护的并且是非虚拟的”。这样,如果有人尝试删除具有非虚拟析构函数的基类的指针,则他很可能在编译时收到访问冲突错误。
再有,有些类并非设计为(公共)基类。我个人的建议是使它们final
使用C ++ 11或更高版本。如果将其设计为方形钉,则可能无法很好地用作圆形钉。这与我倾向于在基类和派生类之间具有显式继承协定,对于NVI(非虚拟接口)设计模式,用于抽象而不是具体的基类以及我不喜欢受保护的成员变量等有关。 ,但我知道所有这些观点都在一定程度上引起争议。
创建vtable的构造函数中会产生开销(如果您没有其他虚函数,则在这种情况下,您可能(但并非总是)也应该具有虚拟析构函数)。而且,如果您没有任何其他虚函数,它会使您的对象的指针大小比其他情况大一个。显然,增大尺寸会对小物体产生很大影响。
读取了额外的内存以获取vtable,然后通过该内存调用indirectory函数,这在调用析构函数时比非虚拟析构函数要大。因此,当然,每次对析构函数的调用都会生成一些额外的代码。这是针对编译器无法推断实际类型的情况-在那些可以推断实际类型的情况下,编译器将不使用vtable,而是直接调用析构函数。
你应该有一个虚析构函数,如果你的类的目的是作为一个基类,特别是如果它可以创建/通过其他一些实体比知道它是什么类型的创作,那么你需要一个虚拟析构函数的代码破坏。
如果不确定,请使用虚拟析构函数。如果将虚拟显示为问题,则将其删除要比尝试查找“未调用正确的析构函数”引起的错误要容易得多。
简而言之,在以下情况下,您不应具有虚拟析构函数:1.您没有任何虚拟函数。2.不要从类派生(final
在C ++ 11中标记它,那样编译器会告诉您是否尝试从它派生)。
在大多数情况下,除非有“大量内容”,否则创建和销毁不是使用特定对象所花费时间的主要部分(创建1MB字符串显然要花费一些时间,因为至少1MB的数据需要从当前所在的位置复制)。销毁1MB的字符串并不比销毁150B的字符串更糟,两者都需要取消分配字符串存储空间,并且不需要太多其他操作,因此花费的时间通常是相同的。 “毒药模式”-但这不是您要在生产中运行实际应用程序的方式]。
简而言之,开销很小,但是对于较小的对象,这可能会有所作为。
还要注意,在某些情况下,编译器可以优化虚拟查找,因此这只是一个惩罚。
与往常一样,在性能,内存占用等方面:进行基准测试,性能分析和测量,将结果与替代方法进行比较,查看在时间/内存上花费的大部分时间,而不要尝试优化90%的时间/内存。运行很少的代码(大多数应用程序大约有10%的代码对执行时间有很大影响,而90%的代码根本没有太大影响)。以较高的优化级别执行此操作,因此您已经受益于编译器的出色表现!并重复一次,再次检查并逐步改善。除非您在特定类型的应用程序上有丰富的经验,否则不要试图变得聪明,不要弄清楚什么是重要的,什么不是什么。
You **should** have a virtual destructor if your class is intended as a base-class
是过分简单化和过早的悲观化。仅当允许任何人通过指向基数的指针删除派生类时才需要此方法。在许多情况下并非如此。如果知道的话,那么肯定会产生开销。其中,顺便说一句,是一直增加,即使实际的呼叫可以被编译器静态解析。否则,当您正确控制人们可以对您的对象执行的操作时,这是不值得的