复制构造函数是什么

1
2
3
4
5
6
7
A(const A &rhs){
  ***函数体***
}

A b;
A a = b;//调用复制构造函数用b来初始化对象a
A c(b);//调用复制构造函数用b来初始化对象c

复制构造函数是构造函数的一个重载形式,其作用是用一个对象来初始化另一个对象,其特点在于无返回值,参数为本类型的一个对象的引用,复制构造前有一个对象,复制构造后有两个对象。

为什么要有复制构造函数

在C语言中,基本类型的对象之间的复制是很简单的比如说

1
2
int a = 5;
int b = a;

但是在c++的自定义类中,里面有复杂的成员变量,基本类型的对象复制是不够用的。

1
2
3
4
5
6
7
8
9
10
class A{
  private:
    int a;
    char s[20];
  public:
    A(const A& rhs){
        a = rhs.a;
        strcpy(s,rhs.s);
    }
}

可以看到,在复制一个对象时,并不是简单的赋值操作,可能还有其他更为复杂的操作,因此,必须要有复制构造函数这种机制

什么时候需要调用复制构造函数

1.对象需要通过另外一个对象进行初始化;

1
2
3
CSet B;
CSet A = B;
CSet C(B);

2.对象以传值的方式传入函数参数

1
2
3
4
5
void Fun(CSet tmp){
  ***函数体***
}
CSet B;
Fun(B);

在参数传递的时候发生了这么些事情

1.在调用Fun时,先产生一个局部对象tmp

2.调用复制构造函数,用B初始化tmp

3.在函数执行后,析构tmp对象

3.对象以值传递的方式从函数返回

1
2
3
4
CSet Fun(){
  CSet tmp;
  return tmp;
}

函数在执行到return语句时,会调用复制构造函数创建一个tmp的副本(一个无名对象),析构掉tmp对象,返回tmp副本,待函数执行完析构tmp副本。

深拷贝与浅拷贝

默认复制构造函数及其危险性

如没有定义复制构造函数,编译器将提供一个默认复制构造函数 ,它采用的是将源对象的所有数据成员的值逐一赋值给目标对象的相应的数据成员。

  • 默认复制构造函数提供的只是浅拷贝 ,也就是简单地将源对象的数据成员的值赋给目标对象的数据成员,那么这样会出什么问题呢

  • 假如有一个成员变量的指针,char *m_data;

    其一,浅拷贝只是拷贝了指针,使得两个指针指向同一个地址,这样在对象块结束,调用函数析构的时,会造成同一份资源析构2次,即delete同一块内存2次,造成程序崩溃。

    其二,浅拷贝使得obj.m_data和obj1.m_data指向同一块内存,任何一方的变动都会影响到另一方。

  • 因此,在涉及动态分配的操作上,必须自定义复制构造函数

自定义复制构造函数与深拷贝

1
2
3
4
5
6
7
8
9
10
11
12
class String{  
  public:  
  String(const String &other);    //拷贝构造函数  
  private:  
  char *m_data;   //用于保存字符串  
};    

String::String(const String &other){     
  int length = strlen(other.m_data);  
  m_data = new char[length + 1];  
  strcpy(m_data, other.m_data);  
}

可以看到在拷贝构造函数中为成员变量申请了新的内存空间,这就使得两个对象的成员变量不指向同一个内存空间

经典的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*一个导致程序崩溃的例子*/
class A {
    int i;
};

class B {
    A *p;
public:
    B() { p = new A; }
    ~B(){ delete p; }
};

void sayHello(B b) {

}

int main()
{
    B b;
    sayHello(b);
}

乍一看并没有什么问题,因为sayhello函数只完成了一个传值,但细细想来,由于涉及了动态分配 而并没有写复制构造函数,所以就会出现一个大问题。sayhello函数执行后把p所指的内存区域析构了一遍,main函数执行完后又要去析构,导致delete同一片区域两次!!!!

复制构造函数为什么参数只能是引用

在写复制构造函数的时候,为什么参数只能是引用而不能为值传递??

结论:为了防止拷贝构造函数的无限递归,最终导致栈溢出。

下面我们来探讨为什么值传递会发生无限递归

如果复制构造函数是这样的 :

1
test(test t);
1
2
3
4
5
6
7
8
//我们调用
test ort;
test a(ort); 
-->test.a(test t = ort) == test.a(test t(ort))     
-->test.a(test t(test t = ort)) == test.a(test t(test t(ort)))             
-->test.a(test t(test t(test t = ort)))              
...    
//就这样会一直无限递归下去。

到这里,我们也就明白了,为什么拷贝构造函数的参数一定要为引用,不能为值传递的原因了。