让我们以c ++为例:
class A
{
public:
A() { cout << "hey" << endl; }
~A() { cout << "by" << endl; }
};
class B : public A
{
public:
B() {}
virtual ~B() { cout << "from b" << endl; }
};
int main()
{
A * a = new B[5];
delete[] a;
return 0;
}
这段代码的结果是&#34;&#34;的无限循环。 , 为什么是这样? B vtable应该被上传到没有vtable的A,所以我希望它在尝试到达虚拟构造函数时抛出异常。
P.S。 我可以阅读所有有线构造函数析构函数的例子吗? (附例子)
答案 0 :(得分:3)
将B
数组删除为A
数组是未定义的行为。只要A
有一个非平凡的析构函数(当它确实有一个简单的析构函数时,它可以被定义,我不记得),这是真的。继承,虚拟 - 在这种情况下要么是不确定的。
之后的所有内容都比您的硬盘驱动器的图像文件和浏览器密码缓存通过电子邮件发送到您的联系人列表更好,这是C ++标准下“未定义行为”的合法示例。你很幸运。
特别是,我猜测A
是一个微不足道的对象。并且delete[]
期望删除一系列size-1普通对象。不知怎的,B
更大而且非平凡(它包含一个vtable)的事实已经搞砸了你的编译器并导致你的无限循环。
可能存储有关如何删除数组的信息的格式与具有非虚拟析构函数的普通对象不同。使用vtable的情况下,也许它会在那里存储一个函数指针,并且在平凡的情况下它会存储一个计数。
然后循环不是无限的,而是迭代(几乎是随机的32或64位数)次,因为指针值往往是相对随机的。
如果你真的关心你的代码在这个具有特定上下文的特定系统上的特定版本的特定版本导致了什么特定的未定义行为,那么你可以使用本机代码。但我不明白为什么你应该关心。
仅为类型创建C ++ vtable,如果该类型需要。之后继承和添加virtual
并不会改变类型执行或不需要vtable的事实。
C中的原始数组不是逆变量,也不是协变量。如果你有一个数组,你就不能安全地将它转换为指向任何不同类型的指针(有一些非常狭窄的异常涉及标准布局和原始字节/字符等)。
如果我们回到您的示例并删除数组:
A * a = new B;
delete a;
这变得不那么糟糕了。删除a仍为UB,因为您要将B
删除为A
。
virtual ~A()
中没有A
,您无法将B
删除为A
。
要解决此问题,请添加:
virtual ~A() { cout << "by" << endl; }
现在A
有一个vtable - A
的实例带有一个指向vtable的指针,其中(除其他外)告诉编译器如何删除A
或派生A
类型。
现在代码定义良好,并打印
hey
from b
by
如果我的头编译器做对了。
答案 1 :(得分:0)
这段代码的结果是“by”的无限循环,为什么会这样?
因为基类A
没有虚析构函数
B vtable应该被上传到没有vtable的A
它不起作用。当这段代码:
delete [] a;
编译后,编译器使用class
A
的定义,它根本没有vtable。事实上,派生类有vtable并不重要。
所以我希望它在尝试访问虚拟构造函数时抛出异常。
你的期望是错误的。首先,没有虚拟构造函数这样的东西。其次 - 在这种情况下,您使用课程A
(通过A*
)。如果派生类具有vtable与否则在这种情况下无关紧要。
注意:C ++语言没有规定虚拟函数解析必须通过vtable完成,尽管以这种方式实现它是很常见的。我使用这个术语是因为你对它提出了质疑。