2

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

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


结果:

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 用于其他事情,与这个问题无关。

4

5 回答 5

3

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

通常,在允许复制具有非平凡析构函数的对象时必须小心。此类类通常管理在析构函数中释放的资源,并且您必须注意副本不要尝试管理相同的资源。做这样的课程时,永远记住三法则。

于 2013-02-28T17:19:34.673 回答
2

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

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

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

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

您在这里遇到问题的原因是您不了解关于析构函数、复制构造函数和复制赋值运算符的三法则。

于 2013-02-28T17:16:01.353 回答
0

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


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

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

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

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

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

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

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

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

于 2013-02-28T17:15:49.173 回答
0

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

于 2013-02-28T17:56:27.960 回答
0

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

只需添加此方法:

A(const A& a){
  m_iValue = a.value;
  cout << "Copy constructor called on " << this << endl;
}
于 2013-02-28T17:16:18.217 回答