C++拷贝构造函数

顾名思义,拷贝构造函数就是通过拷贝对象的方式创建一个新对象。拷贝构造函数有两种原型(我们继续以 book 类来说明拷贝构造函数原型):
book(book &b);
book(const book &b);
这两种原型都是 book 类对象的引用。下面一种原型则规定在创建新对象的时候不得修改被拷贝的对象。如果拷贝构造函数的参数不是对象的引用,则是不允许的。如下面这种构造函数形式则是无法编译通过的。
book(book b);
为什么拷贝构造函数的参数一定要是对象的引用呢?我们可以想一下,如果不是引用,而是通过传值的方式将实参传递给形参,这中间本身就要经历一次对象的拷贝的过程,而对象拷贝则必须调用拷贝构造函数,如此一来则会形成一个死循环,无解。所以拷贝构造函数的参数必须是对象的引用。

拷贝构造函数除了能有对象引用这样的参数之外,同样也能有其它参数。但是其它参数必须给出默认值。例如下面这种拷贝构造函数声明方式。
book(const book &b, price = 5.0);
如果类的设计人员不在类中显示的声明一个拷贝构造函数,则系统会自动地为类生成一个拷贝构造函数,自动生成的拷贝构造函数功能简单,只能将源对象的所有成员变量一一复制给当前创建的对象。

【例 1】
class book
{
public:
    book(){}
    book(book &b);
    book(char* a, double p = 5.0);
    void display();
private:
    double price;
    char * title;
};

book::book(book &b)
{
    price = b.price;
    title = b.title;
}

book::book(char* a, double p)
{
    title = a;
    price = p;
}

void book::display()
{
    cout<<"The price of "<<title<<" is $"<<price<<endl;
}
在本例中的 book 类中就声明了book(book &b);拷贝构造函数,当然这个拷贝构造函数跟系统默认生成的拷贝构造函数功能是一样的,也就只是实现了数据成员的对应拷贝功能。

了解了拷贝构造函数的声明及定义方式,我们再来看一下,设计类的时候什么时候才需要设计拷贝构造函数?先来看下面一个例子,相信看完之后会有一定领会,之后再来揭晓答案。

【例 2】
#include<iostream>
using namespace std;


class Array
{
public:
    Array(){length = 0; num = NULL;};
    Array(int * A, int n);
    void setnum(int value, int index);
    int * getaddress();
        int getlength(){return length;}
    void display();
private:
    int length;
    int * num;
};

Array::Array(int *A, int n)
{
    num = new int[n];
    length = n;
    for(int i=0; i<n; i++)
        num[i] = A[i];
}

void Array::setnum(int value, int index)
{
    if(index < length)
        num[index] = value;
    else
        cout<<"index out of range!"<<endl;
}

void Array::display()
{
    for(int i=0; i<length; i++)
        cout<<num[i]<<" ";
    cout<<endl;
}

int * Array::getaddress()
{
    return num;
}

int main()
{
    int A[5] = {1,2,3,4,5};
    Array arr1(A, 5);
    arr1.display();
    Array arr2(arr1);
    arr2.display();
    arr2.setnum(8,2);
    arr2.display();
    arr1.display();
    cout<<arr1.getaddress()<<" "<<arr2.getaddress()<<endl;
    return 0;
}
程序运行结果如下:

1 2 3 4 5
1 2 3 4 5
1 2 8 4 5
1 2 8 4 5
00331F58 00331F58

在本例中,我们重新定义了一个 Array 类,可以理解为一个整形数组类,这个类中我们定义了两个成员变量:整形指针 num 和数组长度 length。

类中定义了一个默认构造函数,声明了一个带参构造函数。默认构造函数很简单,带参构造函数则是用于将一个已有的数组全部拷贝给类对象。

除了两个构造函数之外,我们还定义四个成员函数,一个是用于修改数组中数值的 setnum() 函数、一个打印数组中所有元素的 display() 函数、一个返回数组首地址的函数 getaddress() 和一个返回数组长度的函数 getlength()。除了默认构造函数之外和 getlength() 函数之外,所有的函数在类外都有定义。

接下来我们看一下主函数。主函数中,我们先定义了一个数组,包含五个元素,分别是从 1 到 5。之后用 Array 类创建对象 arr1,并且用 A 数组初始化对象 arr1,此时 arr1 对象相当于拥有一个数组,该数组包含 5 个元素,打印出来的结果是1 2 3 4 5 ,没有问题。之后用 arr1 对象初始化 arr2 对象,因为我们在类中没有显示地定义一个拷贝构造函数,因此系统会自动为我们生成一个拷贝构造函数,该拷贝构造函数的定义如下:
Array::Array(Array &a)
{
    length = a.length;
    num = a.num;
}
通过系统自动生成的拷贝构造函数完成 arr2 对象的创建,同样的 arr2 也是有 5 个元素的数组,打印出来的结果是1 2 3 4 5,同样没有问题。

之后我们调用成员函数 setnum(),将 arr2 对象下标为 2 的元素修改为 8(原先是 3)。此时打印 arr2 中数组元素,结果为1 2 8 4 5 ,正确,arr2 第三个元素确实被修改掉了。

之后我们再调用 arr1.display(),奇怪的事情发生了,它的打印结果竟然也是1 2 8 4 5!我们之前并未修改过第三个元素的值的,这是怎么一回事呢?不急,我们再来看一下最后一句cout<<arr1.getaddress()<<" "<<arr2.getaddress()<<endl;其显示结果竟然是一样的!看到这里是不是有些明白了上面的问题呢?

很明显,arr1 和 arr2 所指向的数组是同一个数组,在内存中的位置是一致的,因此当我们利用对象 arr2 去修改数组中第三个元素的数值的时候,arr1 中的数组也被修改了,其实它们本来就是使用的是同一个内存中的数组而已。

这问题是怎么产生的呢?不难想到拷贝构造函数参数为引用,系统自动生成的拷贝构造函数功能简单,只是将 arr1 的数组首地址直接赋值给 arr2 的数组首地址,也即 num = a.num;这必然导致两个对象指向同一块内存。既然问题出在系统自动生成的拷贝构造函数上,自然要从拷贝构造函数上下手了。下面我们将正确的程序展示如例 3。

【例 3】
#include<iostream>
using namespace std;

class Array
{
public:
    Array(){length = 0; num = NULL;};
    Array(int * A, int n);
    Array(Array &a);
    void setnum(int value, int index);
    int * getaddress();
    void display();
    int getlength(){return length;}
private:
    int length;
    int * num;
};

Array::Array(Array & a)
{
    if(a.num != NULL)
    {
        length = a.length;
        num = new int[length];
        for(int i=0; i<length; i++)
            num[i] = a.num[i];
    }
    else
    {
        length = 0;
        num = 0;
    }  
}

Array::Array(int *A, int n)
{
    num = new int[n];
    length = n;
    for(int i=0; i<n; i++)
        num[i] = A[i];
}

void Array::setnum(int value, int index)
{
    if(index < length)
        num[index] = value;
    else
        cout<<"index out of range!"<<endl;
}

void Array::display()
{
    for(int i=0; i<length; i++)
        cout<<num[i]<<" ";
    cout<<endl;
}

int * Array::getaddress()
{
    return num;
}

int main()
{
    int A[5] = {1,2,3,4,5};
    Array arr1(A, 5);
    arr1.display();
    Array arr2(arr1);
    arr2.display();
    arr2.setnum(8,2);
    arr2.display();
    arr1.display();
    cout<<arr1.getaddress()<<" "<<arr2.getaddress()<<endl;
    return 0;
}
程序运行结果如下所示:

1 2 3 4 5
1 2 3 4 5
1 2 8 4 5
1 2 3 4 5
00311F58 00487268

看例 3 运行结果,如此一来,程序运行结果正确,而且两个对象 arr1 和 arr2 所指向的内存空间也是不一样的。我们在例 3 中自己定义了一个拷贝构造函数,并且开辟了一个新的空间用于存储数据。如此一来当然是不会有问题的了。本例中所介绍的是一个非常微妙的错误,在程序设计过程中,一定要加以避免。

从这个例子中我们是不是领会到了什么呢?通常,如果一个类中包含指向动态分配存储空间的指针类型的成员变量时,就应该为这个类设计一个拷贝构造函数,除了需要设计一个拷贝构造函数之外,还需要为它添加一个赋值操作符重载函数(即重载“=”操作符,这将会在操作符重载那一章加以介绍)。

由于类会自动生成拷贝构造函数,因此有些时候为了不让对象发生拷贝行为,我们可以显示声明一个拷贝构造函数,并将其设置为 private 属性。这跟通过将默认构造函数设置成 private 属性限制对象的创建时一样的道理。当然,禁止对象发生拷贝的需求较少,如果有这样的需求的话,知道还可以这么做就足够了,这是一个类设计的技巧。