📖定义
拷贝构造函数是构造函数的一个重载,它的本质还是构造函数,那就意味着,只有在创建对象的时候,编译器才会自动调用它,那他和普通的构造函数有什么区别呢?
拷贝构造函数,是创建对象的时候,用一个已存在的对象,去初始化待创建的对象。简单来说,就是在我们创建对象的时候,希望创建出来的对象,和一个已存在的对象一模一样,此时就应该用拷贝构造函数,而不是普通的构造函数。拷贝构造函数有一点类似于克隆技术。
Data d1(2023, 7, 20);//定义一个日期类对象d1
Data d2(d1);//会去调用拷贝构造函数
int a = 10;
int b = a;//不会调用拷贝构造
上面代码,首先定义了一个日期类对象d1
,接着想创建第二个日期类对象d2
,并且希望d2
和d1
一模一样,也就是用d1
去克隆出d2
,d2
相当于是d1
的一份拷贝。所以在创建d2
对象的时候,参数列表直接传递了d1
。
小Tips:拷贝构造函数是针对自定义类型的,自定义类型的对象在拷贝的时候,C++规定必须要调用拷贝构造函数。内置类型不涉及拷贝构造函数,如上,用a
去创建b
,是由编译器直接把a
所表示的空间中的内容直接拷贝到b
所表示的空间,并不涉及拷贝构造函数。
📖拷贝构造函数的错误写法
有了上面的分析,可能很多朋友会觉得,那我直接在类里面再写一个构造函数,把它的形参设置成日期类对象,不就行了嘛,于是便得到了下面的代码:
Data(Data d)//错误的拷贝构造
{
_year = d._year;
_month = d._month;
_day = d._day;
}
是不是觉得很简单?创建d2
对象的时候,实参把d1
传过来,然后用d
接收,最后再把d
的所有值赋值给this
指针(当前this指针就指向d2
),这一切堪称完美,但是我想告诉你,这种写法是大错特错的。
📖为什么是错的
问题出现在传参,就是实参d1
传递给形参d
的时候,上面代码中的形参d
,既不是指针也不是引用,说明是值传递,值传递就意味着,形参d
是实参d1
的一份拷贝,注意:是拷贝,就是说,形参d
要和实参d1
一模一样,怎么才能让d
和d1
一摸一样?调用拷贝构造函数呀。
形参d
在接收实参d1
的时候,又要去调用拷贝构造来创建d
,这次调用拷贝构造,又会有一个形参d
,这个形参d
又需要调用拷贝构造才能创建,相信到这里,小伙伴们已经看出问题所在了———无穷递归,形参在接收的时候,会无穷无尽的去调用拷贝构造函数,就像套娃一样。
为了避免出现这种无穷递归,编译器会自行检查,如果拷贝构造函数的形参是值传递,编译时会直接报错。
📖必须是引用
为了打破上面的魔咒,拷贝构造函数的形参只能有一个,并且必须是类类型对象的引用。下面才是正确的拷贝构造函数:
Data(Data& d)//正确的拷贝构造
{
_year = d._year;
_month = d._month;
_day = d._day;
}
Data d1(2023, 7, 20);//定义一个日期类对象d1
Data d2(d1);//
此时创建d2
的时候,传递d1
调用拷贝构造函数,形参d
是一个日期类的引用,因为引用时区别名,意味着d
是d1
的一个别名,此时就不会再去无穷无尽的调用拷贝构造啦。
📖建议加const
因为存在用一个const
对象去初始化创建一个新对象这种场景,所以建议在拷贝构造函数的形参前面加上const
,此时普通的对象能用,const
对象也能用。
Data(const Data& d)//正确的拷贝构造
{
_year = d._year;
_month = d._month;
_day = d._day;
}
const Data d1(2023, 7, 20);//定义一个日期类对象d1
Data d2(d1);
📖编译器生成的拷贝构造干了什么?
上一节提到,拷贝构造是一种默认成员函数,我们不写编译器会自动生成。编译器生成的默认拷贝构造函数,对内置类型按照字节方式直接拷贝(也叫值拷贝
或浅拷贝
),对自定义类型是调用其拷贝构造函数完成拷贝。
class Time//定义时间类
{
public:
Time()//普通构造函数
{
_hour = 1;
_minute = 1;
_second = 1;
}
Time(const Time& t)//拷贝构造函数
{
_hour = t._hour;
_minute = t._minute;
_second = t._second;
cout << "Time::Time(const Time&)" << endl;
}
private://成员变量
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year = 1970;
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
int main()
{
Date d1;
// 用已经存在的d1拷贝构造d2,此处会调用Date类的拷贝构造函数
// 但Date类并没有显式定义拷贝构造函数,则编译器会给Date类生成一个默认的拷贝构造函数
Date d2(d1);
return 0;
}
📖什么是浅拷贝
上面提到,编译器生成的拷贝构造函数,会对内置类型完成浅拷贝,浅拷贝就是以字节的方式,把一个字节里的内容直接拷贝到另一个字节中。
📖拷贝构造函数可以不写嘛?
通过上面的分析可以得出:编译器自己生成的构造函数对内置类型和自定义类型都做了处理。那是不是意味着我们就可以不写拷贝构造函数了呢?答案是否定的,对于日期类,我们确实可以不写,用编译器自己生成的,但是对于一些需要深拷贝的对象,构造函数是非写不可的。栈就是一个典型的需要我们自己写构造函数的例子
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 10)
{
_array = (DataType*)malloc(capacity * sizeof(DataType));
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
_size = 0;
_capacity = capacity;
}
void Push(const DataType& data)
{
// CheckCapacity();
_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);
s1.Push(2);
s1.Push(3);
s1.Push(4);
Stack s2(s1);
return 0;
}
上面定义了一个栈类Stack
,我们没有写它的拷贝构造函数,编译器会自动生成一个默认的拷贝构造函数,栈中的成员变量都是内置类型,默认的拷贝构造函数会对这三个成员变量都完成值拷贝(浅拷贝)。
此时浅拷贝的问题在于:对象s1
和对象s2
中的_array
存的是同一块空间的地址,他俩指向了同一块空间,当程序退出,往s1
或s2
中的任意一个对象push
值,另一个也会跟着改变。s1
和s2
要销毁,s2
先销毁,s2
销毁时调用析构函数,已经将0X11223344
这块空间释放了,但是s1
并不知道,到s1
销毁的时候,会将0X11223344
这块空间再释放一次,一块内存空间多次释放,最终就会导致程序崩溃。
📖深拷贝
通过上面的分析可以看出,简单的浅拷贝不能满足栈的需求,因此,对于栈,我们需要自己写一个拷贝构造函数,来实现深拷贝,深拷贝就是去堆上重新申请一块空间,把s1
中_array
指向的空间中的内容,拷贝到新申请的空间,再让s2
中的_array
指向该空间。
//自己写的拷贝构造函数,实现深拷贝
Stack(const Stack& st)
{
DataType* tmp = (DataType*)malloc(sizeof(DataType) * st._capacity);
if (nullptr == tmp)
{
perror("malloc申请空间失败");
return;
}
memcpy(tmp, st._array, sizeof(DataType) * st._size);
_array = tmp;
_size = st._size;
_capacity = st._capacity;
}
📖总结:
类中如果没有涉及资源申请时,拷贝构造函数写不写都可以;一旦涉及到资源申请时,拷贝构造函数是一定要写的,否则就是浅拷贝,最终析构的时候,就会释放多次,造成程序崩溃。
📖拷贝构造函数典型的调用场景:
- 使用已存在对象创建新对象。
- 函数参数类型为类类型对象。
- 函数返回值为类类型对象。
class Data
{
public:
Data(int year = 1, int month = 1, int day = 1)
{
cout << "调用构造函数:" << this << endl;
cout << endl;
_year = year;
_month = month;
_day = day;
}
Data(const Data& d)
{
cout << "调用拷贝构造:" << this << endl;
cout << endl;
_year = d._year;
_month = d._month;
_day = d._day;
}
~Data()
{
cout << "~Data()" << this << endl;
cout << endl;
}
private:
int _year;
int _month;
int _day;
//可以不用写析构,因为全是自定义类型,并且没有动态申请的空间,这三个成员变量会随着对象生命周期的结束而自动销毁
};
Data Text(Data x)
{
Data tmp;
return tmp;
}
int main()
{
Data d1(2023, 4, 29);
Text(d1);
return 0;
}
📖总结:
自定义类型在传参的时候,形参最好用引用来接收,这样可以避免调用拷贝构造函数,尤其是深拷贝的时候,会大大的提高效率,函数返回时,如果返回的对象在函数栈帧销毁后还在,最好也用引用返回。
🎁结语:
今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下,您的支持就是春人前进的动力!