运算符重载=修改原始对象

时间:2018-12-19 17:12:36

标签: c++ operator-overloading

struct List {
    int size;
    int* items;
    List& operator=(const List& l);
};

List& List::operator=(const List& l)
{
    delete[] items;
    size = l.size;
    items = new int[20];

    for (int i = 0; i < size; i++)
        items[i] = l.items[i];

    return *this;
}

ostream& operator<<(ostream& out, const List& l)
{
    out << "Size: " << l.size << "\t {";
    for (int i = 0; i < l.size; i++)
        out << l.items[i] << " ";

    return (out << "}");
}

int main()
{
    int size = 6;
    List l1 = { size, new int[size]{ 0, 1, 2, 3, 4, 5 } };
    List l2 = l1;

    l2.items[1] = 50;

    cout << l1 << endl;
    cout << l2 << endl;

    return 0;
}

我很困惑,因为当我使用重载运算符分配 l2 = l1 时,为什么以后更改l2时l1的内容会发生变化?特别是因为l1作为const传递。它们以某种方式指向内存中的同一对象,而不是副本。

3 个答案:

答案 0 :(得分:8)

List l2 = l1;不调用副本分配运算符(operator=(const List& l))。由于“赋值”发生在变量声明期间,因此您将通过复制初始化来初始化l2,该初始化将调用编译器生成的默认复制构造函数。由于它会进行浅表复制,因此两个对象都将指向相同的数据。

如果要编写自己的类来管理内存/资源,则需要至少提供自己的副本构造函数,副本分配运算符和销毁器。这称为the rule of three。如果要包括移动语义,则需要提供一个移动构造函数和一个移动赋值运算符,这被称为5规则。还有零规则,其中使用已经“做正确的事”的使用类型(就像使用std::vector一样),允许编译器生成的默认设置为您工作,您可以在The rule of three/five/zero

上阅读全部内容

答案 1 :(得分:5)

List l2 = l1;

尽管使用=,因为这是一个声明,您正在执行副本构造(通常是“副本初始化”),这与赋值运算符无关。

您没有定义一个复制构造函数(它看起来应该很像您的复制赋值运算符),因此指针确实是共享的。

如果您撰写以下内容,结果将与您预期的一样:

List l2{};
l2 = l1;

顺便说一句,如果我是您,我会给sizeitems作为默认值(分别为0nullptr)。否则,当您忘记了{}时,成员将具有未指定的值,并且所有地狱都会崩溃。这可以通过一个不错的默认构造函数来完成,或者通过忘记整个企业并使用std::vector来代替;)

答案 2 :(得分:3)

List l2 = l1;调用编译器生成的 copy构造函数,该构造函数不会深度复制指针成员。

如果要对std::vector使用items,则可以将构造函数和赋值运算符留给编译器。 (您的代码中没有任何delete[]调用,这意味着您的类将像漏勺漏水一样泄漏内存。)