复制构造函数有什么用处,既然可以使用赋值运算符“=”完成同样的操作?

15

C++为什么要提供复制构造函数?赋值运算符可以完成相同的任务。复制构造函数与赋值运算符相比有什么优点吗?


6
如果没有复制构造函数,你如何执行Foo copy(source);操作? - NathanOliver
3
传值时很难避免进行复制构造。 - user4581301
3
我理解OP想知道Foo copy(source);Foo copy; copy = source;的区别/优劣。前者会创建一个新的对象并将其初始化为source,而后者需要先创建一个空对象,然后再将其复制为source。前者更加简洁直观,同时可以避免未初始化的情况,但是如果类有自定义的拷贝构造函数或赋值运算符重载,这两种方法可能会产生不同的结果。 - Remy Lebeau
复制构造函数可以是深拷贝,而赋值运算符可以用于分配内存地址。 - Nick Gallimore
这个回答解决了你的问题吗?赋值运算符与拷贝构造函数 - xskxzr
这个回答解决了你的问题吗?赋值运算符和复制构造函数有什么区别? - lemuel
4个回答

21

拷贝构造函数可以实现的操作,而赋值运算符则很难或者根本无法实现的操作:

  1. 拷贝那些没有默认构造函数的类。例如,如果一个类表示打开的文件,你可能需要传递文件名才能构造出一个该类的实例。

  2. 拷贝那些拥有昂贵默认构造函数的类。也许构造函数会分配大量内存,但当你使用赋值运算符将新的状态拷贝到对象中时,这些内存就会被释放。

  3. 通过值传递类的实例。这也是拷贝构造函数最初的目的。在C语言中,如果你通过值传递一个结构体,编译器只会对该结构体进行位拷贝,以便接收函数拥有一个本地副本,并且可以修改它而不影响调用者。但是C++意识到对大多数对象进行位拷贝并不是最好的拷贝方式,因此它允许你编写自己的拷贝构造函数(并且默认的拷贝行为也不同,因为类成员可能拥有自定义的拷贝构造函数)。

  • 复制一个包含引用的类,因为在类已经构造后你无法重新分配引用。复制构造函数和赋值运算符对于引用有不同的作用。复制构造函数将引用初始化为指向被复制实例中引用指向的相同对象;赋值运算符则实际上复制了被引用对象的值。

  • 复制一个拥有const成员的类。(注意:一个类可以有默认构造函数但仍然有const成员。)


  • 1
    关于第四点,一个通过引用进行复制构造的类也具备可复制赋值的能力,这不是一个不好的想法吗? - Joseph Sible-Reinstate Monica
    2
    可能吧。我在尝试想出一个你需要这样做的例子,但是并没有想到什么。 - Willis Blackburn
    回复2:你漏掉了一个“高效”的词,因为虽然浪费但是肯定是可能的。 - Deduplicator
    我试图用“要么容易,要么根本不行”的评论来概括这一点。也可以通过去除const来分配具有const成员(#5)的类,或者在不调用其构造函数的情况下分配一个对象,然后再进行赋值。但这并不是一个好主意。 - Willis Blackburn

    11

    无论有没有复制构造函数,您仍然需要将新对象初始化为稳定的初始状态,这样赋值运算符可以稍后更新它。

    虽然您可以在没有复制构造函数的情况下完成此操作,但拥有复制构造函数有助于优化新对象的初始化,通过一开始将其设置为复制另一个对象的状态,而无需先将新对象初始化为默认状态,然后再进行单独的赋值以重置该状态。这样,您可以通过1次操作设置新对象的状态,而不是2次操作。


    4

    是的,这两者是不同的。您不能总是只实现您的复制构造函数为

    Foo(const Foo& f) {
        *this = f;
    }
    

    赋值运算符假设您拥有一个有效的、完全构造的对象。而复制构造函数则没有这样的假设。这意味着,根据您的类,赋值运算符可能会尝试清除对象上的任何数据以便重新初始化。或者甚至可能重新使用对象上已经存在的数据。


    8
    通常情况下,应该使用复制构造函数实现赋值运算符,而不是反过来。 - Remy Lebeau
    绝对正确,@Remy。实际上,在这里可能要提及Copy Swap 习语 - scohe001
    @RemyLebeau 如果这个类是抽象的,我不想将实现移入派生类中怎么办? - Evg
    一个抽象类仍然可以拥有构造函数和赋值运算符,派生类可以根据需要调用它们。 - Remy Lebeau
    @RemyLebeau 如果我想将所有复制构造代码放入已经具有赋值运算符的基类中,我可以使用 *this = f; 类型的构造函数。然而,我无法通过复制构造实现赋值。问题是这个问题是否有一些习惯用法的解决方案。 - Evg

    -1

    拿这个例子来说。

    杰克和约翰是双胞胎。 你可以说这个是杰克,那个是杰克。 但尽管约翰长得和杰克一模一样,他并不是杰克。

    当你使用赋值运算符时,你可以引用内存中的确切对象(返回*this),或提供一些自定义行为。

    当你使用复制构造函数时,你想在内存中创建另一个对象,该对象具有原始对象的属性副本,但可以朝着不同的方向发展。

    如果你想要更深入的答案,我认为这篇博客文章更好。

    赋值比初始化要棘手一些,因为我们实际上正在解构现有对象,然后使用新值重新构造它。在更复杂的类中,您可能需要释放各种资源,然后使用从要复制的对象中复制的资源副本重新分配它们。std::unique_ptr 在这里使我们的生活变得容易,因为将 std::unique_ptr 的新实例分配给使用 =(如上所述)或使用 reset() 的现有 std::unique_ptr 对象首先释放旧指针,因此自动处理释放资源(这是我们的类不需要析构函数的相同原因 - 当 std::unique_ptr 超出范围时,我们的对象超出范围,std::unique_ptr 的析构函数被调用并自动释放资源)。

    在C++中,关于赋值运算符,这并不是真的:“内存中的确切对象”,“指向同一内存块的两个变量”。 - davidbak
    看起来我已经很久没有接触C++了。我发现这个比较有趣 copy-constructor-vs-assignment-operator-in-c - Sérgio Azevedo

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接