1.概念
拷贝构造函数:拷贝构造函数也是特殊的成员函数,只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
2.特征
1. 拷贝构造函数是构造函数的一个重载形式。
2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,
因为会引发无穷递归调用。
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(d1);
return 0;
}
对于上面的这段代码,d2拷贝构造d1是能正常编译通过的,而一旦换成特征2所说的传值方式进行拷贝构造,也就是将11行的代码改写成Date(Date d),便会发生下面的错误:
图1:
究其原因便是引发了无穷递归调用。
具体无穷调用如图2:
图2:
不加引用引起的无穷递归本质上便是由于传值时,形参会形成对实参的拷贝,然后不断递归,所以我们通过使用引用便能很好的规避这一问题,此时引用实质将d作为了d1的别名,便不存在传值时形参拷贝实参的情况。(添加const可以防止在拷贝过程中误操作改变d1)
3. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按
字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
这种浅拷贝会存在一种问题,由于浅拷贝对于基本内置类型(int,double,char等)直接拷贝值,而对于引用类型(数组等)将会拷贝它的内存地址,此时便会造成一个问题,因为拷贝后的对象和拷贝前的对象两者里的引用类型都指向一个相同的内存地址,那么对其中一个引用进行修改势必会影响到另一个,而这也是我们不愿意看到的。
来看下面这段代码及其运行结果:
typedef int DataType;
class Stack //创建一个栈类
{
public:
Stack(size_t capacity = 10)
{ //动态开辟空间
_array = (DataType*)malloc(capacity * sizeof(DataType));
if (nullptr == _array)
{
perror("malloc fail");
return;
}
_size = 0;
_capacity = capacity;
}
void Push(const DataType& data) //插入函数
{
_array[_size] = data;
_size++;
}
~Stack() //显式构造析构函数
{
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
size_t _size;
size_t _capacity;
};
int main()
{
Stack s1;
s1.Push(1); //在栈上push入值
s1.Push(2);
s1.Push(3);
s1.Push(4);
Stack s2(s1); //s2拷贝s1
return 0;
}
图3:
运行后我们发现程序崩溃了!!!这便是因为我们自己并未显式的创建拷贝构造函数,而编译器默认生成的拷贝构造函数对于引用类型的处理,会造成两者指向同一内存地址,注意!!!当程序结束要调用析构函数时,首先对s2进行析构处理,紧接着再对s1进行处理,注意!!!注意!!!注意!!!,此时两者指向的同一块空间,如果s2已经对这块空间处理完了,接下来s1再次对此空间进行处理便会引发错误!!!而这也是引发程序崩溃的原因。
图4:
因此倘若对象内存在引用类型拷贝,便需要我们手动的来写拷贝构造函数来实现。