拷贝构造函数
- 拷贝构造函数
- 前言
- 引入
- 拷贝构造函数
- 特征
- 拷贝构造函数建议参数加上const
- 拷贝构造函数参数传值会引发无穷递归的解释
- 内置类型传参拷贝
- 自定义类型传参拷贝
- 详细解释
- 编译器生成的默认拷贝构造函数
- 默认构造函数做了什么?
- 深拷贝与浅拷贝
- 简单实现一个深拷贝。
- 深浅拷贝总结
- 拷贝构造函数典型使用场景
- 使用已存在的对象创建新对象。
- 函数参数类型为类类型对象。
- 函数返回值类型为类类型对象。
- 总结需要实现拷贝构造的场景
拷贝构造函数
前言
上文中详解了构造函数
和析构函数
,本文详解类的六个默认成员函数中的第三个,拷贝构造函数,并辨析区分深拷贝与浅拷贝。
引入
在现实生活中,可能存在一个与你一样的自己,我们称其为双胞胎。
那在创建对象时
,可否创建
一个与已存在对象一某一样的新对象呢?
答案当然是有的, 拷贝构造函数就是来完成这一工作的。
拷贝构造函数
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
特征
- 拷贝构造函数是构造函数的一个重载形式。
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
//拷贝构造函数实例
class Date {
private:
int _year;
int _month;
int _day;
public:
Date(int year = 2024, int month = 10, int day = 28) {
_year = year;
_month = month;
_day = day;
}
//拷贝构造函数实例
Date(const Date& date) {
_year = date._year;
_month = date._month;
_day = date._day;
}
};
int main(){
Date d1(2025, 2, 8);
Date d2(d1); //调用拷贝构造函数,用已有对象初始化新的对象
}
拷贝构造函数建议参数加上const
Date(const Date& date) {
//const 是为了防止别人写错,防止别人写成以下代码
date._year = _year; //不仔细看真看不出来,赔了夫人又折兵,会出现随机值
date._month = _month;
date._day = _day;
}
- 不加const可能会出现随机值。
拷贝构造函数参数传值会引发无穷递归的解释
解释前,我们需要明晰该概念: 函数调用前,需要先传参。
**C++**中传参时有如下要求:
- 内置类型,无要求,直接拷贝;
- 自定义类型,值传参,必须调用其拷贝构造函数
内置类型传参拷贝
C++内置类型拷贝
我们可以看到,
自定义类型传参拷贝
C++自定义类型拷贝
我们可以看到:
- 内置类型作为参数时,调用函数会直接进入函数内。。
- 自定义类型作为参数时,调用函数,会先调用其拷贝构造函数来拷贝。
此时再来看,若拷贝构造函数为值传递:
Date(const Date date) { //下面解释会无穷递归调用
_year = date._year;
_month = date._month;
_day = date._day;
}
详细解释
栈帧基本概念
- 栈帧(Stack Frame): 当一个函数被调用时,程序会为这个函数分配一个栈帧,其中存放了函数的参数、局部变量、返回地址等信息。
- 函数调用和返回: 每当一个函数调用完成后,其对应的栈帧会被释放。如果函数不断调用自身(递归),每次调用都会在栈上分配一个新的栈帧。如果没有合适的终止条件,栈帧会不断累积,最终导致栈空间耗尽(栈溢出)。
执行语句Date d2(d1)
时,流程如下:
Date d2(d1)
,执行该语句,参数列表匹配,会调用其拷贝构造函数,构造函数会创建一个栈帧,栈帧内空间存放有形参date。形参date
是实参d1
的一份拷贝,拷贝时会调用Date的拷贝构造函数,而拷贝构造函数Date(const Date date)
,参数为自定义类型的值传递,自定义类型的值传递,同样会创建栈帧并调用其拷贝构造函数。- 拷贝构造函数继续传值引发对象的拷贝,之后会层层传值引发对象的拷贝的递归调用。
总结:调用 Date d2(d1)时,新分配一个拷贝构造函数的栈帧,同时栈帧内创建参数的副本,创建参数的副本时又不断分配新的栈帧,层层嵌套,无法返回,直至栈空间耗尽。
编译器生成的默认拷贝构造函数
若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
默认构造函数做了什么?
- 对内置类型成员完成,,值拷贝/浅拷贝
- 对自定义类型,调用各自的拷贝构造函数,如果自定义类型没有拷贝构造函数,则值拷贝。
//默认拷贝构造函数
class Date_1 {
private:
int _year;
int _month;
int _day;
public:
Date_1(int year = 2024, int month = 10, int day = 28) {
this->_year = year;
this->_month = month;
this->_day = day;
}
/*Date_1(const Date_1& date) {
this->_year = date._year;
this->_month = date._month;
this->_day = date._day;
}*/
};
int main() {
Date_1 d1(2025, 6, 6);
Date_1 d2(d1);
return 0;
}
可以看到,编译器自动生成的默认拷贝构造函数,可以完成Date_1
(类内均为自定义类型)的拷贝。
深拷贝与浅拷贝
再看如下代码:
class Stack {
private:
int* _base = nullptr; //C++11支持的成员变量缺省值
int _top = 0;
int _capacity = 0;
public:
Stack(int defaultCapacity = 4) {
this->_base = (int*)malloc(sizeof(int) * defaultCapacity);
if (this->_base == nullptr) {
perror("malloc failed\n");
return;
}
this->_capacity = defaultCapacity;
this->_top = 0;
}
~Stack() {
cout << " ~Stack" << endl;
free(this->_base);
this->_base = nullptr;
this->_capacity = 0;
this->_top = 0;
}
};
int main(){
Stack st1;
Stack st2(st1);
return 0;
}
运行结果如下:
这是为什么呢?原因就是浅拷贝的危害
-
值拷贝(浅拷贝)存在问题,值拷贝时,由于只是简单的值拷贝,导致两个栈类对象中,两个指针存下了同一块空间的地址。
-
析构函数完成的是对象中资源空间的清理和释放。可以看到,当两个栈对象的生命周期结束时,析构函数会调用了两次,那也就是说,会对同一块空间析构(释放)两次,这是这便是引发错误的原因。
-
隐患
,由于两块空间的地址相同,我们对对象st1进行push操作时,也会改变对象st2中的值,这并不是我们所希望的。因此我们应该在此类场景中避免浅拷贝。
此类对象的拷贝构造函数需要实现深拷贝!
简单实现一个深拷贝。
//在拷贝构造函数中简单实现一个深拷贝 st2(st1)
Stack(const Stack& stack) {
this->_base = (int*)malloc(sizeof(int) * stack._capacity);
if (this->_base == nullptr) {
perror("malloc failed\n");
return;
}
memcpy(this->_base, stack._base, sizeof(int) * stack._top);
this->_top = stack._top;
this->_capacity = stack._capacity;
}
可以看到,实现深拷贝后,程序正常返回。
深浅拷贝总结
- 类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;主要是要防止浅拷贝后,两个对象内的成员指针指向同一块空间。
- 一旦涉及到资源申请时,则拷贝构造函数是一定要写的,且要实现深拷贝,否则就是浅拷贝。
拷贝构造函数典型使用场景
拷贝构造函数典型调用场景:
使用已存在的对象创建新对象。
Date d1(2025, 2, 11);
Date d2(d1); //调用拷贝构造函数
- 其作用是通过深拷贝或浅拷贝初始化一个新对象为已有对象的副本。。
函数参数类型为类类型对象。
Date Test(Date d); //自定义类型传参时,必须调用其拷贝构造
- 以值方式传递参数时,会调用一次拷贝构造函数创建形参d,存放在函数栈帧中。
函数返回值类型为类类型对象。
Date Test(Date d) {
Date temp(d); //使用已存在对象创建新对象
return temp; //返回式
}
- 函数返回temp时,若未启用返回值优化(RVO),会调用拷贝构造函数生成临时对象
总结需要实现拷贝构造的场景
声明:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。
因此:
- 类中如果没有涉及资源申请时(如堆区的内存),拷贝构造函数是否写都可以;
- 一旦涉及到资源申请时(开辟了堆区的空间),则拷贝构造函数是一定要写的,否则就是浅拷贝。
文章到此结束啦,以上便是要介绍的关于拷贝构造函数的所有内容。欢迎各位大佬在评论区讨论交流,如果觉得文章写的不错,还请留下免费的赞和收藏!