在 C++ 中,类的默认成员函数是编译器自动生成的重要机制,合理利用这些函数可以简化代码编写,同时避免资源管理错误。本文将从构造函数、析构函数、拷贝构造函数、赋值运算符重载等核心内容展开,结合具体案例深入解析。
一、默认成员函数概述
- 我们不写时,编译器默认⽣成的函数⾏为是什么,是否满⾜我们的需求。
- 编译器默认⽣成的函数不满⾜我们的需求,我们需要⾃⼰实现,那么如何⾃⼰实现?
1. 六大默认成员函数
C++ 编译器在用户未显式定义时,会自动生成以下 6 个默认成员函数(C++11 后新增移动构造和移动赋值):
- 构造函数:对象实例化时初始化成员变量
- 析构函数:对象销毁时清理资源
- 拷贝构造函数:用已存在对象初始化新对象
- 赋值运算符重载:对象间赋值操作
- 普通取地址重载:获取普通对象的地址(
类类型* operator&()
) const
取地址重载:获取const
对象的地址(const 类类型* operator&() const
)
2. 核心关注点
- 默认行为:编译器生成的函数如何处理内置类型和自定义类型成员?
- 自定义实现:何时需要手动编写?如何正确实现?
二、构造函数:对象的初始化入口
1. 核心特性
- 函数名与类同名,无返回值(返回值啥都不需要给,也不需要写void)
- 对象实例化时自动调用,替代传统
Init
函数 - 支持重载,可定义多个参数列表不同的构造函数
2. 默认构造函数
- 定义:无需实参即可调用的构造函数,包括:
- 无参构造函数(
Date()
) - 全缺省构造函数(
Date(int year=1, int month=1, int day=1)
) - 编译器生成的隐式无参构造(用户不写,编译器默认⽣成的构造,对内置类型成员变量的初始化没有要求,也就是说是是否初始化是不确定的,看编译器。对于⾃定义类型成员变量,要求调⽤这个成员变量的默认构造函数初始化。如果这个成员变量,没有默认构造函数,那么就会报错,我们要初始化这个成员变量,需要⽤初始化列表才能解决,初始化列表。)
- 无参构造函数(
- 唯一性:三种形式只能存在一种,避免调用歧义
- 成员初始化:
- 内置类型:初始化状态不确定(依赖编译器)
- 自定义类型:调用其默认构造函数
补充说明:
- 内置类型是编程语言预先定义的基础数据类型(如 C/C++ 中
int
、char
、float
等),可直接使用,有固定操作规则。 - 自定义类型是用户依需求利用语言机制创建的类型,如
struct
(组合不同数据)、enum
(定义命名常量)、class
(C++ 中封装数据与方法),用于表示复杂数据结构或抽象概念。
3. 案例分析:Date 类的构造函数
- 若只定义带参构造(无默认构造),实例化
Date d1;
会编译报错 - 无参构造调用时不加括号:
Date d1;
(避免与函数声明混淆)
4,内置类型成员和自定义类型成员不写构造函数时的情况
1. 内置类型成员在不写构造函数时的情况
在Date
类中,成员变量_year
、_month
和_day
都属于内置类型(int
)。虽然你定义了一个带默认参数的构造函数,但这里我们可以探讨下如果没有这个构造函数时的情况。
class Date
{
public:// 如果不写下面这个构造函数,编译器会生成默认构造函数// Date(int year = 1, int month = 1, int day = 1)// {// _year = year;// _month = month;// _day = day;// }void Print(){cout << this->_year << "/" << this->_month << "/" << _day << endl;}private:// 内置类型int _year;int _month;int _day;
};
若没有显式定义构造函数,编译器生成的默认构造函数不会对内置类型成员进行初始化。也就是说,_year
、_month
和_day
的值是未定义的,它们的值取决于内存中原有的内容。每次运行程序时,这些值可能都不一样。
不过,在你给出的代码里,定义了带默认参数的构造函数Date(int year = 1, int month = 1, int day = 1)
,所以在创建Date
对象时,会调用这个构造函数来初始化成员变量。
Date d1; // 调用带默认参数的构造函数,_year、_month、_day 被初始化为 1
2. 自定义类型成员在不写构造函数时的情况
在Myqueue
类中,成员变量_pushst
和_popst
属于自定义类型(Stack
)。当没有为Myqueue
类显式定义构造函数时,编译器会生成默认构造函数。
typedef int STDataType;
class Stack
{
public:Stack(int n = 4){cout << "Stack(int n = 4)" << endl;_a = (STDataType*)malloc(sizeof(STDataType) * n);if (nullptr == _a){perror("malloc申请空间失败");return;}_capacity = n;_top = 0;}~Stack(){cout << "~Stack()" << endl;free(_a);_a = nullptr;_capacity = _top = 0;}
private:STDataType* _a;size_t _capacity;size_t _top;
};///
class Myqueue
{// 不写构造,也会初始化
private:// 自定义类型Stack _pushst;Stack _popst;
};
在这个默认构造函数中,对于自定义类型成员_pushst
和_popst
,编译器会调用它们各自的默认构造函数来进行初始化。
Stack
类有一个带默认参数的构造函数Stack(int n = 4)
,所以在创建Myqueue
对象时,_pushst
和_popst
会调用Stack(int n = 4)
进行初始化。
Myqueue q;
// 编译器生成的默认构造函数会调用 _pushst 和 _popst 的 Stack(int n = 4) 构造函数
总结
- 内置类型:若类中没有显式定义构造函数,编译器生成的默认构造函数不会对内置类型成员进行初始化,其值是未定义的。
- 自定义类型:若类中没有显式定义构造函数,编译器生成的默认构造函数会调用自定义类型成员的默认构造函数来进行初始化。
三、析构函数:资源清理的守护者
1. 核心特性
- 函数名以
~
开头(如~Date()
) - ⽆参数⽆返回值。 (这⾥跟构造类似,也不需要加void)
- ⼀个类只能有⼀个析构函数。若未显式定义,系统会⾃动⽣成默认的析构函数。
- 对象⽣命周期结束时,系统会⾃动调⽤析构函数。
- 跟构造函数类似,我们不写编译器⾃动⽣成的析构函数对内置类型成员不做处理,⾃定类型成员会调⽤他的析构函数。
- 还需要注意的是我们显⽰写析构函数,对于⾃定义类型成员也会调⽤他的析构,也就是说⾃定义类型成员⽆论什么情况都会⾃动调⽤析构函数。
- 如果类中没有申请资源时,析构函数可以不写,直接使⽤编译器⽣成的默认析构函数,如Date;如果默认⽣成的析构就可以⽤,也就不需要显⽰写析构,如MyQueue;但是有资源申请时,⼀定要⾃⼰写析构,否则会造成资源泄漏,如Stack。
- ⼀个局部域的多个对象,C++规定后定义的先析构。
2. 成员处理逻辑
- 内置类型:无需处理(如
int
、指针本身) - 自定义类型:调用其析构函数(递归清理)
- 关键场景:若类中包含动态内存(如
malloc
/new
),必须手动实现析构释放资源,避免内存泄漏
3. 案例对比:C 与 C++ 的资源管理
// C风格栈(需手动调用Destroy)typedef struct Stack { STDataType* _a;size_t _capacity;size_t _top;} ST;void STInit(ST* st){//...........
}
void STDestroy(ST* st){//...........
}// C++风格栈(构造/析构自动管理)
typedef int STDataType;
class Stack
{
public:Stack(int n = 4){cout << "Stack(int n = 4)" << endl;_a = (STDataType*)malloc(sizeof(STDataType) * n);if (nullptr == _a){perror("malloc申请空间失败");return;}_capacity = n;_top = 0;}~Stack(){cout << "~Stack()" << endl;free(_a);_a = nullptr;_capacity = _top = 0;}
private:STDataType* _a;size_t _capacity;size_t _top;
};
- C++ 通过析构函数自动释放
_a
,无需手动调用Destroy
四、拷贝构造函数:对象复制的深度控制
1. 核心特性
- 拷⻉构造函数是构造函数的⼀个重载。
- 拷⻉构造函数的第⼀个参数必须是类类型对象的引⽤,使⽤传值⽅式编译器直接报错,因为语法逻辑上会引发⽆穷递归调⽤。 拷⻉构造函数也可以多个参数,但是第⼀个参数必须是类类型对象的引⽤,后⾯的参数必须有缺省值。
- C++规定⾃定义类型对象进⾏拷⻉⾏为必须调⽤拷⻉构造,所以这⾥⾃定义类型传值传参和传值返回都会调⽤拷⻉构造完成。
- 若未显式定义拷⻉构造,编译器会⽣成⾃动⽣成拷⻉构造函数。⾃动⽣成的拷⻉构造对内置类型成员变量会完成值拷⻉/浅拷⻉(⼀个字节⼀个字节的拷⻉),对⾃定义类型成员变量会调⽤他的拷⻉构造。
- 像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器⾃动⽣成的拷⻉构造就可以完成需要的拷⻉,所以不需要我们显⽰实现拷⻉构造。像Stack这样的类,虽然也都是内置类型,但是_a指向了资源,编译器⾃动⽣成的拷⻉构造完成的值拷⻉/浅拷⻉不符合我们的需求,所以需要我们⾃⼰实现深拷⻉(对指向的资源也进⾏拷⻉)。像MyQueue这样的类型内部主要是⾃定义类型Stack成员,编译器⾃动⽣成的拷⻉构造会调⽤Stack的拷⻉构造,也不需要我们显⽰实现MyQueue的拷⻉构造。这⾥还有⼀个⼩技巧,如果⼀个类显⽰实现了析构并释放资源,那么他就需要显⽰写拷⻉构造,否则就不需要。
- 传值返回会产⽣⼀个临时对象调⽤拷⻉构造,传值引⽤返回,返回的是返回对象的别名(引⽤),没有产⽣拷⻉。但是如果返回对象是⼀个当前函数局部域的局部对象,函数结束就销毁了,那么使⽤引⽤返回是有问题的,这时的引⽤相当于⼀个野引⽤,类似⼀个野指针⼀样。传引⽤返回可以减少拷⻉,但是⼀定要确保返回对象,在当前函数结束后还在,才能⽤引⽤返回。
- 调用场景:对象传值参数、传值返回、初始化新对象(
Date d2(d1);
) - 默认行为:
- 内置类型:值拷贝(浅拷贝,直接复制字节)
- 自定义类型:调用其拷贝构造函数
2. 在 C++ 中,内置类型与自定义类型在实参拷贝给形参时,遵循不同的拷贝规则:
一、内置类型(如 int
、char
、float
等)的拷贝规则
对于内置类型(如 int
),实参拷贝给形参时采用 “值拷贝(浅拷贝)” 方式,即直接将实参的值按字节复制给形参,形参与实参拥有独立的内存空间,后续对形参的修改不会影响实参。
在示例中:
void func(int i) {//...........
}
int main() {func(10); // 内置类型进行拷贝return 0;
}
当执行 func(10)
时:
- 实参
10
是int
类型(内置类型)。 - 编译器直接将
10
的值复制给形参i
,形参i
获得实参值的一份独立拷贝。 - 在
func
函数内部对i
的任何操作(如修改其值),都不会影响到实参10
,因为它们在内存中是独立的个体。
这种拷贝方式简单直接,仅涉及值的复制,不涉及复杂的对象构造逻辑。
二、自定义类型(以 Date
类为例)的拷贝规则
对于自定义类型(如 Date
类),实参拷贝给形参时必须通过 拷贝构造函数 完成。这是因为自定义类型可能包含复杂的成员变量或资源,需要确保拷贝过程中这些成员或资源能被正确处理。
在示例中:
class Date {
public:Date(int year = 1, int month = 1, int day = 1) {_year = year;_month = month;_day = day;}Date(const Date& other) { // 拷贝构造函数_year = other._year;_month = other._month;_day = other._day;}
private:int _year;int _month;int _day;
};
void func(Date other) {//...........
}
int main() {Date d1(2025,5,20);func(d1); // 自定义类型进行拷贝return 0;
}
当执行 func(d1)
时:
- 实参
d1
是Date
类对象(自定义类型)。 - 编译器会调用
Date
类的拷贝构造函数Date(const Date& other)
来创建形参other
。具体过程是:将实参d1
的成员_year
、_month
、_day
的值逐个复制给形参other
的对应成员。 - 如果没有显式定义拷贝构造函数,编译器会生成默认拷贝构造函数,但默认拷贝构造函数仅对成员进行简单的按字节复制(浅拷贝)。对于
Date
类这种仅包含内置类型成员的情况,默认拷贝构造函数与自定义的拷贝构造函数效果相同;但如果类中包含动态分配的资源(如指针指向的堆内存),默认浅拷贝会导致多个对象指向同一块内存,引发重复释放或数据混乱等问题。
通过拷贝构造函数,自定义类型在拷贝时可以确保对象的完整性和资源的正确处理,满足更复杂的拷贝需求。
三、核心差异对比
特性 | 内置类型 | 自定义类型 |
---|---|---|
拷贝方式 | 直接按字节值拷贝(浅拷贝) | 必须通过拷贝构造函数(浅拷贝或深拷贝) |
是否需要特殊处理 | 无需(编译器自动完成) | 需要(依赖拷贝构造函数) |
形参与实参关系 | 独立内存,互不影响 | 依赖拷贝逻辑:浅拷贝时成员独立,深拷贝时资源独立 |
典型场景 | 简单数值、字符、浮点等基础数据 | 类对象传递(如函数参数、返回值等) |
四、总结
- 内置类型:拷贝规则简单直接,值拷贝即可满足需求,形参与实参完全独立。
- 自定义类型:拷贝规则依赖拷贝构造函数,需确保成员(尤其是动态资源)的正确复制。若无特殊资源管理需求,默认拷贝构造函数可完成浅拷贝;若涉及动态资源,必须自定义深拷贝逻辑,避免资源泄漏或数据错误。
理解这一差异是编写正确 C++ 代码的基础,尤其在处理类对象的传递和复制时,需根据类型特性选择合适的拷贝方式(如引用传递避免拷贝,或自定义深拷贝构造函数)。
以下就是内置类型与自定义类型在实参拷贝给形参动图:
通过以上了解现在知道为什么我们需要引用或者指针了吧,如果你还是不是很明白,以下展示一张图再带你深入了解:
如果是引用或者指针就不会触发拷贝构造,也不会造成栈溢出。
3. 浅拷贝 vs 深拷贝
- 浅拷贝风险:若对象包含指针成员(指向动态资源),默认拷贝会导致多个对象指向同一块内存,析构时重复释放
- 深拷贝实现:为指针成员重新分配内存并复制数据
4. 关键规则
- 若类中包含动态资源(指针、文件句柄等),必须自定义拷贝构造函数
- 传值返回时,若返回局部对象引用会导致野指针,需返回对象或正确引用
五、赋值运算符重载:对象间的赋值逻辑
1. 核心特性(运算符重载)
1.1 运算符重载的基本规则
当运算符被用于类类型的对象时,C++ 语言允许我们通过运算符重载的形式指定新的含义。这意味着,我们可以为自定义类重新定义运算符的行为,使其更贴合类的语义。C++ 规定类类型对象使用运算符时,必须转换成调用对应运算符重载,若没有对应的运算符重载,则会编译报错。
例如,对于一个自定义的 Complex
类(表示复数),我们希望使用 +
运算符来实现复数的加法,就需要重载 +
运算符。
class Complex {
public:double real;double imag;Complex(double r = 0, double i = 0) : real(r), imag(i) {}// 重载 + 运算符Complex operator+(const Complex& other) const {return Complex(real + other.real, imag + other.imag);}
};
在这个例子中,我们为 Complex
类重载了 +
运算符,使得两个 Complex
对象可以直接使用 +
进行加法运算。
1.2 运算符重载函数的构成
运算符重载是具有特殊名字的函数,它的名字是由 operator
和后面要定义的运算符共同构成。和普通函数一样,它也具有其返回类型和参数列表以及函数体。
以赋值运算符重载为例,其函数名是 operator=
。例如:
class MyClass {
public:int value;MyClass(int v = 0) : value(v) {}// 赋值运算符重载MyClass& operator=(const MyClass& other) {if (this != &other) {value = other.value;}return *this;}
};
这里的 operator=
函数就是赋值运算符重载函数,它接收一个 const MyClass&
类型的参数 other
,并返回 *this
的引用。
1.3 运算符重载函数的参数个数
重载运算符函数的参数个数和该运算符作用的运算对象数量一样多。一元运算符有一个参数,二元运算符有两个参数。对于二元运算符,左侧运算对象传给第一个参数,右侧运算对象传给第二个参数。
例如,一元运算符 ++
(前置自增)的重载:
class Counter {
public:int count;Counter(int c = 0) : count(c) {}// 前置 ++ 运算符重载Counter& operator++() {++count;return *this;}
};
这里的 operator++
函数是一元运算符重载,只有一个隐含的 this
指针作为运算对象。
而二元运算符 +
的重载(前面的 Complex
类示例)有两个运算对象,一个通过 this
指针传递,另一个作为参数传递。
1.4 成员函数形式的运算符重载
如果一个重载运算符函数是成员函数,则它的第一个运算对象默认传给隐式的 this
指针,因此运算符重载作为成员函数时,参数比运算对象少一个。
例如,在前面的 MyClass
类的赋值运算符重载函数 operator=
中,第一个运算对象就是 this
指针所指向的当前对象,而参数 other
是第二个运算对象。
1.5 运算符重载后的优先级和结合性
运算符重载以后,其优先级和结合性与对应的内置类型运算符保持一致。这意味着,我们在使用重载后的运算符时,不需要担心运算符的优先级和结合性会发生改变。
例如,+
运算符重载后,其优先级仍然高于 =
运算符,结合性仍然是从左到右。
1.6 不能创建新的操作符
不能通过连接语法中没有的符号来创建新的操作符,比如 operator@
。C++ 只允许对已有的运算符进行重载,而不允许创建新的运算符。
1.7 不能重载的运算符
有 5 个运算符是不能重载的,分别是 .
(成员访问运算符)、.*
(成员指针访问运算符)、::
(作用域解析运算符)、sizeof
(求字节数运算符)和 ?:
(条件运算符)。这些运算符在 C++ 中具有特殊的语义和用途,不允许被重载。这在选择题里面常考,需要牢记。
1.8 重载操作符的参数要求
重载操作符至少有一个(自定义类型)类类型参数,不能通过运算符重载改变内置类型对象的含义。例如,不能定义 int operator+(int x, int y)
这样的函数,因为它试图改变内置类型 int
的 +
运算符的含义。
1.9 根据类的语义选择重载运算符
一个类需要重载哪些运算符,是看哪些运算符重载后有意义。比如 Date
类重载 operator-
就有意义,可以计算两个日期之间的天数差;但是重载 operator+
可能就没有明确的意义(除非有特殊的业务需求,比如Date + day)。
class Date {
public:int year;int month;int day;Date(int y, int m, int d) : year(y), month(m), day(d) {}// 重载 - 运算符,计算两个日期之间的天数差int operator-(const Date& other) const {// 这里简单返回 0 表示未实现具体逻辑return 0;}
};
1.10 区分前置和后置 ++
运算符重载
重载 ++
运算符时,有前置 ++
和后置 ++
,运算符重载函数名都是 operator++
,无法很好地区分。C++ 规定,后置 ++
重载时,增加一个 int
形参(增加这个int参数不是为了接收具体的值,仅仅是占位,跟前置++构成重载),跟前置 ++
构成函数重载,方便区分。
class Counter {
public:int count;Counter(int c = 0) : count(c) {}// 前置 ++ 运算符重载Counter& operator++() {++count;return *this;}// 后置 ++ 运算符重载Counter operator++(int) {Counter temp = *this;++count;return temp;}
};++d1;//d1.operator++()
d1++;//d1.operator++(0) 传什么值都可以,反正没有什么实际用
在这个例子中,前置 ++
直接对对象进行自增操作并返回自身引用;后置 ++
先创建一个临时对象保存当前对象的值,然后对对象进行自增操作,最后返回临时对象。
1.11 为什么需要重载 <<和>> 运算符?
在 C++ 中,cout
和 cin
是标准库提供的输入输出流对象,默认支持内置类型(如 int
、double
、string
)的输入输出。但当我们定义自定义类型(如类或结构体)时,直接使用 cout << obj
会编译报错,因为编译器不知道如何处理自定义类型的输出逻辑。
运算符重载允许我们为自定义类型重新定义 <<
和 >>
的行为,使其能像内置类型一样方便地与 cout/cin
配合使用。
以下就是C++内置类型的输出和输入:
二、重载 << 运算符:让 cout 认识你的类型
1. 基本语法与规则
// 重载 << 运算符(输出运算符)
ostream& operator<<(ostream& out, const Date& d) {// 定义输出逻辑:将 d 的数据写入 out
out << "自定义输出格式:out << d._year << "年" << d._month << "月" << d._day << "日" << endl;;return out; // 返回流对象引用,支持连续输出(如 cout << a << b;)
}
- 返回值:必须是
ostream&
,用于支持链式输出(如cout << a << b << endl;
)。 - 参数:第一个参数是
ostream&
类型不能用const修饰因为你是要写入数据的(代表输出流对象,如cout
),第二个参数是自定义类型的 常量引用(避免拷贝构造,且不修改原对象)。 - 作用域:通常定义为 全局函数,并声明为类的
friend
(友元),以便访问类的私有成员。
2. 为什么不能重载为成员函数?
如果尝试将 operator<<
定义为成员函数:
ostream& Date::operator<<(ostream& out) { // 错误!语法错误out << d._year << "年" << d._month << "月" << d._day << "日" << endl;return out;
}调用:d1 << cout; / d1.operator<<(cout);
会导致调用方式变为 d1.operator<<(cout)
,即 d1 << cout
,这与我们期望的 cout << d1
完全相反。
结论:<<
的左操作数必须是 ostream
对象(如 cout
),而成员函数的左操作数是 this
指针(自定义类型对象),因此 必须作为全局函数重载。
以下详细图解步骤:
三、重载 >> 运算符:让 cin 支持自定义输入
1. 基本语法与规则
// 重载 >> 运算符(输入运算符)
istream& operator>>(istream& in, Date& d) {// 定义输入逻辑:从 in 读取数据到 din >> d._year >>d._month>>d._day;// 支持连续输入return in; // 返回流对象引用,支持链式输入(如 cin >> a >> b;)
}
- 返回值:
istream&
,用于支持链式输入。 - 参数:第一个参数是
istream&
类型(代表输入流对象,如cin
),第二个参数是自定义类型的 非 const 引用(因为需要修改对象的数据)。 - 作用域:同样作为全局友元函数,以便访问私有成员。
2. 示例:为 Date 类重载 >>
#include<iostream>
#include<assert.h>
using namespace std;class Date
{// 友元函数声明 如果不用友元函数声明在全局是不能用private:以下的成员的,声明之后就可以使用friend ostream& operator<<(ostream& out, const Date& d); //放到哪里都可以,只要是类内friend istream& operator>>(istream& in, Date& d);public://构造函数Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}//析构函数~Date() {//..........}//拷⻉构造函数Date(const Date& d){cout << " Date(const Date& d)" << endl;_year = d._year;_month = d._month;_day = d._day;}//赋值运算符重载Date& operator=(const Date& d)// 传引⽤返回减少拷⻉{// 不要检查⾃⼰给⾃⼰赋值的情况if (this != &d){_year = d._year;_month = d._month;_day = d._day;}// d1 = d2表达式的返回对象应该为d1,也就是*thisreturn *this;}private:int _year;int _month;int _day;
};
//自定义类型<<重载
ostream& operator<<(ostream& out, const Date& d)
{out << d._year << "年" << d._month << "月" << d._day << "日" << endl;return out;
}
//自定义类型>>重载
istream& operator>>(istream& in, Date& d)
{in >> d._year >>d._month>>d._day;return in;
}int main()
{Date d1(2025, 5, 20);cin >> d1;cout << d1;return 0;
}
输入输出示例:
请输入年月日:2025 9 1
输入的日期:2025年9月1日
四、cout 自动识别的原理:模板与运算符重载的协作
1. iostream 库的底层实现
cout
本质上是 ostream
类的对象,其 <<
运算符的重载依赖于 模板函数 和 运算符重载规则:
- 标准库为内置类型(如
int
、double
)提供了大量operator<<
的全局重载函数。 - 当我们为自定义类型定义
operator<<
时,编译器会在编译期通过 参数匹配 找到对应的重载函数。
2. 编译器如何找到正确的重载函数?
假设我们执行 cout << obj;
:
cout
是ostream
对象,<<
的左操作数固定为ostream&
。- 右操作数是自定义类型
obj
,编译器会在当前作用域中寻找参数为(ostream&, MyClass&)
的operator<<
函数。 - 由于我们定义了全局友元函数,编译器匹配到该函数并调用,从而实现自定义输出逻辑。
3. 模板的作用:让代码更通用
标准库中的 operator<<
对字符串、数值等类型的重载,本质上是模板函数的实例化。例如:
template <class T>
ostream& operator<<(ostream& os, const T& value); // 简化示意
当我们为自定义类型重载时,相当于为特定类型 T
提供了特化的实现,编译器会根据实参类型选择最匹配的版本。
五、注意事项与常见错误
-
必须返回流对象引用:
若忘记返回os
或is
,会导致cout << a << b
无法连续输出,因为第二次调用时流对象无法被访问。 -
输入运算符处理错误情况:
应考虑输入失败的情况(如用户输入非数字数据),可通过is.fail()
检查流状态并处理。 -
友元的必要性:
如果类的成员是私有的,重载函数必须声明为friend
,否则无法访问私有数据。若成员是公共的,则无需友元(但通常建议封装私有成员)。
六、总结
重载 <<
和 >>
运算符是 C++ 中增强自定义类型易用性的重要技巧,其核心在于:
- 全局友元函数:解决左操作数必须为
ostream/istream
的问题。 - 流对象引用返回:支持链式输入输出。
- 编译器的参数匹配:通过运算符重载规则和模板机制,让
cout/cin
自动识别自定义类型。
合理使用这一特性,可以让自定义类的输入输出像内置类型一样自然,提升代码的可读性和简洁性。
完整代码示例
#include <iostream>
#include <string>
using namespace std;class Student {
private:string name;int age;
public:Student(string n = "", int a = 0) : name(n), age(a) {}// 声明友元函数friend ostream& operator<<(ostream& os, const Student& s);friend istream& operator>>(istream& is, Student& s);
};// 重载 << 运算符
ostream& operator<<(ostream& os, const Student& s) {os << "Name: " << s.name << ", Age: " << s.age;return os;
}// 重载 >> 运算符
istream& operator>>(istream& is, Student& s) {cout << "Enter name and age: ";is >> s.name >> s.age;return is;
}int main() {Student alice("Alice", 20);cout << "Initial student: " << alice << endl;Student bob;cin >> bob;cout << "Input student: " << bob << endl;return 0;
}
通过以上讲解,你可以轻松为自己的类添加友好的输入输出支持,让 cout
和 cin
像处理内置类型一样处理自定义对象。这一特性是 C++ 运算符重载中最常用的场景之一,也是实现库扩展(如日志类、数据结构输出)的基础。
讲了这么多作为铺垫,现在我们来进入主题默认成员函数(赋值运算符重载),必须是赋值(=)其他符号不算赋值运算符重载默认成员函数。
2. 核心特性(赋值运算符)
一、赋值运算符重载的本质:默认成员函数的特殊存在
在 C++ 中,赋值运算符重载(operator=
)是类的 6 大默认成员函数之一(其余为构造函数、析构函数、拷贝构造函数、普通取地址和 const
取地址运算符)。当用户未显式定义(不写)时,编译器会自动生成一个默认版本。它的核心作用是 完成两个已存在对象之间的拷贝赋值,这与拷贝构造函数(用于新对象的拷贝初始化)形成鲜明对比。
核心区别:拷贝构造 vs 赋值运算符
特性 | 拷贝构造函数 | 赋值运算符重载 |
---|---|---|
作用 | 用已存在对象 初始化新创建的对象 | 对 已存在的对象 进行赋值操作 |
调用时机 | Date d2(d1); 或 Date d2 = d1; | d2 = d1; |
参数形式 | Date(const Date& other) | Date& operator=(const Date& other) |
对象状态 | 新对象尚未存在,需分配内存并初始化 | 目标对象已存在,只需修改成员值 |
二、赋值运算符重载的核心特点
1. 必须作为成员函数重载
C++ 规定,赋值运算符 必须定义为类的成员函数,其隐式第一个参数是 this
指针(指向当前对象),显式参数为右侧对象的 const
引用(避免值传递带来的拷贝开销)。
标准形式:
class MyClass {
public:Date& operator=(const Date& other) { // other等于右侧的 如:d1 = d2; if (this != &other) { // other等于d2// 赋值逻辑 }return *this;}
};
const
引用参数:确保不修改右侧对象,同时避免值传递时调用拷贝构造函数的开销。- 自赋值检查:
if (this == &other)
用于跳过自身赋值(如d1 = d1
),避免无效操作(如重复释放内存)。
2. 返回值为当前类引用(Date&
)
- 目的:支持连续赋值(如
d1 = d2 = d3
),其本质是d1.operator=(d2.operator=(d3))
。 - 效率优势:返回引用而非对象,避免临时对象的创建和销毁,提升性能。
3. 默认实现:浅拷贝与自定义类型处理
当用户未显式定义(不写)时,编译器生成的默认赋值运算符会执行 浅拷贝:
- 内置类型成员:直接按字节复制(如
int
、指针等)。 - 自定义类型成员:调用该成员的赋值运算符重载(递归处理)。
示例:简单类:Date 日期(无需自定义赋值运算符)
class Date {
public:int _year, _month, _day;// 未显式定义赋值运算符,使用编译器默认版本
};int main() {Date d1{2024, 1, 1}, d2{2025, 2, 2};d2 = d1; // 调用默认赋值运算符,完成内置类型成员的浅拷贝return 0;
}
- 特点:成员全为内置类型,无动态资源,默认浅拷贝足够安全。
反例:含动态资源的类(必须自定义深拷贝)
class Stack {
private:int* _a; // 指向堆内存的指针size_t _capacity, _top;
public:Stack() {//...............}~Stack() {//...............} // 未显式定义赋值运算符,使用默认浅拷贝// Stack& operator=(const Stack& d){// //...............// }
};int main() {Stack s1, s2;s2 = s1; // 问题:s1和s2的_a指向同一块内存// 析构时两次释放_a,导致程序崩溃return 0;
}
- 问题根源:默认浅拷贝仅复制指针地址,未复制指针指向的资源,导致 “悬挂指针” 和内存重复释放。
4. 何时必须显式定义赋值运算符?
- 规则:若类中包含 指向动态资源的指针成员,或需要自定义赋值逻辑(如数据校验、资源重新分配),必须显式定义赋值运算符,实现 深拷贝。
- 经验技巧:若类显式定义了 析构函数释放资源,则必须同时显式定义赋值运算符和拷贝构造函数(“三法则” 原则)。
正确实现:深拷贝赋值运算符
class Stack {
public:Stack& operator=(const Stack& other) {if (this == &other) return *this;delete[] _a; // 释放当前对象的旧资源_capacity = other._capacity;_top = other._top;_a = new int[_capacity]; // 分配新内存memcpy(_a, other._a, sizeof(int) * _top); // 复制数据return *this;}// 配套的拷贝构造函数和析构函数需同步实现
};
三、深度补充:默认赋值运算符的 “隐藏行为”
1. 自定义类型成员的赋值逻辑
若类包含自定义类型成员(如 Stack
对象),默认赋值运算符会调用该成员的赋值运算符:
#include <iostream>
#include <cstring> // 用于 memcpy
#include <cstdlib> // 用于 malloc/freeusing 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& s) {// 分配与原栈相同大小的内存_a = (STDataType*)malloc(sizeof(STDataType) * s._capacity);if (nullptr == _a) {perror("malloc申请空间失败");return;}// 复制数据、容量、栈顶指针memcpy(_a, s._a, sizeof(STDataType) * s._top); // 仅复制已使用的元素_capacity = s._capacity;_top = s._top;}// 赋值运算符重载(深拷贝)Stack& operator=(const Stack& s) {// 1. 检查自赋值(避免重复释放/分配内存)if (this == &s) {return *this;}// 2. 释放当前对象的旧内存free(_a);// 3. 分配新内存(与源对象相同容量)_a = (STDataType*)malloc(sizeof(STDataType) * s._capacity);if (nullptr == _a) {perror("malloc申请空间失败");// 若分配失败,需将对象置为有效状态(避免野指针)_capacity = _top = 0;return *this;}// 4. 复制数据、容量、栈顶指针memcpy(_a, s._a, sizeof(STDataType) * s._top); // 仅复制已使用的元素_capacity = s._capacity;_top = s._top;// 5. 返回当前对象引用,支持连续赋值return *this;}// 析构函数(释放动态内存)~Stack() {cout << "~Stack()" << endl;free(_a);_a = nullptr;_top = _capacity = 0;}private:STDataType* _a; // 存储数据的动态数组size_t _capacity; // 栈的容量size_t _top; // 栈顶指针(指向待插入位置)
};// MyQueue 类(依赖 Stack 的拷贝构造和赋值运算符)
class MyQueue {
private:Stack pushst; // 入队栈Stack popst; // 出队栈
};
- 若
Stack
已正确实现深拷贝赋值运算符,则MyQueue
的默认赋值运算符无需额外定义。
2. 移动赋值运算符(C++11 新增)
C++11 引入移动语义后,赋值运算符分为两种:
- 拷贝赋值:
MyClass& operator=(const MyClass&)
(处理左值)。 - 移动赋值:
MyClass& operator=(MyClass&&)
(处理右值,转移资源所有权,避免深拷贝)。
两者可共存,共同优化赋值操作的效率。
3. 禁止赋值:删除默认赋值运算符
若不希望类支持赋值操作,可显式删除默认版本:
class NonCopyable {
public:NonCopyable& operator=(const NonCopyable&) = delete; // 禁止赋值
};
四、总结:赋值运算符重载的最佳实践
1. 基础准则
- 默认够用场景:成员全为内置类型或无需资源管理的类,直接使用编译器生成的默认赋值运算符。
- 必须自定义场景:包含动态资源(如指针)的类,需显式实现深拷贝赋值运算符,配套实现拷贝构造函数和析构函数(“三法则”)。
2. 代码模板
#include <iostream>
#include <cstring> // 用于 memcpy
#include <cstdlib> // 用于 malloc/freeusing 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& s) {// 分配与原栈相同大小的内存_a = (STDataType*)malloc(sizeof(STDataType) * s._capacity);if (nullptr == _a) {perror("malloc申请空间失败");return;}// 复制数据、容量、栈顶指针memcpy(_a, s._a, sizeof(STDataType) * s._top); // 仅复制已使用的元素_capacity = s._capacity;_top = s._top;}// 赋值运算符重载(深拷贝)Stack& operator=(const Stack& s) {// 1. 检查自赋值(避免重复释放/分配内存)if (this == &s) {return *this;}// 2. 释放当前对象的旧内存free(_a);// 3. 分配新内存(与源对象相同容量)_a = (STDataType*)malloc(sizeof(STDataType) * s._capacity);if (nullptr == _a) {perror("malloc申请空间失败");// 若分配失败,需将对象置为有效状态(避免野指针)_capacity = _top = 0;return *this;}// 4. 复制数据、容量、栈顶指针memcpy(_a, s._a, sizeof(STDataType) * s._top); // 仅复制已使用的元素_capacity = s._capacity;_top = s._top;// 5. 返回当前对象引用,支持连续赋值return *this;}// 析构函数(释放动态内存)~Stack() {cout << "~Stack()" << endl;free(_a);_a = nullptr;_top = _capacity = 0;}private:STDataType* _a; // 存储数据的动态数组size_t _capacity; // 栈的容量size_t _top; // 栈顶指针(指向待插入位置)
};
3. 关键记忆点
- 赋值运算符是 默认成员函数,但仅在必要时(资源管理)才需显式定义。
- 永远检查 自赋值,避免无效操作和资源泄漏。
- 遵循 “三法则”:若定义了析构函数,必定义拷贝构造函数和赋值运算符。
六、取地址运算符重载:细节控制
1. 取地址运算符重载的基本概念
在 C++ 里,取地址运算符 &
一般用于获取对象的内存地址。不过,你能够对这个运算符进行重载,从而让它表现出与默认行为不同的功能。在类里重载取地址运算符时,通常会提供普通版本和 const
版本这两种形式。
2. 两种重载形式的用途
- 普通版本:
Date* operator&()
,当对一个非const
对象使用取地址运算符时,就会调用这个版本。它返回的是指向该对象的指针。 const
版本:const Date* operator&() const
,当对一个const
对象使用取地址运算符时,会调用此版本。它返回的是指向const
对象的指针,以此保证不会通过这个指针修改对象的内容。
在 C++ 中,将const
关键字放在成员函数声明的括号后面,主要目的就是修饰this
指针。它表明该成员函数是一个const
成员函数,在这个函数内部,this
指针所指向的对象被视为常量,不能通过this
指针来修改对象的状态,即不能修改对象的成员变量(除非成员变量被声明为mutable
关键字)。
3. 自定义场景的详细分析
- 默认实现:在绝大多数情况下,使用编译器的默认实现就可以了。编译器默认的取地址运算符会返回对象的真实内存地址。
- 特殊需求:某些特殊场景下,你或许不想让外部代码获取对象的真实地址。这时,就可以通过重载取地址运算符来返回一个固定值(像
nullptr
),以此隐藏真实地址。不过,这种场景非常少见,因为直接隐藏对象地址可能会对代码的正常使用产生影响。
4. 代码示例分析
下面是你提供的代码,我们来详细分析一下:
#include<iostream>
#include<string>
using namespace std;class Date
{
public:// 普通版本的取地址运算符重载Date* operator&(){cout << "Date* operator&()" << endl;// 这里返回 nullptr 隐藏了真实地址return nullptr;}// const 版本的取地址运算符重载const Date* operator&() const{cout << "const Date* operator&() const" << endl;// 这里返回 nullptr 隐藏了真实地址return nullptr;}
private:int _year = 1; // 年int _month = 1; // 月int _day = 1; // 日
};int main()
{Date d1;const Date d2;// 对非 const 对象使用取地址运算符cout << &d1 << endl;// 对 const 对象使用取地址运算符cout << &d2 << endl;return 0;
}
运行显示结果:
代码执行流程:
- 定义
Date
类:- 在类里重载了取地址运算符的普通版本和
const
版本。 - 这两个重载函数都会输出一条信息,并且返回
nullptr
,这样就隐藏了对象的真实地址。
- 在类里重载了取地址运算符的普通版本和
- 在
main
函数中:- 创建了一个非
const
对象d1
和一个const
对象d2
。 - 对
d1
使用取地址运算符&
,此时会调用普通版本的operator&()
,输出"Date* operator&()"
,接着输出nullptr
。 - 对
d2
使用取地址运算符&
,这时会调用const
版本的operator&() const
,输出"const Date* operator&() const"
,然后输出nullptr
。
- 创建了一个非
注意事项:
- 重载取地址运算符可能会让代码的行为变得难以预测,所以在使用时要谨慎。
- 隐藏对象的真实地址可能会对代码的调试和正常使用造成影响,要确保这种做法确实有必要。
总结
取地址运算符重载为你提供了对对象地址获取行为进行自定义的能力。不过,由于其可能带来的复杂性,一般情况下建议使用编译器的默认实现。只有在有特殊需求时,才考虑重载这个运算符。
七、实战:日期类的完整实现(详细讲解)
我们主要分为三个文档:Date.h(声明) Date.cpp(实现) test.cpp(测试)
Date.h:
#pragma once
#include <iostream>
#include <assert.h>using namespace std;// 日期类的定义,用于表示和操作日期
class Date
{// 友元函数声明,允许全局的输出流和输入流运算符访问 Date 类的私有成员friend ostream& operator<<(ostream& out, const Date& d);friend istream& operator>>(istream& in, Date& d);public:// 构造函数,带有默认参数,用于初始化日期对象// 如果用户不提供参数,默认日期为 1 年 1 月 1 日Date(int year = 1, int month = 1, int day = 1);// 打印日期的成员函数,以年-月-日的格式输出// const 修饰表示该函数不会修改对象的成员变量void Print() const;// 拷贝构造函数,用于创建一个新的 Date 对象,其值与另一个 Date 对象相同Date(const Date& d);// 比较运算符重载函数,用于比较两个 Date 对象的大小关系bool operator<(const Date& x) const;bool operator==(const Date& x) const;bool operator<=(const Date& x) const;bool operator>(const Date& x) const;bool operator>=(const Date& x) const;bool operator!=(const Date& x) const;// 获取指定年份和月份的天数,考虑闰年情况int GetMonthDay(int year, int month);// 算术运算符重载函数,用于对日期进行加减操作Date& operator+=(int day);Date operator+(int day) const;Date& operator-=(int day);Date operator-(int day) const;// 自增自减运算符重载函数,分为前置和后置两种形式Date& operator++();Date operator++(int);Date& operator--();Date operator--(int);// 计算两个日期之间相差的天数int operator-(const Date& d) const;private:// 日期的年、月、日成员变量,私有属性,外部无法直接访问int _year;int _month;int _day;
};// 全局的输出流运算符重载,用于将 Date 对象以特定格式输出到输出流中
ostream& operator<<(ostream& out, const Date& d);
// 全局的输入流运算符重载,用于从输入流中读取日期并赋值给 Date 对象
istream& operator>>(istream& in, Date& d);
- 头文件保护与包含:
#pragma once
防止头文件多次编译。#include <iostream>
用于输入输出,#include <assert.h>
用于调试时检查非法日期。using namespace std;
引入标准命名空间,简化代码书写(但在大型项目中可能引发命名冲突)。 - 类定义:
- 友元函数:
operator<<
和operator>>
允许直接访问类私有成员,实现自定义输入输出格式。 - 构造函数:
Date(int year = 1, int month = 1, int day = 1);
带默认参数,若用户不传入值,创建默认日期1-1-1
。 - 成员函数声明:
Print() const
以固定格式打印日期,const
保证函数不修改对象。- 比较运算符(
operator<
、operator==
等)用于日期大小比较。 GetMonthDay
辅助计算指定年月的天数。- 算术运算符(
operator+=
、operator+
等)实现日期加减。 - 自增自减运算符(
operator++
、operator--
)分前置和后置形式。 operator-
计算两日期差值。
- 私有成员:
_year
、_month
、_day
存储日期,外部无法直接访问,保证数据封装性。
- 友元函数:
Date.cpp:
#include "Date.h"
// 包含 Date.h 头文件,引入 Date 类的定义// 构造函数的实现
Date::Date(int year, int month, int day): _year(year), _month(month), _day(day) // 成员初始化列表,先初始化成员变量
{// 检查输入的日期是否合法// 月份需在 1 到 12 之间,天数需在该月的合法范围内if (month > 0 && month < 13&& day > 0 && day <= GetMonthDay(year, month)){// 若日期合法,将输入的年、月、日赋值给对象的成员变量_year = year;_month = month;_day = day;}else{// 若日期不合法,输出错误信息并终止程序cout << "非法日期" << endl;assert(false);}
}// 拷贝构造函数实现
Date::Date(const Date& other) { // 拷贝构造函数_year = other._year;_month = other._month;_day = other._day;
}// 小于运算符重载的实现
bool Date::operator<(const Date& x) const
{// 先比较年份if (_year < x._year){return true;}// 年份相同,比较月份else if (_year == x._year && _month < x._month){return true;}// 年份和月份都相同,比较天数else if (_year == x._year && _month == x._month && _day < x._day){return true;}// 都不满足则返回 falsereturn false;
}
// Print 函数的实现
void Date::Print() const
{cout << _year << "-" << _month << "-" << _day << endl;
}
// 等于运算符重载的实现
bool Date::operator==(const Date& x) const
{// 只有当年份、月份和天数都相等时才返回 truereturn _year == x._year&& _month == x._month&& _day == x._day;
}// 小于等于运算符重载的实现,基于小于和等于运算符
bool Date::operator<=(const Date& x) const
{return *this < x || *this == x;
}// 大于运算符重载的实现,基于小于等于运算符取反
bool Date::operator>(const Date& x) const
{return !(*this <= x);
}// 大于等于运算符重载的实现,基于小于运算符取反
bool Date::operator>=(const Date& x) const
{return !(*this < x);
}// 不等于运算符重载的实现,基于等于运算符取反
bool Date::operator!=(const Date& x) const
{return !(*this == x);
}// 获取指定年份和月份的天数的实现
int Date::GetMonthDay(int year, int month)
{// 静态数组存储每个月的天数,下标从 1 开始对应 1 月到 12 月static int daysArr[13] = { 0, 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))){// 闰年 2 月返回 29 天return 29;}else{// 非闰年 2 月或其他月份按数组返回天数return daysArr[month];}
}// 加等于运算符重载的实现
Date& Date::operator+=(int day)
{// 如果 day 为负数,调用减等于运算符进行减法操作if (day < 0){return *this -= -day;}// 将天数加到当前日期的天数上_day += day;// 处理日期进位while (_day > GetMonthDay(_year, _month)){// 天数超过该月最大天数,减去该月天数_day -= GetMonthDay(_year, _month);// 月份加 1++_month;// 如果月份超过 12,年份加 1,月份重置为 1if (_month == 13){++_year;_month = 1;}}// 返回当前对象的引用return *this;
}// 加运算符重载的实现
Date Date::operator+(int day) const
{// 创建一个临时对象,初始化为当前对象Date tmp(*this);// 调用加等于运算符对临时对象进行加法操作tmp += day;// 返回临时对象return tmp;
}// 减等于运算符重载的实现
Date& Date::operator-=(int day)
{// 如果 day 为负数,调用加等于运算符进行加法操作if (day < 0){return *this += -day;}// 将天数从当前日期的天数中减去_day -= day;// 处理日期借位while (_day <= 0){// 月份减 1--_month;// 如果月份小于 1,年份减 1,月份重置为 12if (_month == 0){_month = 12;--_year;}// 天数加上该月的天数_day += GetMonthDay(_year, _month);}// 返回当前对象的引用return *this;
}// 减运算符重载的实现
Date Date::operator-(int day) const
{// 创建一个临时对象,初始化为当前对象Date tmp = *this;// 调用减等于运算符对临时对象进行减法操作tmp -= day;// 返回临时对象return tmp;
}// 前置自增运算符重载的实现
Date& Date::operator++()
{// 调用加等于运算符将当前日期加 1 天*this += 1;// 返回当前对象的引用return *this;
}// 后置自增运算符重载的实现
// int 参数作为占位符,用于区分前置和后置自增
Date Date::operator++(int)
{// 创建一个临时对象保存当前日期Date tmp = *this;// 调用加等于运算符将当前日期加 1 天*this += 1;// 返回临时对象return tmp;
}// 前置自减运算符重载的实现
Date& Date::operator--()
{// 调用减等于运算符将当前日期减 1 天*this -= 1;// 返回当前对象的引用return *this;
}// 后置自减运算符重载的实现
// int 参数作为占位符,用于区分前置和后置自减
Date Date::operator--(int)
{// 创建一个临时对象保存当前日期Date tmp = *this;// 调用减等于运算符将当前日期减 1 天*this -= 1;// 返回临时对象return tmp;
}// 计算两个日期之间相差天数的实现
int Date::operator-(const Date& d) const
{// 初始化较大日期和较小日期Date max = *this;Date min = d;// 差值符号,默认为 1int flag = 1;// 如果当前日期小于传入日期,交换较大和较小日期,并将符号设为 -1if (*this < d){max = d;min = *this;flag = -1;}// 初始化天数差值为 0int n = 0;// 不断递增较小日期,直到与较大日期相等while (min != max){++min;++n;}// 返回天数差值乘以符号return n * flag;
}// 输出流运算符重载的实现
ostream& operator<<(ostream& out, const Date& d)
{// 将日期以年-月-日的格式输出到输出流中out << d._year << "年" << d._month << "月" << d._day << "日" << endl;// 返回输出流的引用,支持链式输出return out;
}// 输入流运算符重载的实现
istream& operator>>(istream& in, Date& d)
{// 临时变量用于存储输入的年、月、日int year, month, day;// 从输入流中读取年、月、日in >> year >> month >> day;// 检查输入的日期是否合法if (month > 0 && month < 13&& day > 0 && day <= d.GetMonthDay(year, month)){// 若合法,将输入的年、月、日赋值给 Date 对象d._year = year;d._month = month;d._day = day;}else{// 若不合法,输出错误信息并终止程序cout << "非法日期" << endl;assert(false);}// 返回输入流的引用,支持链式输入return in;
}
- 构造函数:使用成员初始化列表初始化
_year
、_month
、_day
,检查日期合法性(月份范围、天数不超过当月最大值),非法日期通过assert(false)
终止程序。 - 拷贝构造函数:直接复制已有对象的年、月、日,实现深拷贝。
- 比较运算符:
operator<
按年→月→日顺序比较,其他比较运算符(如operator<=
)通过逻辑组合(||
、!
)复用已有代码,减少冗余。 GetMonthDay
:用静态数组存储各月默认天数,特殊处理2
月(闰年返回29
天)。- 算术运算符:
operator+=
处理天数累加,超过当月最大值则进位(月份加1
,超12
则跨年)。operator+
通过创建临时对象调用operator+=
,返回新日期。operator-=
和operator-
类似,处理天数减少和借位(月份减1
,小于1
则跨年)。
- 自增自减运算符:前置运算符(
++d
/--d
)先修改自身再返回引用;后置运算符(d++
/d--
)先返回副本再修改自身。 operator-
:通过递增较小日期统计天数差,注意日期大小关系(flag
控制符号)。- 流运算符:
operator<<
按年-月-日
格式输出。operator>>
从输入流读取年、月、日,验证合法后赋值给对象。
test.cpp:
#include "Date.h"
// 包含 Date.h 头文件,引入 Date 类的定义// 测试拷贝构造函数
void TestCopyConstructor() {// 创建一个 Date 对象 d1Date d1(2023, 12, 31);// 使用拷贝构造函数创建 d2,其值与 d1 相同Date d2(d1);cout << "拷贝构造测试:";// 打印 d2 的日期d2.Print();
}// 测试比较运算符
void TestComparisonOperators() {// 创建不同日期的 Date 对象Date d1(2024, 5, 20);Date d2(2024, 9, 1);Date d3(2024, 5, 20);// 测试小于运算符cout << "d1 < d2: " << (d1 < d2) << endl;// 测试等于运算符cout << "d1 == d3: " << (d1 == d3) << endl;// 测试小于等于运算符cout << "d2 <= d1: " << (d2 <= d1) << endl;// 测试大于运算符cout << "d2 > d1: " << (d2 > d1) << endl;// 测试不等于运算符cout << "d1 != d2: " << (d1 != d2) << endl;
}// 测试获取月份天数
void TestGetMonthDay() {// 创建一个 Date 对象用于调用 GetMonthDay 函数Date d;// 测试闰年 2 月的天数cout << "2024年2月天数: " << d.GetMonthDay(2024, 2) << endl;// 测试平年 2 月的天数cout << "2023年2月天数: " << d.GetMonthDay(2023, 2) << endl;// 测试普通月份的天数cout << "4月天数: " << d.GetMonthDay(2024, 4) << endl;
}// 测试日期加法(+= 和 +)
void TestAdditionOperators() {// 创建一个 Date 对象Date d(2024, 5, 20);// 使用 += 运算符将日期增加 10 天d += 10;cout << "d += 10后: ";// 打印增加后的日期d.Print();// 使用 + 运算符创建一个新的 Date 对象,其值为 d 增加 32 天后的日期Date d2 = d + 32;cout << "d + 32后: ";// 打印新日期d2.Print();
}// 测试日期减法(-= 和 -)
void TestSubtractionOperators() {// 创建一个 Date 对象Date d(2024, 7, 1);// 使用 -= 运算符将日期减少 32 天d -= 32;cout << "d -= 32后: ";// 打印减少后的日期d.Print();// 使用 - 运算符创建一个新的 Date 对象,其值为 d 减少 10 天后的日期Date d2 = d - 10;cout << "d - 10后: ";// 打印新日期d2.Print();
}// 测试自增运算符(前置++ 和 后置++)
void TestIncrementOperators() {// 创建一个 Date 对象Date d(2024, 5, 31);// 使用前置自增运算符,先增加日期再返回对象引用Date d1 = ++d;cout << "前置++:";// 打印增加后的日期d.Print();cout << "前置++返回值:";// 打印前置自增返回的对象d1.Print();// 使用后置自增运算符,先返回原对象副本,再增加日期Date d2 = d++;cout << "后置++后d:";// 打印增加后的日期d.Print();cout << "后置++返回副本:";// 打印后置自增返回的原对象副本d2.Print();
}// 测试自减运算符(前置-- 和 后置--)
void TestDecrementOperators() {// 创建一个 Date 对象Date d(2024, 6, 1);// 使用前置自减运算符,先减少日期再返回对象引用Date d1 = --d;cout << "前置--:";// 打印减少后的日期d.Print();cout << "前置--返回值:";// 打印前置自减返回的对象d1.Print();// 使用后置自减运算符,先返回原对象副本,再减少日期Date d2 = d--;cout << "后置--后d:";// 打印减少后的日期d.Print();cout << "后置--返回副本:";// 打印后置自减返回的原对象副本d2.Print();
}// 测试日期差值(operator-)
void TestDateDifference() {// 创建两个不同日期的 Date 对象Date d1(2024, 5, 20);Date d2(2024, 9, 1);// 计算 d2 与 d1 的日期差值int days = d2 - d1;cout << "d2 - d1 = " << days << "天" << endl;// 计算 d1 与 d2 的日期差值days = d1 - d2;cout << "d1 - d2 = " << days << "天" << endl;
}// 测试流插入和流提取(operator<< 和 operator>>)
void TestStreamOperators() {// 创建一个 Date 对象Date d;cout << "请输入日期(年 月 日): ";// 从输入流中读取日期并赋值给 dcin >> d;cout << "输入的日期:" << d;
}int main() {// 1. 构造函数和 Print() 测试// 创建一个 Date 对象 d1 并初始化Date d1(2024, 5, 20);cout << "初始日期 d1:";// 打印 d1 的日期d1.Print();// 创建一个常量 Date 对象 d2 并初始化const Date d2(2024, 9, 1);cout << "常量对象 d2:";// 打印 d2 的日期d2.Print();// 2. 拷贝构造函数测试TestCopyConstructor();// 3. 比较运算符测试TestComparisonOperators();// 4. 获取月份天数测试TestGetMonthDay();// 5. 加法运算符测试TestAdditionOperators();// 6. 减法运算符测试TestSubtractionOperators();// 7. 自增运算符测试TestIncrementOperators();// 8. 自减运算符测试TestDecrementOperators();// 9. 日期差值测试TestDateDifference();// 10. 流运算符测试(手动输入部分需手动验证)TestStreamOperators();return 0;
}
TestCopyConstructor
:创建d1
并通过拷贝构造函数生成d2
,验证d2
日期与d1
是否一致。TestComparisonOperators
:创建不同日期对象,测试operator<
、operator==
等比较运算符的逻辑是否正确。TestGetMonthDay
:验证GetMonthDay
函数对闰年2
月(2024
年)、平年2
月(2023
年)及普通月份(4
月)天数计算的准确性。TestAdditionOperators
:测试operator+=
和operator+
对日期的加法操作,包括跨月(如5
月20
日+10
天→5
月30
日,+32
天→跨月)。TestSubtractionOperators
:测试operator-=
和operator-
对日期的减法操作,如7
月1
日-32
天→跨月。TestIncrementOperators
:区分前置(++d
先变后用)和后置(d++
先用后变)自增运算符,验证日期变化和返回值。TestDecrementOperators
:类似自增测试,验证前置(--d
)和后置(d--
)自减运算符的逻辑。TestDateDifference
:计算两日期差值,验证operator-
对正负数差值的处理(如d2 - d1
和d1 - d2
)。TestStreamOperators
:通过cin
输入日期,验证operator>>
的合法性检查和operator<<
的输出格式。main
函数:按顺序调用各测试函数,全面验证Date
类的功能,包括构造函数、常量对象打印、拷贝构造、各类运算符及流操作。
八、总结:何时需要自定义默认成员函数?
场景 | 构造函数 | 析构函数 | 拷贝构造 | 赋值重载 |
---|---|---|---|---|
简单数据(无资源) | 可选 | 不需要 | 可选 | 可选 |
含动态资源(指针) | 需要 | 必须 | 必须 | 必须 |
含自定义类型成员 | 可选 | 不需要 | 可选 | 可选 |
理解默认成员函数的行为机制,是写出安全、高效 C++ 代码的基础。当类涉及资源管理(如内存、文件、网络连接)时,必须手动实现拷贝构造和赋值重载,避免浅拷贝引发的灾难性后果。通过合理利用构造 / 析构的自动调用特性,结合运算符重载提升代码可读性,C++ 的类机制能显著简化复杂场景的开发。