C++ vtable 究竟是如何工作的? (在 q 中有例子)
How exactly c++ vtable works? (with example in the q.)
让我们举一个 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;
}
这段代码的结果是"by"的死循环,这是为什么呢?
B vtable 应该向上转换为没有 vtable 的 A,所以我希望它在他尝试访问虚拟构造函数时抛出异常。
p.s。
我在哪里可以阅读有关有线构造函数析构函数行为的所有示例? (有例子)
the results of this code is an infinite loop of "by" , why is this?
因为基classA
没有虚析构函数
B vtable supposed to be upcast to A that don't have vtable
这样不行。当这段代码:
delete [] a;
编译,编译器使用 class
A
的定义,它根本没有 vtable。事实上,派生 class 具有 vtable 在这里并不重要。
so i would expected it to throw an exception when he try to reach the virtual constructor.
你的期望是错误的。首先,没有虚拟构造函数这样的东西。其次 - 在这种情况下,您使用 class A
(通过 A*
)。在这种情况下,如果派生 class 是否具有 vtable 并不重要。
注意:C++语言并没有规定虚函数解析必须通过vtable来完成,尽管以这种方式实现它是很常见的。我用这个词是因为你质疑它。
删除B
的数组作为A
的数组是未定义的行为。只要 A
有一个非平凡的析构函数(当它确实有一个平凡的析构函数时,它可能被定义,我不记得了),这是真的。继承,虚拟——在这种情况下,要么与它的未定义有关。
之后的一切都比将硬盘驱动器的图像文件和浏览器密码缓存通过电子邮件发送到您的联系人列表要好,这是 C++ 标准下 "undefined behavior" 的合法示例。你走运了。
特别是,我猜想 A
是一个微不足道的对象。并且 delete[]
期望删除一系列大小为 1 的琐碎对象。 B
更大且不平凡(它包含一个 vtable)这一事实不知何故弄乱了你的编译器并导致你的无限循环。
也许它存储有关如何删除数组的信息的格式与具有非虚拟析构函数的普通对象不同。对于 vtable 的情况,也许它在那里存储一个函数指针,而在普通情况下它存储一个计数。
循环不是无限的,而是迭代(几乎随机的 32 或 64 位数)次,因为指针值往往是相对随机的。
如果您真的关心您的代码在具有此特定上下文的此特定系统上的此特定编译器的此特定版本上导致了哪些特定的未定义行为,您可以对您的代码进行 godbot。但我不明白你为什么要关心。
C++ vtables 只为一个类型创建如果该类型需要。之后继承和添加 virtual
不会改变类型需要或不需要 vtable 的事实。
C 中的原始数组不是逆变或协变。如果你有一个数组,你 不能安全地 将它转换为指向任何不同类型的指针(有一些非常狭窄的例外涉及标准布局和原始 bytes/chars 等)。
如果我们回到您的示例并删除数组:
A * a = new B;
delete a;
这变得不那么糟糕了。删除 a 仍然是 UB,因为您将 B
作为 A
.
删除
A
中没有 virtual ~A()
,您无法将 B
作为 A
删除。
为了解决这个问题,我们添加:
virtual ~A() { cout << "by" << endl; }
现在 A
有一个 vtable -- A
的实例带有一个指向 vtable 的指针,它(除其他事项外)告诉编译器如何删除 A
或派生的 A
类型。
现在 代码定义良好,它打印
hey
from b
by
如果我的头编译器正确的话。
让我们举一个 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;
}
这段代码的结果是"by"的死循环,这是为什么呢? B vtable 应该向上转换为没有 vtable 的 A,所以我希望它在他尝试访问虚拟构造函数时抛出异常。
p.s。 我在哪里可以阅读有关有线构造函数析构函数行为的所有示例? (有例子)
the results of this code is an infinite loop of "by" , why is this?
因为基classA
没有虚析构函数
B vtable supposed to be upcast to A that don't have vtable
这样不行。当这段代码:
delete [] a;
编译,编译器使用 class
A
的定义,它根本没有 vtable。事实上,派生 class 具有 vtable 在这里并不重要。
so i would expected it to throw an exception when he try to reach the virtual constructor.
你的期望是错误的。首先,没有虚拟构造函数这样的东西。其次 - 在这种情况下,您使用 class A
(通过 A*
)。在这种情况下,如果派生 class 是否具有 vtable 并不重要。
注意:C++语言并没有规定虚函数解析必须通过vtable来完成,尽管以这种方式实现它是很常见的。我用这个词是因为你质疑它。
删除B
的数组作为A
的数组是未定义的行为。只要 A
有一个非平凡的析构函数(当它确实有一个平凡的析构函数时,它可能被定义,我不记得了),这是真的。继承,虚拟——在这种情况下,要么与它的未定义有关。
之后的一切都比将硬盘驱动器的图像文件和浏览器密码缓存通过电子邮件发送到您的联系人列表要好,这是 C++ 标准下 "undefined behavior" 的合法示例。你走运了。
特别是,我猜想 A
是一个微不足道的对象。并且 delete[]
期望删除一系列大小为 1 的琐碎对象。 B
更大且不平凡(它包含一个 vtable)这一事实不知何故弄乱了你的编译器并导致你的无限循环。
也许它存储有关如何删除数组的信息的格式与具有非虚拟析构函数的普通对象不同。对于 vtable 的情况,也许它在那里存储一个函数指针,而在普通情况下它存储一个计数。
循环不是无限的,而是迭代(几乎随机的 32 或 64 位数)次,因为指针值往往是相对随机的。
如果您真的关心您的代码在具有此特定上下文的此特定系统上的此特定编译器的此特定版本上导致了哪些特定的未定义行为,您可以对您的代码进行 godbot。但我不明白你为什么要关心。
C++ vtables 只为一个类型创建如果该类型需要。之后继承和添加 virtual
不会改变类型需要或不需要 vtable 的事实。
C 中的原始数组不是逆变或协变。如果你有一个数组,你 不能安全地 将它转换为指向任何不同类型的指针(有一些非常狭窄的例外涉及标准布局和原始 bytes/chars 等)。
如果我们回到您的示例并删除数组:
A * a = new B;
delete a;
这变得不那么糟糕了。删除 a 仍然是 UB,因为您将 B
作为 A
.
A
中没有 virtual ~A()
,您无法将 B
作为 A
删除。
为了解决这个问题,我们添加:
virtual ~A() { cout << "by" << endl; }
现在 A
有一个 vtable -- A
的实例带有一个指向 vtable 的指针,它(除其他事项外)告诉编译器如何删除 A
或派生的 A
类型。
现在 代码定义良好,它打印
hey
from b
by
如果我的头编译器正确的话。