如果一个构造函数的第⼀个参数是自身类类型的引用,且任何额外的参数都有默认值,则此构造函数也叫做拷贝构造函数,也就是说拷贝构造是⼀个特殊的构造函数。
关于无穷递归图示:

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(int year=1,int month=1,int day=1)
{
cout << _year << '/' << _month << '/' << _day << '\n';
}
private:
int _year;
int _month;
int _day;
};
//自定义类型传值传参要调用拷贝构造
//void Func(Date& d)
void Func1(Date d)
{
}
int main()
{
Date d1(2025,9,3);
//拷贝构造
Date d2(d1);
//Date d2(&d1);
const Date d3(2025, 9, 4);
//Date d4(d3);如果拷贝构造不加const,这条语句就是错的,属于权限放大
Func1(d1);
d1.Print();
d2.Print();
return 0;
}
关于对内置类型处理和深浅拷贝的相关示例(栈):
#include<iostream>
using namespace std;
typedef int STDataType;
class Stack
{
public:
Stack(int n = 4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (nullptr == _a)
{
perror("malloc申请空间失败");
return;
}
_capacity = n;
_top = 0;
}
//Stack(Stack& s)//浅拷贝,一个对象修改会影响类一个对象,释放空间会释放两次
//{
// _a = s._a;
// _top = s._top;
// _capacity = s._capacity;
//}
Stack(const Stack& st)
{ // 需要对_a指向资源创建同样⼤的资源再拷⻉值
_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;
}
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");
return;
}
_a = tmp;
_capacity = newcapacity;
}
_a[_top++] = x;
}
~Stack()
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
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;
}
完整源代码:
往期回顾:
《一篇拿下!C++:类和对象(上)、封装、实例化和this指针详解》
总结:本篇博客就到此结束了,在学完类和对象的这些知识后,虽然还没学完,但博主后续会先更新实现一个完整的日期类的博客,这个还是有点难度的。大家可以看完之后自己去试一下,检验一下自己的学习成果,如果文章对你有帮助的话,欢迎评论,点赞,收藏加关注,感谢大家的支持。