类和对象(中):三十分钟吃透C++拷贝构造和赋值运算符重载
目录
拷贝构造函数
(一)拷贝构造函数的定义
如果一个构造函数的第一个参数是自身类类型的引用,且其余的参数都有缺省值,则此构造函数也叫做拷贝构造函数,也就是说拷贝构造函数是一种特殊的构造函数。
(二)拷贝构造函数的特点
1.拷贝构造函数是构造函数的一个重载(定义格式与构造函数相同,只有参数部分不同)。
2.拷贝构造函数的第一个参数必须是类类型对象的引用,使用传值传参编译器直接报错,因为语法逻辑上会引起无穷递归。
拷贝构造函数传值传参会引起无穷递归的原因:C++规定自定义类型对象进行传值传参和传值返回时都会临时拷贝一个对象给形参,拷贝行为必须调用拷贝构造。
当形参是传值时,需要拷贝实参到形参 → 拷贝又需要调用拷贝构造函数 → 新调用的拷贝构造函数又需要传值拷贝它的实参……形成无限递归,编译会报错。
所以拷贝构造函数的第一个参数必须是引用(通常是 const 左值引用),从而避免拷贝。
如下图:

3.未显式定义拷贝构造,编译器会自动生成一个拷贝构造函数。不过自动生成的拷贝构造函数只能进行浅层拷贝(类似于挨个赋值,下面有详细解释),对于有些较为复杂的成员函数无法正确拷贝,特别是涉及到内存资源的(例如数组)。自动生成的拷贝构造函数对内置类型浅拷贝,对自定义类型则会调用他的拷贝构造函数。
1.浅拷贝
浅拷贝又叫值拷贝,只适用于较简单的类:如Date类,只需要值一对一赋值即可,而对于Stack类,浅拷贝远远不够

如果栈使用浅拷贝赋值,不足:
1.一个对象修改会影响另一个对象
2.析构时,会释放两次内存空间
2.深拷贝
不仅对成员进行拷贝,还要对指向资源空间数据进行处理

3.如何判断需要浅拷贝还是深拷贝
像Date这样的类,成员变量全是内置类型且没有指向什么资源,浅拷贝即可,不用我们自己实现一个拷贝构造函数,自动生成的就足够。像Stack这样的类,虽然int*、int都是内置类型,但数组指向了资源,仅仅浅拷贝不符合预期,所以需要我们自己实现一个拷贝构造函数进行深拷贝(如上图,开辟一个新的空间,不会造成数据污染和释放野指针的问题)
(三)代码演示
class Date
{
public:
Date(const Date& d)
{
_year=d._year;
_month=d._month;
_day=d._day;
}
//...
private:
//...
}
int main()
{
Date d1(2026,5,22);//构造
Date d2(d1);//拷贝构造
return 0;
}
赋值运算符重载
(一)运算符重载
1.运算符重载的定义
1.当运算符被用于类类型的对象时,C++允许我们通过运算符重载的形式指定新的含义。
比如:Date-Date=相差天数、Date-天数=Date......
2.运算符重载是具有特殊名字的函数,格式为:返回类型 operator运算符(参数列表)
3.重载运算符函数的参数个数与该运算符作用的运算对象数量一样多。一元运算符只有一个参数,二元运算符有两个参数,左侧运算符传给第一个参数,右侧运算符传给第二个参数。
4.不能重载的五个运算符:
2.前置++与后置++如何重载
C++规定,后置++重载时,增加一个int形参,跟前置++构成重载,方便区分。
class MyClass {
public:
// 前缀++
MyClass& operator++() {
(*this)+=1;
return *this;
}
// 后缀++
MyClass operator++(int) {
MyClass temp = *this;
// 实现自增
return temp;
}
};
3.根据运算符的语义选择返回值类型
1.修改自身并返回引用的运算符(如前置++)通常返回引用
2.创建新对象的运算符(如后置++)通常返回值
| 特性 | 前置 ++ ( ++i ) | 后置 ++ ( i++ ) |
|---|---|---|
| 核心语义 | 先自增,再返回自身 | 先返回原值,再自增 |
| 返回类型 | 引用 (T&) | 值 (const T 或 T) |
| 返回值身份 | 原对象本身 (左值) | 一个临时副本 (右值) |
| 是否产生临时对象 | 否 (高效) | 是 (有额外开销) |
| 典型用法 | ++i; ++++i; func(++i); | i++; func(i++); |
4.<<和>>如何重载
这里的<<不是左移运算符,是流插入运算符:cout<<...
重载<<和>>时,需要重载为全局函数,因为静态成员函数的第一个参数默认有一个this指针,抢占了第一个形参的位置,第一个形参位置是左侧运算对象,调用时就变成了:对象<<cout,不符合预期。
1.通常声明为友元函数,以便访问私有成员。
2.返回 std::ostream& 和 std::istream&,以支持链式输入输出。

(二)赋值运算符重载
与拷贝构造区分:拷贝构造用于一个对象拷贝初始化给另一个要创建的对象,而赋值运算符重载函数用于完成两个已经存在的对象的直接拷贝。
1.赋值运算符重载的特点
1.赋值运算符重载是一个运算符重载,规定必须重载为成员函数,赋值运算重载的参数建议写成 const当前类类型引用,否则会传值传参会有拷贝。
2.有返回值,且建议写成当前类类型引用,引用返回可以提高效率,有返回值目的是为了支持连续赋值的情景。
3.没有显式实现时,编译器会自动生成一个默认的赋值运算符重载,跟自动生成的拷贝构造类似对内置成员进行浅拷贝,对自定义成员会调用他的赋值运算符重载函数。
4、(1)像Date类这样的类的成员变量全是内置类型且没有指向什么资源,编译器自动生成的赋值运算符重载就可以完成需要的拷贝,所以不需要我们显示实现赋值运算符重载。
(2)像Stack这样的类,虽然也都是内置类型,但是_a指向了资源,编译器自动生成的赋值运算符重载完成的浅拷贝(值拷贝)不符合我 们的需求,所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。
(3)像MyQueue这样的类型内部 主要是自定义类型Stack成员,编译器自动生成的赋值运算符重载会调用Stack的赋值运算符重载, 也不需要我们显示实现MyQueue的赋值运算符重载。
5.如果类需要自定义析构函数,那么通常也需要自定义拷贝构造函数和拷贝赋值运算符。
2.代码实现
class MyClass {
public:
MyClass& operator=(const MyClass& rhs) { // 参数为 const 引用
if (this != &rhs) { // 自赋值检查
// 执行深拷贝或其他资源管理逻辑
}
return *this; // 返回当前对象引用
}
};
日期类的实现(多种运算符重载:+、+=、++、==...)
Test.cpp · 田梦娴/日常代码 - 码云 - 开源中国
https://gitee.com/tian-mengxian/tmx666/blob/master/Test.cpp
Date.cpp · 田梦娴/日常代码 - 码云 - 开源中国
https://gitee.com/tian-mengxian/tmx666/blob/master/Date.cpp
Date.h · 田梦娴/日常代码 - 码云 - 开源中国
https://gitee.com/tian-mengxian/tmx666/blob/master/Date.h
小结
拷贝构造函数和赋值运算符重载是 C++ 类中两个至关重要的默认成员函数。拷贝构造用于“用一个已存在的对象初始化另一个新对象”,而赋值重载用于“两个都已存在的对象之间的拷贝”。它们共同决定了对象在复制时的行为。
理解浅拷贝与深拷贝的区别是避免资源重复释放、数据污染的关键。当一个类管理了动态内存、文件句柄等资源时,必须显式定义拷贝构造和赋值重载(以及析构函数),这正体现了“三法则”——“如果类需要自定义析构函数,那么通常也需要自定义拷贝构造函数和拷贝赋值运算符”的设计思想。对于像 Date 这样仅包含内置类型且不涉及资源管理的类,编译器自动生成的浅拷贝完全够用。
此外,赋值运算符重载必须作为成员函数实现,而流插入/提取运算符则需要重载为全局函数。掌握这些规则和习惯,才能写出健壮、高效的 C++ 类。
最后,建议你动手实现一个完整的Stack类,亲自体验深拷贝和自赋值的处理,这会让理解更深刻。类和对象的中阶内容到这里就结束了,下一篇我们将进入“类和对象(下)”,探讨初始化列表、const 成员函数、取地址运算符重载等更高级的话题。敬请期待!
更多推荐

所有评论(0)