
🔥个人主页:@草莓熊Lotso 🎬作者简介:C++研发方向学习者 📖个人专栏: 《C语言》 《数据结构与算法》《C语言刷题集》《Leetcode刷题指南》 ⭐️人生格言:生活是默默的坚持,毅力是永久的享受。

前言: 在上篇博客中我们学习了构造函数和析构函数这两个类中的默认成员函数,今天这篇博客我想继续为大家分享拷贝构造函数和赋值运算符重载。主要是先介绍特点再通过举例说明,所以举例中的代码注释是很重要的。
如果一个构造函数的第⼀个参数是自身类类型的引用,且任何额外的参数都有默认值,则此构造函数也叫做拷贝构造函数,也就是说拷贝构造是⼀个特殊的构造函数。
--上述特点在举例说明中都会体现出来
关于无穷递归图示:

#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//拷贝构造函数
//Date(Date d),这种写法不行,我们可以拿个func的例子看看,会发现传值要先调用拷贝函数,下面有提到
// 但是拷贝函数本身再一直调用拷贝函数本身的话,会出现无限递归的问题,所以要使用传引用传参
//加个const可以让下面传实参的选择更多,避免出现权限扩大等问题
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
//其实这里用默认生成的也会处理内置类型,进行浅拷贝,在这里是没问题的。这也是和构造和析构的一个区别
//用指针实现拷贝,但是这里并不算拷贝函数
/*Date(Date* d)
{
_year = d->_year;
_month = d->_month;
_day = d->_day;
}*/
void Print()
{
cout << _year << "/" << _month << "/" << _day << '\n';
}
private:
int _year;
int _month;
int _day;
};
//自定义类型传值传参和传值返回都会调用拷贝构造完成
//传值传参会调用拷贝函数
void func(Date d)
{
}
//传值返回也会调用拷贝函数,但是传引用就不会(后续会用Stack继续来讲),这里就不过多介绍了
//Date& func2()
Date func2()//这里最好用传值返回
{
Date d(2025,7,31);
return d;
}
int main()
{
Date d1(2025,8,1);
Date d2(d1);
//func(d1);//我们调试发现,他会先去调用Date里面的拷贝函数再去func
//传值返回可以,传引用返回不行,这里不细讲
Date ret = func2();
//Date ret = (func2());,这样写也是可以的
//由这个我们还可以看出,那其实之前的拷贝也可以写出这样
//Date d2 = d1;
// 这里可以完成拷⻉,但是不是拷⻉构造,只是⼀个普通的构造
//Date d2(&d1);
d1.Print();
d2.Print();
return 0;
}
关于对内置类型处理和深浅拷贝的相关示例: (注意注释,用的栈)
#include<iostream>
using namespace std;
typedef int STDataType;
class Stack
{
public:
Stack(int n=4)
{
_a = (int*)malloc(n * sizeof(int));
if (_a == nullptr)
{
perror("malloc fail!");
exit(1);
}
_top = 0;
_capacity = n;
}
//这样写是错的,因为这里的数组如果像这样写仅仅是浅拷贝,后续析构函数释放空间会释放同一块空间
// 画图理解,调试也会报错
/*Stack(const Stack& s)
{
_a = s._a;
_capacity = s._capacity;
_top = s._top;
}*/
//所以我们需要这样写
Stack(const Stack& s)
{
_a = (STDataType*)malloc(sizeof(STDataType) * s._capacity);//申请一块同样大小的空间
if (_a == nullptr)
{
perror("malloc fail!");
exit(1);
}
//把值再拷贝过去
memcpy(_a, s._a, s._top * sizeof(STDataType));
//这两个直接这样就可以了
_capacity = s._capacity;
_top = s._top;
}
//补充一点,这里也不能不写,用编译器自动生成的默认的拷贝构造函数,因为这个函数虽然会处理内置类型
//但是只会是浅拷贝/值拷贝,像Stack这样需要有深拷贝的就不行了
//可以这样说,如果一个类必须显示实现析构函数(需要释放资源),那么他就一定也要显示写拷贝构造函数
void Push(STDataType x)
{
if (_top == _capacity)
{
int newcapacity = _capacity * 2;
STDataType* tmp = (STDataType*)realloc(_a, newcapacity *sizeof(STDataType));
if (tmp == NULL)
{
perror("realloc fail");
exit(1);
}
_a = tmp;
_capacity = newcapacity;
}
_a[_top++] = x;
}
~Stack()
{
cout<<"~Stack()"<<'\n';
if (_a)
{
free(_a);
_a = nullptr;
}
_top = 0;
_capacity = 0;
}
private:
//内置类型
STDataType* _a;
int _top;
int _capacity;
};
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
Stack s2(s1);
return 0;
}图示如下: 理解技巧:可以这样说,如果一个类必须显示实现析构函数(需要释放资源),那么他就一定也要显示写拷贝构造函数

借助上面的栈的类(这里就不再写出来了),给大家对比看看传引用返回在这里的弊端,同时也是在说第6个特点 :(注意注释)
int& func1()
{
int x = 1;
return x;
}
//自定义类型传值返回是会调用拷贝函数的,但是传引用返回不会,画图分析。
//它没调拷贝函数的话,在后面函数栈帧销毁,st析构掉了之后。你再通过别名来找,就出问题了,画图
//Stack func2()
Stack& func2()
{
Stack st;
return st;
}
int main()
{
int ret1 = func1();
cout << ret1 << '\n';//可能是1也可能是随机值,我们之前判断过
//但是这个栈就很明显了,我们调试看看
Stack ret2 = func2();//这里其实也是拷贝
return 0;
}
--会报realloc fail,但是上面的结构需要改一下(int改size_t),不然不会报这个错误


再来看看如果是默认生成的拷贝构造函数对自定义类型的处理:(注意注释)
#include<iostream>
using namespace std;
typedef int STDataType;
class Stack
{
public:
Stack(int n = 4)
{
_a = (int*)malloc(n * sizeof(int));
if (_a == nullptr)
{
perror("malloc fail!");
exit(1);
}
_top = 0;
_capacity = n;
}
Stack(const Stack& s)
{
_a = (STDataType*)malloc(sizeof(STDataType) * s._capacity);//申请一块同样大小的空间
if (_a == nullptr)
{
perror("malloc fail!");
exit(1);
}
//把值再拷贝过去
memcpy(_a, s._a, s._top * sizeof(STDataType));
//这两个直接这样就可以了
_capacity = s._capacity;
_top = s._top;
}
//补充一点,这里也不能不写,用编译器自动生成的默认的拷贝构造函数,因为这个函数虽然会处理内置类型
//但是只会是浅拷贝/值拷贝,像Stack这样需要有深拷贝的就不行了
//可以这样说,如果一个类必须显示实现析构函数(需要释放资源),那么他就一点也要显示写拷贝构造函数
void Push(STDataType x)
{
if (_top == _capacity)
{
int newcapacity = _capacity * 2;
STDataType* tmp = (STDataType*)realloc(_a, newcapacity * sizeof(STDataType));
if (tmp == NULL)
{
perror("realloc fail");
exit(1);
}
_a = tmp;
_capacity = newcapacity;
}
_a[_top++] = x;
}
~Stack()
{
cout << "~Stack()" << '\n';
if (_a)
{
free(_a);
_a = nullptr;
}
_top = 0;
_capacity = 0;
}
private:
//内置类型
STDataType* _a;
int _top;
int _capacity;
};
class MyQueue
{
public:
//编译器默认生成MyQueue的构造函数调用了Stack的构造函数,完成了两个成员的初始化
//编译器默认生成MyQueue的拷贝构造函数调用了Stack的拷贝构造函数,完成了拷贝
//编译器默认生成MyQueue的析构函数调用了Stack的析构函数,释放的Stack内部的资源
private:
//自定义类型
Stack _pushst;
Stack _popst;
//内置类型,但很奇怪,混在这里它却能处理,这里大家可以自己去试试
//int size = 0;
};
int main()
{
Stack st1;
st1.Push(1);
st1.Push(2);
// Stack如果不显示实现拷⻉构造,用自动生成的拷⻉构造完成浅拷⻉
// 会导致st1和st2里面的_a指针指向同⼀块资源,析构时会析构两次,程序崩溃
Stack st2 = st1;
MyQueue mq1;
// MyQueue自动生成的拷⻉构造,会自动调用Stack拷⻉构造完成pushst/popst的拷⻉。
// 只要Stack拷⻉构造自己实现了深拷⻉,这里就没问题
MyQueue mq2 = mq1;
return 0;
}
--在正式学习赋值运算符重载之前我们需要先了解一下运算符重载
--上述特点在举例说明中大多都会提到,其中最后三个特点会在后续的博客中讲解
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 1, 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;
}
//其实这里用默认生成的也会处理内置类型,进行浅拷贝,在这里是没问题的。
//这也是和构造和析构的一个区别,构造和析构不会处理内置类型
int Getyear()
{
return _year;
}
//d1==d2,传d2就行,d1有this指针,但是在实参和形参不能直接写出来,函数体内可以
bool operator==(const Date& d2)
{
return this->_year == d2._year
&& this->_month == d2._month
&& this->_day == d2._day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << '\n';
}
private:
int _year;
int _month;
int _day;
};
// 重载为全局的面临对象访问私有成员变量的问题
// 有几种方法可以解决:
// 1、成员放公有--这个最容易,但是不那么好
// 2、Date提供getxxx函数--上面有在类里面展现出来可以自己看看,然后在底下的函数体内需要修改一下
// 3、友元函数--这里先不讲这个
// 4、重载为成员函数--这个我也在类里重载为成员函数了,但是有些需要注意的地方,我最后选取这种
//bool operator==(const Date& d1, const Date& d2)
//{
// return d1._year == d2._year//如果用了Get**就是这样写的:d1.Getyear() == d2.Getyear()
// && d1._month == d2._month
// && d1._day == d2._day;
//}
int main()
{
Date d1(2025,8,1);
Date d2(2025,10,1);
Date d3(2025, 8, 1);
//我们在这里就需要实现运算符重载函数
d1 == d2;
//运算符重载函数可以显示调用
//operator==(d1, d2);
//如果成成员函数了,显示调用是这样的
//d1.operator==(d2);//只要传一个参d2就行,d1通过this指针,但是不能在实参和形参显示写出来的
//再加上运算符重载要求参数个数和运算符作用对象一样多,所以只能传一个
//可以具体去看看上面类里面怎么实现的
cout << (d1 == d2) << '\n';//这里需要打括号,优先级的问题,0
cout << (d1 == d3) << '\n';//1
return 0;
}--0表示不相等,1表示相等

给大家大概看一下 .* 这个符号:(注意注释)
// .*符号普及,了解即可,刚好提到了这个运算符不能重载
#include<iostream>
using namespace std;
void func1()
{
cout << "void func()" << endl;
}
class A
{
public:
void func2()
{
cout << "A::func()" << endl;
}
};
int main()
{
// 普通函数指针
void(*pf1)() = func1;
(*pf1)();
// A类型成员函数的指针
void(A::*pf2)() = &A::func2;
A aa;
(aa.*pf2)();//这里就是使用的.*
return 0;
}赋值运算符重载是一个默认成员函数,用于完成两个已经存在的对象直接的拷贝赋值,这里要注意跟拷贝构造区分,拷贝构造用于⼀个对象拷贝初始化给另一个要创建的对象。
--上述特点大部分会在后续的举例说明中解释
//赋值运算符重载
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 1, 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;
}
//传引用返回可以减少拷贝(之前提到过在这里传值返回是自动调用拷贝函数的)
//这里能使用是传引用返回是因为第一个参数用this来的,函数栈帧销毁也不会找不到
//函数要返回类型是为例更好处理连续赋值的情况(d3=d1=d2),用void不好处理
Date& operator=(const Date& d)//const和传引用传参的作用就不再多说了
{
//自己等于自己就可以不用赋值了
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
//比如:d1=d2表达式的返回对象应该为d1,也就是*this
return *this;
}
//赋值运算符重载,但其实在Date类型里面不写也没影响,跟拷贝构造函数处理内置类型原理一样
//思考联想方法也一样,不再说了
void Print()
{
cout << _year << "/" << _month << "/" << _day << '\n';
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(2025, 8, 1);
d1 = d2;
Date d3;
d3 = d1 = d2;//从右往左
d1.Print();
d2.Print();
d3.Print();
// 需要注意这里是拷⻉构造,不是赋值重载
// 要牢牢记住赋值重载完成两个已经存在的对象直接的拷⻉赋值
// 而拷⻉构造用于一个对象拷⻉初始化给另⼀个要创建的对象
Date d4 = d1;//因为拷贝构造如果写出这样就有点容易混
//Date d4(d1);//写成这样的时候不太容易混淆
return 0;
}
完整源代码:
cpp-exclusive-warehouse: 【CPP知识学习仓库】 - Gitee.com
往期回顾:
《吃透 C++ 类和对象(上):封装、实例化与 this 指针详解》
《吃透 C++ 类和对象(中):构造函数与析构函数的核心逻辑》
结语:本篇博客就到此结束了,在学完类和对象的这些知识后,虽然还没学完,但博主后续会先更新实现一个完整的日期类的博客,这个还是有点难度的。大家可以看完之后自己去试一下,检验一下自己的学习成果,如果文章对你有帮助的话,欢迎评论,点赞,收藏加关注,感谢大家的支持。