菜鸟笔记
提升您的技术认知

拷贝构造函数总结

0. 什么是拷贝构造函数

拷贝构造函数用于将一个对象复制到一个新创建的对象中。也就是说,它用在初始化过程中,而不是常规的赋值操作中。类的拷贝构造函数的原型如下:

Class_name(const Class_name&);

这个构造函数接受一个指向类对象的常量引用作为参数。如果没有定义,c++会提供一个默认的拷贝构造函数,不过这个默认的拷贝构造函数有一些坑需要注意(见后面的例子)。编译器新建一个对象,并将其初始化为同类的现有对象时,都会调用拷贝构造函数。最常见的情况就是将新对象显式地初始化为现有的对象。例如,假如有一个定义好的类Man和它的一个实例a,那么下面四种声明都将调用拷贝构造函数。

Man b(a);
Man b = a;
Man b = Man(a);
Man* b = new Man(a);

另外,每当程序生成对象副本时,编译器也会使用拷贝构造函数。具体来说,就是当函数按值传递对象或者函数返回对象时,拷贝构造函数会被调用。下面看几个例子:

例1

#include<iostream>
#include<cstring>
using namespace std;
class Man {
    char* name;
    int age;
    public:
        Man(char* name, int age);
        Man();
        void show();
};

Man::Man(char* name, int age)
{
    cout<<"call self-def Constructor"<<endl;
    this->name = new char[strlen(name) + 1];
    strcpy(this->name, name);
    this->age = age;
}
Man::Man()
{
    cout<<"call default Constructor"<<endl;
    this->name = new char[8];
    strcpy(this->name, "Unknown");
    age = -1;
}
void Man::show()
{
    cout<<"name:"<<name<<endl;
    cout<<"age:"<<age<<endl;
    printf("str = %p\n",name);
}
int main()
{
    Man a;  //调用默认构造函数
    Man b(a);//调用默认拷贝构造函数
    Man c = Man((char*)"zhengkang", 26);//调用带参数的构造函数
    Man d = c;//调用默认拷贝构造函数
    a.show();
    b.show();
    cout<<"-----"<<endl;
    c.show();
    d.show();
    return 0;
}

程序很简单,不解释,直接看运行结果:

程序定义了4个Man对象。a使用默认构造函数,b拷贝了a的内容,c使用带参数的构造函数,d拷贝了c的内容。可以看到,a和b的成员变量值是一模一样的,c和d的成员变量值也是一模一样的。a的name指针和b的name指向同一片内存地址。c,d同理。
这就是默认拷贝构造函数的作用,默认拷贝构造函数逐个复制非静态成员(成员复制也称为浅拷贝),复制的是成员的值

1. 默认拷贝构造函数(浅拷贝)带来的问题

对于含有指针成员的类来说,c++提供的默认拷贝构造函数是存在问题的。如上例所示,a和b的name指针指向同一块内存。修改任意一个对象的指针指向的内存区,另外一个对象的内容也会跟着变化。如果a的name指针指向的内存片被释放掉,此时b也不能访问这片内存,b的name成为一个野指针。b被销毁时,会试图释放b的name指向的内存片,造成同一个内存片被释放两次,有可能导致程序异常终止。

例2

我们把main函数修改一下,看一下运行结果

int main()
{
    Man* a = new Man((char*)"zhengkang", 26);
    Man* b = new Man(*a);
    a->show();
    delete a;
    b->show();
    delete b;
}

程序运行结果如下:

咦?程序正常运行,并没有崩溃,也没有打印出乱码,貌似一切正常。为什么会这样呢?
原因也简单,因为我们没有提供析构函数。当调用delete a;时,a的成员变量age和name被销毁,注意,name被销毁,但是name指向的内存片并没有被释放。因此调用b->show()照样能打印出正确的结果。c++提供的默认析构函数并不能释放通过由new申请的指针指向的内存。我们来提供一个析构函数。

例3

定义一个析构函数

Man::~Man()
{
    cout<<"call Destructor"<<endl;
    delete[] name;
}

程序运行结果如下:

调用delete a;之后会调用a的析构函数,释放掉a的name指向的内存片。打印b的name显示乱码。顺便看一下delete a;执行前后,a和*a的变化

2.定义一个显式的拷贝构造函数(深度拷贝)

通过上面的几个例子,可以看出。c++提供的隐式拷贝构造函数只能完成浅拷贝,浅拷贝带来的问题上面已经讲过。解决这些问题的方法是进行深度拷贝。也就是说,拷贝构造函数应当复制name指针指向的字符串并将副本的地址复制给新对象的name成员,而不仅仅是复制字符串地址。这样每个对象都有自己的字符串,互不干扰,而不是引用另一个对象的字符串。调用析构函数时,各自释放自己的字符串,而不是试图释放别的对象可能已经被释放的字符串。定义一个这样的显式拷贝构造函数,在这个函数中,重新申请内存,复制内存块的地址到这块内存中,并把内存地址复制给新对象的指针成员。

Man::Man(const Man & m)
{
    cout<<"call copy Constructor"<<endl;
    this->age = m.age;
    this->name = new char[strlen(m.name) + 1];
    strcpy(this->name, m.name);
}

再次执行,结果如下:

从运行结果可以看出,a的name和b的name指向不同的内存地址,delete a不会影响到b.