【问题标题】:Strange object assignment behaviour c++奇怪的对象赋值行为c ++
【发布时间】:2017-11-07 18:28:05
【问题描述】:

我对对象分配有一个奇怪的行为。如果您能解释为什么这项作业会这样工作,我将不胜感激。它已经花费了我很多时间。 我正在使用 Visual Studio Enterprise 2017(所有默认设置)。

代码:

#include "stdafx.h"
#include <iostream>

using namespace std;

class Test
{
public:

    Test()
    {
        cout << "Constructor of " << this << endl;
    }

    ~Test()
    {
        cout << "Destructor of " << this << endl;
    }
};


int main()
{
    cout << "Assignment 1" << endl;
    auto t = Test();
    cout << "Assignment 2" << endl;
    t = Test();

    int i = 0;
    cin >> i;
    return 0;
}

输出(最高为 cin):

Assignment 1
Constructor of 006FFC9F
Assignment 2
Constructor of 006FFBC7
Destructor of 006FFBC7

预期输出(最高为 cin):

Assignment 1
Constructor of 006FFC9F
Assignment 2
Destructor of 006FFC9F
Constructor of 006FFBC7

我想编写一个测试函数来创建我的(模板)类的对象,进行一些测试,然后创建一个新对象并进行更多测试。问题是 t 在第二次赋值后保存了已经破坏的对象。 我知道我可以只使用导致预期行为的动态分配,但是为什么这个程序的行为不同?

非常感谢。 问候。

PS:结果是一样的,与 Release/Debug 或 64/32 位编译无关

编辑:更详细的示例:

#include "stdafx.h"
#include <iostream>

using namespace std;

class Test
{
private:
    float* val;
public:
    Test()
    {
        val = new float;
        cout << "Constructor of " << this << ", addr. of val: " << val << endl;
    }

    ~Test()
    {
        cout << "Destructor of " << this << ", addr. of val: " << val << " --> DELETING VAL!" << endl;
        delete val;
    }

    float* getVal() { return this->val; }
};


int main()
{
    cout << "Assignment 1" << endl;
    auto t = Test();
    cout << "Assignment 2" << endl;
    t = Test();
    cout << "Val Address: " << t.getVal() << endl;

    int i = 0;
    cin >> i;
    return 0;
}

输出(它在末尾保存了一个已删除的指针!!!):

Assignment 1
Constructor of 004FFBDC, addr. of val: 0072AEB0
Assignment 2
Constructor of 004FFB04, addr. of val: 00723928
Destructor of 004FFB04, addr. of val: 00723928 --> DELETING VAL!
Val Address: 00723928

【问题讨论】:

  • 看看复制省略,从 C++17 开始在某些情况下必须执行。
  • auto t = Test(); 不是赋值,而是初始化。
  • t = Test();中你构造了一个Test的临时对象,传递给toperator=,然后,销毁这个临时对象。为什么你认为,这是一个问题?如果您将 print 语句放入 operator=(为了完整起见,可能还有复制构造函数),您会清楚地看到这种行为。
  • "t 持有第二次赋值后已经销毁的对象" 不,它没有持有新构建的对象,该对象已经替换了不再需要的对象。您的代码在任何时候都不需要两个 Test 对象同时存在。
  • 局部变量的生命周期直到它们被声明的代码块的结尾。所以你不应该期望看到t的析构函数被调用直到你到达}结尾main.

标签: c++ class constructor visual-studio-2017 destructor


【解决方案1】:

auto t = Test();

您实际上构造了 两个 对象。首先是构造一个临时对象的Test()。第二个是t的构造,通过copy-construction进行。这里没有赋值,即使使用=操作符,也是拷贝构造。

如果您在Test 类中添加类似于构造函数和析构函数的复制构造函数,您应该可以清楚地看到它。


至于

t = Test();

这里使用Test() 创建了一个临时 对象。然后将该临时对象传递给Test 类的(编译器生成的)赋值运算符,然后立即销毁该临时对象。

对象t 本身没有被破坏,它不应该被破坏,因为它是分配的目的地。

【讨论】:

  • 可能看不到副本,因为它可以被省略。
  • 我认为混淆在于第二个作业,而不是第一个。
  • 加上复制构造函数:Test(const Test& obj) { cout
  • @Fhnx 哎哟!如果没有the rules of three, five or zero,该指针将导致未定义行为。当编译器生成的复制构造函数或赋值操作符被调用时,它将复制指针,而不是它指向的内容。这导致两个指针指向同一个内存。现在想想当一个对象delete指向指针时,另一个对象中的指针会发生什么?
  • 如果没有复制省略,您的“第二个”将是“复制/移动构造”,而不是复制构造。 (这意味着移动构造,除非类的移动构造函数根本没有生成)
【解决方案2】:

您的困惑似乎是错误地期望原始对象在分配发生时被破坏。就像,在这段代码中:

cout << "Assignment 2" << endl;
t = Test();

这段代码调用了移动赋值运算符。由于您没有定义一个,因此编译器生成的默认值或多或少看起来像这样:

Test & operator=(Test &&) {}

请注意代码中没有调用构造函数或(关键地)析构函数。唯一要运行的构造函数和析构函数在临时对象上(这是您在实际输出中观察到的)。在代码超出范围之前,原始对象不会被销毁;为什么会这样?在此之前你不能停止使用堆栈空间。

编辑:可能会帮助您了解正在发生的事情:

#include<iostream>

struct Test {
    Test() {std::cout << "Constructed.\n";}
    ~Test() {std::cout << "Destructed.\n";}
    Test(Test const&) {std::cout << "Copy-Constructed.\n";}
    Test(Test &&) {std::cout << "Move-Constructed.\n";}
    Test & operator=(Test const&) {std::cout << "Copy-Assigned.\n"; return *this;}
    Test & operator=(Test &&) {std::cout << "Move-Assigned.\n"; return *this;}
};

int main() {
    std::cout << "Test t;\n";
    Test t; //Construction
    std::cout << "Test t2(t);\n";
    Test t2(t); //Copy-Construct
    std::cout << "Test t3(std::move(t2));\n";
    Test t3(std::move(t2)); //Move-Construct
    std::cout << "Test t4 = t;\n";
    Test t4 = t; //Copy Construct, due to Copy Ellision
    std::cout << "Test t5 = Test();\n";
    Test t5 = Test(); //Will probably be a normal Construct, due to Copy Ellision
    std::cout << "t = t2;\n";
    t = t2; //Copy Assign
    std::cout << "t = Test();\n";
    t = Test(); //Move Assign, will invoke Constructor and Destructor on temporary
    std::cout << "Done! Cleanup will now happen!\n";
    return 0;
}

compiled here时看到的结果:

Test t;
Constructed.
Test t2(t);
Copy-Constructed.
Test t3(std::move(t2));
Move-Constructed.
Test t4 = t;
Copy-Constructed.
Test t5 = Test();
Constructed.
t = t2;
Copy-Assigned.
t = Test();
Constructed.
Move-Assigned.
Destructed.
Done! Cleanup will now happen!
Destructed.
Destructed.
Destructed.
Destructed.
Destructed.

双重编辑组合!

正如我在 cmets 中提到的,val 只是一个指针。作为Test 存储的一部分分配的8 个字节(在64 位机器上)。如果您要确保Test 始终包含尚未删除的val 的有效值,则需要实施Rule of Five(以前称为三规则):

class Test {
    float * val;
public:
    Test() {val = new float;}
    ~Test() {delete val;
    Test(Test const& t) {
        val = new float(*(t.val));
    }
    Test(Test && t) {std::swap(val, t.val);}
    Test & operator=(Test const& t) {
        float * temp = new float(*(t.val)); //Gives Strong Exception Guarantee
        delete val; 
        val = temp;
        return *this;
    }
    Test & operator=(Test && t) {std::swap(val, t.val); return *this;};

    float & get_val() const {return *val;} //Return by reference, not by pointer, to 
        //prevent accidental deletion.
};

【讨论】:

  • 我添加了一个带有浮点指针的更详细的示例。指针被删除了,值仍然分配给对象?!
  • @Fhnx 关注我刚刚所做的编辑,看看发生了什么。另外,仅供参考,deleteing 指针不会清除指针本身的值,它只会删除它指向的内存。 float * 是分配在堆栈上的 8 个字节,指向一个 4 字节的段,该段使用 new 分配并使用 delete 销毁。无论val 是否指向有效内存、已被销毁或其他任何原因,都不会阻止您打印出val 的值或重新分配它。
  • 感谢您的详细示例。这和@Some other程序员老兄的回复解释了我的错误。所以我应该可以使用额外的复制构造函数。
猜你喜欢
  • 1970-01-01
  • 2015-04-01
  • 2016-06-01
  • 1970-01-01
  • 1970-01-01
  • 2012-01-22
  • 2012-12-15
  • 2016-09-01
  • 2012-12-16
相关资源
最近更新 更多