
◆ 博主名称: 晓此方-CSDN博客
大家好,欢迎来到晓此方的博客。
⭐️C++系列个人专栏:
目录
0.1概要&序論
这里是此方,久しぶりです! 今天,为大家带来的是类和对象中的两位超级容易混淆的概念 :拷贝构造和赋值重载,相信很多小伙伴都了解过构造函数和运算符重载,相信大家看完本文后能对他们有更新的认识。「此方」です。それでは、始めましょう!
一,拷贝构造函数
1.1拷贝构造函数的定义
拷贝构造函数是构造函数的重载
如果一个构造函数的第一个参数是自身类类型的引用 ,且任何额外的参数都有默认值 ,则此构造函数也叫做拷贝构造函数(又名复制构造函数) ,也就是说拷贝构造是一个特殊的构造函数。
1.2拷贝构造函数的创建
cpp
Date(const Date& d, int x = 0){
_year = d._year;
_month = d._month;
_day = d._day;
}
逐层分析:
- 函数和构造函数基本一致,无返回值(包括void)
- 第一个参数必须是自身类类型的引用。(Date&d)
- 后面的参数(如果有)都必须加上缺省值。(int x = 0)
- 引用前面最好加上const限制。
1.2.1采用const引用的原因
可以保护形式参数不被改变。传值传参,拷贝构造函数只是一个中转。我不希望它在函数体内对我的原来的对象造成修改。
cpp
Date(const Date& d){
if (d._year = _year){
}
}
此外,常见的错误:在if语句种将==写成= ,const也可以提醒你避免这些错误。
1.3拷贝构造函数的调用
注意:拷贝构造函数的调用必须以现有的自身类类型对象为第一个参数。
cpp
int main()
{
Date d1(2024,2,23);
Date d2(d1);
func (d1);
}
1.3.1情形一:拷贝构造初始化
cpp
Date(const Date& d){
this->_year = d._year;
this->_month = d._month;
this->_day = d._day;
}
采用 Date d2(d1) 调用拷贝构造函数完成初始化 ,d2调用拷贝构造函数所以this指针就是d2的指针 ,d就是d1的别名,d将d1的值传递给this指针指向的内容,完成拷贝初始化。
1.3.2情形二传值调用拷贝构造
传值拷贝的时候,别名d任然指的是传递的形参 。但是此时的this指针是函数形式参数的指针。
1.3.3情形三
传值返回的时候会调用拷贝构造函数完成拷贝: 传值返回会产生一个临时对象调用拷贝构造。
1.3.4总结
C++规定自定义类型对象进行拷贝行为 必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成。
对于内置类型**,传值调用实际上影响不大。** 对于自定义类型,传值传参还要到用拷贝构造函数,费劲 。所以一般建议传引用传参,跳过拷贝构造函数这一步。 (不可避免传值传参的时候传值)
但是:
传引用返回,返回的是返回对象的别名(引用),没有产生拷贝 。如果返回对象是一个当前函数局部域的局部对象 ,函数结束就销毁了 ,那么使用引用返回是有问题的,这时的引用相当于一个野引用,类似一个野指针一样。传引用返回可以减少拷贝,但是一定要确保返回对象,在当前函数结束后还在,才能用引用返回。
1.3.5补充
- 未定义行为:对多个参数进行传值调用的时候,C++没有规定这些参数传值调用拷贝构造函数的顺序。由编译器自动决定
- **调用拷贝构造函数的另外一种方法:第二种调用方法实质上等价于第一种。(你以后会喜欢第二种的)
cpp
Stack st2(st1);
Stack st3 = st1;
1.4传值拷贝构造与无穷递归
拷贝构造函数的参数:必须是类类型对象的引用,使用传值方式编译器必定报错,因为语法逻辑上会引发无穷递归调用。

1.4.1浅拷贝与深拷贝
要理清这个问题,首先要明白C语言和C++在传值拷贝上的区别。
|---------|----------|--------------|
| 语言 | 拷贝方式 | 解释 |
| C语言 | 浅拷贝(值拷贝) | 将每个字节一次拷贝给目标 |
| C++ | 深拷贝 | 调用拷贝构造函数完成拷贝 |
cpp
class Date{
public:
Date(const Date& d){
_year=d._year;
_month=d._month;
_day=d._day;
}
void Func(Date d);
private:
int _year;
int _month;
int _day;
}
void Func(Date d){
cout<<d._year<<d._month<<d._day<<endl;
}
int main(){
Date d1;
Func(d1);
return 0;
}
如上代码:
- 如果是C语言环境:调用Func()函数传值d1会直接把实参d1一个字节一个字节的拷贝 给形参d,这就是值拷贝(浅拷贝)。
- 如果是C++的环境:调用Func()函数会先调用拷贝构造函数Date() ,此时的d就是d1的别名,this指针是Func()函数的形参d的指针**。完成深拷贝。**
(注意:值拷贝和浅拷贝不是一回事,但是这里讲不清楚,放在后面的章节,)
1.4.2无穷递归原理

能否发生无穷递归的关键是Date拷贝构造函数能否结束调用,如图,传引用调用,Date拷贝构造函数执行完成拷贝后自动转到Func()函数。避免了无穷递归。
cpp
Date(const Date d){
_year=d._year;
_month=d._month;
_day=d._day;
}
假设我们把拷贝构造函数的传引用改成传值 ,灾难性的问题就发生了:

过程分析:
- 调用拷贝构造函数(第一个)采用传值调用。传值调用需要调用拷贝构造函数(第二个)。
- 调用拷贝构造函数(第二个)采用传值调用,传值调用需要调用拷贝构造函数(第三个)。
- 调用拷贝构造函数(第三个)采用传值调用,传值调用需要调用拷贝构造函数(第四个)。
- 调用拷贝构造函数(第四个)采用传值调用,传值调用需要调用拷贝构造函数(第五个)。
- ...........以此类推,无穷无尽,永远无法结束调用拷贝构造函数(第n个),也就永远无法调用到拷贝构造函数(第n-1个)
1.5深究浅拷贝与深拷贝
1..5.1自动生成拷贝构造函数
与构造函数与析构函数不同,自动生成的拷贝构造函数会管内置类型。
若未显示定义拷贝构造,**编译器会自动生成拷贝构造函数。**自动生成的拷贝构造对内置类型成员变量会完成值拷贝 / 浅拷贝,对自定义类型成员变量会调用他的拷贝构造。
-
情况一: 类成员均为内置类型,且不管理资源
例如Date类。编译器生成的拷贝构造函数会完成值拷贝,因此不需要我们显式实现拷贝构造。
-
情况二: 类成员包含自定义类型,且该类型已正确实现拷贝构造函数
例如Myqueue类,其内部成员主要是自定义类型Stack的对象。编译器自动生成的拷贝构造函数会调用Stack的拷贝构造函数,因此也不需要我们显式实现Myqueue的拷贝构造。
-
情况三:类管理动态资源
例如Stack 类,其成员_a指向了资源。编译器自动生成的拷贝构造函数仅完成值拷贝 / 浅拷贝(仅复制指针值),并不符合我们的需求,因此必须手动实现深拷贝,即对指针所指向的资源也进行完整拷贝。
设计者的小巧思:
这种设计某种意义上是为了兼容C语言 ,浅拷贝------**类似于memcpy函数,也就是C语言传值调用的方式,**C语言结构体传参会完成值拷贝。
1.5.2需要自己实现深拷贝的场景

如图:对栈类型调用拷贝构造函数初始化。
但是你发现:程序崩溃 。原因:同一个空间被析构两次。调试查找问题你发现:_a存放的地址在两个栈中一致。

原理解释: Stack不显示实现拷贝构造,用自动生成的拷贝构造完成浅拷贝 ,会导致st1和st2里面的_a指针指向同一块资源,析构时会析构两次,程序崩溃
于是,对于栈这种向内存申请资源的类,不建议使用编译器自带的拷贝方式。必须手动实现拷贝构造函数。
cpp
Stack(const Stack& st){
_a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);
if (nullptr == _a){
perror("malloc申请空间失败!!!");
return;
}
memcpy(_a, st._a, sizeof(STDataType) * st._top);
_top = st._top;
_capacity = st._capacity;
}

1.5.3C语言在此的致命缺陷
cpp
void func(Stack st){
}
int main(){
Stack st1;
func(st1);
return 0;
}
C语言传值传参, 形式参数确实不会改变实际参数的值,但是传值传参把指针变量的指针一个字节一个字节的拷贝过去,让形式参数的对象的指针和实际参数的指针指向同一块空间 ,会引发连锁改变效应。
二,赋值运算符重载
2.1赋值运算符重载的定义
赋值运算符重载是一个默认成员函数 ,用于完成两个已经存在的对象直接的拷贝赋值,这里要注意跟拷贝构造区分,拷贝构造用于一个对象拷贝初始化给另一个要创建的对象。
cpp
Date d1(2024, 7, 5);
Date d2(2024, 7, 6);
// 赋值重载
d1 = d2;
// 拷贝构造
Date d3(d2);
Date d4 = d2;
尤其注意这个不是赋值运算符重载:Date d4 = d2;这里的d4也是要创建的对象(优先是构造再是拷贝。),所以不是赋值重载,一定要强调是两个已经存在的对象。这里非常容易混淆。
2.2赋值运算符重载的特点
1, 赋值运算符重载是一个运算符重载 ,规定必须重载为成员函数 。赋值运算符重载的参数建议写成 const 当前类类型引用,否则会传值传参会有拷贝。
cpp
operator=(const Date& d)
{
}
2,有返回值,且建议写成当前类类型引用,引用返回可以提高效率,有返回值目的是为了支持连续赋值场景。
cpp
Date& operator=(const Date& d)
{
_year=d._year;
_month=d._month;
_day=d._day;
return *this:
}
赋值运算符的结合性是从左向右 :k=1运算符结果是左操作数k,k=j运算符结果是左操作数j,以此类推。如图:

2.3赋值运算符重载是否需要手动实现
没有显式实现时,编译器会自动生成一个默认赋值运算符重载 ,默认赋值运算符重载行为跟默认构造函数类似,对内置类型成员变量会完成值拷贝/浅拷贝 (一个字节一个字节的拷贝),对自定义类型成员变量会调用他的 赋值重载。
2.3.1不需要手动实现的情况
-
情况一:类成员均为内置类型,且不管理资源
例如Date类。编译器生成的赋值运算符会进行值拷贝。
-
情况二:类成员包含自定义类型,且该类型已正确实现赋值运算符
例如Myqueue类,其成员为Stack对象。编译器会递归调用Stack的赋值运算符,只要Stack已正确实现深拷贝,Myqueue就无需手动实现。
2.3.2必须手动实现的情况
-
类管理动态资源(如堆内存、文件句柄等)
例如Stack类,其成员_a指向动态数组。编译器生成的赋值运算符仅进行浅拷贝(复制指针值),会导致:内存泄漏,重复释放,悬空指针等一系列问题。
2.3.3实用技巧:一个简单的判断法则
如果类显式实现了析构函数(因为需要释放资源),那么它通常也需要显式实现:
拷贝构造函数
赋值运算符重载
这就是著名的 "Rule of Three" (C++98/03)或 "Rule of Five"(C++11 后,包括移动语义)。反之,如果不需要写析构函数,通常也不必写拷贝控制函数。
好了,本期内容到此结束,我是此方,我们下期再见。バイバイ!