4

我很难理解为什么以下代码没有按照我期望的方式构造和破坏我创建的两个对象:

#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. 解决方法是不使用指向数组的简单指针,而是使用引用计数指针,这样,当它被赋值运算符复制时,它会增加 refcount,从而防止过早释放它。
  4. 总的来说,我在这里展示的效果并没有什么危险——它不会损坏任何东西,就像我得到的印象一样。危险的部分是我没有使用 ref 计数 ptrs。
4

8 回答 8

11

该行a = MyClass(2);不会调用析构函数,它将调用MyClass::operator=您尚未实现的赋值运算符 (),因此编译器为您提供了一个 - 它不会“打印”任何内容,因此您看不到。

你得到destrucing 2两次的原因是在 line 之后立即销毁a = MyClass(2);临时MyClass(2)对象。main然后在变量的末尾a被销毁,因为myVar现在是 2,所以它再次打印 2。

于 2013-08-14T14:03:15.387 回答
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
于 2013-08-14T14:05:52.397 回答
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 存储的是一个值,而不是一个指针。如果您的类的数据是简单值,则您的示例编码得很好,并且没有任何错误。如果您的类改为包含指针,那么实际上一个默认构造函数和赋值运算符(或像我上面展示的那样操作的用户定义的默认构造函数和赋值运算符)不再足够,因为它们提供了指向的浅表副本- 数据。您的类要么需要合并某种形式的智能指针,要么手动处理指向资源的复制,可能涉及引用计数以提高效率。某种形式的智能指针可能是更安全的选择。

于 2013-08-14T15:19:31.877 回答
2

编译器优化第一次调用:

MyClass a = MyClass(1);

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

a = MyClass(2);

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

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

于 2013-08-14T14:03:11.807 回答
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另一个对象的副本。

于 2013-08-14T14:18:17.633 回答
1

当您的程序到达 main 的末尾时,它会破坏a,此时其变量myVar的值为 2。如果您改为编写:

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

您会看到预期的输出。

于 2013-08-14T14:02:58.527 回答
1

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

而是尝试:

int main(int argc, const char * argv[])
{
    MyClass a( 1 );
    MyClass b( 2 );
    return 0;
}
于 2013-08-14T14:05:44.417 回答
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 ++没有错;)

于 2013-08-14T15:34:03.900 回答