【问题标题】:Why are constructors and destructors called when they are? What are they actually doing? C++为什么要调用构造函数和析构函数?他们实际上在做什么? C++
【发布时间】:2013-02-14 23:57:16
【问题描述】:

我对构造函数和析构函数的技术原因有点陌生。我编写了一个程序,其中包含显示对象开始状态、结束状态、内存位置、值以及何时调用构造函数和析构函数的函数。

我无法理解为什么在调用它们时调用它们以及它们实际在做什么。我将发布测试运行的结果和我正在使用的代码 - 以及我所知道的哈哈。


结果:

Constructor called on 0x7fffc053f070

Initial state: 
Location: 0x7fffc053f070
  Value: 0

--- FOO ----
Location: 0x7fffc053f070
  Value: 1

--- BAR ----
Location: 0x7fffc053f080
  Value: 2

Destructor called on 0x7fffc053f080

--- BAZ ----
Location: 0x7fffc053f070
  Value: 2


Final state: 
Location: 0x7fffc053f070
  Value: 2

Destructor called on 0x7fffc053f070


代码:

#include <iostream>
#include <vector>

using namespace std;

//Short memory addresses are on the heap
//Long memory addresses are on the stack

class A {

public:

A(){
    m_iValue = 0;
    cout << "Constructor called on " << this << endl;
}

/*      A(const A & a){
            m_iValue = a.m_iValue;
            cout << "Copy constructor called on " << this << endl;
    }
*/
void increment(){
    m_iValue++;
}

void display(){
    cout << "Location: " << this << endl;
    cout << "  Value: " <<m_iValue << endl;
    cout << endl;
}

virtual ~A(){
    cout << "Destructor called on " << this << endl;
}

private:
    int m_iValue;
};

void foo(A & a){
    a.increment();
    a.display();
}

void bar(A a){
    a.increment();
    a.display();
}

void baz(A * a){
    a->increment();
    a->display();
}

void blah(vector<A*> vA){
    vA.back()->display();
    delete vA.back();
    vA.pop_back();
}

int main(int argc, char * argv[]){

    A a;

    cout << "Initial state: " << endl;
    a.display();

    cout << endl;

    foo(a);
    bar(a);
    baz(&a);
    cout << endl;
    cout << "Final state: " << endl;
    a.display();

    return 0;
}

我认为正在发生的事情:

因此,构造函数被调用一次,而析构函数被调用两次。在 main 中创建对象时调用构造函数。在 foo() 中,m_iVariable 是通过引用传递的,并且该函数为内存中该位置的对象递增 m_iValue。所以程序将值显示为 1(从 0 递增)。

这是我感到困惑的地方。第三个位置与前两个位置不同。该对象被直接传递给 bar()。我不明白如果不调用构造函数,位置会如何不同,或者为什么在它递增后调用析构函数,使值变为 2。

但是 baz 也会增加值。所以这意味着 bar 实际上没有做任何事情?我仍然不明白 bar 如何显示新的内存位置并破坏,但从不构造。


对不起所有的文字,但任何事情都会有所帮助。谢谢!

哦,注释掉的代码,函数 blah 用于其他事情,与这个问题无关。

【问题讨论】:

  • 你几乎破解了它 :) 了解复制构造函数。
  • 其实注释掉的代码和这个问题很相关。
  • bar(a) 创建对象的副本。那也被破坏了。
  • bar(a) 采用其参数“按值”,从而创建一个副本。
  • 你已经注释掉了复制构造函数。取消注释并再次运行

标签: c++ memory-management reference constructor destructor


【解决方案1】:

当您将值传递给bar() 时,您正在创建一个新对象,该对象是该函数的本地对象。当bar() 返回时,该对象被销毁。它由复制构造函数A(A const &amp;) 初始化,它是隐式生成的,因为您自己没有声明一个。如果您取消注释复制构造函数,那么您将看到它正在发生。

一般来说,当允许复制具有非平凡析构函数的对象时,您必须小心。此类类通常管理在析构函数中释放的资源,并且您必须注意副本不要尝试管理相同的资源。在制作这样的课程时,请始终记住Rule of Three。

【讨论】:

  • 好的,谢谢,这是有道理的。因此,由于我已将复制构造函数注释掉,所以当 bar() 被调用时 - 它基本上什么都不做?它在调用该函数时创建一个对象,并在函数退出时解构?
  • @ModdedLife:隐式复制构造函数通过复制每个数据成员来初始化对象。所以当bar 被调用时,它会创建一个带有旧对象计数器副本的新对象;增加并显示新对象;然后销毁新对象。旧对象没有任何反应。
  • 非常感谢。这很有帮助。
【解决方案2】:

一般来说,当您像在main 例程中那样通过在堆栈上创建对象来隐式初始化对象时,或者当您使用new 显式分配和初始化它时,都会调用构造函数。需要注意的是,按值向方法传递参数具有使用复制构造函数创建对象副本的效果,这是一种初始化类型。

当分配在堆栈上的对象超出范围或使用delete 释放动态分配的对象时,将调用析构函数。

您在这里看到两个析构函数调用,因为您的方法之一是按值传递,并创建了它的副本,然后在方法完成后销毁。这两个对象的内存地址不同。

如果您按值传递,对副本所做的任何修改都不会反映在原始文件中。这就是为什么在许多 C++ 应用程序中,方法通过 reference 传递事物,例如 foo(A&amp; a) 以允许修改,或通过 const 引用以明确没有更改允许像foo(const A&amp; a)。指针有时也用于相同的目的。

您在这里遇到问题的原因是因为您不知道Rule of Three 与析构函数、复制构造函数和复制赋值运算符有关。

【讨论】:

    【解决方案3】:

    在您的方法bar(A a) 中,调用的是复制构造函数A(const A&amp; a)。如果你没有声明它,它是由编译器隐式创建的,并为每个成员调用复制构造函数。

    只需添加这个方法:

    A(const A& a){
      m_iValue = a.value;
      cout << "Copy constructor called on " << this << endl;
    }
    

    【讨论】:

    • 隐式生成的拷贝构造函数为每个成员调用拷贝构造函数,这不是memcpy。
    【解决方案4】:

    首先,您的输出显示不同数量的构造函数和析构函数调用的原因是它没有显示所有此类调用。在您的情况下,这是您尝试检测代码的失败,而不是考虑所有构造函数。一般来说,也可能是构造函数抛出异常造成的,因为只有成功构造函数cakks的数量与析构函数调用的数量相匹配,并且它可能是由于对象没有被销毁(例如动态分配并且没有被删除)。


    忽略(ab)使用非常低级的语言特性,C++ 规则旨在确保

    • 对于每个 T 类型的对象,只有一个 T 构造函数调用,它发生在任何其他调用之前。

    • 对于每个成功的构造函数调用,都有一个对应的析构函数调用(如果对象被销毁)。

    这些规则递归地保留在对象层次结构中。具有子对象(数据成员)的对象就是这种层次结构的一个示例。子对象可以是基类子对象,而不是数据成员。

    从技术上讲,构造函数的职责是将一块原始内存转换为类型化且有意义的对象(将其与复制赋值运算符进行对比,它将现有值替换为新值,可能会释放早期分配的内存)。在设计级别,构造函数的主要职责是建立类不变量,无论您可以假设调用公共方法之间的对象状态如何。析构函数的工作是清理,例如释放资源。

    从语言的角度来看,构造失败是指构造函数抛出异常。

    new-表达式在内存分配和构造之间提供了一种类似于事务的非常强的耦合。它允许您提供两组参数:分配函数的第一组参数和构造函数的第二组参数。除了下面提到的情况,如果构造函数失败,那么内存会自动释放并传播异常。 IE。一般来说,要么都成功,要么撤消任何副作用,并通知调用代码失败。

    没有完成清理的唯一例外是您定义了自定义分配函数,即所谓的“放置新”运算符,但未能提供相应的解除分配函数。我不知道为什么需要自定义释放函数,事实上,这是唯一隐式调用它的情况。

    【讨论】:

      【解决方案5】:

      将打印语句简单地放入构造函数和析构函数中以查看它们在运行时运行总是很有趣。上面有很多很好的答案,所以我不会说什么,但是这样做对我早期的编程生涯很​​有帮助。

      【讨论】:

        猜你喜欢
        • 2017-07-23
        • 2020-08-24
        • 2021-04-01
        • 2018-05-24
        • 2014-04-22
        • 1970-01-01
        • 2019-11-16
        • 2010-10-13
        • 2021-08-30
        相关资源
        最近更新 更多