我对以下C ++代码感到困惑(在http://cpp.sh/8bmp在线运行)。它结合了我在课程中学到的几个概念。
#include <iostream>
using namespace std;
class A {
public:
A() {cout << "A ctor" << endl;}
virtual ~A() {cout << "A dtor" << endl;}
};
class B: public A {
public:
B() {cout << "B ctor" << endl;}
~B() {cout << "B dtor" << endl;}
void foo(){cout << "foo" << endl;}
};
int main(){
B *b = new B[1];
b->~B();
b->foo();
delete b;
return 0;
}
输出:
A ctor
B ctor
B dtor
A dtor
foo
A dtor
这是我不明白的地方:
foo
?delete
?delete b;
,此代码会泄漏内存吗?A
的析构函数是虚拟的。我认为在子类中重载的虚函数不会被调用。为什么~A()
被调用呢?b->~B();
,则会在B dtor
后打印foo
行。为什么?b->~B();
两次,则输出为:B dtor\nA dtor\nA dtor
。咦?delete B;
切换delete[] b;
,我会得到相同的输出。我认为第二个是正确的,因为b
是使用new[]
创建的,但它并不重要,因为我只是将B
的一个实例推送到堆中。这是对的吗?我很抱歉提出这么多问题,但这对我来说非常困惑。如果我的个别问题被误导,那么告诉我在理解每个析构函数运行时我需要知道什么。
答案 0 :(得分:6)
&#34;未定义的行为&#34; (简称UB)是允许编译器执行任何操作的地方 - 这通常意味着介于&#34;崩溃&#34;,&#34;提供不正确的结果&#34;并且&#34;做你期望的事情&#34;。您的b->foo()
肯定是未定义的,因为它发生在b->~B()
来电之后,
由于您的foo
函数实际上并未使用被析构函数破坏的任何内容,因此对foo
&#34;的调用起作用&#34;,因为没有使用任何内容已被摧毁。 [这绝不是保证 - 它只是起作用,有点像有时候在没有看的情况下过马路是好的,有时它不是。取决于它是什么道路,它可能是一个非常糟糕的主意,或者可能在大多数时间工作 - 但是有一个原因人们会说'向左看,向右看,向左看,然后交叉,如果它'&#39;安全&#34; (或类似的东西)]
在已被销毁的对象上调用delete
也是UB,所以再一次,它运气良好,并且#34;工作&#34; (在&#34;意义上导致程序崩溃&#34;)。
同样将delete
与new []
混合或反之亦然 - 再次,编译器[及其相关的运行时]可能会做对或错的事情,具体取决于具体情况和条件
不依赖于程序中的未定义行为[1]。它肯定会回来咬你。 C和C ++有相当多的UB案例,至少在最常见的情况下理解是很好的,例如&#34;在销毁之后使用&#34;,&#34;免费使用&#34;等等,并留意这些情况 - 并不惜一切代价避免它!
答案 1 :(得分:1)
- 为什么我可以在调用析构函数后调用
醇>foo
?
C ++并没有阻止你在脚下射击自己。仅仅因为你可以做到(并且代码没有立即崩溃)并不意味着它是合法的或定义明确的。
- 为什么我可以在调用析构函数后调用
醇>delete
?
与答案#1相同。
- 如果我发表评论
醇>delete b;
,此代码会泄漏内存吗?
是。您必须 delete
您new
(以及delete[]
您new[]
)的内容。
- 醇>
A
的析构函数是虚拟的。我认为在子类中重载的虚函数不会被调用。为什么~A()被调用呢?
我认为你想要的词是覆盖,而不是重载。无论如何,你并没有覆盖~A()
。请注意,~B()
和~A()
具有不同的名称。
析构函数有点特殊。当派生类的析构函数完成运行时,它会隐式调用基类的析构函数。为什么?因为C ++标准说明会发生什么。
虚拟析构函数是一种特殊的析构函数。我允许你以多态方式删除一个对象。这意味着您可以执行以下代码:
B *b = new B;
A *a = b;
delete a; // Legal with virtual destructors, illegal without virtual.
如果A
在上面的代码中没有虚拟析构函数,则不会调用~B()
,这将是未定义的行为。使用虚拟析构函数,编译器将在运行~B()
时正确调用delete a;
,即使a
是A*
而不是B*
。
- 如果我发表评论
醇>b->~B();
,则会在B dtor
后打印foo
行。为什么?
因为它在foo()
之后运行。 delete b;
隐式调用b
的析构函数,该函数在foo()
已经运行之后。
- 如果我重复行
醇>b->~B();
两次,则输出为:B dtor\nA dtor\nA dtor
。咦?
它未定义的行为。所以任何事都可能发生,真的。是的,这是奇怪的输出。未定义的行为很奇怪。
- 如果我用
醇>delete B;
切换delete[] b;
,我会得到相同的输出。我认为第二个是正确的,因为b
是使用new[]
创建的,但它并不重要,因为我只是将B
的一个实例推送到堆中。这是对的吗?
重要的是你所说的。 delete
和delete[]
不是一回事。你不能用一个来代替另一个。您必须仅在已使用delete
分配的内存上调用new
,并delete[]
使用new[]
分配内存delete[]
1}}。您无法根据需要混合搭配。这样做是未定义的行为。
您应该在此代码中使用new[]
,因为您使用了{{1}}。
答案 2 :(得分:0)
Q1:在被破坏的对象上调用方法是&#34;未定义的行为&#34;。意味着标准没有规定应该发生什么。 UB背后的想法是它们应该是应用程序逻辑中的错误,但出于性能原因,我们不强迫编译器对它做一些特殊的事情,因为这会降低我们正确执行操作时的性能。
在这种情况下,因为foo()方法不依赖于b
指向的内存中的任何内容,所以它将按预期工作。仅仅因为编译器没有对它进行任何测试。
Q2:这也是&#34;未定义的行为&#34;。你可以看到已经发生了一些奇怪的事情。首先,不要调用B析构函数,只调用A析构函数。发生的事情是,当您之前调用b->~B()
时,调用了B析构函数,然后将对象的vtable更改为A vtable(意味着对象运行时类型变为A),然后调用A析构函数。当你调用delete b
时,运行时称为对象的虚拟析构函数,如前所述,这是&#34; Undefined Beheviour&#34;。编译器选择生成在调用delete b
时以这种方式工作的代码,但它可能生成了不同的代码并且仍然是正确的。
事实上,由于您的代码中存在另一个错误,因此在调用A析构函数后可能会出现更糟糕的情况:您使用delete
代替delete[]
。 C ++规则声明必须使用operator new[]
释放分配有operator delete[]
的数组,并且使用operator delete
是&#34;未定义的行为&#34;。事实上,在大多数实现中,我知道,这样做会在第一次工作,但很有可能破坏内存管理数据,以便将来调用new
或delete
,甚至有效,可能会崩溃或导致内存泄漏。
问题3:如果您取消呼叫,将会出现内存泄漏。如果你保持通话,你可能会有内存损坏。如果您使用delete[] b
,将避免内存泄漏。仍然存在未定义的行为,因为将在已经销毁的对象上调用析构函数,但由于这些析构函数不执行任何操作,因此它们不会对您的程序造成更多损害
问题4:这是所有析构函数的规则,而不仅仅是虚拟析构函数:它们会破坏成员对象,然后是代码末尾的基础对象。
问题5:所以当编译器为B析构函数生成代码时,它会在最后添加对A析构函数的调用。但是有一个规则:此时,this
不再是B对象,并且在调用A析构函数中的任何虚方法时,必须调用A方法,而不是B方法,即使是虚方法。因此,在调用A析构函数之前,B析构函数将降级&#34;降级&#34;从B到A的对象的动态类型(实际上,这意味着将对象的vtable设置为A vtable)。由于编译器的目标是生成有效的代码,因此不必在A析构函数的末尾更改vtable。记住:在析构函数被调用之后对对象的任何方法调用都是&#34; Undefined Behavior&#34;。优先级是性能,而不是错误检测。
问题6:与Q5相同的答案,我也谈到了第二季度
问:问:重要的是。很多。 delete []需要知道创建的对象的数量,以便它可以调用数组中所有对象的析构函数。 new []的实现通常会分配一个size_t
元素来存储数组元素之前的数组中的对象数。因此返回的指针不是分配的块的开始,而是大小之后的位置(32位系统上的4个字节,64位系统上的8个字节)。因此,首先new B[1]
将分配4或8个字节,new B
和第二个delete []需要在释放指针之前将指针递减4或8个字节。因此delete b
和delete[] b
非常不同。
注意:编译器不被授权以这种方式实现new[]
和delete[]
。并且一些实现提供了运行时库的版本,其执行更多检查以便更容易检测错误。但是,为了获得最佳性能,如果使用delete[]
,则必须调用new[]
。如果你错误地在由delete
分配的指针上调用new[]
,大多数时候,它会在第一次执行时失败或失败。在完全合法的new
,new[]
,delete
或delete[]
操作中,它可能会在以后崩溃或失败。这通常意味着很多头脑搔痒,想知道为什么完全正确的操作失败。这是&#34;未定义行为&#34;