我怎样才能理解这些析构函数?

时间:2015-12-14 00:56:39

标签: c++ oop heap destructor virtual-destructor

我对以下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()被调用呢?
  5. 如果我发表评论b->~B();,则会在B dtor后打印foo行。为什么?
  6. 如果我重复行b->~B();两次,则输出为:B dtor\nA dtor\nA dtor。咦?
  7. 如果我用delete B;切换delete[] b;,我会得到相同的输出。我认为第二个是正确的,因为b是使用new[]创建的,但它并不重要,因为我只是将B的一个实例推送到堆中。这是对的吗?
  8. 我很抱歉提出这么多问题,但这对我来说非常困惑。如果我的个别问题被误导,那么告诉我在理解每个析构函数运行时我需要知道什么。

3 个答案:

答案 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;)。

同样将deletenew []混合或反之亦然 - 再次,编译器[及其相关的运行时]可能会做对或错的事情,具体取决于具体情况和条件

依赖于程序中的未定义行为[1]。它肯定会回来咬你。 C和C ++有相当多的UB案例,至少在最常见的情况下理解是很好的,例如&#34;在销毁之后使用&#34;,&#34;免费使用&#34;等等,并留意这些情况 - 并不惜一切代价避免它!

答案 1 :(得分:1)

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

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

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

与答案#1相同。

  
      
  1. 如果我发表评论delete b;,此代码会泄漏内存吗?
  2.   

是。您必须 deletenew(以及delete[]new[])的内容。

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

我认为你想要的词是覆盖,而不是重载。无论如何,你并没有覆盖~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;,即使aA*而不是B*

  
      
  1. 如果我发表评论b->~B();,则会在B dtor后打印foo行。为什么?
  2.   

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

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

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

  
      
  1. 如果我用delete B;切换delete[] b;,我会得到相同的输出。我认为第二个是正确的,因为b是使用new[]创建的,但它并不重要,因为我只是将B的一个实例推送到堆中。这是对的吗?
  2.   

重要的是你所说的。 deletedelete[]不是一回事。你不能用一个来代替另一个。您必须仅在已使用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;。事实上,在大多数实现中,我知道,这样做会在第一次工作,但很有可能破坏内存管理数据,以便将来调用newdelete,甚至有效,可能会崩溃或导致内存泄漏。

问题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 bdelete[] b非常不同。

注意:编译器不被授权以这种方式实现new[]delete[]。并且一些实现提供了运行时库的版本,其执行更多检查以便更容易检测错误。但是,为了获得最佳性能,如果使用delete[],则必须调用new[]。如果你错误地在由delete分配的指针上调用new[],大多数时候,它会在第一次执行时失败或失败。在完全合法的newnew[]deletedelete[]操作中,它可能会在以后崩溃或失败。这通常意味着很多头脑搔痒,想知道为什么完全正确的操作失败。这是&#34;未定义行为&#34;

的真正含义