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

C++ stack based constructor/destructor not working as expected

本文关键字:析构函数 工作 构造函数 于堆栈 堆栈 C++      更新时间:2023-10-16

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

#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. 总的来说,我在这里展示的效果并不危险 - 它不会像我得到的印象那样损坏任何东西。危险的部分是我没有使用参考计数 ptrs。

a = MyClass(2);不会调用析构函数,它会调用你尚未实现的赋值运算符 ( MyClass::operator=(,所以编译器为你提供了一个 - 它不会"打印"任何东西,所以你看不到。

你得到两次destrucing 2的原因是,在行a = MyClass(2);之后,临时MyClass(2)对象立即被销毁。然后在main结束时销毁变量a,由于myVar现在是 2,它再次打印 2。

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

您创建的 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 存储的是值,而不是指针。 您的示例编码得很好,并且没有任何错误,如果您的类的数据是简单的值。 如果您的类包含指针,那么默认的构造函数和赋值运算符(或与默认运算符类似的用户定义运算符,例如我上面显示的那些(不再足够,因为它们提供了指向数据的浅层副本。 您的类要么需要合并某种形式的智能指针,要么手动处理指向资源的复制,可能涉及引用计数以提高效率。 某种形式的智能指针可能是一个更安全的选择。

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修改为另一个对象的副本。

编译器优化了第一次调用:

MyClass a = MyClass(1);

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

a = MyClass(2);

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

销毁

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

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

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

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++ ;) 没有错

当你的程序到达main的末尾时,它会破坏a,其变量myVar在这一点上的值是2。如果你改为写:

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

你会看到预期的输出。

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

相反,请尝试:

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