【问题标题】:Understanding and using a copy assignment constructor理解和使用复制赋值构造函数
【发布时间】:2019-10-23 08:54:40
【问题描述】:

我试图了解复制赋值构造函数在 C++ 中的工作原理。我只使用过java,所以我真的不在我的水域。我已经阅读并看到返回参考是一个很好的做法,但我不明白我应该如何做到这一点。我写了这个小程序来测试这个概念:

main.cpp:

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

using namespace std;

int main() {
    Test t1,t2;
    t1.setAge(10);
    t1.setId('a');
    t2.setAge(20);
    t2.setId('b');

    cout << "T2 (before) : " << t2.getAge() << t2.getID() << "\n";

    t2 = t1; // calls assignment operator, same as t2.operator=(t1)

    cout << "T2 (assignment operator called) : " << t2.getAge() << t2.getID() << "\n";

    Test t3 = t1; // copy constr, same as Test t3(t1)

    cout << "T3 (copy constructor using T1) : " << t3.getAge() << t3.getID() << "\n";

    return 1;
}

test.h:

class Test {
    int age;
    char id;

    public:
        Test(){};
        Test(const Test& t); // copy
        Test& operator=(const Test& obj); // copy assign
        ~Test();
        void setAge(int a);
        void setId(char i);
        int getAge() const {return age;};
        char getID() const {return id;};
};

test.cpp:

#include "test.h"

void Test::setAge(int a) {
    age = a;
}

void Test::setId(char i) {
    id = i;
}

Test::Test(const Test& t) {
    age = t.getAge();
    id = t.getID();
}

Test& Test::operator=(const Test& t) {

}

Test::~Test() {};

我似乎无法理解我应该在 operator=() 中放入什么。我见过人们返回*this,但我读到的只是对对象本身的引用(在= 的左侧),对吧?然后我考虑返回const Test&amp; t 对象的副本,但是使用这个构造函数就没有意义了,对吧?我要返回什么,为什么?

【问题讨论】:

  • Test::operator=(const Test&amp;) 不是构造函数。它是一个复制赋值运算符。构造函数创建一个新对象;赋值运算符修改现有对象。
  • @SteliosPapamichail 看来你的老师对 C++ 概念不太熟悉。
  • 旁注:通读Copy and Swap Idiom。它不仅是一个防弹(假设复制构造函数是正确的)赋值运算符,而且它很容易编写并且很难出错。它也有点重量级,所以它并不总是正确的解决方案,但它几乎总是一个很好的起点和停留,直到分析证明不是这样。
  • Test t3 = t1;Test t3(t1); 并不完全相同。例如,如果复制构造函数被标记为explicit,则只有第二个替代方案会编译。

标签: c++ copy-assignment


【解决方案1】:

我已经阅读并看到返回引用是一个很好的做法,但我不明白我应该如何做到这一点。

如何

添加

return *this;

作为函数的最后一行。

Test& Test::operator=(const Test& t) {
   ...
   return *this;
}

为什么

至于为什么要返回*this,答案是惯用的。

对于基本类型,您可以使用以下内容:

int i;
i = 10;
i = someFunction();

您可以在链式操作中使用它们。

int j = i = someFunction();

您可以在条件中使用它们。

if ( (i = someFunction()) != 0 ) { /* Do something */ }

您可以在函数调用中使用它们。

foo((i = someFunction());

它们之所以有效,是因为 i = ... 评估为对 i 的引用。即使对于用户定义的类型,保持这种语义也是惯用的。您应该可以使用:

Test a;
Test b;

b = a = someFunctionThatReturnsTest();

if ( (a = omeFunctionThatReturnsTest()).getAge() > 20 ) { /* Do something */ }

然后

更重要的是,您应该避免为发布的类编写析构函数、复制构造函数和复制赋值运算符。编译器创建的实现对于Test 来说已经足够了。

【讨论】:

  • "我看到有人返回*this,但我读到的只是对对象本身的引用(在= 的左侧),对吧? “听起来 OP 知道这是他们应该做的事情。也许您可以为为什么添加一些解释这是正确的(因为这似乎是他们所缺少的)。
  • @scohe001 我不知道建议编写复制构造函数然后在复制构造函数中调用赋值运算符是否是个好主意。最好让拷贝构造函数独立存在,然后简单地使用拷贝/交换来实现拷贝构造函数中的赋值运算符。
  • @Stelios -- 如果需要分配资源,赋值运算符有一个额外的步骤来释放当前资源,而复制构造函数不会这样做。将所有代码从复制构造函数移到赋值运算符有点混淆了这种差异。您需要确保复制构造函数具有“空”资源,否则赋值运算符将尝试释放垃圾。这就是海事组织的危险。我见过很多次程序员忘记将指向资源的指针清空,然后调用赋值运算符,结果却发生了段错误。
  • 一定要阅读三法则的链接。如果没有很好地掌握三法则,您就无法编写健壮、重要的 C++ 代码。基于复制构造函数而不是相反的方式编写赋值运算符消除了一些代码(自赋值变得不可能),资源释放通过副本的破坏自动释放,并且移动赋值是免费的。
【解决方案2】:

我们从赋值运算符返回一个引用,这样我们就可以做一些很酷的技巧,比如@SomeWittyUsername shows

我们想要返回引用的对象是操作符被调用的对象,或者this。所以——就像你听说的那样——你会想要返回*this

所以你的赋值运算符可能看起来像:

Test& Test::operator=(const Test& t) {
    age = t.getAge();
    id = t.getID();
    return *this;
}

您可能会注意到,这看起来与您的复制构造函数非常相似。在更复杂的类中,赋值运算符将完成复制构造函数的所有工作,但除此之外,它还必须安全地删除类已经存储的任何值。

由于这是一个非常简单的类,我们不需要安全删除任何内容。我们可以重新分配两个成员。所以这将与复制构造函数几乎完全相同。

这意味着我们实际上可以将您的构造函数简化为只使用运算符!

Test::Test(const Test& t) {
    *this = t;
}

同样,虽然这适用于您的简单类,但在具有更复杂类的生产代码中,我们通常希望为我们的构造函数使用初始化列表(请阅读 here 了解更多信息):

Test::Test(const Test& t) : age(t.getAge()), id(t.getId()) { }

【讨论】:

  • 有趣的是,我实际上最初编写了初始化列表变体,但不确定它是否正确。因此,在更复杂的课程中,我应该按原样使用该变体吗?我的意思是在{} 内部不做任何事情,还是应该在其中添加remove values that the class already has stored 部分?
  • @SteliosPapamichail 因为这是构造函数,它会在我们的对象第一次创建时被调用一次(而且只有一次!)。所以我们实际上还没有存储任何东西!即使在一个更复杂的类中,构造函数的唯一工作就是实际构建对象(如果需要的话,可能会分配东西)。在您的情况下,除了分配这两个值之外,不需要做任何额外的工作,因此将主体留空就可以了。我在我的答案中添加了一个链接,说明为什么首选初始化列表。
  • 正如 R Sahu 回答的 cmets 中所讨论的,更喜欢编写赋值运算符以使用复制构造函数,而不是使用赋值运算符编写复制构造函数。这一点无关紧要,但是当类变得更复杂并且资源管理变得重要时,Copy and Swap 方法可以更好地管理资源的处置并提供正确异常处理的保证(假设复制构造函数和析构函数是正确实施)。
【解决方案3】:

为了支持嵌套操作,需要返回对原始对象的引用。 考虑

a = b = c

【讨论】:

    猜你喜欢
    • 2012-02-12
    • 1970-01-01
    • 2016-09-13
    • 2011-07-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多