C++的拷贝构造函数和赋值函数是两个特别比较让人混淆的概念,在使用中也经常容易出错,在这里我把C++的拷贝构造函数和赋值函数总结下。我从以下几个方面来总结:

     1、什么是拷贝构造函数和赋值函数,二者的区别

     2、C++拷贝构造函数和赋值函数的形式 ,为什么是拷贝构造函数式这种形式

     3、什么是默认拷贝构造函数和默认赋值函数

     4、什么是浅拷贝和深度拷贝

     5、拷贝构造函数和赋值函数使用中应该注意的问题

  

一、什么是拷贝构造函数和赋值函数,二者区别

       首先,拷贝构造函数从名字看它是个构造函数,又加了个定语"拷贝",总结来说,拷贝构造函数是一种特殊的构造函数,它是用一个已经存在的类的对象去创建该类的一个新对象。赋值函数指对于两个已经存在的类对象,用其中一个类对象对另外一个类对象进行赋值操作,拷贝构造函数和赋值函数非常容易混淆。拷贝构造函数是在对象被创建时调用的,而赋值函数只能被已经存在了的对象调用。

      请看下面例子:

       String a(“hello”);
  String b(“world”);
  String c = a; // 虽然用了“=”,但是调用了拷贝构造函数,最好写成 c(a);
  c = b; // 调用了赋值函数
  本例中第三个语句的风格较差,宜改写成String c(a) 以区别于第四个语句。

二、C++拷贝构造函数和赋值函数的形式 ,为什么是拷贝构造函数式这种形式

        对于Class A的拷贝构造函数的一般形式为A (A const &a),思考下为什么是这种形式,如果只是死记硬背记住了那没什么意思。

      首先因为拷贝构造函数是一种特殊的构造函数,既然是构造函数,函数名称必须和类名一样,且没有返回值。那为什么参数必须是引用类型呢?如果不加引用,A(A   a)会怎么样?

      如果你尝试会发现编译通过不了。为什么编译通过不了,编译器为什么会报错,试想如果你这样写

      Class A

     {

           public:

               A(A  a); // 拷贝构造函数

               A(int value)  // 一般构造函数

               {

                    val = value;

               }

               ......

          private:

           int val;

     };

    调用应该是是这样:

          A a1(7); // 调用一般构造函数

          A b(a1);// 调用拷贝构造函数

   注意:在调用拷贝构造函数时,会把a1的副本a1_bak传给形参a,那a1_bak又是如何产生的,“拷贝构造函数”,那拷贝构造函数呢?就是它本身!A(Aa)。这就是用需要调用自身的拷贝构造函数时又需要调用自身的拷贝构造函数,这样就会陷入无穷递归!所以编译器认为这样是不合法的!

     为什么要加入const,其实不加也可以,加了就是read-only,防止在拷贝构造函数中修改源对象的内容。

    赋值函数形式为:

    A & A::operator =(const A &other)

    实际是运算符的重载,为什么参数和返回值类型是引用,这里是仅仅为了提高效率,如果不是引用类型也没什么问题,仅仅效率差些。

   看String类的拷贝构造函数和赋值函数
  // 拷贝构造函数
  String::String(const String &other)
  {
  // 允许操作other 的私有成员m_data
  int length = strlen(other.m_data);
  m_data = new char[length+1];
  strcpy(m_data, other.m_data);
  }
  // 赋值函数
  String & String::operator =(const String &other)
  {
  // (1) 检查自赋值
  if(this == &other)
  return *this;
  // (2) 释放原有的内存资源
  delete [] m_data;
  // (3)分配新的内存资源,并复制内容
  int length = strlen(other.m_data);
  m_data = new char[length+1];
  strcpy(m_data, other.m_data);
  // (4)返回本对象的引用
  return *this;
  }

      类String 拷贝构造函数与普通构造函数的区别是:在函数入口处无需与NULL 进行比较,这是因为“引用”不可能是NULL,而“指针”可以为NULL。类String 的赋值函数比构造函数复杂得多,分四步实现:
  (1)第一步,检查自赋值。你可能会认为多此一举,难道有人会愚蠢到写出 a = a 这样的自赋值语句!的确不会。但是间接的自赋值仍有可能出现,例如
  // 内容自赋值
  b = a;
  …
  c = b;
  …
  a = c;
  // 地址自赋值
  b = &a;
  …
  a = *b;
  也许有人会说:“即使出现自赋值,我也可以不理睬,大不了化点时间让对象复制自己而已,反正不会出错!”他真的说错了。看看第二步的delete,自杀后还能复制自己吗?所以,如果发现自赋值,应该马上终止函数。注意不要将检查自赋值的if 语句
  if(this == &other)
  错写成为
  if( *this == other)
  (2)第二步,用delete 释放原有的内存资源。如果现在不释放,以后就没机会了,将造成内存泄露。
  (3)第三步,分配新的内存资源,并复制字符串。注意函数strlen 返回的是有效字符串长度,不包含结束符‘\0’。函数strcpy 则连‘\0’一起复制。
  (4)第四步,返回本对象的引用,目的是为了实现象 a = b = c 这样的链式表达。注意不要将 return *this 错写成 return this 。那么能否写成return other 呢?效果不是一样吗?不可以!因为我们不知道参数other 的生命期。有可能other 是个临时对象,在赋值结束后它马上消失,那么return other 返回的将是垃圾。


三、什么是默认拷贝构造函数和默认赋值函数

      默认拷贝构造函数和默认赋值函数顾名思义就是当user没有显式定义拷贝构造函数和赋值函数时,编译器隐式定义的拷贝构造函数和赋值函数。

      默认拷贝构造函数和默认赋值函数都是按照“位”拷贝来实现的,即对类中的对象所有的成员逐一拷贝,如果类中还嵌套着子类,那么拷贝时按照子类的拷贝构造函数执行(子类的默认拷贝构造函数,赋值函数或者user定义的拷贝构造函数,赋值函数)。

      这样就会有一个问题,如果类中的成员有指针,那么默认的拷贝构造函数或者默认的赋值函数将会是对指针进行拷贝,而并非对指针的内容进行拷贝。借用网上看到的一个例子:以类String 的两个对象a,b 为例,假设a.m_data 的内容为“hello”,b.m_data 的内容为“world”。现将a 赋给b,缺省赋值函数的“位拷贝”意味着执行b.m_data = a.m_data。这将造成三个错误:一是b.m_data 原有的内存没被释放,造成内存泄露;二是b.m_data 和a.m_data 指向同一块内存,a 或b 任何一方变动都会影响另一方;三是在对象被析构时,m_data 被释放了两次。

     所以对于class中的成员有指针的情况,需要自己定义拷贝构造函数和赋值函数,不能用编译器默认的拷贝构造函数和赋值函数。

四、什么是浅拷贝和深度拷贝

         所谓浅拷贝就是对Class中的成员赋值,不进行堆分配,默认拷贝构造函数属于浅拷贝,深度拷贝即对类中成员有指针的情况进行堆分配。如果对类中成员有指针的情况不采取深度拷贝,可能会出现前面说过的默认拷贝构造函数出现的问题。

五、拷贝构造函数和赋值函数使用中应该注意的问题

   

     1、默认的拷贝构造函数没有处理静态数据成员

          以下是我在网上找到的一个例子:

class Rect  

{  
public:  
     Rect()      // 构造函数,计数器加1  
     {  
          count++;  
     }  
     ~Rect()     // 析构函数,计数器减1  
     {  
            count--;  
     }  
     static int getCount()       // 返回计数器的值  
     {  
            return count;  
     }  

private:  
      int width;  
      int height;  
      static int count;       // 一静态成员做为计数器 
}; 
 
int Rect::count = 0;        // 初始化计数器 
 
int main() 

    Rect rect1; 
    cout<<"The count of Rect: "<<Rect::getCount()<<endl; 
 
    Rect rect2(rect1);   // 使用rect1复制rect2,此时应该有两个对象 
     cout<<"The count of Rect: "<<Rect::getCount()<<endl; 
 
    return 0; 

运行后两次的输出结果都是1。

修改后的class为:

class Rect 

public: 
    Rect()      // 构造函数,计数器加1 
    { 
        count++; 
    } 
    Rect(const Rect& r)   // 拷贝构造函数 
    { 
        width = r.width; 
        height = r.height; 
        count++;          // 计数器加1 
    } 
    ~Rect()     // 析构函数,计数器减1 
    { 
        count--; 
    } 
    static int getCount()   // 返回计数器的值 
    { 
        return count; 
    } 
private: 
    int width; 
    int height; 
    static int count;       // 一静态成员做为计数器 
}; 

2、防止默认拷贝发生

        一个小技巧可以防止按值传递——声明一个私有拷贝构造函数。甚至不必去定义这个拷贝构造函数,这样因为拷贝构造函数是私有的,如果用户试图按值传递或函数返回该类对象,将得到一个编译错误,从而可以避免按值传递或返回对象。

3、拷贝构造函数形式

     对于一个类X, 如果一个构造函数的第一个参数是下列之一:
a) X&
b) const X&
c) volatile X&
d) const volatile X&
且没有其他参数或其他参数都有默认值,那么这个函数是拷贝构造函数.

类中可以存在超过一个拷贝构造函数。

class X {  
public:        
  X(const X&);      // const 的拷贝构造 
  X(X&);            // 非const的拷贝构造 
}; 

如果一个类中只存在一个参数为 X& 的拷贝构造函数,那么就不能使用const X或volatile X的对象实行拷贝初始化