如果将变量设置为等于新对象,旧对象会发生什么?

时间:2017-06-22 18:50:31

标签: c++

假设我们有一个X类,有一个重载的operator=()函数。

class X {
    int n;

    X() {n = 0;}
    X(int _n) {n = _n;}
};

int main() {
    X a;    // (1) an object gets constructed here

    // more code...

    a = X(7);    // (2) another object gets constructed here (?)

    // some more code...

    a = X(12);    // (3) yet another object constructed here (?)
    return 0;
}

是否在(2)处构建了新对象?如果是,那么(1)中构造的旧对象会发生什么?是自动销毁还是解除分配(这是它)?它被覆盖了吗?

在(3)的代码中发生了什么呢?

最重要的是,是否有可能通过编写如上所述的代码来导致内存泄漏?

3 个答案:

答案 0 :(得分:6)

在第(2)点发生了三件事:

  1. 使用X(int _n)构造函数构建临时对象。
  2. 默认赋值运算符用于将临时内容复制到a
  3. 临时超出范围,并调用其默认析构函数。
  4. 同样的事情发生在第(3)点。

    在函数结束时,调用a上的默认析构函数。

答案 1 :(得分:2)

您需要了解的是,作为一个新手,您不了解编译器生成的大量“隐式”代码。我们将class X的代码用作直接示例:

class X {
    int n;
public: //You didn't include this, but this won't work at all unless your constructors are public
    X() {n = 0;}
    X(int _n) {n = _n;}
};

在代码转换为Object Code之前,但是在编译器获得了类定义之后,它会将您的类转换为看起来像这样的内容:

class X {
    int n;
public:
    X() {n = 0;} //Default-Constructor
    X(int _n) {n = _n;} //Other Constructor
    //GENERATED BY COMPILER
    X(X const& x) {n = x.n;} //Copy-Constructor
    X(X && x) {n = x.n;} //Move-Constructor
    X & operator=(X const& x) {n = x.n; return *this;} //Copy-Assignment
    X & operator=(X && x) {n = x.n; return *this;} //Move-Assignment
    ~X() noexcept {} //Destructor
};

自动创建这些成员的规则并不是非常明显(A good starting reference here),但是现在,你可以相信,在这种情况下,这正是发生的事情。

所以在你的main函数中,让我们回顾一下所发生的事情,注意具体细节和注释:

int main() {
    X a; //Default-Constructor called
    a = X(7);//Other Constructor called, then Move-Assignment operator called,
    //then Destructor called on temporary created by `X(7)`
    a = X(12); //Same as previous line

    return 0;
    //Destructor called on `a`
}

我们将添加更多行以显示这些调用的各种排列(如果不是全部):

int main() {
    X a; //Default-Constructor
    X b = a; //Copy-Constructor (uses copy-ellision to avoid calling Default + copy-assign)
    X c(5); //Other Constructor
    X d{7}; //Also Other Constructor
    X e(); //Declares a function! Probably not what you intended!
    X f{}; //Default-Constructor
    X g = X(8); //Other Constructor (uses copy-ellision to avoid calling Other + move-assign + Destructor)
    X h = std::move(b); //Move-Constructor (uses copy-ellision to avoid calling Default + move-assign)
    b = c; //Copy-assignment
    b = std::move(d); //Move-assignment
    d = X{15}; //Other Constructor, then Move-Assignment, then Destructor on `X{15}`.
    //e = f; //Will not compile because `e` is a function declaration!
    return 0;
    //Destructor on `h`
    //Destructor on `g`
    //Destructor on `f`
    //Destructor will NOT be called on `e` because `e` was a function declaration, 
    //not an object, and thus has nothing to clean up!
    //Destructor on `d`
    //Destructor on `c`
    //Destructor on `b`
    //Destructor on `a`
}

这应该涵盖基础知识。

  

最重要的是,是否有可能通过编写如上所述的代码来导致内存泄漏?

如上所述,没有。但是,假设你的班级做了类似的事情:

class X {
    int * ptr;
public:
    X() {
        ptr = new int{0};
    }
};

现在,您的代码会泄漏,因为每次创建X时,您都会有一个永远不会被删除的指针。

要解决此问题,您需要确保A)析构函数正确清理指针,B)您的复制/移动构造函数/运算符是正确的。

class X {
    int * ptr;
public:
    X() {
        ptr = new int{0};
    }
    X(int val) {
        ptr = new int{val};
    }
    X(X const& x) : X() {
        *ptr = *(x.ptr);
    }
    X(X && x) : X() {
        std::swap(ptr, x.ptr);
    }
    X & operator=(X const& x) {
        *ptr = *(x.ptr);
        return *this;
    }
    X & operator=(X && x) {
        std::swap(ptr, x.ptr);
        return *this;
    }
    ~X() noexcept {
        delete ptr;
    }
};

如果在main函数或我的函数中按原样使用,此代码不会泄漏内存。但是,当然,如果你这样做,它不会阻止泄漏:

int main() {
    X * ptr = new X{};
    return 0;
    //Whelp.
}

一般情况下,如果您根本不需要使用指针,建议您使用类似std::unique_ptr的内容,因为它可以免费提供大部分内容。

int main() {
    std::unique_ptr<X> ptr{new X{}};
    return 0;
    //Destructor called on *ptr
    //`delete` called on ptr
}

这对你原来的课程来说是一个好主意,但需要注意的是,除非你明确地改变它,否则你的课程将不再是可复制的(虽然它仍然可以移动):

class X {
    std::unique_ptr<int> ptr;
public:
    X() {
        ptr.reset(new int{0});
    }
    X(int val) {
        ptr.reset(new int{val});
    }
    //X(X && x); //auto generated by compiler
    //X & operator=(X && x); //auto generated by compiler
    //~X() noexcept; //auto generated by compiler

    //X(X const& x); //Deleted by compiler
    //X & operator=(X const& x); //Deleted by compiler
};

我们可以看到我之前版本main中的更改:

int main() {
    X a; //Default-Constructor
    //X b = a; //Was Copy-Constructor, no longer compiles
    X c(5); //Other Constructor
    X d{7}; //Also Other Constructor
    X f{}; //Default-Constructor
    X g = X(8); //Other Constructor (uses copy-ellision to avoid calling Other + move-assign + Destructor)
    X h = std::move(c); //Move-Constructor (uses copy-ellision to avoid calling Default + move-assign)
    //b = c; //Was Copy-assignment, no longer compiles
    c = std::move(d); //Move-assignment
    d = X{15}; //Other Constructor, then Move-Assignment, then Destructor on `X{15}`.
    return 0;
    //Destructor on `h`
    //Destructor on `g`
    //Destructor on `f`
    //Destructor on `d`
    //Destructor on `c`
    //Destructor on `a`
}

如果你想使用std::unique_ptr,但也希望生成的类是可复制的,你需要使用我讨论的技术自己实现复制构造函数。

这应该是关于它的!如果我错过了什么,请告诉我。

答案 2 :(得分:-1)

  

是否在(2)构建了一个新对象?

是的,临时对象X(7)。但它在声明结束时就被摧毁了。

  

如果是,那么在(1)构造的旧对象会发生什么?

调用编译器生成的默认operator=,传入临时对象X(7)

  

是自动销毁还是解除分配(这是它)?

不在作业中。当函数退出时,它会被释放。

  

被覆盖了吗?

只有其成员n的值发生变化,但可以将其视为被覆盖。

  

在(3)的代码中发生了什么呢?

与(2)中的相同。

  

是否有可能通过编写如上所述的代码来导致内存泄漏?

是的,如果你分配动态内存或其他资源,你在这里没有这样做。