首页 > 编程笔记

C++拷贝构造函数(深拷贝+浅拷贝)

在程序中,经常使用已有对象完成新对象的初始化。例如,在定义变量 inta=3 后,再定义新变量 intb=a。在类中,需要定义拷贝构造函数才能完成这样的功能。接下来,本文将针对拷贝构造函数进行详细讲解。

拷贝构造函数的定义

拷贝构造函数是一种特殊的构造函数,它具有构造函数的所有特性,并且使用本类对象的引用作为形参,能够通过一个已经存在的对象初始化该类的另一个对象。

拷贝构造函数的定义格式如下所示:

class 类名
{
public:
    构造函数名称(const 类名& 对象名)
    {
        函数体
    }
    ...  //其他成员
};

在定义拷贝构造函数时,为了使引用的对象不被修改,通常使用 const 修饰引用的对象。

【示例1】下面通过案例演示拷贝构造函数的定义与调用,C++ 代码如下:
#include<iostream>
using namespace std;
class Sheep       //定义绵羊类Sheep
{
public:
    Sheep(string name,string color);   //声明有参构造函数
    Sheep(const Sheep& another);    //声明拷贝构造函数
    void show();       //声明普通成员函数
    ~Sheep();        //声明析构函数
private:
    string _name;       //声明表示绵羊名字的成员变量
    string _color;      //声明表示绵羊颜色的成员变量
};
Sheep::Sheep(string name, string color)
{
    cout<<"调用构造函数"<<endl;
    _name=name;
    _color=color;
}
Sheep::Sheep(const Sheep& another)  //类外实现拷贝构造函数
{
    cout<<"调用拷贝构造函数"<<endl;
    _name=another._name;
    _color=another._color;
}
void Sheep::show()
{
    cout<<_name<<" "<<_color<<endl;
}
Sheep::~Sheep()
{
    cout<<"调用析构函数"<<endl;
}

int main()
{
    Sheep sheepA("Doly","white");
    cout<<"sheepA:";
    sheepA.show();
    Sheep sheepB(sheepA);    //使用sheepA初始化新对象sheepB
    cout<<"sheepB:";
    sheepB.show();
    return 0;
}
运行结果:

调用构造函数
sheepA:Doly white
调用拷贝构造函数
sheepB:Doly white
调用析构函数
调用析构函数

示例分析:
由运行结果可知,对象 sheepA 与对象 sheepB 的信息是相同的。

程序首先调用构造函数创建了对象 sheepA,然后调用拷贝构造函数创建了对象 sheepB。程序运行结束之后,调用析构函数先析构对象 sheepB,然后析构对象 sheepA。

当涉及对象之间的赋值时,编译器会自动调用拷贝构造函数。拷贝构造函数的调用情况有以下三种。

浅拷贝

拷贝构造函数是特殊的构造函数,如果程序没有定义拷贝构造函数,C++ 会提供一个默认的拷贝构造函数,默认拷贝构造函数只能完成简单的赋值操作,无法完成含有堆内存成员数据的拷贝。

例如,如果类中有指针类型的数据,默认的拷贝构造函数只是进行简单的指针赋值,即将新对象的指针成员指向原有对象的指针指向的内存空间,并没有为新对象的指针成员申请新空间,这种情况称为浅拷贝。

浅拷贝在析构指向堆内存空间的变量时,往往会出现多次析构而导致程序错误。C++ 初学者自定义的拷贝构造函数往往实现的是浅拷贝。

【示例2】下面通过案例演示浅拷贝,C++ 代码如下:
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<string.h>
using namespace std;
class Sheep           //定义绵羊类Sheep
{
public:
    Sheep(string name,string color,const char* home);  //声明有参构造函数
    Sheep(const Sheep& another);        //声明拷贝构造函数
    void show();          //声明普通成员函数
    ~Sheep();           //声明析构函数
private:
    string _name;          //声明表示绵羊名字的成员变量
    string _color;          //声明表示绵羊颜色的成员变量
    char* _home;          //声明表示绵羊家的成员变量
};
Sheep::Sheep(string name, string color,const char* home)
{
    cout<<"调用构造函数"<<endl;
    _name=name;
    _color=color;
    //为指针成员home分配空间,将形参home的内容复制到_home指向的空间
    int len=strlen(home)+1;
    _home=new char[len];
    memset(_home,0,len);
    strcpy(_home,home);
}
Sheep::Sheep(const Sheep& another)  //类外实现拷贝构造函数
{
    cout<<"调用拷贝构造函数"<<endl;
    _name=another._name;
    _color=another._color;
    _home=another._home;     //浅拷贝
}
void Sheep::show()
{
    cout<<_name<<" "<<_color<<" "<<_home<<endl;
}
Sheep::~Sheep()
{
    cout<<"调用析构函数"<<endl;
    delete []_home;
    _home!=NULL)
}
int main()
{
    const char *p = "beijing";
    Sheep sheepA("Doly","white",p);
    cout<<"sheepA:";
    sheepA.show();
    Sheep sheepB(sheepA);    //使用sheepA初始化新对象sheepB
    cout<<"sheepB:";
    sheepB.show();
    return 0;
}
运行程序抛出异常,在第 43 行代码处触发异常断点,如图 1 所示。

【示例2】是对【示例1】的修改,在绵羊类 Sheep 中增加了一个 char 类型的指针变量成员 _home,用于表示绵羊对象的家。增加了 _home 成员变量之后,类 Sheep 的构造函数、拷贝构造函数、析构函数都进行了相应修改。

示例分析:
在这个过程中,使用对象 sheepA 初始化对象 sheepB 是浅拷贝过程,因为对象 sheepB 的 _home 指针指向的是对象 sheepA 的 _home 指针指向的空间。

浅拷贝过程如图 2 所示。

图1   触发异常断电
图1 触发异常断点


图2   浅拷贝过程
图2 浅拷贝过程

由图 2 可知,在浅拷贝过程中,对象 sheepA 中的 _home 指针与对象 sheepB 中的 _home 指针指向同一块内存空间。

当程序运行结束时,析构函数释放对象所占用资源,析构函数先析构对象 sheepB,后析构对象 sheepA。

在析构 sheepB 对象时释放了 _home 指向的堆内存空间的数据,当析构 sheepA 时 _home 指向的堆内存空间已经释放,再次释放内存空间的资源,程序运行异常终止,即存储“beijing”的堆内存空间被释放了两次,因此程序抛出异常,这种现象被称重析构(double free)。

深拷贝

所谓深拷贝,就是在拷贝构造函数中完成更深层次的复制,当类中有指针成员时,深拷贝可以为新对象的指针分配一块内存空间,将数据复制到新空间。

例如,在【示例1】中,使用对象 sheepA 初始化对象 sheepB 时,为对象 sheepB 的指针 _home 申请一块新的内存空间,将数据复制到这块新的内存空间。

下面修改【示例2】中的拷贝构造函数,实现深拷贝过程。修改后的拷贝构造函数代码如下所示:
Sheep::Sheep(const Sheep& another)    //类外实现拷贝构造函数
{
    cout<<"调用拷贝构造函数"<<endl;
    _name=another._name;
    _color=another._color;
    //完成深拷贝
    int len = strlen(another._home)+1;
    _home=new char[len];
    strcpy(_home,another._home);
}
拷贝构造函数修改之后,再次运行程序,程序不再抛出异常。

在深拷贝过程中,对象 sheepB 中的 _home 指针指向了独立的内存空间,是一份完整的对象拷贝,如图 3 所示。 

图3 深拷贝过程
图3 深拷贝过程

由图 3 可知,对象 sheepA 中的 _home 指针与对象 sheepB 中的 _home 指针指向不同的内存空间,在析构时,析构各自对象所占用的资源不会再产生冲突。

优秀文章