简体   繁体   English

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

[英]How can I understand these destructors?

I'm confused about the following C++ code (run it online at http://cpp.sh/8bmp ). 我对以下C ++代码感到困惑(可在http://cpp.sh/8bmp在线运行)。 It combines several concepts I'm learning about in a course. 它结合了我在课程中正在学习的几个概念。

#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;
}

Output: 输出:

A ctor
B ctor
B dtor
A dtor
foo
A dtor

Here's what I don't understand: 这是我不明白的:

  1. Why can I call foo after calling the destructor? 为什么在调用析构函数之后可以调用foo
  2. Why can I call delete after calling the destructor? 为什么在调用析构函数后可以调用delete
  3. If I comment out delete b; 如果我注释掉, delete b; will this code leak memory? 此代码会泄漏内存吗?
  4. The destructor for A is virtual. A的析构函数是虚拟的。 I thought virtual functions overloaded in subclasses wouldn't get called. 我认为不会调用子类中重载的虚函数。 Why does ~A() get called then? 为什么〜A ~A()被调用?
  5. If I comment out b->~B(); 如果我注释掉b->~B(); then the line B dtor is printed after foo . 然后在foo之后打印B dtor行。 Why? 为什么?
  6. If I repeat the line b->~B(); 如果我重复行b->~B(); twice, then the output is: B dtor\\nA dtor\\nA dtor . 两次,则输出为: B dtor\\nA dtor\\nA dtor Huh? ??
  7. I get the same output if I switch delete B; 如果切换delete B;我将得到相同的输出delete B; with delete[] b; delete[] b; . I think the second one is correct because b is created with new[] , but it doesn't matter because I'm only pushing one instance of B to the heap. 我认为第二个是正确的,因为b是用new[]创建的,但这没关系,因为我只是将B一个实例推入堆中。 Is that correct? 那是对的吗?

I'm sorry for asking so many questions, but this is pretty confusing to me. 很抱歉问了这么多问题,但这令我感到困惑。 If my individual questions are misguided, then tell me what I need to know to understand when each destructor will run. 如果我的个人问题被误导了,请告诉我了解每个析构函数何时运行所需的知识。

"Undefined behaviour" (UB for short) is where the compiler is allowed to do anything - this commonly means somewhere between "crash", "give incorrect result" and "do what you'd expect anyway". “未定义的行为”(简称UB)是允许编译器执行任何操作的位置-这通常意味着介于“崩溃”,“给出错误的结果”和“仍然执行您期望的操作”之间。 Your b->foo() is definitely undefined, since it happens after your b->~B() call, 您的b->foo()绝对是未定义的,因为它发生在您的b->~B()调用之后,

Since your foo function doesn't actually USE anything that gets destroyed by the destructor, the call to foo "works", because there is nothing being used that has been destroyed. 由于您的foo函数实际上并没有使用析构函数销毁的任何东西,因此对foo的调用“有效”,因为没有使用任何已销毁的东西。 [This is by no means guaranteed - it JUST HAPPENS to work, a bit like sometimes it's fine to cross a road without looking, at other times it's not. [这是绝对不能保证的-它只是工作而已,有点像有时候跨过马路不看就好,有时则不然。 Depending on what road it is, it may be a really bad idea, or might work most of the time - but there is a reason people say "look left, look right, look left, then cross if it's safe" (or something like that)] 视其路况而定,这可能不是一个好主意,或者在大多数情况下可能会奏效-但是有一个原因,人们说“向左看,向右看,向左看,然后在安全的情况下越过”(或类似的意思)那)]

Calling delete on an object that has been destroyed is also UB, so again, it's pure luck that it "works" (in the sense of "doesn't cause your program to crash"). 在已被破坏的对象上调用delete也是UB,所以再次幸运的是,它“有效”(就“不会导致程序崩溃”的意义而言)。

Also mixing delete with new [] or vice versa is UB - again, the compiler [and it's related runtime] may do the right or the wrong thing, depending on circumstances and conditions. 同样,将deletenew []混合使用,反之亦然是UB-再次,编译器[及其相关的运行时]可能根据情况和条件做对还是错事。

Do Not rely on undefined behaviour in your program [1]. 不要依赖程序中未定义的行为[1]。 It is bound to come back and bite you. 它一定会回来咬你。 C and C++ have quite a few UB-cases, and it's good to understand at least the most common cases, such as "use after destruction", "use after free" and such, and be on the lookout for such cases - and avoid it at all costs! C和C ++有很多UB案例,因此最好至少了解一些最常见的案例,例如“销毁后使用”,“销毁后使用”等,并留意此类情况-并避免不惜一切代价!

  1. Why can I call foo after calling the destructor? 为什么在调用析构函数之后可以调用foo

C++ doesn't stop you from shooting yourself in the foot. C ++并不会阻止您脚下射击。 Just because you can do it (and the code doesn't immediately crash) doesn't mean it's legal or well defined. 仅仅因为您可以做到(并且代码不会立即崩溃),并不意味着它是合法的或定义明确的。

  1. Why can I call delete after calling the destructor? 为什么在调用析构函数后可以调用delete

Same as answer #1. 与答案#1相同。

  1. If I comment out delete b; 如果我注释掉, delete b; will this code leak memory? 此代码会泄漏内存吗?

Yes. 是。 You must delete what you new (and delete[] what you new[] ). 必须 delete您的new (并delete[]您的new[] )。

  1. The destructor for A is virtual. A的析构函数是虚拟的。 I thought virtual functions overloaded in subclasses wouldn't get called. 我认为不会调用子类中重载的虚函数。 Why does ~A() get called then? 为什么〜A()被调用?

I think the word you want is override , not overload . 我认为您想要的词是优先 ,而不是过载 Anyway, you're not overriding ~A() . 无论如何,您不会覆盖〜A ~A() Notice that ~B() and ~A() have different names. 注意~B()~A()具有不同的名称。

Destructors are kinda special. 析构函数有点特殊。 When the derived class's destructor is finished running, it implicitly calls the base class's destructor. 当派生类的析构函数完成运行时,它将隐式调用基类的析构函数。 Why? 为什么? Because the C++ standard says that's what will happen. 因为C ++标准说的就是那样。

A virtual destructor is a special destructor. 虚拟析构函数是特殊的析构函数。 I lets you polymorphically delete an object. 我让您多态删除一个对象。 That means you can do code like the following: 这意味着您可以执行以下代码:

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

If A did not have a virtual destructor in the code above, it would not call ~B() , which would be undefined behavior. 如果A在上面的代码中没有虚拟析构函数,则不会调用~B() ,这将是未定义的行为。 With a virtual destructor, the compiler will correctly call ~B() when delete a; 使用虚拟析构函数,编译器将在delete a;时正确调用~B() delete a; is run, even though a is an A* and not a B* . 运行,即使aA*而不是B*

  1. If I comment out b->~B(); 如果我注释掉b->~B(); then the line B dtor is printed after foo . 然后在foo之后打印B dtor行。 Why? 为什么?

Because it's run after foo() . 因为它在foo()之后运行。 delete b; implicitly calls b 's destructor, which is after foo() has already run. foo()已经运行之后,隐式调用b的析构函数。

  1. If I repeat the line b->~B(); 如果我重复行b->~B(); twice, then the output is: B dtor\\nA dtor\\nA dtor . 两次,则输出为: B dtor\\nA dtor\\nA dtor Huh? ??

It's undefined behavior. 这是未定义的行为。 So anything can happen, really. 所以任何事情都可能发生。 Yeah, that's weird output. 是的,那是奇怪的输出。 Undefined behavior is weird. 未定义的行为很奇怪。

  1. I get the same output if I switch delete B; 如果切换delete B;我将得到相同的输出delete B; with delete[] b; delete[] b; . I think the second one is correct because b is created with new[] , but it doesn't matter because I'm only pushing one instance of B to the heap. 我认为第二个是正确的,因为b是用new[]创建的,但这没关系,因为我只是将B一个实例推入堆中。 Is that correct? 那是对的吗?

It matters what you call. 您所谓的事情很重要。 delete and delete[] are not the same thing. deletedelete[]是不同的东西。 You can't call one in place of the other. 您不能用一个代替另一个。 You must call delete only on memory that's been allocated with new , and delete[] with memory that's been allocated with new[] . 必须仅在分配有new内存上调用delete ,并在分配有new delete[]的内存上调用delete new[] You cannot mix and match as you want. 不能随意混合和匹配。 Doing so is undefined behavior. 这样做是未定义的行为。

You should be using delete[] here, in this code, because you used new[] . 您应该在此代码中使用delete[] ,因为您使用了new[]

Q1: Calling a method on a destroyed object is "Undefined Behaviour". Q1:在被销毁的对象上调用方法是“未定义的行为”。 Meaning that the standard does not specify what should happen. 这意味着该标准未指定应发生的情况。 The idea behind UB is that they are supposed to be bugs in the application logic, but for performance reason, we do not force the compiler to do something special about it, as that would degrade performance for when we do things correctly. UB背后的想法是,它们应该是应用程序逻辑中的错误,但是出于性能原因,我们不强迫编译器对此做一些特殊的事情,因为如果正确执行操作,这会降低性能。

In this case, because the foo() method does not depends on anything in the memory pointed by b , it will work as expected. 在这种情况下,由于foo()方法不依赖于b指向的内存中的任何内容,因此它将按预期工作。 Just because the compiler does not do any test on it. 只是因为编译器没有对此进行任何测试。

Q2: This is also "Undefined Behaviour". Q2:这也是“未定义的行为”。 And you can see already some bizarre things are happening. 您已经看到一些奇怪的事情正在发生。 First, the B destructor is not called, just the A destructor. 首先,不调用B析构函数,仅调用A析构函数。 What is happening is that when you called previously b->~B() , the B destructor was called, then the vtable of the object was changed to a A vtable (meaning the object runtime type became A) and then the A destructor was called. 发生的是,当您先前调用b->~B() ,调用了B析构函数,然后将对象的vtable更改为A vtable(这意味着对象运行时类型变为A),然后将A析构函数更改为叫。 When you called delete b , the runtime called the virtual destructor of the object which was A. Like said previously, this is "Undefined Beheviour". 当您调用delete b ,运行时将调用对象的虚拟析构函数A。如前所述,这是“未定义的行为”。 The compiler chhose to generate code that works that way when calling delete b , but it could have generated different code and still be right. 编译器选择生成在调用delete b时以这种方式工作的代码,但是它可能生成了不同的代码,但仍然正确。

If fact, something worse has probably been done after calling the A destructor because of another bug in your code: you use delete instead of delete[] . 如果确实如此,则由于代码中的另一个错误,在调用A析构函数后可能已经做得更糟:使用delete而不是delete[] C++ rules states that arrays allocated with operator new[] must be freed using operator delete[] and that using operator delete is "Undefined behaviour". C ++规则规定必须使用operator delete[]释放使用operator delete[] operator new[]分配的数组,而使用operator delete则是“未定义的行为”。 In fact, in most implementation I know, doing that will work the first time, but has a good chance of corrupting the memory management data, so that a future call to new or delete , even valid, may crash or cause memory leak. 实际上,我知道在大多数实现中,这样做都是第一次,但是很有可能破坏内存管理数据,因此将来对newdelete甚至有效的调用都可能崩溃或导致内存泄漏。

Q3: There will be a memory leak if you remove the call. Q3:如果您删除该呼叫,将发生内存泄漏。 You will probably have memory corruption if you keep the call. 如果您继续通话,则可能会导致内存损坏。 Memory leak will be avoided if you use delete[] b instead. 如果使用delete[] b可以避免内存泄漏。 There is still undefined behaviour because a destrutor will be called on the already destroyed object, but because these destructors do nothing, they will not likely do any more damage to your program 仍然存在未定义的行为,因为将对已销毁的对象调用析构函数,但是由于这些析构函数不执行任何操作,因此它们可能不会对您的程序造成更多损害

Q4: This is the rule with all destructors, not just virtual destructors: they will destroy the member objects then the base objects at the end of the code. 问题4:这是所有析构函数的规则,而不仅仅是虚拟析构函数:它们将破坏成员对象,然后破坏代码末尾的基础对象。

Q5: So when the compiler generates the code for the B destructor, it adds at the end a call to the A destructor. Q5:因此,当编译器为B析构函数生成代码时,它将在最后添加对A析构函数的调用。 But there is a rule: at this time, this is not a B object anymore and during the call to any virtual method in the A destructor, A methods must be called, not B methods, even if virtual. 但是有一条规则:目前, this不再是B对象,并且在对A析构函数中的任何虚拟方法的调用期间,即使是虚拟的,也必须调用A方法,而不是B方法。 So before calling the A destructor, the B destructor will "downgrade" the dynamic type of the object from B to A (in practice, that means setting the vtable of the object to the A vtable). 因此,在调用A析构函数之前,B析构函数会将对象的动态类型从B“降级”到A(实际上,这意味着将对象的vtable设置为A vtable)。 As the goal of the compiler is to generate efficient code, it does not have to change the vtable at the end of the A destructor. 由于编译器的目标是生成有效的代码,因此不必在A析构函数的末尾更改vtable。 Remember: any method call on the object after the destructor is called is "Undefined Behaviour". 请记住:在调用析构函数之后,对象上的任何方法调用都是“未定义的行为”。 Priority is performance, not bug detection. 优先是性能,而不是错误检测。

Q6: same answer as Q5, and I also talked about is in Q2 问题6:与问题5的答案相同,我也谈到了问题2

Q7: It matters. 问题7:重要。 A lot. 很多。 delete[] needs to know the number of objects created so it can call the destrutors for all the objects in the array. delete []需要知道创建的对象数量,以便可以为数组中的所有对象调用销毁器。 Often implementations of new[] will actually allocate a size_t element to store the number of objects in the array before the elements of the array. 通常,new []的实现实际上会分配一个size_t元素,以将数组中对象的数量存储在数组元素之前。 So the returned pointer is not the start of the allocated bloc, but the location after the size (4 bytes on 32 bits system, 8 bytes on 64 bits system). 因此,返回的指针不是分配的块的开始,而是大小之后的位置(在32位系统上为4个字节,在64位系统上为8个字节)。 So first new B[1] will allocate 4 or 8 bytes more that new B and second delete[] will need to decrement the pointer by 4 or 8 bytes before deallocating it. 因此,第一个new B[1]将比new B多分配4或8个字节,而第二个delete []将需要在分配它之前将指针递减4或8个字节。 So delete b and delete[] b are VERY different. 因此, delete bdelete[] b非常不同。

Note: compilers are not mandated to implement new[] and delete[] that way. 注意:编译器没有强制以这种方式实现new[]delete[] And some implementations offer version of the runtime library that do more checkings so that bugs are easier to detect. 并且某些实现提供了运行时库的版本,该库可以执行更多检查,以便更容易检测到错误。 However, for the best performance, delete[] MUST be called if new[] is used. 但是,为了获得最佳性能,如果使用new[]则必须调用delete[] And if you make the mistake to call delete on a pointer allocated by new[] , most of the time, it will not crash or fail the first time you do it. 而且,如果您在大多数情况下在对new[]分配的指针上调用delete的操作出错,则第一次执行该操作不会崩溃或失败。 It will likely crash or fail later in a perfectly legal new , new[] , delete or delete[] operation. 稍后可能会在完全合法的newnew[]deletedelete[]操作中崩溃或失败。 Which often means a lot of head scratching wondering why a perfectly correct operation is failing. 这通常意味着很多人挠头想知道为什么完全正确的操作会失败。 This is the true meaning of "Undefined Behaviour" 这是“未定义行为”的真实含义

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

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