玩技术,Geeker
一个原创技术文章分享网站

C++中复制构造函数与重载赋值操作符总结

前言

这篇文章将对C++中复制构造函数和重载赋值操作符进行总结,包括以下内容:

  1. 复制构造函数和重载赋值操作符的定义;
  2. 复制构造函数和重载赋值操作符的调用时机;
  3. 复制构造函数和重载赋值操作符的实现要点;
  4. 复制构造函数的一些细节。

复制构造函数和重载赋值操作符的定义

我们都知道,在C++中建立一个类,这个类中肯定会包括构造函数、析构函数、复制构造函数和重载赋值操作;即使在你没有明确定义的情况下,编译器也会给你生成这样的四个函数。例如以下类:

class CTest
{
public:
     CTest();
     ~CTest();

     CTest(const CTest &);
     CTest& operator=(const CTest &);
};

对于构造函数和析构函数不是今天总结的重点,今天的重点是复制构造函数和重载赋值操作。类的复制构造函数原型如下:

class_name(const class_name &src);

一般来说,如果我们没有编写复制构造函数,那么编译器会自动地替每一个类创建一个复制构造函数(也叫隐式复制构造函数);相反的,如果我们编写了一个复制构造函数(显式的复制构造函数),那么编译器就不会创建它。

类的重载赋值操作符的原型如下:

class_name& operator=(const class_name &);

重载赋值操作符是一个特别的赋值运算符,通常是用来把已存在的对象指定给其它相同类型的对象。它是一个特别的成员函数,如果我们没有定义这个成员函数,那么编译器会自动地产生这个成员函数。编译器产生的代码是以单一成员进行对象复制的动作。

总结了复制构造函数和重载赋值操作符的定义,只是让我们了解了它们,而没有真正的深入它们。接下来,再仔细的总结一下它们的调用时机。关于它们的调用时机,我一直都没有真正的明白过,所以这里一定要好好的总结明白了。

复制构造函数和重载赋值操作符的调用时机

对复制构造函数和重载赋值操作符的调用总是发生在不经意间,它们不是经过我们显式的去调用就被执行了。对于这种隐式调用的地方一定要多注意了,这也一般是有陷阱的地方。现在我就用实际的例子来进行验证;例子如下:

#include <iostream>
using namespace std;

class CTest
{
public:
     CTest(){}
     ~CTest(){}

     CTest(const CTest &test)
     {
          cout<<"copy constructor."<<endl;
     }

     void operator=(const CTest &test)
     {
          cout<<"operator="<<endl;
     }

     void Test(CTest test)
     {}

     CTest Test2()
     {
          CTest a;
          return a;
     }

     void Test3(CTest &test)
     {}

     CTest &Test4()
     {
          CTest *pA = new CTest;
          return *pA;
     }
};

int main()
{
     CTest obj;

     CTest obj1(obj); // 调用复制构造函数

     obj1 = obj; // 调用重载赋值操作符

     /* 传参的过程中,要调用一次复制构造函数
     * obj1入栈时会调用复制构造函数创建一个临时对象,与函数内的局部变量具有相同的作用域
     */
     obj.Test(obj1);

     /* 函数返回值时,调用复制构造函数;将返回值赋值给obj2时,调用重载赋值操作符
     * 函数返回值时,也会构造一个临时对象;调用复制构造函数将返回值复制到临时对象上
     */
     CTest obj2;
     obj2 = obj.Test2();

     obj2.Test3(obj); // 参数是引用,没有调用复制构造函数

     CTest obj3;
     obj2.Test4(); // 返回值是引用,没有调用复制构造函数

     return 0;
}

在代码中都加入了注释,这里就不再做详细的说明了。再次总结一下,如果对象在声明的同时将另一个已存在的对象赋给它,就会调用复制构造函数;如果对象已经存在了,然后再将另一个已存在的对象赋给它,调用的就是重载赋值运算符了。这条规则很适用,希望大家能记住。

复制构造函数和重载赋值操作符的实现要点

在一般的情况下,编译器给我们生成的默认的复制构造函数和重载赋值操作符就已经够用了;但是在一些特别的时候,需要我们手动去实现自己的复制构造函数。

我们都知道,默认的复制构造函数和赋值运算符进行的都是”shallow copy”,只是简单地复制字段,因此如果对象中含有动态分配的内存,就需要我们自己重写复制构造函数或者重载赋值运算符来实现”deep copy”,确保数据的完整性和安全性。这也就是大家常常说的深拷贝与浅拷贝的问题。下面我就提供一个比较简单的例子来说明一下:

#include <iostream>
using namespace std;

const int MAXSIZE = 260;

class CTest
{
public:
     CTest(wchar_t *pInitValue)
     {
          // Here, I malloc the memory
          pValue = new wchar_t[MAXSIZE];
          memset(pValue, 0, sizeof(wchar_t) * MAXSIZE);
          wcscpy_s(pValue, MAXSIZE, pInitValue);
     }

     ~CTest()
     {
          if (pValue)
          {
               delete[] pValue; //finalseabiscuit指出,谢谢。2014.7.24
               pValue = NULL;
          }
     }

     CTest(const CTest &test)
     {
          // Malloc the new memory for the pValue
          pValue = new wchar_t[MAXSIZE];
          memset(pValue, 0, sizeof(wchar_t) * MAXSIZE);
          wcscpy_s(pValue, MAXSIZE, test.pValue);
     }

     CTest& operator=(const CTest &test)
     {
          // This is very important, please remember
          if (this == &test)
          {
               return *this;
          }

          // Please delete the memory, this maybe cause the memory leak
          if (pValue)
          {
               delete[] pValue; // 方恒刚指出的问题。非常感谢 2014.3.15
          }

          // Malloc the new memory for the pValue
          pValue = new wchar_t[MAXSIZE];
          memset(pValue, 0, sizeof(wchar_t) * MAXSIZE);
          wcscpy_s(pValue, MAXSIZE, test.pValue);
          return *this;
     }

     void Print()
     {
          wcout<<pValue<<endl;
     }

private:
     wchar_t *pValue; // The pointer points the memory
};

int main()
{
     CTest obj(L"obj");
     obj.Print();

     CTest obj2(L"obj2");
     obj2.Print();
     obj2 = obj;
     obj2.Print();

     obj2 = obj2;
     obj2.Print();

     return 0;
}

特别是在实现重载赋值构造函数时需要多多的注意,在代码中我也添加了注释,大家可以认真的阅读一下代码,然后就懂了,如果不懂的就可以留言问我;当然了,如果我哪里理解错了,也希望大家能给我提出,我们共同进步。

复制构造函数的一些细节

  1. 以下哪些是复制构造函数
    X::X(const X&);   
    X::X(X);   
    X::X(X&, int a=1);   
    X::X(X&, int a=1, int b=2);
    这些细节问题在这里也说一说,我也是从别人的博客里看到的,这里自己也总结一下。对于一个类X, 如果一个构造函数的第一个参数是下列之一:
    a) X&
    b) const X&
    c) volatile X&
    d) const volatile X&
    且没有其他参数或其他参数都有默认值,那么这个函数是拷贝构造函数。
    X::X(const X&);  //是拷贝构造函数   
    X::X(X&, int=1); //是拷贝构造函数  
    X::X(X&, int a=1, int b=2); //当然也是拷贝构造函数
  2. 类中可以存在超过一个拷贝构造函数
    class X 
    { 
    public:       
      X(const X&);      // const 的拷贝构造
      X(X&);            // 非const的拷贝构造
    };
    注意,如果一个类中只存在一个参数为 X& 的拷贝构造函数,那么就不能使用const X或volatile X的对象实行拷贝初始化。如果一个类中没有定义拷贝构造函数,那么编译器会自动产生一个默认的拷贝构造函数。这个默认的参数可能为 X::X(const X&)或 X::X(X&),由编译器根据上下文决定选择哪一个。在我的Visual Studio 2012中,当定义了多个复制构造函数以后,编译器就会有warning,但是程序还能正确运行。

总结

这篇文章对复制构造函数和重载赋值操作符进行了一些总结,重点是在复制构造函数与重载赋值操作符的调用时机上;对于大家喜欢总结的深拷贝与浅拷贝问题,我没有用过多的文字进行说明,我认为上面的代码就足以说明问题了。最后自己纠结已久的问题也就这样总结了,自己也彻底的明白了。

2014年2月21日 于大连,东软。

打赏

未经允许不得转载:果冻想 » C++中复制构造函数与重载赋值操作符总结

分享到:更多 ()

评论 25

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
  1. #7

    C++11来了,加上move constructor and move assignment吧。

    anthony4年前 (2014-02-24)回复
    • 确实是个好主意。

      Jelly4年前 (2014-02-24)回复
  2. #6

    CTest obj1(obj); // 调用复制构造函数这一句为什么不是直接初始化,复制构造函数吧应该是CTest obj1=obj;吗?感谢回复!

    呆呆兽进化4年前 (2014-02-27)回复
    • 你好!很高兴能回答你的问题。> CTest obj1(obj); // 调用复制构造函数由于在类中存在构造函数和复制构造函数,为什么没有直接初始化呢?应为没有对应的构造函数进行初始化,而这行代码中,参数类型正好是CTest类型,所以就调用了C++类中的复制构造函数了。> 复制构造函数吧应该是CTest obj1=obj;吗?是的,这里是调用复制构造函数的。以上是我的一些理解,希望我的回答对你有帮助,同时希望能和楼主继续讨论,分享。

      宁采臣4年前 (2014-02-28)回复
      • 我也思考了一下这个问题,如果要直接初始化的话,应该有一个构造函数,其形参为(CTest &c),由于复制构造函数的形参为(const CTest&c),由于非const对象也可以使用形参为const的构造函数,就不需要另外再去定义一个了,我是这么理解的:)

        呆呆兽进化4年前 (2014-03-06)回复
    • 对于提问,我都会进行仔细回复的;可能有点晚。请谅解。谢谢你和我分享你的理解和认识。

      宁采臣4年前 (2014-02-28)回复
      • 另外,我本科在大连海事读的,当时还在软件园的埃森哲实习过,离东软很近,东软很美,小城堡一样的公司:)

        呆呆兽进化4年前 (2014-03-06)回复
  3. #5

    你好,delete pValue 是不是需要改成 delete [] pValue

    方恒刚4年前 (2014-03-13)回复
    • 谢谢你的指正。非常感谢。已经修改过来了。非常希望有希望和你继续探讨。

      vipygd@126.com4年前 (2014-03-15)回复
      • 那为啥析构函数 delete pValue? 上面修改的出发点是什么?

        finalseabiscuit3年前 (2014-07-24)回复
        • Sorry。应该是delete[] pValue.已经修改,谢谢你的review。

          果冻想3年前 (2014-07-24)回复
          • 内建类型delete pValue; 也OK的,不过还是推荐写delete[] pValue;

            路过3年前 (2014-09-02)
          • 恩。是的。这个问题我已经意识到了。在http://www.jellythink.com/archives/101#comments这里,也有网友提出了同样的问题。谢谢你的再次提出,也谢谢你的分享。

            果冻想3年前 (2014-09-02)
  4. #4

    类的重载赋值操作符的原型如下:void operator=(const class_name &); //是否应该是void operator=(const class_name &src);

    LW3年前 (2015-03-13)回复
    • ^_^,我只是声明,不加参数名称也是可以的。

      果冻想3年前 (2015-03-13)回复
  5. #3

    这个赋值操作符的返回值不能为空吧。

    朱杰骏3年前 (2015-03-19)回复
  6. #2

    /* 函数返回值时,调用复制构造函数;将返回值赋值给obj2时,调用重载赋值操作符 * 函数返回值时,也会构造一个临时对象;调用复制构造函数将返回值复制到临时对象上 */ CTest obj2; obj2 = obj.Test2();此处调用Test2虽然创建了临时对象并返回给obj2,但是并没有调用复制构造函数,它仅仅只调用重载赋值操作符

    Yuvahai2年前 (2015-06-09)回复
    • 编译器有优化。

      果冻想2年前 (2015-06-09)回复
      • 这个还是没明白什么意思,难道是它本质上是要调用复制构造函数,但是由于编译器优化,最终没调用?我把这段代码编译测试了,运行结果显示没调用复制构造函数,望楼主能够帮忙解惑,小弟不胜感激

        Yuvahai2年前 (2015-06-10)回复
        • 您好,这几天比较忙,回复的有点晚了。首先,请确认上述代码,我使用的vs2012进行编译。请在Project->【Properties】->【Configuration Properties】->【C/C++】->【Optimization】->【Optimization】进行设置。如果对于该选项不是很明白,请Google一下。

          果冻想2年前 (2015-06-11)回复
          • 不好意思,我是在Linux环境下编译运行的,没有用windows工具编译。。。。不知道是不是系统的原因

            Yuvahai2年前 (2015-06-11)
          • 你好,对于Linux系统,也应该有对应的编译选项。

            果冻想2年前 (2015-06-11)
          • 额。。。。。。我是用Linux下gcc/g++编译的,没用其他的工具

            Yuvahai2年前 (2015-06-12)
          • 我知道你使用的gcc/g++编译工具,我说的编译选项。-O0-O1-O2-O3编译器的优化选项的4个级别,-O0表示没有优化,-O1为缺省值,-O3优化级别最高。

            果冻想2年前 (2015-06-12)
  7. #1

    http://liam0205.me/2016/08/14/copy-control-in-Cpp/
    我也写了一篇类似的。感觉调用时机并不怎么重要,反而会给人一种「如果我不在这些时候调用,那么就没必要实现自己的拷贝构造函数和重载赋值运算符」的错觉。然而实际上,类实现出来之后,作为类的作者,是没有办法控制别人怎么用的(兴许自己可以不去这样调用,但不保证别人会不会调用,因此不管怎么样还是应该实现一下)。

    L1年前 (2016-08-14)回复

在这里玩技术,享受技术带来的疯狂

捐赠名单关于果冻