基于C ++堆栈的构造函数/析构函数不能按预期工作

时间:2013-08-14 13:59:17

标签: c++

我无法理解为什么下面的代码没有构造和破坏我创建的两个对象:

#include <iostream>

class MyClass {

    int myVar;

public:
    MyClass(int x) {
        myVar = x;
        std::cout << "constructing " << myVar << ", " << (long)this << std::endl;
    }

    ~MyClass() {
        std::cout << "destructing " << myVar << ", " << (long)this << std::endl;
    }
};

int main(int argc, const char * argv[])
{
    MyClass a = MyClass(1);
    a = MyClass(2);
    return 0;
}

我认为在main中我首先创建一个值为1的对象,然后创建一个值为2的新对象。并且每个对象都被构造和销毁,因此我希望看到以下输出:

constructing 1, 3456
constructing 2, 6789
destructing 1, 3456
destructing 2, 6789

然而,我明白了:

constructing 1, 3456
constructing 2, 6789
destructing 2, 6789   <- note the "2"
destructing 2, 3456

更新:我添加了对象地址的输出( this ),以便人们可以更好地查看哪个对象做了什么。

当我使用“new MyClass”时,我不会遇到这种奇怪的效果。

造成这种情况的原因是什么,并了解我的目标,未来避免类似错误的正确方法是什么?

虽然这个例子看起来无害,但我遇到了代码崩溃,因为我在构造函数中分配了其他对象并在析构函数中释放它们。这确实导致在对象仍在使用时释放对象。

结论

现在我的所有问题都得到了解答,让我总结一下:

  1. 在上面的例子中我使用的是“myVar”,它甚至没有显示导致我提出这个问题的问题。我为此道歉。
  2. 我对代码的实际问题是我没有使用简单的int var,而是在析构函数中使用“new”创建的数组,并在析构函数中使用delete释放。有了它,数组将被删除两次,导致我的程序中的数据不正确。
  3. 修复是不使用指向数组的简单指针而是使用引用计数指针,这样当它被赋值运算符复制时,它会增加引用计数,从而防止它过早释放。
  4. 一般来说,我在这里展示的效果并不危险 - 它不会损害任何东西,因为我得到了印象。危险的部分是我没有使用ref count ptrs。

8 个答案:

答案 0 :(得分:11)

a = MyClass(2);不会调用析构函数,它会调用你没有实现的赋值运算符(MyClass::operator=),因此编译器会为你提供一个 - 它不会“打印”什么,所以你没有看到。

两次获得destrucing 2的原因是紧接在行a = MyClass(2);之后,临时MyClass(2)对象被销毁。然后在main结束时,变量a被销毁,并且由于myVar现在为2,因此它再次打印2。

答案 1 :(得分:3)

a = MyClass(2);

使用编译器提供的复制赋值运算符operator=。这就是您看到destructing 2

的原因

因此,在复制过程中,a.myVar获取值2而不是1

临时对象在分号后面被销毁:

a = MyClass(2);
//             ^- Here

在阻止结束时,a也被破坏了。


此处的所有流程:

int main(int argc, const char * argv[])
{
    MyClass a = MyClass(1);    // Create an object
    a = MyClass(2); // Create a temporary object and use the operator= to proceed to the copy, now a.intVar = 2
                // ^- Here the temporary object is destructed 
    return 0;
}               // a is now destructed

答案 2 :(得分:3)

你创建的cout语句应该被认为是中级调试工具,有助于理解C ++程序的内容(实际上没有深入到低级汇编代码)。我在下面对您发布的代码进行了一些修改,将编译器生成的默认构造函数和赋值运算符替换为与编译器生成的代码运算符有效运行的代码运算符(如果您没有添加它们本身就足够了) cout陈述,看看幕后发生了什么)....

#include <iostream>

class MyClass {

    int myVar;

public:
    MyClass(int x) {
        myVar = x;
        std::cout << "                            constructing " << myVar << ", " << this << std::endl;
    }

    ~MyClass() {
        std::cout << "                            destructing  " << myVar << ", at " << this << std::endl;
    }

    MyClass() {
        myVar = 999;
        std::cout << "                            constructing " << myVar << ", at " << this << std::endl;
    }

    MyClass& operator=(const MyClass& rhs) {
        std::cout << "                            object " << myVar << " (at " << this <<
                ") = object " << rhs.myVar << " (at " << &rhs << ")\n";
        myVar = rhs.myVar;
        return *this;
    }

    friend std::ostream& operator<<(std::ostream& s, const MyClass& m);
};

std::ostream& operator<<(std::ostream& s, const MyClass& m) {
    s << m.myVar;
}

int main(int argc, const char * argv[])
{
    MyClass a = MyClass(1);   // <---- the way you initialize 'a'
//    MyClass a(1);   //   // <---- another way to initialize 'a'
    std::cout << "Variable 'a' is now: " << a << "\n";
    std::cout << "Now setting 'a' to 2...\n";
    a = MyClass(2);
    std::cout << "Variable 'a' is now: " << a << "\n";
    return 0;
}

以这种方式编码,我向右侧缩进了你的中级调试cout语句,并添加了cout语句(没有缩进)来显示程序员通常关心的内容,如果他们没有进行中级调试。当我运行这个时,我得到了这个:

                            constructing 1, 0xbfcbfb48
Variable 'a' is now: 1
Now setting 'a' to 2...
                            constructing 2, 0xbfcbfb4c
                            object 1 (at 0xbfcbfb48) = object 2 (at 0xbfcbfb4c)
                            destructing  2, at 0xbfcbfb4c
Variable 'a' is now: 2
                            destructing  2, at 0xbfcbfb48

程序员通常关心的是左边的内容,这正是您最初发布的C ++程序所提供的内容。请注意,您的MyClass存储的是值,而不是指针。如果您的班级数据是简单值,那么您的示例编码很好,并且没有任何错误。如果你的类包含指针,那么确实一个默认的构造函数和赋值运算符(或者用户定义的那些操作类似于默认的操作符,例如我上面显示的那些),已经不够用了,因为它们提供了指向的浅拷贝 - 数据。您的类要么需要包含某种形式的智能指针,要么手动处理指向资源的复制,可能涉及引用计数以提高效率。某种形式的智能指针可能是一个更安全的赌注。

答案 3 :(得分:2)

编译器优化第一个调用:

MyClass a = MyClass(1);

只对一个构造函数调用而不是构造,然后调用复制构造函数。 但是在第二行:

a = MyClass(2);

首先创建一个临时对象,然后将其绑定到a。接下来发生的事情是临时对象被销毁(因此第一个destructing 2)然后a被销毁(因此第二个destructing 2)。

在销毁a时打印destructing 2的原因是为您的类创建了默认赋值运算符,因为您没有定义一个,此赋值运算符将复制myVar的值。 / p>

答案 4 :(得分:2)

MyClass a = MyClass(1);

这构造了一个值为1的对象,所以你看到了

constructing 1

然后

a = MyClass(2);

构造一个值为2的临时对象,所以你看到了

constructing 2

临时对象被分配给a,给a相同的值,2,然后临时对象超出范围并被销毁,所以你看到

destructing 2

然后在main结束时,变量a被破坏,并且因为它被重新分配了一个新值,你会看到

destructing 2

这是C ++,而不是Java或C#,因此a是一个对象而不是引用。行a = MyClass(2);不会使a引用另一个对象,它会将对象a修改为另一个对象的副本。

答案 5 :(得分:1)

当你的程序到达main的末尾时,它会销毁a,其变量myVar在该点的值为2。 如果您改为写:

 MyClass a = MyClass(1);
 MyClass b = MyClass(2);

你会看到你的预期输出。

答案 6 :(得分:1)

当你说a = MyClass(2)时;您将默认赋值运算符应用于对象a。在这种情况下,a.myVar的值应更改为2.

而是尝试:

int main(int argc, const char * argv[])
{
    MyClass a( 1 );
    MyClass b( 2 );
    return 0;
}

答案 7 :(得分:1)

问题得到解答,但我想我应该解释为什么作者的真实项目崩溃。

我们有一些类,它包含一些对象,这个对象在构造函数中创建并在析构函数中删除:

class SomeClass
{
    public:
        SomeClass(int param) { mObject = new SomeObj(param); }
        ~SomeClass() { delete mObject; }
    private:
        SomeObj * mObject;
}

当我们做

之类的事情时
int main(int argc, const char * argv[])
{
    SomeClass a = SomeClass(1);//1
    a = SomeClass(2);//2
    return 0;//3
}

我们在第1行调用SomeObj构造函数,然后在第2行调用。之后我们打电话给 SomeClass::operator=(SomeClass& rhs)
这是我们自动生成的,它的身体只是
{ mObject = rhs.mObject; }
那么我们看到了什么?

object1.mObject = object2.mObject;
//old object1.mObject is leaked now, we have no pointer to it.
delete object2; // it was temporary, its lifetime is just one line of code
//it calls
delete object2.mObject; // it equals to delete object1.mObject, because both pointers point to same object
delete object1;//after end of main()
//it calls
delete object1.mObject; // ERROR! object was deleted

所以c ++没什么问题;)