拷贝构造函数
定义
1.通过拷贝对象的方式创建一个新对象。作用就是用来复制对象的,再使用这个对象的实例来初始化这个对象的一个新的实例。
2.拷贝构造函数有两种原型:
book(book &b);
book(const book &b);//在创建新对象的时候不得修改被拷贝的对象
这两种原型都是 book 类对象的引用。如果拷贝构造函数的参数不是对象的引用,则是不允许的。如下面这种构造函数形式则是无法编译通过的。
3.如果不是引用,而是通过传值的方式将实参传递给形参,这中间本身就要经历一次对象的拷贝过程,而对象拷贝则必须调用拷贝构造函数,如此一来则会形成一个死循环,无解。
4.拷贝构造函数除了能用对象引用这样的参数之外,同样也能有其他参数,但是其他参数必须给出默认值。例如下面这种拷贝构造函数声明方式。
book (const book &b,price = 5.0);
5.如果类的设计人员不在类中显示的声明一个拷贝构造函数,则系统会自动为类生成一个拷贝构造函数,自动生成的拷贝构造函数功能简单,只能将源对象的所有成员变量一一复制给当前创建的对象。
示例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
例2:
#include<iostream>
using namespace std;
class Array//整形数组类
{
public:
Array(){length = 0;num = NULL;}//定义了一个默认构造函数,成员变量初始化
//声明了一个带参构造函数。
//带参构造函数则是用于将一个已有的数组全部拷贝给类对象。
Array(int *A,int n);
void setnum(int vallue,int index);
int *getaddress();
int getlength(){return length;}
void display();
private://两个成员变量:整形指针 num 和数组长度 length。
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 *Arry::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
(1)主函数中,我们先定义了一个数组,包含五个元素,分别是从 1 到 5。
(2)之后用 Array 类创建对象 arr1,并且用 A 数组初始化对象 arr1,此时 arr1 对象相当于拥有一个数组,该数组包含 5 个元素,打印出来的结果是 “1 2 3 4 5”,没有问题。
(3)之后用 arr1 对象初始化 arr2 对象,因为我们在类中没有显示地定义一个拷贝构造函数,因此系统会自动为我们生成一个拷贝构造函数,该拷贝构造函数的定义如下:
Array::Array(Array &a)
{
length = a.length;
num = a.num;
}
通过系统自动生成的拷贝构造函数完成 arr2 对象的创建,同样的 arr2 也是有 5 个元素的数组,打印出来的结果是 “1 2 3 4 5”,同样没有问题。
(4)之后我们调用成员函数 setnum,将 arr2 对象下标为 2 的元素修改为 8(原先是 3)。此时打印 arr2 中数组元素,结果为 “1 2 8 4 5”,正确,arr2 第三个元素确实被修改掉了。
(5)最后我们再调用 arr1.display (),奇怪的事情发生了,它的打印结果竟然也是 “1 2 8 4 5”!我们之前并未修改过第三个元素的值的,这是怎么一回事呢?不急,我们再来看一下最后一句 “cout<<arr1.getaddress ()<<" "<<arr2.getaddress ()<<endl;” 其显示结果竟然是一样的!看到这里是不是有些明白了上面的问题呢?很明显,arr1 和 arr2 所指向的数组是同一个数组,在内存中的位置是一致的,因此当我们利用对象 arr2 去修改数组中第三个元素的数值的时候,arr1 中的数组也被修改了,其实它们本来就是使用的是同一个内存中的数组而已.
(6)这问题是怎么产生的呢?不难想到拷贝构造函数参数为引用,系统自动生成的拷贝构造函数功能简单,只是将 arr1 的数组首地址直接赋值给 arr2 的数组首地址,也即 num = a.num; 这必然导致两个对象指向同一块内存。
既然问题出在系统自动生成的拷贝构造函数上,自然要从拷贝构造函数上下手了。下面我们将正确的程序展示如例 3。
示例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();
Arry 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 中自己定义了一个拷贝构造函数,并且开辟了一个新的空间用于存储数据。如此一来当然是不会有问题的了。本例中所介绍的是一个非常微妙的错误,在程序设计过程中,一定要加以避免。
总结
1.深拷贝和浅拷贝,区别及应用场景
当出现类的等号赋值时,会调用拷贝函数,在未定义显示拷贝构造函数的情况下,系统会调用默认的拷贝函数——即浅拷贝,它能够完成成员的一一复制。当数据成员中没有指针时,浅拷贝是可行的。但当数据成员中有指针时,如果采用简单的浅拷贝,则两类中的两个指针将指向同一个地址,当对象快结束时,会调用两次析构函数,而导致指针悬挂现象。所以,这时,必须采用深拷贝。
深拷贝与浅拷贝的区别就在于深拷贝会在堆内存中另外申请空间来储存数据,从而也就解决了指针悬挂的问题。简而言之,当数据成员中有指针时,必须要用深拷贝。
2.为什么拷贝构造函数必须是引用传递,不能是值传递?
简单的回答是为了防止递归引用。
具体一些可以这么讲:
当一个对象需要以值方式传递时,编译器会生成代码调用它的拷贝构造函数以生成一个复本。如果类 A 的拷贝构造函数是以值方式传递一个类 A 对象作为参数的话,当需要调用类 A 的拷贝构造函数时,需要以值方式传进一个 A 的对象作为实参; 而以值方式传递需要调用类 A 的拷贝构造函数;结果就是调用类 A 的拷贝构造函数导致又一次调用类 A 的拷贝构造函数,这就是一个无限递归。
3.拷贝构造函数里能调用 private 成员变量吗?
拷贝构造函数其时就是一个特殊的构造函数,操作的还是自己类的成员变量,所以不受 private 的限制。
————————————————
版权声明:本文参考CSDN博主「踏实IT精英」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_43700340/article/details/89308144