拷贝构造函数

拷贝构造函数是构造函数的一种, 也称复制构造函数, 只有一个参数, 参数类型是该类的引用.
[拷贝构造函数的参数可以是 const 引用,也可以是非 const 引用。 一般使用前者,这样既能以常量对象(初始化后值不能改变的对象)作为参数,也能以非常量对象作为参数去初始化其他对象。一个类中可以同时存在这两种类型的拷贝构造函数(一个的参数是 const 引用,另一个的参数是非 const 引用)]。

拷贝构造函数的作用:

作用是使用类中已经创建好的对象来初始化新创建的对象

拷贝构造函数的注意要点:

参数拷贝构造函数的参数必须是引用传递, 不能是值传递
当一个对象需要以值方式传递时,编译器会生成代码调用它的拷贝构造函数以生成一个复本。如果类A的拷贝构造函数是以值方式传递一个类A对象作为参数的话,当 需要调用类A的拷贝构造函数时,需要以值方式传进一个A的对象作为实参; 而以值方式传递需要调用类A的拷贝构造函数;结果就是调用类A的拷贝构造函数导 致又一次调用类A的拷贝构造函数,这就是一个无限递归

默认拷贝构造函数

如果类的设计者不写拷贝构造函数,编译器就会自动生成拷贝构造函数。大多数情况下,其作用是实现从源对象到目标对象逐个字节的复制,即使得目标对象的每个成员变量都变得和源对象相等。编译器自动生成的复制构造函数称为“默认拷贝构造函数”。

拷贝构造函数被调用的三种情况:

  1. 用类的一个对象去初始化另一个对象时
  2. 当函数的形参是类的对象时
  3. 当函数的返回值是类的对象或者引用时

1.用类的一个对象去初始化另一个对象时(通过一个同类型的对象来初始化新创建的对象)

Date d2(d1); // 初始化对象 d2
Date d3 = d1; // 初始化对象 d3
//这两条语句都会调用拷贝构造函数, 这两行代码的语句作用是等价的

2.当函数的形参是类的对象时(作为形参的对象, 是用拷贝构造函数初始化的, 这里指值传递,如果是引用传递则不会调用)

#include<iostream>
class A{
  public:
  A(){};
  A(const A & a){
      std::cout << "使用拷贝构造函数初始化参数" << std::endl;
  }  
};
void Test(A a){}
int main(){
    A a;
    Test(a);
    return 0;
}

程序输出结果:

使用拷贝构造函数初始化参数

这里函数 Test 的形参a在初始化时调用了拷贝构造函数

3.当函数的返回值是类的对象或者引用时(作为函数返回值的对象是用拷贝构造函数初始化的, 调用拷贝构造函数时的实参, 就是return语句返回的对象

#include<iostream>
class A {
public:
    A(int a = 0) :_a(a){};
    A(const A& a) {
        _a = a._a;
        std::cout << "拷贝构造函数" << std::endl;
    }
    int _a;
};
A Test() {
    A a(2);
    return a;
}
int main() {
    auto tmp = Test();
    std::cout << tmp._a << std::endl;
    return 0;
}

程序输出结果是:

拷贝构造函数
2

第16行调用了 Test 函数,其返回值是一个对象,该对象就是用复制构造函数初始化的, 而且调用拷贝构造函数时,实参就是第 13 行 return 语句所返回的 a。拷贝构造函数在第 6 行确实完成了复制的工作,所以第 16行 Test函数的返回值和第 12 行的 a 相等。

在什么情况下需要用户自己定义拷贝构造函数:

一般情况下,当类中成员有指针变量、类中有动态内存分配时常常需要用户自己定义拷贝构造函数。
当出现类的等号赋值时,会调用拷贝函数,在未定义显示拷贝构造函数的情况下,系统会调用默认的拷贝函数——即浅拷贝,它能够完成成员的对应复制。但当数据成员中有指针时,如果采用简单的浅拷贝,则两类中的两个指针将指向同一个地址,当对象快结束时,会调用两次析构函数,而导致指针悬挂现象。所以,这时,必须采用深拷贝。 深拷贝与浅拷贝的区别就在于深拷贝会在堆内存中另外申请空间来储存数据,从而也就解决了指针悬挂的问题。简而言之,当数据成员中有指针时,必须要用深拷贝。
如果不显式声明拷贝构造函数的时候,编译器也会生成一个默认的拷贝构造函数,而且在一般的情况下运行的也很好。但是在遇到指针数据成员时就出现问题 了:因为默认的拷贝构造函数是按成员拷贝构造,这导致了两个不同的指针(如ptr1=ptr2)指向了相同的内存。当一个实例销毁时,调用析构函数 free(ptr1)释放了这段内存,那么剩下的一个实例的指针ptr2就无效了,在被销毁的时候free(ptr2)就会出现错误了, 这相当于重复释放一块内存两次。这种情况必须显式声明并实现自己的拷贝构造函数,来为新的实例的指针分配新的内存。