官方解释的虚函数是:
虚函数是您希望在派生类中重新定义的成员函数。当您使用指针或对基类的引用引用派生类对象时,可以为该对象调用虚函数并执行派生类的函数版本。
请先看代码:
CREATE TABLE tag (
tag_uuid CHAR(36) PRIMARY KEY,
name VARCHAR(255) UNIQUE
);
CREATE TABLE type (
type_uuid CHAR(36) PRIMARY KEY,
name VARCHAR(255) UNIQUE
);
CREATE TABLE type_tags (
type_uuid CHAR(36),
tag_uuid CHAR(36),
PRIMARY KEY (type_uuid, tag_uuid),
FOREIGN KEY (type_uuid) REFERENCES type (type_uuid),
FOREIGN KEY (tag_uuid) REFERENCES tag (tag_uuid)
);
输出是:
#include<iostream>
using namespace std;
class A
{
public:
A(){cout << "A()" << endl;}
~A(){cout << "~A()" << endl;}
};
class B:public A
{
public:
B(): A(){cout << "B()" << endl;}
~B(){cout << "~B()" << endl;}
};
int main()
{
A * pt = new B;
delete pt;
}
我的问题是:
答案 0 :(得分:2)
delete pt;
的析构函数不是虚拟的, A
会导致undefined behaviour。
使A
析构函数为虚拟的原因是允许使用delete pt;
删除B
对象。
这样做的理由是,当编译器看到delete pt;
时,通常无法知道pt
是否指向B
对象,因为该决定可能直到运行时才进行。因此,您需要查找对象的某些运行时属性(在本例中为vtable)以找出要调用的正确析构函数。
其他一些评论/答案表明原始代码的定义行为是不要调用B的析构函数或其他东西。不过那是错的。您只是看到未定义行为的症状,可能是那个或其他任何行为。
答案 1 :(得分:1)
如果析构函数被标记为虚拟,那么当您调用delete
时,将调用已分配的对象的动态类型的析构函数。在您的示例中,堆上对象的静态类型是A,而动态类型是B.
由于您没有将析构函数标记为虚拟,因此不会调度运行时并调用A的析构函数。这是错误的,应该修复。如果您计划以多态方式使用类,请确保它的析构函数是虚拟的,以便派生类的实例可以释放它们已获取的任何资源。
答案 2 :(得分:1)
可以想象如何实现vtable。
具有虚方法的类具有指向函数指针表的指针作为其第一个元素。
虚拟方法意味着虚拟功能表中有一个条目。
对于方法,继承的类在覆盖时替换该条目。
对于析构函数,该条目实际上是&#34;如何在此对象上调用delete&#34;。所有下降的类都会自动覆盖它。它在概念上将delete base_ptr
调用为if (base_ptr) base_ptr->vtable->deleter(base_ptr);
。
然后,导出的删除有效(几乎)delete static_cast<derived*>(ptr);
这就像通常的删除调用一样,它按顺序调用析构函数。
如果不这样做,会留下未定义的行为。 UB通常是调用基类dtor。
答案 3 :(得分:0)
如果函数不是virtual
,则C ++运行库将直接调用 mangled 函数。例如,在上面的代码中,析构函数可能会被修改为_ZNK3AXXXXXXXXX
(假名)。因此,当您调用delete pt
时,运行时将执行_ZNK3AXXXXXXXXX
。
但是,如果函数为virtual
,结果会有所不同。就像@Yakk所说的那样,具有虚函数的类将具有vtable
,其条目是函数指针。它可能位于此类的地址空间的顶部,也可能位于底部,具体取决于类模型的实现。任何虚拟功能的调用都将查找此表。如果dtors是virtual
,派生类中的函数将覆盖表中的相应条目。当您使用指针或引用调用它时,C ++运行时将调用表中的函数。再看看你的例子。 pt
指向的对象条目已被类B
覆盖,因此当您调用delete pt
时,将调用dtor的overrode版本。这就是区别。