目录
[一. 类的默认成员函数](#一. 类的默认成员函数)
[二. 构造函数](#二. 构造函数)
[三. 析构函数](#三. 析构函数)
[四. 拷贝构造函数](#四. 拷贝构造函数)
[五. 赋值运算符重载](#五. 赋值运算符重载)
[1. 运算符重载](#1. 运算符重载)
[2. 赋值运算符重载](#2. 赋值运算符重载)
[3. 日期类实现](#3. 日期类实现)
[日期 +/ -天 :](#日期 +/ -天 :)
[日期前置 / 后置自增 / 自减:](#日期前置 / 后置自增 / 自减:)
[流运算符重载 (输入 / 输出):](#流运算符重载 (输入 / 输出):)
一. 类的默认成员函数
用户没有显式实现,编译器会自动生成的成员函数称为默认成员函数 。在 C++ 里,只要你写了一个类,就算什么成员函数都不写,编译器也会自动帮你生成 6个基础的默认成员函数。
这 6 个函数可以分成 3 大类,每一类都对应对象生命周期里的一个关键场景:
初始化和清理类:
- 构造函数:对象被创建 的时候自动调用,主要完成初始化工作 ,比如++给成员变量赋初值++。
- 析构函数:对象被销毁 的时候自动调用,主要完成清理工作 ,比如++释放所申请的内存资源++。
拷贝复制类:
- 拷贝构造函数:用一个已经存在的同类对象 ,初始化创建一个新对象 ,比如++Student s2 (s1) ;++
- 赋值重载函数:把一个已经存在的对象,赋值给另一个同样已经存在的对象 ,比如++s2 = s1 ;++
取地址重载类:
- 分别给普通对象 和const对象提供取地址的功能,这两种平时用不到,很少需要自己实现。
二. 构造函数
构造函数是 C++ 类中一种特殊的成员函数,它的核心任务并非为对象开辟内存空间(局部对象的空间在栈帧创建时就已分配完成),而是在对象实例化时 对其进行初始化 ,本质是替代早期手动编写的Init初始化函数,并且凭借自动调用的特性,让初始化过程更安全、更便捷。
下面借用一份代码来分点列举出构造函数的特点:
cpp
#include<iostream>
using namespace std;
class Date
{
public:
//⽆参构造函数
Date()
{
_year = 1;
_month = 1;
_day = 1;
}
//带参构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
//全缺省构造函数 //与无参构造函数无法同时存在,因为构成函数重载,存在调用歧义
/*Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}*/
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Print(); //函数名与类名相同,自动调用
Date d2(2004, 7, 28); //无参数不加括号,带参数要加,否则与函数声明无法区分开
d2.Print();
Date d3();
return 0;
}
① 构造函数的函数名必须与所属类的类名完全相同。
代码里的类名叫 Date ,所以它的构造函数名字也必须是 Date,其他任意自定义名称都不可。
cpp
class Date
{
public:
// 正确:函数名和类名完全一致
Date()
{}
Date(int year, int month, int day)
{}
// 错误示例:名字和类名不一样,编译器不会把它当成构造函数
void InitDate()
{}
};
② 构造函数没有返回值 ,既不需要指定具体的返回类型 ,也无需使用 void 关键字声明。
看代码里的两个构造函数,前面什么返回类型都没写,不用加int、void等等返回类型,如果加了会报错:++C2533 "Date": 构造函数不能有返回类型++。
cpp
// 正确写法:什么都不写
Date()
{}
Date(int year, int month, int day)
{}
// 错误写法:写了void或其他返回类型
void Date()
{} //错咯
int Date()
{} //错咯
③ 在对象实例化的过程中,系统会自动调用对应的构造函数。
在主函数里创建对象时,不需要手动调用构造函数,编译器会根据你写的创建方式,自动匹配并调用对应的构造函数。
cpp
int main()
{
//自动调用无参构造函数
Date d1;
//自动调用带参构造函数
Date d2(2004, 7, 28);
//这里d1.Print()只是打印数据,不是调用构造函数
d1.Print();
d2.Print();
}
④ 构造函数支持重载,可以定义多个参数列表不同的版本。
比如代码的下图部分,用了三种函数重载。
cpp
class Date
{
public:
//无参构造函数
Date()
{
_year = 1;
_month = 1;
_day = 1;
}
//带三个参数的构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
//全缺省构造函数
/*Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}*/
};
⑤ 如果类中没有显式定义任何构造函数,C++ 编译器会自动生成一个无参的默认构造函数;一旦用户显式定义了构造函数,编译器将不再自动生成默认构造函数。
当类里面一个构造函数都不定义时:
cpp
class Date {
public:
void Print()
{}
private:
int _year, _month, _day;
};
int main()
{
Date d1; //编译器自动生成无参默认构造,这里可以正常编译
}
当类里面存在定义的构造函数时 (只有带参构造,但是没定义无参构造) :
cpp
class Date {
public:
//只写了带参构造,没写无参构造
Date(int year, int month, int day)
{}
void Print()
{}
private:
int _year, _month, _day;
};
int main()
{
Date d1; //编译报错:没有合适的默认构造函数可用
Date d2(2004,7,28); //正常调用带参构造
}
⑥ 无参构造函数、全缺省构造函数, 以及用户未定义构造函数时编译器自动生成的构造函数 ,都属于默认构造函数 ,且三者中只能存在一种 ,不可同时存在。无参构造函数与全缺省构造函数虽然构成函数重载,但调用时会产生歧义。默认构造函数的定义为:无需传入实参即可调用的构造函数。
其中全缺省构造函数和无参构造函数无法共存,因为会存在调用歧义,调用时编译器不知道选哪个 (匹配无参构造时不需要传任何参数,结果就是函数体内直接给定的;匹配全缺省构造时,不传参的话结果为默认值)。
报错代码:++E0399 类 "Date" 包含多个默认构造函数++。
⑦ 用户未显式定义构造函数时,编译器默认生成的构造函数,对内置类型成员变量的初始化行为没有强制要求,其初始值不确定,具体表现由编译器决定 ;对于自定义类型成员变量,会自动调用该成员变量所属类的默认构造函数进行初始化。若该自定义类型成员变量没有默认构造函数,代码编译会报错,此时需要使用初始化列表完成初始化。
注意:C++把类型分成内置类型 (基本类型)和自定义类型 。内置类型就是语言提供的原生数据类型, 如:++int / char / double / 指针++ 等;自定义类型就是我们使用++class / struct++等关键字自己定义的类型。
下面用一份代码来对内置类型和自定义类型作以区分:
Stack类的构造 (内置类型):
cpp
class Stack
{
public:
//这是一个全缺省构造函数,也是默认构造函数
Stack(int n = 4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (nullptr == _a)
{
perror("malloc申请空间失败");
return;
}
_capacity = n;
_top = 0;
}
private:
STDataType* _a; //指针(内置类型)
size_t _capacity;//size_t(内置类型,本质是unsigned int)
size_t _top; //size_t(内置类型)
};
- 其构造函数是默认构造函数 (全缺省参数,不传参也能调用)。
- 其成员变量都是内置类型 (_a是指针,_capacity和_top都是原生提供的无符号整数)。
假如把Stack的构造函数删掉 (不强制初始化 ),则编译器在运行时会自动生成一个默认构造函数,但他不会初始化_a、_capacity和_top,其运行结果是随机值 (初始值不确定),具体随机区间是由编译器运行环境的不同而决定。
总结:原代码中,Stack写了构造函数,所以 _a、_capacity和_top 会被显式初始化;如果不写构造,这些内置成员的值就是不确定的。
MyQueue类的构造 (自定义类型):
cpp
class MyQueue
{
private:
Stack pushst; //自定义类型成员(Stack类的对象)
Stack popst; //自定义类型成员
};
int main()
{
MyQueue mq; //创建MyQueue对象
return 0;
}
- 其没有写构造函数,则编译器在运行时会自动生成一个默认构造函数。
- 其成员变量都是自定义型 (pushst、popst都是自定义Stack类的对象)。
当创建 MyQueue mq; 时,该默认构造函数会自动依据Stack 类的默认构造函数 Stack (int n = 4) 来构造pushst和popst成员,将其_capacity设置为4,_top设置为0。
如果 Stack 类中不存在默认构造函数, 则 MyQueue 类的默认构造函数无法自动初始化 pushst 和 popst 成员,此时则需要使用初始化列表进行显示初始化成员变量。
三. 析构函数
析构函数与构造函数功能相反,它的核心作用并非销毁对象本身 (如局部对象的栈空间会随函数结束自动释放) ,而是在对象销毁时由 C++ 自动调用,完成对象中资源的清理与释放工作 ,其功能类似 Stack 类的 Destroy 方法;而像 Date 这类无资源需要释放的类,则不需要显式定义析构函数。
下面借用一份代码来分点列举出构造函数的特点:
cpp
#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()
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
// 两个Stack实现队列
class MyQueue
{
public:
//编译器默认⽣成MyQueue的析构函数调⽤了Stack的析构,释放的Stack内部的资源
// 显⽰写析构,也会⾃动调⽤Stack的析构
/*~MyQueue()
{
free();
cout << "~MyQueue()" << endl;
}*/
private:
Stack pushst;
Stack popst;
};
int main()
{
Stack st;
MyQueue mq;
return 0;
}
① 析构函数的函数名由 " ~ " 加上类名构成。
cpp
class Stack
{
public:
//构造函数:类名
Stack(int n = 4)
{}
//析构函数:~ + 类名
~Stack()
{
cout << "~Stack()" << endl;
free(_a); //释放动态申请的内存
_a = nullptr;
_top = _capacity = 0;
}
};
MyQueue 类里的 ~MyQueue() 也是同理。
② 析构函数无参数 ,也无返回值,无需使用 void 声明。
cpp
//正确写法:无参数、无返回值
~Stack()
{}
~MyQueue()
{}
//错误写法:
void ~Stack()
{} //不能写void声明
~Stack(int n)
{} //不能带参数
int ~Stack()
{
return 0; //不能有返回值
}
C++ 规定:析构函数是自动调用的,无法手动传参,也不需要返回任何数据。
③ 一个类只能定义一个析构函数 ;若用户未显式定义,编译器会自动生成默认析构函数。
- 析构函数不能重载,一个类里只能有一个析构函数。
- 如果你不写析构函数,编译器会自动生成一个默认析构函数,依然会正常助力你destroy。
cpp
class MyQueue
{
public:
//即使不写~MyQueue(),编译器也会自动生成默认析构函数
~MyQueue()
{
free();
cout << "~MyQueue()" << endl;
}
private:
Stack pushst;
Stack popst;
};
④ 当对象的生命周期结束时,系统会自动调用其析构函数。
cpp
int main()
{
Stack st; //构造函数被调用
MyQueue mq; //构造函数被调用
return 0;
}
函数结束时析构函数析构顺序:
先析构 mq ,再析构mq中的 Stack pushst 和 Stack popst ,最后析构 st。
⑤ 编译器自动生成的默认析构函数,对内置类型成员变量不做任何处理 ;对自定义类型成员变量,会自动调用其所属类的析构函数。
- 对于内置类型成员 (Stack中的_a、_capacity、_top属于原生数据类型):默认析构函数不会处理,需要手动写析构函数释放资源。
- 对于自定义类型成员 (MyQueue中的pushst、popst):默认析构函数可自动调用所属类的析构函数。
⑥ 即使用户显式定义了析构函数,类中的自定义类型成员变量依然会自动调用其析构函数 ,该行为不受用户定义析构函数的影响。
cpp
class MyQueue
{
public:
~MyQueue()
{
cout << "~MyQueue()" << endl;
//即使这里不写任何代码,pushst和popst的~Stack()也会被自动调用
}
private:
Stack pushst;
Stack popst;
};
⑦ 若类中未申请动态资源,则无需显式定义析构函数 ,可直接使用编译器生成的默认版本;若类中申请了动态资源,则必须显式定义析构函数以释放资源,避免资源泄漏。
- 对于 Date 类:成员变量都是内置类型,没有动态申请内存,不需要释放资源,所以可以不写析构函数,用编译器默认生成的就行。
- 对于 Stack 类:用 malloc 申请了动态内存,必须自己写析构函数调用 free (),否则会造成内存泄漏。
- 对于 MyQueue 类:它的资源都封装在 Stack 成员里,而 Stack 已经写了析构函数,所以 MyQueue 不写析构也不会内存泄漏。
⑧ 在同一局部域中定义的多个对象,析构顺序遵循 "后定义的对象先析构" 的规则。
cpp
int main()
{
Stack st; //第1个创建
MyQueue mq; //第2个创建
return 0;
}
对于main函数中的两个对象:
- 构造顺序:st 👉 mq
- 析构顺序:mq 👉 st
cpp
class MyQueue
{
private:
Stack pushst; //第1个创建
Stack popst; //第2个创建
};
对于 MyQueue 类中的两个成员:
- 构造顺序:pushst 👉 popst
- 析构顺序:popst 👉 pushst
四. 拷贝构造函数
如果一个构造函数的第一个参数是自身类类型的引用 ,且其他所有额外参数都带有默认值,那么这个构造函数就叫做拷贝构造函数,它是一种特殊的构造函数。
下面用两个代码来详细讨论拷贝构造函数的六个特点:
cpp
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//编译报错:error C2652 : "Date":⾮法的复制构造函数:第⼀个参数不应是"Date"
//Date(Date d)
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 << endl;
}
private:
int _year;
int _month;
int _day;
};
void Func1(Date d)
{
cout << &d << endl;
d.Print();
}
// Date Func2()
Date& Func2()
{
Date tmp(2026, 4, 19);
tmp.Print();
return tmp;
}
int main()
{
Date d1(2026, 4, 19);
//C++规定⾃定义类型对象进⾏拷⻉⾏为必须调⽤拷⻉构造,所以这⾥传值传参要调⽤拷⻉构造
//所以这⾥的d1传值传参给d要调⽤拷⻉构造完成拷⻉,传引⽤传参可以较少这⾥的拷⻉
Func1(d1);
cout << &d1 << endl;
//这⾥可以完成拷⻉,但是不是拷⻉构造,只是⼀个普通的构造
Date d2(&d1);
d1.Print();
d2.Print();
//这样写才是拷⻉构造,通过同类型的对象初始化构造,⽽不是指针
Date d3(d1);
d2.Print();
//也可以这样写,这⾥也是拷⻉构造
Date d4 = d1;
d2.Print();
//Func2返回了⼀个局部对象tmp的引⽤作为返回值
//Func2函数结束,tmp对象就销毁了,相当于了⼀个野引⽤
Date ret = Func2();
ret.Print();
return 0;
}
cpp
#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(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;
};
//两个Stack实现队列
class MyQueue
{
public:
private:
Stack pushst;
Stack popst;
};
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;
}
① 拷贝构造函数是构造函数的一个重载。
拷贝构造函数本质上是构造函数的重载版本 ,它的唯一作用是:用一个已经存在的同类对象,初始化另一个新创建的对象。
cpp
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;
}
};
- 无参构造函数、带参构造函数和全缺省构造函数与拷贝构造函数是重载关系,编译器会根据传入参数自动匹配。
- 拷贝构造函数必须和类同名 ,且没有返回值 ,和普通构造函数的规则一致。
调用场景:
cpp
int main()
{
Date d1(2026, 4, 19); //调用普通构造函数
Date d3(d1); //调用拷贝构造函数(直接初始化)
Date d4 = d1; //调用拷贝构造函数(拷贝初始化)
}
注意:拷贝初始化和直接初始化,本质上都是用一个已有对象创建新对象,都会调用拷贝构造函数,只是写法和规则有区别:直接初始化用括号把原对象当参数传给构造函数,一步就完成创建,支持所有构造函数;拷贝初始化用等号,看着像赋值,语法上会先生成临时对象再拷贝,而且如果构造函数被 explicit 修饰,这种写法就会报错。
② 拷贝构造函数的第一个参数 必须是类类型对象的引用 ,使用传值方式会引发无穷递归 调用,编译器直接报错;拷贝构造函数也可以有多个参数,但是第一个参数必须是类类型对象的引用,后面的参数必须有缺省值。
如果第一个参数拷贝值写成Date(Date d) 的形式,是传值传参,会导致无限递归。
cpp
//错误写法:编译报错
Date(Date d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
报错原因:
- 如果拷贝构造函数是传值*Date(Date d)*的话,首先需要把实参d1传给形参d。
- 而传值传参本身也需要拷贝构造函数,所以用d1去初始化d。
- 这样无限循环往复,就会发生无穷递归。

cpp
//正确写法
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
拷贝构造函数可以有多个参数,但第一个参数必须是类类型的引用,后面的参数必须带有缺省值。
此处使用const既可以防止原对象被误修改,也可以兼容不同调用场景 (可接收普通对象、可接收const修饰对象、也可接收临时对象)。
③ C++ 规定自定义类型对象进行拷贝行为必须调用拷贝构造,因此自定义类型传值传参和传值返回,都会调用拷贝构造完成。
传值传参时:
cpp
void Func1(Date d) //传值传参
{
cout << &d << endl;
d.Print();
}
int main()
{
Date d1(2026, 4, 19);
Func1(d1); //调用拷贝构造,用 d1 初始化形参 d
return 0;
}
为什么传值传参会调用拷贝构造:
形参d和实参d1是两个分离的全新变量,有各自的内存地址,为了让二者内容一样,C++ 会调用拷贝构造函数,来让d1初始化d。
为什么传引用传参不会调用拷贝构造:
因为引用相当于是对d1起d的别名,未创建新对象,所以直接可以一一对应。
通过一串检查地址的代码来说明是否会发生调用拷贝构造:
cpp
//传值传参
void Func1(Date d)
{
cout << "形参d的地址:" << &d << endl;
d.Print();
}
//传引用传参
void Func2(const Date& d)
{
cout << "引用形参d的地址:" << &d << endl;
d.Print();
}
int main()
{
Date d1(2026, 4, 19);
cout << "主函数中d1的地址:" << &d1 << "\n" << endl;
cout << "调用Func1(传值传参)" << endl;
Func1(d1); //这里会调用拷贝构造
cout << "\n" << "调用Func2(传引用传参)" << endl;
Func2(d1); //这里不会调用拷贝构造
return 0;
}
运行结果:

- Func1中形参d地址和主函数中d1的地址不同,所以的是由拷贝构造而来的新对象。
- Func2中形参d地址和主函数中d1的地址相同,所以d只是d1的别名,莫创建新对象,同样没有调用拷贝构造。
④ 若未显式定义拷贝构造 ,编译器会自动生成拷贝构造函数 ;自动生成的拷贝构造,对内置类型成员变量 会完成值拷贝 (浅拷贝) ,对自定义类型成员变量 会调用其拷贝构造。
对内置类型成员变量执行值拷贝 (浅拷贝):
默认拷贝构造会对内置类型成员一个字节一个字节的复制,也就是直接把原对象的成员值原样拷贝到新对象中。
- 比如Date类成员中的_year,_month,_day都是int类型,拷贝构造会直接将这些值复制过去。
- 比如Stack类成员中的_a,_capacity,_top,默认拷贝构造会复制指针的值,这会导致两个对象的_a指向同一块内存,析构时会析构两次导致程序崩溃(下一点就说)。
对自定义类型成员变量调用该成员的拷贝构造函数:
cpp
class MyQueue
{
private:
Stack pushst; //自定义类型成员
Stack popst; //自定义类型成员
};
int main()
{
MyQueue mq1;
MyQueue mq2 = mq1; //调用MyQueue的默认拷贝构造
return 0;
}
因为MyQueue没有写属于他的拷贝构造函数,所以会自动从Stack中寻找拷贝构造函数,用以初始化pushst和popst。
⑤ 成员变量全为内置类型且无动态资源的类 ,编译器自动生成的拷贝构造即可满足需求,无需显式实现;成员变量包含指向动态资源的指针的类 ,编译器自动生成的浅拷贝无法满足需求,需要自己实现深拷贝 ;内部主要为自定义类型成员的类 ,编译器自动生成的拷贝构造会调用成员所属类的拷贝构造 ,无需显式实现;若一个类显式实现了释放资源的析构函数 ,则需要显式实现拷贝构造函数。
成员变量全为内置类型且无动态资源的类:
比如Date类的成员变量都是内置类型,没有指向任何动态申请的资源,默认的浅拷贝完全满足需求,所以不需要显式实现拷贝构造。
cpp
class Date
{
private:
int _year;
int _month;
int _day;
};
成员变量包含指向动态资源的指针的类:
Stack类中的 _a 指针指向堆上动态申请的内存,默认的浅拷贝会导致程序崩溃的问题:
cpp
class Stack
{
public:
Stack(int n = 4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
_capacity = n;
_top = 0;
}
~Stack()
{
free(_a); //析构时释放内存
_a = nullptr;
}
private:
STDataType* _a; //指向堆内存的指针
size_t _capacity;
size_t _top;
};
int main()
{
Stack st1;
st1.Push(1);
st1.Push(2);
Stack st2 = st1; //默认浅拷贝
//st1._a和st2._a指向同一块堆内存
return 0;
}
程序结束后,st2和st1会先后发生析构,两次调用的free(_a)指向同一块内存,相当于同一块内存析构了两次,程序直接寄了。
解决方案 (自行实现深拷贝):
深拷贝的关键是为新对象重新申请独立的内存,再拷贝数据。
cpp
Stack(const Stack& st)
{
//申请一块和原来一样大的独立内存块
_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;
}
实现深拷贝后,st1和st2的_a分别指向不同的内存块,析构的时候也是释放各自的内存块,不会再出现之前的情况。
内部主要为自定义类型成员的类:
比如MyQueue类,自身类中都是自定义类型成员,又因为其封装在Stack类中,所以在Stack可以正确的调用拷贝构造函数的时候,MyQueue就无所事事了。
cpp
class MyQueue
{
private:
Stack pushst;
Stack popst;
};
int main()
{
MyQueue mq1;
MyQueue mq2 = mq1; //调用默认拷贝构造
return 0;
}
默认拷贝构造会调用Stack的深拷贝,给pushst和popst分别实现创建独立内存块进行深拷贝,不会出现析构问题。
⑥ 传值返回 会产生临时对象并调用拷贝构造 ;引用返回直接返回对象别名,不产生拷贝 ,但不能返回当前函数局部域的局部对象,否则会形成野引用 ;引用返回可以减少拷贝 ,但需确保返回对象在函数结束后仍然存在。
传值返回:
cpp
Date Func2()
{
Date tmp(2026, 4, 19);
return tmp; //传值返回,调用拷贝构造生成临时对象
}
int main()
{
Date ret = Func2(); //用临时对象拷贝构造ret
return 0;
}
- 传值返回会创建一个临时对象,需要调用拷贝构造函数,存在性能开销。
- 临时对象在拷贝完成后会被销毁,不会出现野引用问题。
传引用返回 (错误):
cpp
Date& Func2() 、
{
Date tmp(2026, 4, 19); //局部对象,函数结束时销毁
return tmp; //错误:返回局部对象的引用
}
int main()
{
Date ret = Func2(); //访问已经销毁的tmp,行为未定义
ret.Print();
return 0;
}
函数结束时,局部对象tmp会被销毁,返回的引用会变成野引用,访问它会导致程序崩溃。
传引用返回 (正确):
cpp
//返回传入的参数对象
Date& Func4(Date& d)
{
d.Print();
return d; //传入的对象由调用者管理,函数结束后依然存在
}
引用返回可以避免拷贝,性能更高,但必须确保返回的对象在函数结束后依然存在。
五. 赋值运算符重载
1. 运算符重载
① 当运算符被用于类类型对象时,C++ 允许通过运算符重载为其指定新含义。类对象使用运算符时,编译器会自动转换为调用对应的运算符重载函数,没有对应重载则编译报错。
cpp
//Date类重载==运算符
bool operator==(const Date& d)
{
return _year == d._year && _month == d._month && _day == d._day;
}
int main()
{
Date d1(2026, 4, 19), d2(2026, 4, 20);
d1 == d2; //编译器自动转换为:d1.operator==(d2);
}
" == " 原本是内置类型的运算符,此处被Date重载后,转换为调用对应的运算符重载函数 (bool operator==(const Date& d) ),如果不写Date的这个运算符重载函数,则编译会报错。
② 运算符重载是特殊的函数,名字由operator + 运算符 构成,和普通函数一样,有返回值、参数列表和函数体。
cpp
//全局运算符重载
bool operator==(const Date& d1, const Date& d2)
{
return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day;
}
//成员运算符重载
Date& operator++()
{
//日期自增逻辑
return *this;
}
③ 重载运算符的参数个数 ,和运算符作用的运算对象数量一致 。一元运算符有 1 个参数,二元运算符有 2 个参数。二元运算符左侧对象传给第一个参数 ,右侧对象传给第二个参数。
cpp
//二元运算符==,全局重载:2个参数(左右两个对象)
bool operator==(const Date& d1, const Date& d2);
//一元运算符前置++,成员重载:0个显式参数(this指针是隐式参数)
Date& operator++();
//一元运算符后置++,成员重载:1个int形参(占位区分)
Date operator++(int);
一元运算符 ++ 只有一个运算对象,所以成员重载时有显式参数和无显式参数都可以。
二元操作符 == 有两个运算对象,所以全局重载需要两个显式参数。
④ 如果运算符重载是成员函数,第一个运算对象会默认传给隐式的 this 指针,因此参数数量比运算对象数少1个。
cpp
class Date
{
public:
//成员形式的==重载,参数只有1个(右侧对象),this指针接收左侧对象
bool operator==(const Date& d)
{
return _year == d._year && _month == d._month && _day == d._day;
}
};
int main()
{
Date d1(2026, 4, 19), d2(2026, 4, 19);
d1 == d2; //等价于:d1.operator==(d2);
}
隐式参数this指针指向左操作数d1,d是右操作数d2,所以参数数量比运算对象数少一个。
⑤ 运算符重载后,优先级和结合性与对应的内置类型运算符保持一致,不会改变。
比如 + 的优先级低于 *,为类重载后,a + b * c 依然先算 b * c,再算 a + 结果,和内置类型的规则完全相同。
⑥ 不能通过连接语法中没有的符号创建新运算符(如 operator@);也有5个运算符不能重载 :++.*++ 、++::++ 、++sizeof++ 、++?:++ 、++.++。
.* 成员指针访问运算符:用来通过成员函数指针调用类的成员函数。
cpp
class A
{
public:
void func()
{
cout << "A::func" << endl;
}
};
int main()
{
void (A::*pf)() = &A::func; //成员函数指针
A aa;
(aa.*pf)(); //用.*调用成员函数
return 0;
}
:: 作用域解析运算符:用来指明名字的所属范围 (命名空间、类名)。
cpp
std::cout << "hello"; //访问std命名空间里的cout
Date::operator==(d1, d2); //调用Date类的成员函数
sizeof 运算符:用来计算一个类型或变量所占的字节数。
cpp
int a;
cout << sizeof(a) << endl; //输出4(假设32位int)
cout << sizeof(int) << endl; //输出4
?: 三目运算符:用来做条件判断的。
cpp
int max = (a > b) ? a : b; //取a和b中较大的值
. 成员访问限定符:用来访问对象的成员变量或成员函数。
cpp
Date d;
d.Print(); //访问成员函数
//若_year是public成员
cout << d._year << endl;
⑦ 重载运算符至少要有一个类类型参数,不能通过运算符重载改变内置类型的含义(如 int operator + (int x, int y) 是错误的) 。
cpp
//错误
// int operator+(int x, int y) //两个参数都是int,没有类类型
//{
// return x - y; //试图把+操作改为-操作
// }
//正确
Date operator+(const Date& d, int days);
⑧ 一个类需要重载哪些运算符,取决于重载后是否有意义。比如 Date 类重载 operator - (计算日期差) 有意义,但重载 operator + (日期 + 日期) 就没意义。
cpp
//有意义的重载: 日期-日期,计算天数差
int operator-(const Date& d1, const Date& d2);
//无意义的重载: 日期+日期,有啥用啊
//Date operator+(const Date& d1, const Date& d2);
⑨ ++ 有前置和后置两种形式,重载函数名都是 operator++,为了区分,C++ 规定后置 ++ 增加一个 int 形参,和前置 ++ 构成重载。
cpp
class Date
{
public:
//前置++: 无额外参数
Date& operator++()
{
//日期+1天
return *this;
}
//后置++; 增加int形参
Date operator++(int)
{
Date tmp = *this;
//日期+1天
return tmp;
}
};
int main()
{
Date d(2026, 4, 19);
++d; //调用前置++:d.operator++();
d++; //调用后置++:d.operator++(0); //编译器自动传0占位
}
⑩ 重载 << 和 >> 时,必须重载为全局函数。如果重载为成员函数,this指针会默认抢占第一个形参位置,导致调用时变成++对象 << this++ ,不符合使用习惯。重载为全局函数时,把 ostream / istream 放到第一个形参位置,类对象放到第二个参数。
cpp
//正确写法:全局重载<<
ostream& operator<<(ostream& os, const Date& d)
{
os << d._year << "-" << d._month << "-" << d._day;
return os;
}
//错误写法:成员重载<<,调用会变成d<<cout,不符合习惯
// ostream& operator<<(ostream& os)
//{
// os << _year << "-" << _month << "-" << _day;
// return os;
// }
int main()
{
Date d(2026, 4, 19);
cout << d; //等价于:operator<<(cout, d); 符合使用习惯
}
错误写法错误原因:
ostream& operator<<(ostream& os) 实际是 ostream& operator<<(Date* this, ostream& os)
当写 cout << d 时,编译器会尝试匹配 operator << (cout, d),但成员函数的顺序是 (this, os),也就是 (d, cout),与其对应的形式相反(d << cout ≠ cout << d),所以在编译主函数中所写的 cout << d时会报错。
2. 赋值运算符重载
赋值运算符重载是一个默认成员函数,用于完成两个已经存在的对象直接的拷贝赋值 ,这里要注意跟拷贝构造区分,拷贝构造用于一个对象拷贝初始化给另一个要创建的对象 (一个存在一个不存在)。
cpp
Date d2(d1); //拷贝构造:创建新对象,用d1初始化d2
Date d3 = d1; //拷贝构造:用d1初始化d3
d1 = d4; //赋值重载:修改已存在的d1,把d4的值赋给它
下面借用一份代码来分点列举出构造函数的特点:
cpp
#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)
{
cout << " Date(const Date& d)" << endl;
_year = d._year;
_month = d._month;
_day = d._day;
}
// 传引⽤返回减少拷⻉
// d1 = d2;
Date& operator=(const Date& d)
{
// 不要检查⾃⼰给⾃⼰赋值的情况
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
// d1 = d2表达式的返回对象应该为d1,也就是* this
return *this;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 7, 5);
Date d2(d1);
Date d3(2024, 7, 6);
d1 = d3;
// 需要注意这⾥是拷⻉构造,不是赋值重载
// 请牢牢记住赋值重载完成两个已经存在的对象直接的拷⻉赋值
// ⽽拷⻉构造⽤于⼀个对象拷⻉初始化给另⼀个要创建的对象
Date d4 = d1;
return 0;
}
① 赋值运算符重载必须作为成员函数实现 ;其参数**++建议++为当前类类型的const引用**,否则传值传参会产生拷贝。
赋值运算符 operator= 是特殊的运算符,C++ 规定它必须作为类的成员函数 ,不能重载为全局函数;参数建议写成const + 当前类类型&,避免传值带来的拷贝开销。
cpp
class Date
{
public:
//赋值运算符重载(成员函数形式)
Date& operator=(const Date& d)
{ //参数是const引用
if (this != &d)
{ //不要检查⾃⼰给⾃⼰赋值的情况
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
};
- 赋值运算符的语义是修改已存在的对象,而全局函数无法访问隐士 this 指针,不能直接修改对象状态,因此C++强制要求它作为成员函数。
- 传引用可以减少一次拷贝构造的调用 (也可以用传引用传参,就是多一次拷贝构造调用),提升效率,且const 保证不会修改传入的原对象,语义安全,也能接收 const 对象、临时对象作为右值。
② 赋值运算符重载需要有返回值,建议返回当前类类型的引用 ;引用返回可提高效率,有返回值是为了支持连续赋值场景。
赋值运算符必须有返回值,建议返回 *this(当前对象的引用),目的是支持连续赋值(如 d1 = d2 = d3),同时减少拷贝开销。
cpp
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this; //返回当前对象的引用
}
- 当写 d1 = d2 = d3 时,编译器会从右往左运行,先执行 d2 = d3,然后返回 d2的引用,之后执行 d1 = (d2 的 引用) 。
- 如果返回值类型时Date (传值返回),则会多一次拷贝构造调用,而用Date& (传引用返回)可以直接返回该对象本身,少一次调用性能消耗。
③ 未显式实现时,编译器会自动生成默认赋值运算符重载 ;默认赋值运算符重载的行为与默认拷贝构造函数类似,对内置类型成员变量完成值拷贝 (浅拷贝) ,对自定义类型成员变量调用其赋值重载函数。
cpp
//即使不写operator=,编译器也会生成默认版本
Date& operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
- Date 类的成员都是 int 这种内置类型 ,默认赋值运算符会直接把d的成员值拷贝到当前对象中。
- 对于包含自定义类型 成员的类 (如 MyQueue 包含 Stack 成员),默认赋值运算符会自动调用 Stack 类的 operator=,为每个成员完成赋值。
④ 对于成员变量全为内置类型且无动态资源的类 ,如Date,编译器自动生成的赋值运算符重载可满足需求,无需显式实现;对于成员变量包含指向动态资源指针的类 ,如 Stack,默认的浅拷贝赋值运算符重载不符合需求,需要显式实现深拷贝;对于内部主要为自定义类型成员的类 ,比如MyQueue,编译器自动生成的赋值运算符重载会调用成员所属类的赋值运算符重载,无需显式实现;若一个类显式实现了释放资源的析构函数,则需要显式实现赋值运算符重载 (参考析构函数特点的第⑦条)。
成员变量全为内置类型且无动态资源的类 (Date):完全不需要显式实现赋值运算符重载。
cpp
class Date
{
private:
int _year; //内置类型
int _month; //内置类型
int _day; //内置类型
};
cpp
//编译器自动生成的默认operator=
Date& operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
这类类的成员全是 int 这样的内置类型,没有指向任何堆上的动态内存,编译器自动生成的默认赋值运算符,会直接做一个字节一个字节的拷贝 (浅拷贝)。
成员变量包含指向动态资源指针的类 (Stack):必须显式实现深拷贝的赋值运算符重载。
cpp
class Stack
{
private:
int* _a; //指向堆上动态内存的指针
size_t _top;
size_t _capacity;
};
cpp
//编译器自动生成的默认operator=(危险!)
Stack& operator=(const Stack& st)
{
_a = st._a; //两个对象的_a指向同一块堆内存!
_top = st._top;
_capacity = st._capacity;
return *this;
}
cpp
Stack& operator=(const Stack& st)
{
if (this != &st)
{
free(_a);
_a = (int*)malloc(sizeof(int) * st._capacity);
memcpy(_a, st._a, sizeof(int) * st._top);
_top = st._top;
_capacity = st._capacity;
}
return *this;
}
这类类里有_a这样的指针,指向你用malloc申请的堆内存,编译器自动生成的默认赋值运算符,只会浅拷贝指针的值 ,也就是把_a直接复制过去,这不就是之前析构函数中提到的会导致同一块内存会被析构两次导致程序崩溃的问题,所以应该自己写一个深拷贝函数进行赋值运算符重载。
内部主要为自定义类型成员的类 (MyQueue):
cpp
class MyQueue
{
private:
Stack pushst; //自定义类型成员
Stack popst; //自定义类型成员
};
cpp
//编译器自动生成的默认operator=
MyQueue& operator=(const MyQueue& mq)
{
pushst = mq.pushst; //调用Stack的operator=
popst = mq.popst; //调用Stack的operator=
return *this;
}
MyQueue 自己没有动态资源,所有资源都封装在 Stack 成员里,编译器自动生成的默认赋值运算符,会自动调用每个成员所属类的赋值运算符重载。只要 Stack 类自己实现了正确的深拷贝赋值运算符,MyQueue 的默认赋值就完全安全,不会出现重复释放的问题。
3. 日期类实现
先列出整个日期类实现的蓝图:
cpp
#pragma once
#include<iostream>
using namespace std;
#include<assert.h>
class Date
{
// 友元函数声明:重载流运算符,让cout/cin可以直接操作Date对象
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
public:
// 构造函数:默认值参数,支持无参/单参/多参构造
Date(int year = 2004, int month = 7, int day = 28);
// 打印日期的const成员函数,支持const对象调用
void Print() const;
// 获取指定月份的天数(静态函数,不依赖对象状态)
int GetMonthDay(int year, int month)
{
assert(month > 0 && month < 13);
static int monthDayArray[13] = { -1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
// 闰年2月特殊处理
if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
{
return 29;
}
else
{
return monthDayArray[month];
}
}
// 日期合法性校验
bool CheckDate();
// 比较运算符重载
bool operator<(const Date& d) const;
bool operator<=(const Date& d) const;
bool operator>(const Date& d) const;
bool operator>=(const Date& d) const;
bool operator==(const Date& d) const;
bool operator!=(const Date& d) const;
// 日期+/-天数(复合赋值与普通运算符)
Date& operator+=(int day);
Date operator+(int day) const;
Date& operator-=(int day);
Date operator-(int day) const;
// 两个日期相减,计算相差天数
int operator-(const Date& d) const;
// 自增/自减运算符重载(前置+后置)
Date& operator++(); // 前置++
Date operator++(int); // 后置++,int是占位参数,用于区分重载
Date& operator--(); // 前置--
Date operator--(int); // 后置--
private:
int _year;
int _month;
int _day;
};
// 流运算符重载声明(全局友元)
ostream& operator<<(ostream& out, const Date& d);
istream& operator>>(istream& in, Date& d);
对于蓝图中的默认构造函数 *++Date(int year = 2004, int month = 7, int day = 28);++*支持Date d1; → (2004, 7, 28)、Date d2(2026); → (2026, 7, 28)、Date d3(2026, 4, 25); → (2026, 4, 25)这三种构造方式。
const成员函数:void Print() const 和所有比较运算符都加了 const 修饰,保证函数不会修改对象状态,同时支持 const Date 对象调用。
对于后置 ++ / -- 的 int 占位参数 ,运算符重载的第九个特点提到:为了区分,C++ 规定后置 ++ 增加一个 int 形参,和前置 ++ 构成重载,-- 同理。
日期合法性校验:
cpp
bool Date::CheckDate()
{
if (_month < 1 || _month > 12
|| _day < 1 || _day > GetMonthDay(_year, _month))
{
return false;
}
else
{
return true;
}
}
要求月数必须在1~12之间,且调用GetMonthDay函数要求天数必须在1到当月最大天数之间,且闰年非闰年也由GetMonthDay函数来判断。
构造函数与打印函数:
cpp
Date::Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
if (!CheckDate())
{
cout << "日期非法" << endl;
}
}
void Date::Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
通过CheckDate的日期合法性检查函数来判断输入的或者原始构造函数的日期是否合法,并cout输出。
日期 +/ -天 :
cpp
Date& Date::operator+=(int day) //+= 时自身值需要改变
{
if (day < 0)
{
return *this -= -day; //复用-=,处理负数天数
}
_day += day;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
++_month;
if (_month == 13)
{
++_year;
_month = 1;
}
}
return *this;
}
核心逻辑:每次加上天数后,通过GetMonthDay函数判断月数天数合法性,如果天数超了当前月份的最大天数,则借一位月数 (月数 + 1),且要加的总天数 - 当前月份最大天数;如果月数超超了12月,则年数+1,且月数变为1月。 若一个月不够加,则通过while循环不断借一位月数或年数来判断。
cpp
Date Date::operator+(int day) const // + 时自身值不需要改变
{
Date tmp = *this;
tmp += day; //复用operator+=
return tmp;
}
核心逻辑:因为单+时自身不需要改变,所以创建一个临时变量来存储结果,且复用 += 操作来渐简便代码。
cpp
Date& Date::operator-=(int day) //-= 需要改变自身值
{
if (day < 0)
{
return *this += -day; //复用+=,处理负数天数
}
_day -= day;
while (_day <= 0)
{
--_month;
if (_month == 0)
{
_month = 12;
_year--;
}
//借上一个月的天数
_day += GetMonthDay(_year, _month);
}
return *this;
}
核心逻辑:每次减去天数后,通过GetMonthDay函数判断月数天数合法性,如果天数小于等于0,则借一位月数,月数 - 1,且判断月数是否减到0,如是则借一位年数,年数 - 1,月重定为 12,且要减的天数加上上一个月总共的天数。若一个月不够减,则通过while循环不断借一位月数或年数来判断。
cpp
Date Date::operator-(int day) const
{
Date tmp = *this;
tmp -= day; //复用operator-=
return tmp;
}
核心逻辑:因为单-时自身不需要改变,所以创建一个临时变量来存储结果,且复用 -= 操作来简便代码。
日期前置 / 后置自增 / 自减:
cpp
Date& Date::operator++() //前置++
{
*this += 1; //复用+=1
return *this;
}
Date& Date::operator--() //前置--
{
*this -= 1; //复用-=1
return *this;
}
前置操作先修改再返回, 且复用 -= / -= 操作来简便代码**。**
cpp
Date Date::operator++(int) //后置++
{
Date tmp(*this); //保存修改前的状态
*this += 1; //修改当前对象
return tmp; //返回修改前的状态
}
Date Date::operator--(int) //后置--
{
Date tmp(*this);
*this -= 1;
return tmp;
}
后置操作先返回再修改 ,且复用 -= / -= 操作来简便代码**。**
比较运算符重载:(全是复用)
cpp
bool Date::operator<(const Date& d) const //基础比较:d1 < d2
{
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) const //d1 == d2:直接比较年月日
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
bool Date::operator<=(const Date& d) const //d1 <= d2:复用 < 和 ==
{
return *this < d || *this == d;
}
bool Date::operator>(const Date& d) const //d1 > d2:复用 <= 的取反
{
return !(*this <= d);
}
bool Date::operator>=(const Date& d) const //d1 >= d2:复用 < 的取反
{
return !(*this < d);
}
bool Date::operator!=(const Date& d) const //d1 != d2:复用==的取反
{
return !(*this == d);
}
细心观察就可以发现,这一堆判断只需要写出 == 和 < 的判断操作之后,就可以疯狂复用简化代码了。
两个日期相减:
cpp
int Date::operator-(const Date& d) const
{
Date max = *this;
Date min = d;
int flag = 1;
if (*this < d) //确保max是较大的日期,min是较小的日期
{
max = d;
min = *this;
flag = -1; //标记结果为负数
}
int n = 0;
while (min != max)
{
++min; //复用前置++,每次加1天
++n;
}
return n * flag;
}
核心逻辑:通过循环将min的日期每次 + 1,直到等于max的日期,统计循环次数就是相差天数。
流运算符重载 (输入 / 输出):
cpp
istream& operator>>(istream& in, Date& d)
{
cout << "请依次输入年月日:>";
in >> d._year >> d._month >> d._day;
if (!d.CheckDate())
{
cout << "日期非法" << endl;
}
return in;
}
自动调用CheckDate来检查日期合法性。
cpp
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}