繁体   English   中英

我如何理解这些析构函数?

[英]How can I understand these destructors?

我对以下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

这是我不明白的:

  1. 为什么在调用析构函数之后可以调用foo
  2. 为什么在调用析构函数后可以调用delete
  3. 如果我注释掉, delete b; 此代码会泄漏内存吗?
  4. A的析构函数是虚拟的。 我认为不会调用子类中重载的虚函数。 为什么〜A ~A()被调用?
  5. 如果我注释掉b->~B(); 然后在foo之后打印B dtor行。 为什么?
  6. 如果我重复行b->~B(); 两次,则输出为: B dtor\\nA dtor\\nA dtor ??
  7. 如果切换delete B;我将得到相同的输出delete B; delete[] b; 我认为第二个是正确的,因为b是用new[]创建的,但这没关系,因为我只是将B一个实例推入堆中。 那是对的吗?

很抱歉问了这么多问题,但这令我感到困惑。 如果我的个人问题被误导了,请告诉我了解每个析构函数何时运行所需的知识。

“未定义的行为”(简称UB)是允许编译器执行任何操作的位置-这通常意味着介于“崩溃”,“给出错误的结果”和“仍然执行您期望的操作”之间。 您的b->foo()绝对是未定义的,因为它发生在您的b->~B()调用之后,

由于您的foo函数实际上并没有使用析构函数销毁的任何东西,因此对foo的调用“有效”,因为没有使用任何已销毁的东西。 [这是绝对不能保证的-它只是工作而已,有点像有时候跨过马路不看就好,有时则不然。 视其路况而定,这可能不是一个好主意,或者在大多数情况下可能会奏效-但是有一个原因,人们说“向左看,向右看,向左看,然后在安全的情况下越过”(或类似的意思)那)]

在已被破坏的对象上调用delete也是UB,所以再次幸运的是,它“有效”(就“不会导致程序崩溃”的意义而言)。

同样,将deletenew []混合使用,反之亦然是UB-再次,编译器[及其相关的运行时]可能根据情况和条件做对还是错事。

不要依赖程序中未定义的行为[1]。 它一定会回来咬你。 C和C ++有很多UB案例,因此最好至少了解一些最常见的案例,例如“销毁后使用”,“销毁后使用”等,并留意此类情况-并避免不惜一切代价!

  1. 为什么在调用析构函数之后可以调用foo

C ++并不会阻止您脚下射击。 仅仅因为您可以做到(并且代码不会立即崩溃),并不意味着它是合法的或定义明确的。

  1. 为什么在调用析构函数后可以调用delete

与答案#1相同。

  1. 如果我注释掉, delete b; 此代码会泄漏内存吗?

是。 必须 delete您的new (并delete[]您的new[] )。

  1. A的析构函数是虚拟的。 我认为不会调用子类中重载的虚函数。 为什么〜A()被调用?

我认为您想要的词是优先 ,而不是过载 无论如何,您不会覆盖〜A ~A() 注意~B()~A()具有不同的名称。

析构函数有点特殊。 当派生类的析构函数完成运行时,它将隐式调用基类的析构函数。 为什么? 因为C ++标准说的就是那样。

虚拟析构函数是特殊的析构函数。 我让您多态删除一个对象。 这意味着您可以执行以下代码:

B *b = new B;
A *a = b;
delete a; // Legal with virtual destructors, illegal without virtual.

如果A在上面的代码中没有虚拟析构函数,则不会调用~B() ,这将是未定义的行为。 使用虚拟析构函数,编译器将在delete a;时正确调用~B() delete a; 运行,即使aA*而不是B*

  1. 如果我注释掉b->~B(); 然后在foo之后打印B dtor行。 为什么?

因为它在foo()之后运行。 delete b; foo()已经运行之后,隐式调用b的析构函数。

  1. 如果我重复行b->~B(); 两次,则输出为: B dtor\\nA dtor\\nA dtor ??

这是未定义的行为。 所以任何事情都可能发生。 是的,那是奇怪的输出。 未定义的行为很奇怪。

  1. 如果切换delete B;我将得到相同的输出delete B; delete[] b; 我认为第二个是正确的,因为b是用new[]创建的,但这没关系,因为我只是将B一个实例推入堆中。 那是对的吗?

您所谓的事情很重要。 deletedelete[]是不同的东西。 您不能用一个代替另一个。 必须仅在分配有new内存上调用delete ,并在分配有new delete[]的内存上调用delete new[] 不能随意混合和匹配。 这样做是未定义的行为。

您应该在此代码中使用delete[] ,因为您使用了new[]

Q1:在被销毁的对象上调用方法是“未定义的行为”。 这意味着该标准未指定应发生的情况。 UB背后的想法是,它们应该是应用程序逻辑中的错误,但是出于性能原因,我们不强迫编译器对此做一些特殊的事情,因为如果正确执行操作,这会降低性能。

在这种情况下,由于foo()方法不依赖于b指向的内存中的任何内容,因此它将按预期工作。 只是因为编译器没有对此进行任何测试。

Q2:这也是“未定义的行为”。 您已经看到一些奇怪的事情正在发生。 首先,不调用B析构函数,仅调用A析构函数。 发生的是,当您先前调用b->~B() ,调用了B析构函数,然后将对象的vtable更改为A vtable(这意味着对象运行时类型变为A),然后将A析构函数更改为叫。 当您调用delete b ,运行时将调用对象的虚拟析构函数A。如前所述,这是“未定义的行为”。 编译器选择生成在调用delete b时以这种方式工作的代码,但是它可能生成了不同的代码,但仍然正确。

如果确实如此,则由于代码中的另一个错误,在调用A析构函数后可能已经做得更糟:使用delete而不是delete[] C ++规则规定必须使用operator delete[]释放使用operator delete[] operator new[]分配的数组,而使用operator delete则是“未定义的行为”。 实际上,我知道在大多数实现中,这样做都是第一次,但是很有可能破坏内存管理数据,因此将来对newdelete甚至有效的调用都可能崩溃或导致内存泄漏。

Q3:如果您删除该呼叫,将发生内存泄漏。 如果您继续通话,则可能会导致内存损坏。 如果使用delete[] b可以避免内存泄漏。 仍然存在未定义的行为,因为将对已销毁的对象调用析构函数,但是由于这些析构函数不执行任何操作,因此它们可能不会对您的程序造成更多损害

问题4:这是所有析构函数的规则,而不仅仅是虚拟析构函数:它们将破坏成员对象,然后破坏代码末尾的基础对象。

Q5:因此,当编译器为B析构函数生成代码时,它将在最后添加对A析构函数的调用。 但是有一条规则:目前, this不再是B对象,并且在对A析构函数中的任何虚拟方法的调用期间,即使是虚拟的,也必须调用A方法,而不是B方法。 因此,在调用A析构函数之前,B析构函数会将对象的动态类型从B“降级”到A(实际上,这意味着将对象的vtable设置为A vtable)。 由于编译器的目标是生成有效的代码,因此不必在A析构函数的末尾更改vtable。 请记住:在调用析构函数之后,对象上的任何方法调用都是“未定义的行为”。 优先是性能,而不是错误检测。

问题6:与问题5的答案相同,我也谈到了问题2

问题7:重要。 很多。 delete []需要知道创建的对象数量,以便可以为数组中的所有对象调用销毁器。 通常,new []的实现实际上会分配一个size_t元素,以将数组中对象的数量存储在数组元素之前。 因此,返回的指针不是分配的块的开始,而是大小之后的位置(在32位系统上为4个字节,在64位系统上为8个字节)。 因此,第一个new B[1]将比new B多分配4或8个字节,而第二个delete []将需要在分配它之前将指针递减4或8个字节。 因此, delete bdelete[] b非常不同。

注意:编译器没有强制以这种方式实现new[]delete[] 并且某些实现提供了运行时库的版本,该库可以执行更多检查,以便更容易检测到错误。 但是,为了获得最佳性能,如果使用new[]则必须调用delete[] 而且,如果您在大多数情况下在对new[]分配的指针上调用delete的操作出错,则第一次执行该操作不会崩溃或失败。 稍后可能会在完全合法的newnew[]deletedelete[]操作中崩溃或失败。 这通常意味着很多人挠头想知道为什么完全正确的操作会失败。 这是“未定义行为”的真实含义

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM