如果一个构造函数的第⼀个参数是自身类类型的引用,且任何额外的参数都有默认值,则此构造函数也叫做拷贝构造函数,也就是说拷贝构造是⼀个特殊的构造函数。
拷贝构造使用于当前类型的对象去初始化当前类型的另一个要创建的对象,换句话来说就是当你完成的是自身这个类型的拷贝初始化就会用拷贝构造,拷贝构造函数是构造函数的一个重载。
拷贝构造函数的特点:
既然拷贝构造函数是构造函数的一个重载,接下来,我们来写一个拷贝构造函数:
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day =day;
}
~Date()
{
cout << "~Date()" << endl;
}
Date(Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day<<endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2025,8,4);
Date d2(d1);//拷贝构造
d2.Print();
return 0;
}
我们看到,在拷贝构造特点中的第二点中写到:拷贝构造函数的第⼀个参数必须是类类型对象的引用。
那为什么要用引用传参?为什么不能用传值传参?

有人会说,func(d1)不是在调用函数吗?怎么跑到拷贝构造上面去了 ? 在调用一个函数前,编译器会先完成传参(传值传参就是拷贝,对于自定义类型的拷贝,都是调用拷贝构造来完成拷贝的),然后再去调用这个函数
总结: C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成。
如果拷贝构造函数使用传值调用,会形成无穷递归:

每次要调用靠别构造函数之前要进行传值传参,传值传参是一种拷贝,又形成一个新的拷贝构造,调用新的拷贝构造之前要进行传值传参,传值传参是一种拷贝,又形成一个新的拷贝构造,以此下去,就形成了无穷递归,但是当我们使用引用传参时,就不需要进行拷贝构造,这样就不会形成无穷递归了,所以我们使用引用传参。
当我们使用拷贝构造函数的时候,引用传参建议使用const:
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day =day;
}
~Date()
{
cout << "~Date()" << endl;
}
//加上const可以避免权限放大
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day<<endl;
}
private:
int _year;
int _month;
int _day;
};
void func(Date d)
{
}
int main()
{
Date d1(2025,8,4);
func(d1);
return 0;
}
通过上面的学习,我们知道拷贝构造函数是适用于当前类型的对象去初始化当前类型的另一个要创建的对象,比如上面的Date类,那如果说拷贝构造仅仅适用于这种一一拷贝,那拷贝构造是不是有点太简单了。接下来,我们来看一下稍微难点的应用:
class Stack
{
public:
Stack(int n=4)
{
_a = (int*)malloc(sizeof(int) * n);
_top = 0;
_capacity = n;
}
~Stack()
{
if (_a)
{
free(_a);
_a = nullptr;
_top = 0;
_capacity = 0;
}
}
Stack(const Stack& s)
{
_a = s._a;
_top = s._top;
_capacity = s._capacity;
}
private:
int* _a;
int _top;
int _capacity;
};
int main()
{
Stack s1;
Stack s2(s1);
return 0;
}ok,我们创建了一个栈,然后使用拷贝构造函数给s2进行初始化,在这个初始化的过程中会发生什么不可预期的东西呢?

我们看到红方框内的地址是一样的。那就有同学会问了,地址一样会有什么问题吗?ok,当你使用编译器进行调试的时候,就会发现编译器崩了:

这是什么原因呢?当我们调试编译器的时候,返现拷贝构造时没有发生任何错误,但是当我们执行到第二次析构函数的时候,此时发生了报错,只是因为编译器对空间进行了两次释放(同一块空间)
那为什么会对空间进行两次释放呢?

那我们该如何解决呢? 对于这种情况,我们要使用深拷贝:不仅仅是对成员拷贝,还是对指向资源空间数据进行拷贝
class Stack
{
public:
Stack(int n=4)
{
_a = (int*)malloc(sizeof(int) * n);
_top = 0;
_capacity = n;
}
~Stack()
{
if (_a)
{
free(_a);
_a = nullptr;
_top = 0;
_capacity = 0;
}
}
//深拷贝
Stack(const Stack& s)
{
_a = (int*)malloc(sizeof(int) * s._capacity);
if (_a == nullptr)
{
perror("malloc fail!");
exit(1);
}
//将数据拷贝过去
memcpy(_a,s._a,sizeof(int)* s._capacity);
_top = s._top;
_capacity = s._capacity;
}
private:
int* _a;
int _top;
int _capacity;
};
int main()
{
Stack s1;
Stack s2(s1);
return 0;
}

前面我们学习的构造函数和析构函数,当我们不写时,编译器自动生成的构造函数和析构函数对内置类型成员不做处理,自定类型成员会调用他的析构函数。但是拷贝构造有点不同:若未显式定义拷贝构造,编译器会生成自动生成拷贝构造函数。自动生成的拷贝构造对内置类型成员变量会完成值拷贝/浅拷贝(⼀个字节⼀个字节的拷贝),对自定义类型成员变量会调用他的拷贝构造。


Stack这样的类,虽然也都是内置类型,但是_a指向了资源,编译器自动生成的拷贝构造完成的值拷贝/浅拷贝不符合我们的需求,所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。

总结:像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的拷贝构造就可以完成需要的拷贝,所以不需要我们显示实现拷贝构造。像Stack这样的类,虽然也都是内置类型,但是_a指向了资源,编译器自动生成的拷贝构造完成的值拷贝/浅拷贝不符合我们的需求,所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。像MyQueue这样的类型内部主要是自定义类型Stack成员,编译器自动生成的拷贝构造会调用Stack的拷贝构造,也不需要我们显示实现MyQueue的拷贝构造。
这里还有一个小技巧,如果⼀个类显示实现了析构并释放资源,那么他就需要显示写拷贝构造,否则就不需要。(一般析构函数和拷贝构造是绑在一起的)
在前面的学习中,我们学习到引用返回适用于返回值出了作用域还存在的对象,有些同学对此还不是很理解,通过今天的学习,我们可以举出一个更直接的例子:

通过上面,我们知道了传值返回会产生一个临时对象调用拷贝构造,传值引用返回,返回的是返回对象的别名(引用),没有产生构造。但是如果返回对象是一个当前函数局部域的局部对象,函数结束就销毁了,那么使用引用返回是有问题的,这时的引用相当于一个野引用,类似一个野指针一样。传引用返回可以减少拷贝,但是一定要确保返回对象,在当前函数结束后还在,才能用引用返回。 总结:若出了作用域,返回对象不销毁,用引用返回;若返回对象是局部对象,用传值返回
拷贝构造的写法:

所谓的运算符重载就是自定义类型使用运算符,内置类型是可以直接使用运算符的,但是自定义类型不可以,编译器也不知道自定义类型如何使用各种运算符,所以在默认情况下,自定义类型是不支持运算符的,太复杂了,编译器也不知道咋算。
但是当运算符被用于自定义类型的对象时,C++是允许我们通过运算符重载的形式指定新的含义。C++规定自定义类型对象使用运算符时,必须转换成调用对应运算符重载,若没有对应的运算符重载,则会编译报错。
总结一下:就是说默认是不支持的,但是实现一个运算符重载的函数还是支持的。(注意:这里的运算符重载和函数重载不一样)
运算符重载的特点:
我们先来看运算符重载的前两个特点,通过这两个特点,我们就可以写出一个运算符重载的函数:
class Date
{
public:
Date(int year=1,int month=1,int day=1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
bool operator==(Date x1, Date x2)
{
return x1._year == x2._year &&
x1._month == x2._month &&
x1._day == x2._day;
}
int main()
{
Date d1(2025, 8, 1);
Date d2(2025, 10, 1);
d1 == d2;
return 0;
}如果按照上面的写法,好像会有问题:

那我们该如何解决上图中的问题呢?难道要将成员变量全部变成公有?这很明显是不可能的。
我们可以将运算符重载的函数写成成员函数,这样不就可以了嘛,ok,根据三四两个特点就可以写出一个正确的运算符重载的函数:
class Date
{
public:
bool operator==(const Date& d)
{
return _year == d._year &&
_month == d._month &&
_day == d._day;
}
Date(int year=1,int month=1,int day=1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2025, 8, 1);
Date d2(2025, 10, 1);
cout << (d1 == d2)<<endl;//d1==d2 -->隐式调用
d1.operator==(d2);//显示调用
return 0;
}有同学会有疑问,为什么要这么写呢?接下来,我们一起来看一下这段代码:

剩余特点:
特点二:“ .* ”是什么?

特点三:重载操作符至少有一个类类型参数(自定义类型),不能通过运算符重载改变内置类型对象的含义,如: int operator+(int x, int y) ,运算符重载的参数还可以是其他类型,例如:日期加天数:int operator+(const Date& d , int x);
赋值运算符重载是⼀个默认成员函数,用于完成两个已经存在的对象直接的拷贝赋值,这里要注意跟拷贝构造区分,拷贝构造用于⼀个对象拷贝初始化给另⼀个要创建的对象。
赋值运算符重载的特点:
话不多说,直接上代码:
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date& operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
void print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2025, 8, 5);
Date d2(2025, 8, 4);
d2 = d1;//隐式调用
d2.operator=(d1);//显示调用
d2.print();
return 0;
}
特点2:有返回值,且建议写成当前类类型引用,引用返回可以提高效率,有返回值目的是为了支持连续赋值场景。
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date& operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
void print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2025, 8, 5);
Date d2(2025, 8, 4);
Date d3(2025, 8, 3);
d1 = d2 = d3;//连续赋值
d1=d1;//自己给自己赋值
return 0;
}
特点3:没有显式实现时,编译器会自动生成⼀个默认赋值运算符重载,默认赋值运算符重载行为跟默认拷贝构造函数类似,对内置类型成员变量会完成值拷贝/浅拷贝(⼀个字节一个字节的拷贝),对自定义类型成员变量会调用他的赋值重载函数。


总结:像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的赋值运算符重载就可以完成需要的拷贝,所以不需要我们显示实现赋值运算符重载。像Stack这样的类,虽然也都是内置类型,但是_a指向了资源,编译器自动生成的赋值运算符重载完成的值拷贝/浅拷贝不符合我们的需求,所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)(这里的拷贝后面再说)。像MyQueue这样的类型内部主要是自定义类型Stack成员,编译器自动生成的赋值运算符重载会调用Stack的赋值运算符重载,也不需要我们显示实现MyQueue的赋值运算符重载。
这里还有一个小技巧,如果⼀个类显示实现了析构并释放资源,那么他就需要显示写赋值运算符重载,否则就不需要。
在前面学习运算符重载时,其中有这么一个特点:重载++运算符时,有前置++和后置++,运算符重载函数名都是operator++,无法很好的区分。C++规定,后置++重载时,增加⼀个int形参,跟前置++构成函数重载,方便区分。 我们一起来看一下

调用前置++和后置++的完整代码: Date.h文件
#pragma once
#include<iostream>
using namespace std;
class Date
{
public:
//全缺省的构造函数
Date(int year = 1, int month = 1, int day = 1);
//拷贝构造函数
Date(const Date& d);
//赋值运算符重载
Date& operator=(const Date& d);
//打印
void print();
// 日期+=天数
Date& operator+=(int day);
//前置++
Date& operator++();
//后置++
Date operator++(int);
private:
int _year;
int _month;
int _day;
};Date.c文件
#include"Date.h"
//全缺省的构造函数
Date::Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
//拷贝构造函数
Date::Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
//赋值运算符重载
Date& Date::operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
// 日期+=天数
Date& Date::operator+=(int day)
{
_day += day;
return *this;
}
//前置++ -> 返回++之后的值
Date& Date::operator++()
{
*this += 1;
return *this;
}
//后置++
Date Date::operator++(int)
{
Date tmp(*this);
*this += 1;
return tmp;
}
void Date::print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}test.c文件
#include"Date.h"
int main()
{
Date d1(2025, 8, 1);
Date d2(d1);
Date d3(2025, 8, 6);
d1++;//显示调用 d1.operator++(0);
++d3;//显示调用 d3.operator++();
d1.print();
d3.print();
return 0;
}补充: 建议使用拷贝多的复用拷贝少的

对上面的前置++和后置++ 的函数进行修改,写出了日期+天数 和 日期+=天数 的两个代码,发现+复用+=只需要拷贝2次,而+=复用+需要拷贝5次 尽量使用前置++,拷贝次数少,效率较高!!!
Date.h
#pragma once
#include<iostream>
using namespace std;
class Date
{
public:
//全缺省的构造函数
Date(int year = 1, int month = 1, int day = 1);
//拷贝构造函数
Date(const Date& d);
//赋值运算符重载
Date& operator=(const Date& d);
//打印
void print();
//每月的天数
int GetMonthDay(int year, int month)
{
static int days[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
if (month == 2 && (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))
{
return 29;
}
else
{
return days[month];
}
}
// 日期+=天数
Date& operator+=(int day);
//日期-=天数
Date& operator-=(int day);
//日期+天数
Date operator+(int day);
//日期-天数
Date operator-(int day);
//日期-日期
int operator-(const Date& d);
//前置++
Date& operator++();
//后置++
Date operator++(int);
bool operator<(const Date& d);
bool operator<=(const Date& d);
bool operator>(const Date& d);
bool operator>=(const Date& d);
bool operator==(const Date& d);
bool operator!=(const Date& d);
private:
int _year;
int _month;
int _day;
};Date.cpp
#define _CRT_SECURE_NO_WARNINGS
#include"Date.h"
//全缺省的构造函数
Date::Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
//拷贝构造函数
Date::Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
//赋值运算符重载
Date& Date::operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
// 日期+=天数
Date& Date::operator+=(int day)
{
_day += day;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
++_month;
if (_month == 13)
{
_year++;
_month = 1;
}
}
return *this;
}
//Date& Date::operator+=(int day)
//{
// *this = *this + day;
// return *this;
//}
//日期-=天数
Date& Date::operator-=(int day)
{
_day -= day;
while (_day <= 0)
{
_month--;
if (_month == 0)
{
_year--;
_month = 12;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
//日期+天数
//Date Date::operator+(int day)
//{
// Date tmp(*this);
// tmp._day += day;
// while (tmp._day > GetMonthDay(tmp._year, tmp._month))
// {
// tmp._day -= GetMonthDay(tmp._year, tmp._month);
// ++tmp._month;
// if (tmp._month == 13)
// {
// tmp._year++;
// tmp._month = 1;
// }
// }
// return tmp;
//}
Date Date::operator+(int day)
{
Date tmp(*this);
tmp += day;
return tmp;
}
//日期-天数
Date Date::operator-(int day)
{
Date tmp(*this);
tmp._day -= day;
while (tmp._day <= 0)
{
tmp._month--;
if (tmp._month == 0)
{
tmp._year--;
tmp._month = 12;
}
tmp._day += GetMonthDay(tmp._year, tmp._month);
}
return tmp;
}
//日期-日期
int Date::operator-(const Date& d)
{
Date max = *this;
Date min = d;
int flag = 1;
if (*this < d)
{
max = d;
min = *this;
flag = -1;
}
int n = 0;
while (min != max)
{
++min;
++n;
}
return n* flag;
}
//前置++ -> 返回++之后的值
Date& Date::operator++()
{
*this += 1;
return *this;
}
//后置++
Date Date::operator++(int)
{
Date tmp(*this);
*this += 1;
return tmp;
}
//判断两个日期是否相等
bool Date::operator==(const Date& d)
{
return _year == d._year &&
_month == d._month &&
_day == d._day;
}
//判断两个日期是否不相等
bool Date::operator!=(const Date& d)
{
return !(*this == d);
}
bool Date::operator<(const Date& d)
{
if (_year < d._year)
{
return true;
}
else if (_year == d._year)
{
if (_month < d._month)
{
return true;
}
else if (_month == d._month)
{
return _day < d._day;
}
}
return false;
}
bool Date::operator<=(const Date& d)
{
return *this < d || *this == d;
}
bool Date::operator>(const Date& d)
{
return !(*this <= d);
}
bool Date::operator>=(const Date& d)
{
return !(*this < d);
}
void Date::print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}test.cpp
#define _CRT_SECURE_NO_WARNINGS
#include"Date.h"
int main()
{
Date d1(2025, 8, 7);
Date d2(d1);
Date d3(2025, 8, 6);
Date d4 = d1 -= 249;
d4.print();
d1.print();
Date d5 = d2 - 249;
d2.print();
d5.print();
/*cout << (d5.operator==(d4)) << endl;
cout << (d1.operator!=(d3)) << endl;*/
cout << (d1.operator<(d3)) << endl;
//d1++;//显示调用 d1.operator++(0);
//++d3;//显示调用 d3.operator++();
//d1.print();
//d3.print();
//Date d4 = d1 + 100;
//d1.print();
//d4.print();
//Date d5 = d3 += 100;
//d3.print();
//d5.print();
return 0;
}