C++内容往期回顾:
c++类与对象(构造和析构函数)
c++类与对象(初识2)
c++类与对象(初识)
本期内容我们将继续介绍c++类的成员函数:
一. 拷贝构造函数
拷贝构造函数(Copy Constructor)是 C++ 中类的一种特殊构造函数,用于创建一个对象是基于另一个同类型对象的副本,即:
ClassName(const ClassName& other);
1、拷贝构造函数的定义形式
class MyClass {
public:
MyClass(const MyClass& other); // 拷贝构造函数
};
为什么需要传实参的引用&呢?
不妨假设我们创建了一个这样的类 Date:
class Date{
public:
Date(int year = 2025,int month = 7, int day = 29){
_year = year;
_month = month;
_day = day;
}
void Print(){
// 普通函数
cout<< _year<<"-"<<_month<<"-"<<_day<<endl;
};
private:
int _year;
int _month;
int _day;
};
这里如果常规我们想要创建两个相同的类实例化的对象,需要这么做
int main(){
//构造一样的类
Date d1(2025,7,29);
Date d2(2025,7,29); //利用构造函数实例化两个相同的类对象
}
但是这样你相当于就需要传两次相同的参数,冗余的代码。那么如何实现拷贝构造呢?我们定义了下面这个拷贝构造函数:
Date(Date d){
_year = d._year;
_month = d._month;
_day = d._day;
}
int main(){
Date d1(2025,7,29);
//拷贝构造 构造几乎一样的类
Date d2(d1);
}
当你运行时,编译器会报错:
error: copy constructor must pass its first argument by reference 13 | Date(Date d){ | ^ | const & 1 error generated.
这里编译器告诉你,在定义拷贝构造函数时,需要传入的是引用,而不是传形参。这里其实就是考察我们对于函数调用的理解,普通的传值调用,传入的参数会在栈上再进行临时的拷贝,也即是临时在拷贝一份Date d1,这样d1在拷贝的时候相当于又重复调用了拷贝构造,那这样不断的传值调用–就相当于无法停止的递归调用一样,是错误的。
总结:
一调用就拷贝一次,而拷贝又需要调用自己 → 递归调用 → 栈溢出(Stack Overflow)。
所以正确的拷贝构造如下:
Date(const Date& d){
_year = d._year;
_month = d._month;
_day = d._day;
}
引用相当于参数地址的别名,传入的就相当于地址。不需要拷贝。参考:c++基础关于指针和阴引用。
int main(){
//构造一样的类
Date d1(2025,7,29);
Date d2(2025,7,29); //利用构造函数实例化两个相同的类对象
//拷贝构造 构造几乎一样的类
Date d3(d1);
d3.Print();
}
输出描述:
2025-7-29
2、拷贝构造函数的调用时机
1. 用一个对象初始化另一个对象时:
MyClass obj1;
MyClass obj2 = obj1; // 调用拷贝构造函数
2. 对象作为值参数传入函数时:
void func(MyClass obj); // 形参是值传递,也会调用拷贝构造
3. 函数返回类对象时(返回值优化前):
MyClass func() {
MyClass tmp;
return tmp; // 可能会调用拷贝构造
}
3、拷贝构造函数的默认行为
如果你不显式定义拷贝构造函数,编译器会默认生成一个,进行逐个成员变量的浅拷贝(shallow copy)。
我们这里不定义任何的拷贝构造函数
class Date{
public:
void Print(){
// 普通函数
cout<< _year<<"-"<<_month<<"-"<<_day<<endl;
};
public:
int _year;
int _month;
int _day;
};
主函数定义类对象:
int main(){
Date d1;
d1._year = 2025;
d1._month = 7;
d1._day = 29;
// 编译器生成默认拷贝构造函数
Date d3 = d1;
d3.Print();
}
输出描述:
2025-7-29
默认的拷贝函数同样可以帮我完成实例化同一类对象,那么我们是否需要自定义拷贝构造函数呢?
这里我们就要理解两个概念:”浅拷贝和深拷贝“
浅拷贝(默认行为):
仅复制指针地址,两个对象共享同一块内存,可能导致 二次释放或悬空指针。
这里我们没有定义拷贝构造函数
class Stack{
public:
Stack(int capactity=10){
_arr = (int*)malloc(sizeof(int)*capactity);
cout<<"malloc"<<_arr<<endl;
}
~Stack(){
cout<<"free"<<_arr<<endl;
free(_arr);
_arr = nullptr;
}
private:
int* _arr;
int _size;
int _capacity;
};
int main(){
Stack s1;
Stack s2 = s1;
}
输出描述:
malloc0x126605fc0 free0x126605fc0 free0x126605fc0 copy_construct(34585,0x204ad1f00) malloc: Double free of object 0x126605fc0 copy_construct(34585,0x204ad1f00) malloc: *** set a breakpoint in malloc_error_break to debug zsh: abort "/Users/junye/Desktop/cplusplus/"copy_construct
原因:我们可以看的这里程序崩溃了,我们free了两次同一 0x126605fc0 的地址导致程序崩溃,这是因为默认的拷贝构造函数是“浅拷贝”仅仅只是复制了指针地址,也就是两个对象共用同一块内存,当调用析构函数清理内存时,相当于对同一地址清理了两次导致程序崩溃。
深拷贝(自定义的构造函数):
为每个对象申请独立内存,复制数据,互不影响。
举个例子:
class Stack{
public:
Stack(int capactity=10){
_arr = (int*)malloc(sizeof(int)*capactity);
cout<<"malloc"<<_arr<<endl;
}
~Stack(){
cout<<"free"<<_arr<<endl;
free(_arr);
_arr = nullptr;
}
Stack(const Stack& s){
_size = s._size;
_capacity = s._capacity;
_arr = (int*)malloc(sizeof(int)*_capacity);
memcpy(_arr,s._arr,_capacity);
cout<<"copy construct "<<_arr<<endl;
}
private:
int* _arr;
int _size;
int _capacity;
};
int main(){
Stack s1;
// Stack s2 = s1;// 默认拷贝构造
Stack s2 = Stack(s1);//自定义拷贝构造
}
输出描述:
malloc0x14f605fc0 copy construct 0x14f605e70 free0x14f605e70 free0x14f605fc0
这里我们定义了自己的拷贝构造函数–>深拷贝,给我们拷贝对象单独分配内存,然后复制数据,两者在析构的时候不会互相影响。
4、运算符重载(operator)
定义:运算符重载是 C++ 提供的一种机制,允许程序员自定义已有运算符(如 +、-、=、<<、[] 等)在用户自定义类型(类/结构体)中的行为,使得这些类型的对象也能像内建类型那样自然地参与运算。主要针对于自定义类型的运算。
举个例子,比如我们要比较两个日期对象是否相等,而日期是我们自定义的类,我们就要自定义运算符去比较这两个对象。假如你定义了一个复数类:
Complex a(1, 2); Complex b(3, 4); Complex c = a + b; // 这句默认是非法的!因为系统的运算符只只针对于内置类型。
基本语法格式
返回类型 operator运算符(参数列表) { // 自定义运算行为 }
bool operator ==(const Complex& a,const Complex& b){
// 自定义运算行为
}
这样我们就定义一个类的运算符,当然这样的运算符不止一个,当定义多个运算符时,有着不一致的参数,也就构成了“运算符重载”.
bool operator==(const Date& D1,const Date& D2){
return D1._year == D2._year && D1._month == D2._month
&& D1._day == D2._day;
};
int main(){
//构造一样的类
Date d1(2025,7,29);
//利用构造函数实例化两个相同的类对象
//拷贝构造 构造几乎一样的类
// Date d3(d1);
Date d3 = Date(d1);
if(operator==(d3,d1)){
cout<< "True" << endl;
}
}
输出描述:
True
注意这里我们定义的类的成员变量都是公有的,但实际上我们类的成员变量一般都为私有的。所以上述定义运算符通常不可用。那么如何定义一个更好的运算符呢?
我们将定义的运算符定义在类内部。如下这样就可以访问私有的成员变量,但是定义在类内部,我们在调用运算符比较两个同类时,不需要传入两个类的引用,因为类的内部成员函数隐含了“this 指针”,所用调用和定义规则如下:
class Date{
public:
Date(int year = 2025,int month = 7, int day = 29){
_year = year;
_month = month;
_day = day;
}
void Print(){
// 普通函数
cout<< _year<<"-"<<_month<<"-"<<_day<<endl;
};
Date(const Date& d1){
_year = d1._year;
_month = d1._month;
_day = d1._day;
}
// 定义类的运算符
bool operator==(const Date& D2){
return _year == D2._year && _month == D2._month
&& _day == D2._day;
};
public:
int _year;
int _month;
int _day;
};
//主函数
int main(){
//构造一样的类
Date d1(2025,7,29);
//拷贝构造 构造几乎一样的类
Date d3 = Date(d1);
if(d3.operator==(d1)){
cout<< "True" << endl;
}
}
赋值运算符的实现:
赋值运算符的本质其实也就是“深拷贝”。
class MyString {
private:
char* _str;
public:
MyString(const char* str = "") {
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
// 自定义赋值运算符
MyString& operator=(const MyString& other) {
if (this == &other)
return *this; // 1. 处理自赋值
delete[] _str; // 2. 释放原有资源
_str = new char[strlen(other._str) + 1]; // 3. 分配新空间
strcpy(_str, other._str); // 4. 拷贝数据
return *this; // 5. 返回自身引用
}
~MyString() {
delete[] _str;
}
};
注意:以下运算符无法重载
有 5个运算符不能重载:
-
::(作用域解析)
-
.(成员访问)
-
.*(成员指针访问)
-
sizeof
-
typeid。
-
5、完善日期类的实现
1. 类中成员变量的定义:
年,月,日,注意这里我们要考虑,平闰年,每个月的天数。
class Date{
public:
void Print(){
// 普通函数
cout<< _year<<"-"<<_month<<"-"<<_day<<endl;
};
private:
int _year;
int _month;
int _day;
};
这里我们定义了一个基本的Date类里面包括,年月日的成员变量的定义,以及打印相应的变量。
测试函数:
int main(){
Date d2;
d2.Print();
}
输出描述:
2-75841712-2
这里我们简单实例化了一个类对象,没有定义类的构造函数,使用默认的构造函数,则类的成员变量都为随机值。
2. 定义构造函数以及拷贝构造函数(初始化类对象)
初始化对象时,我们需要检查输入的参数是否满足日期的标准包括检查平闰年,每个月的天数。
//平闰年,每个月的天数
int GetMonthDay(int year, int month){
static int monthDays[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;
}
return monthDays[month];
}
//构造函数
Date(int year = 2025,int month = 7, int day = 29){
if(year>=0 && month>=1 &&month<=12 && day>= 1 && day<= GetMonthDay(year,month)){
_year = year;
_month = month;
_day = day;
}
else{
cout << "非法日期" << endl;
}
}
//拷贝构造函数
Date(const Date& d1){
_year = d1._year;
_month = d1._month;
_day = d1._day;
}
这里定义了我们初始化类对象所需要的函数。测试的主函数如下:
int main(){
//构造函数
Date d2;
d2.Print();
//拷贝构造
Date d3(d2);
d3.Print();
//初始化
Date d4(2036,13,67);
}
输出描述如下:
2025-7-29 2025-7-29 非法日期
3. 运算符重载的定义
1️⃣判断两个日期是否一致“==,!=”
//隐藏了this指针,相当于bool operator==(Date* &D1,const Date& D2)
bool operator==(const Date& D2){
return _year == D2._year && _month == D2._month
&& _day == D2._day;
};
bool operator != (const Date& D2){ // bool operator!=(Date* this, const Date& D2)
return !(*this == D2);
};
测试函数
int main(){
//构造函数
Date d2;
d2.Print();
//拷贝构造
Date d3(d2);
d3.Print();
if(d3 == d2){
cout<<"true"<<endl;
}
}
输出描述:
true
2
2️⃣判断两个日期的大小“>, >=,<,<=”
//调用时d1. operator==(&D1,D2);
bool operator > (const Date& D2){ // bool operator <= (Date* this, const Date &d);
if(_year>D2._year){
return true;
}
if(_year == D2._year){
if(_month > D2._month){
return true;
}
else if(_month == D2._month){
if(_day > D2._day){
return true;
}
}
}
return false;
};
//逻辑运算符复用
// 在实现 >= 的运算符时,可以使用已实现的运算符如>和 == 运算符。>= 等价于 > || ==;
bool operator>=(const Date& D2){ // bool operator>=(Date* this, const Date& D2)
return *this > D2 || *this == D2;
};
bool operator<(const Date& D2){ // bool operator>=(Date* this, const Date& D2)
return !(*this > D2);
};
bool operator <= (const Date& D2){ // bool operator>=(Date* this, const Date& D2)
return !(*this >= D2);
};
这里我们运用以及定义的运算符来定义其他运算符,这是一种非常方便的操作。
测试函数:
int main(){
//构造函数
Date d2;
d2.Print();
//拷贝构造
Date d3(2026,12,11);
if(d3>d2){
cout<<"True"<<endl;
}
Date d4(1998,7,8);
if(d4<d2){
cout<<"True"<<endl;
};
}
输出描述:
2025-7-29 True True
3️⃣. 日期的加减操作“日期-日期
日期-日期
//先将日期转换成天数,再进行运算。
int DateToDays(){
int total = 0;
for(int y = 1; y < _year; ++y)
total += ((y % 4 == 0 && y % 100 != 0) || y % 400 == 0) ? 366 : 365;
for(int m = 1; m < _month; ++m)
total += GetMonthDay(_year, m);
total += _day;
return total;
};
int operator-(Date& d2){
return this->DateToDays() – d2.DateToDays();
}
测试函数:
int main(){
//构造函数
Date d2;
d2.Print();
Date d3(2025,8,16);
cout<<"2025,8,16 – 2025,7,30 = "<< d3-d2 <<endl;
}
输出描述:
2025,8,16 – 2025,7,30 = 17
4️⃣日期+天数,-天数
Date operator+(int Days){
Date ret(*this); //拷贝构造一个ret
int Day_std = GetMonthDay(ret._year,ret._month);
ret._day += Days;
while (ret._day > Day_std)
{
ret._day-=Day_std;
ret._month++;
if(ret._month>12){
ret._month = 1;
ret._year++;
}
Day_std = GetMonthDay(ret._year,ret._month);
}
return ret;
};
Date operator-(int Days){
Date ret(*this);
int Day_std = GetMonthDay(ret._year,ret._month);
ret._day-=Days;
while(ret._day<=0){
ret._month–;
if(ret._month<=0){
ret._month = 12;
ret._year–;
}
int Day_std = GetMonthDay(ret._year,ret._month);
ret._day+=Day_std;
}
return ret;
};
测试函数:
int main(){
//构造函数
Date d2;
d2.Print();
Date d3(2025,8,16);
Date d4 = d2+10;
d4.Print();
Date d5 = d3-10;
d5.Print();
}
输出结果:
2025-7-30 2025-8-9 2025-8-6
5️⃣. 日期+=days,-=days
Date& operator-=(int Days){
int Day_std = GetMonthDay(_year,_month);
_day-=Days;
while(_day<=0){
_month–;
if(_month<=0){
_month = 12;
_year–;
}
int Day_std = GetMonthDay(_year,_month);
_day+=Day_std;
}
return *this;
};
//————————————————————-"
Date& operator+=(int Days){
int Day_std = GetMonthDay(_year,_month);
_day += Days;
while (_day > Day_std)
{
_day-=Day_std;
_month++;
if(_month>12){
_month = 1;
_year++;
}
Day_std = GetMonthDay(_year,_month);
}
return *this;
};
测试函数:
int main(){
//构造函数
Date d2;
d2.Print();
Date d3(2025,8,16);
Date d4 = d2+=10;
d4.Print();
Date d5 = d3-=10;
d5.Print();
}
输出描述:
2025-7-30 2025-8-9 2025-8-6
6️⃣. 日期的前置++,–,后置++,–
//前置++
Date& operator++(){
int Day_std = GetMonthDay(_year,_month);
_day++;
if(_day>Day_std){
_day-=Day_std;
_month++;
if(_month>12){
_month = 1;
_year++;
}
}
return *this;
};
//————————————————————-"
//后置++
Date operator++(int) {
Date ret(*this); // 保存原始值
++(*this); // 调用前置 — 逻辑
return ret; // 返回原始值
};
//————————————————————-"
//前置–
Date& operator–(){
_day–;
if(_day<=0){
_month–;
if(_month<=0){
_month = 12;
_year–;
}
int Day_std = GetMonthDay(_year,_month);
_day+= Day_std;
}
return *this;
};
//————————————————————-"
//后置–
Date operator–(int) {
Date ret(*this); // 保存原始值
–(*this); // 调用前置 — 逻辑
return ret; // 返回原始值
};
测试主函数:
int main(){
Date d1(2025, 7, 30);
d1.Print(); // 输出:2025-7-30
Date d2 = d1++;
d2.Print(); // 输出:2025-7-30(旧值)
d1.Print(); // 输出:2025-7-31(加了一天)
Date d3 = d1++;
d3.Print(); // 输出:2025-7-31(旧值)
d1.Print(); // 输出:2025-8-1(跨月)
Date d4 = –d1;
d4.Print(); // 输出:2025-7-31
d1.Print(); // 输出:2025-7-31
Date d5 = d1–;
d5.Print(); // 输出:2025-7-31
d1.Print(); // 输出:2025-7-30
return 0;
}
输出结果:
2025-7-30 2025-7-30 2025-7-31 2025-7-31 2025-8-1 2025-7-31 2025-7-31 2025-7-31 2025-7-30
关于日期类的实现我们就到这里了。
本节内容有关于拷贝函数,运算符重载的讲解就到这了结束了。拷贝函数以及类的运算符定义是非常重要对于理解类与对象的核心知识点。
6、总结
拷贝构造函数是用于创建对象时的初始化,而赋值运算符重载是用于对象之间的赋值;两者都是处理“对象拷贝”的关键机制,通常需要成对实现以保证资源安全。
评论前必须登录!
注册