C++类和对象(四)
你知道为什么一个简单的 a = b 会导致程序崩溃吗?为什么有的类里 &obj 取到的地址不是真的地址?这两个看似基础的运算符,背后隐藏着资源管理、异常安全、甚至整个 C++ 设计哲学的秘密。这篇文章将带你揭开 operator= 和 operator& 的神秘面纱,顺便手撕一下Date类的代码 !ƪ(˘⌣˘)ʃ
文章目录
- C++类和对象(四)
-
- [1. 赋值运算符重载](#1. 赋值运算符重载)
-
- [1.1 赋值运算符重载的特点:](#1.1 赋值运算符重载的特点:)
- [2. 取地址运算符重载](#2. 取地址运算符重载)
-
- [2.1 `const` 成员函数](#2.1
const成员函数) -
- [2.1.1 语法与含义](#2.1.1 语法与含义)
- [2.1.2 为什么需要 `const` 成员函数?](#2.1.2 为什么需要
const成员函数?)
- [2.2 取地址运算符重载](#2.2 取地址运算符重载)
- [2.1 `const` 成员函数](#2.1
- [3. 日期类代码实现](#3. 日期类代码实现)
- 结语
1. 赋值运算符重载
赋值运算符重载是 C++ 中一个特殊的成员函数,用于完成两个已经存在的对象之间的拷贝赋值。注意,它和拷贝构造函数有着本质区别:
- 拷贝构造:用一个对象初始化另一个新创建的对象。
- 赋值重载:将已有对象的值赋给另一个已经存在的对象。
当你写下 a = b 时,编译器会调用 a.operator=(b)。如果你没有显式定义,编译器会生成一个默认版本------逐成员拷贝(浅拷贝)。对于只包含内置类型的类(如 Date),这通常没问题。但一旦类管理了动态资源(如 Stack 中的指针),浅拷贝就会导致两个对象指向同一块内存,析构时引发双重释放或内存泄漏。
1.1 赋值运算符重载的特点:
先来看一段代码:՞˶・֊・˶՞
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,把d2的值赋给了d1,应该返回d1的值,就是*this
return *this;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2026, 3, 27);
Date d2(d1);
d1.Print();
d2.Print();
Date d3(2026, 3, 28);
d1 = d3;
d1.Print();
// 需要注意这⾥是拷贝构造,不是赋值重载
// 赋值重载完成两个已经存在的对象直接的拷贝赋值
// ⽽拷贝构造⽤于⼀个对象拷贝初始化给另⼀个要创建的对象
Date d4 = d1;
d4.Print();
return 0;
}
效果:

特点一:赋值运算符重载必须重载为成员函数,参数建议用 const 引用
cpp
Date& operator=(const Date& d)
为什么必须是成员函数**(՞•Ꙫ•՞)ノ???**
赋值运算符=是 C++ 中少数几个必须作为非静态成员函数重载的运算符之一(与 []、()、-> 等类似)。如果试图定义为全局函数,编译器会报错。原因很简单:赋值操作天然与左操作数(即 this)紧密绑定,只有成员函数才能访问当前对象的私有成员。你可以理解为:赋值是"对象自己的事",只有它自己才能决定怎么把别人的值赋给自己。
为什么参数用 const 引用?
- 避免拷贝 :如果参数写成
Date d,那么传参时会调用拷贝构造函数,产生一次额外的拷贝。对于大对象来说,这是不必要的开销。 - 保护右操作数 :加
const限定符表明赋值过程中不会修改右操作数,符合赋值操作的语义,同时也允许将常量对象(用const修饰的对象,一旦创建就不能被修改,只能读取)作为右操作数。
特点二:必须有返回值,且返回 *this 的引用
代码里这样写:
cpp
return *this;
为什么要有返回值?
为了支持连续赋值,比如:
cpp
d1 = d2 = d3;
它等价于 d1 = (d2 = d3)。如果 d2 = d3 没有返回值,d1 就不知道右边是什么,连续赋值便无法实现,编译就会报错。
为什么返回引用,而不是对象?
返回引用能避免一次拷贝。如果返回 Date 对象,那么 d2 = d3 结束后会生成一个临时对象 ,再把这个临时对象赋给 d1,多了一次拷贝构造和析构。返回 *this 的引用(即Date&),直接就是 d2 本身,效率更高,而且仍然支持连续赋值!
特点三:默认赋值运算符执行浅拷贝
如果你不写 operator=,编译器会自动帮你合成一个。它的行为是:
- 对内置类型成员(如
int、char*)进行逐字节拷贝(浅拷贝)。- 对自定义类型成员,调用该成员自己的赋值运算符。
什么时候默认版本够用?
像 Date 类,所有成员都是 int,没有指向堆内存的指针,默认的浅拷贝就能正确工作。所以,即使我们删除上面代码中的 operator= 定义,程序依然完美运行。
什么时候默认版本不够用?
来看一个典型的 Stack 类:
cpp
typedef int STDataType;
class Stack
{
public:
Stack(int n = 4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (nullptr == _a)
{
perror("malloc fail");
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;
};
如果执行 s1 = s2,默认赋值运算符只会浅拷贝指针 _a,结果 s1._a 和 s2._a 指向同一块内存。当两个对象销毁时,同一块内存被释放两次,程序直接崩溃。这就是著名的浅拷贝陷阱。
因此,对于管理资源的类,我们必须自己实现深拷贝的赋值运算符:释放原有资源,重新分配内存,并复制数据。这也是"三法则"(有析构则应有拷贝构造和拷贝赋值)的由来。
特点四: 显式实现析构时,通常也需要显式实现赋值运算符
根据类成员的类型和资源管理情况,我们可以总结出以下规则:
- 像
Date这样的类:成员变量全是内置类型,且没有指向外部资源,编译器生成的默认赋值运算符已经足够,因此不需要显式实现。 - 像
Stack这样的类:虽然成员也都是内置类型,但_a指向了动态分配的内存。默认浅拷贝会导致资源被多个对象共享,必须自己实现深拷贝。 - 像
MyQueue这样的类:内部主要包含自定义类型的成员(例如两个Stack对象)。编译器生成的默认赋值运算符会依次调用每个成员的赋值运算符,只要Stack的赋值运算符已经正确实现了深拷贝,MyQueue就不需要再显式实现。
敲黑板:
如果一个类显式实现了析构函数(释放资源),那么它几乎肯定需要显式实现拷贝构造函数和拷贝赋值运算符。
反过来,如果类没有资源管理,通常可以依赖编译器生成的默认版本。
这个技巧非常实用:看到
~Stack(),立刻想到需要补上拷贝构造和赋值重载! ƪ(˘⌣˘)ʃ
2. 取地址运算符重载
2.1 const 成员函数
先看一段熟悉的 Date 类代码:
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;
}
//void Print(const Date* const this) const
void Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2026, 3, 28);
d1.Print();
const Date d2(2026, 3, 29);
d2.Print();
return 0;
}
2.1.1 语法与含义
- 语法: 在成员函数的参数列表后面加上
const关键字,就定义了一个const成员函数。 - 作用:
const修饰的是隐含的this指针,表明在该成员函数内部不能修改任何成员变量。也就是说,这个函数是"只读"的。
编译器会这样理解:
- 普通成员函数: 隐含的
this类型为Date* const this(指针本身不可改,但指向的内容可改)。 const成员函数: 隐含的this类型变为const Date* const this(指针本身不可改,指向的内容也不可改)。
2.1.2 为什么需要 const 成员函数?
关键原因在于: const 对象只能调用 const 成员函数。
假设我们有一个 const Date 对象,如果 Print() 不是 const 成员函数,那么 d2.Print() 就会编译报错。因为编译器需要保证 const 对象不会被修改,而调用一个可能修改成员的非 const 成员函数是不允许的。
因此,对于那些只读取成员、不修改成员的函数,应该一律加上 const。这既是良好的设计习惯,也能让代码更通用。
2.2 取地址运算符重载
在 C++ 中,取地址运算符 & 用于获取对象的地址。你可能不知道,这个运算符其实也有重载版本,而且编译器默认就为我们生成了两个版本:一个用于普通对象,另一个用于 const 对象。绝大多数情况下,我们根本不需要去动它------除非你有一些非常特殊的需求,比如"禁止别人取我的地址"。
编译器会自动为每个类生成两个取地址运算符重载:
cpp
// 普通对象的取地址
Date* operator&()
{
return this;
}
// const 对象的取地址
const Date* operator&() const
{
return this;
}
这两个函数很简单,就是返回 this 指针。第一个返回 Date*,第二个返回 const Date*,保证了 const 对象取到的地址也是 const 指针,防止通过该指针修改对象。
如果不想让别人取我的地址就可以这样写:
cpp
Date* operator&()
{
//return this;
return nullptr;
}
const Date* operator&()const
{
//return this;
return nullptr;
}
敲黑板:
- 两个版本都必须提供,否则
const对象无法取地址。- 返回类型必须是指针类型:普通版本返回
Date*,const版本返回const Date*。
嘿嘿,重载取地址运算符最致命的不是技术问题,而是社交问题------你的同事会困惑,甚至愤怒 ( ⩌ - ⩌ ) 。
如果你写了一个类,重载了 operator& 返回 nullptr。然后你的同事写代码的时候用到了你的类中的取地址**╭(°A°`)╮** 。
同事花了两天时间调试,最后发现罪魁祸首是你重载了取地址运算符!你觉得他会怎么想?这种"惊喜"绝对会让人怀疑人生。所以一定要慎重使用取地址运算符重载!!
3. 日期类代码实现
补充一个小知识------友元函数: (以后会再详细讲解,这里只是浅浅提一下,帮助我们实现代码)
友元函数是类的"特殊朋友",它虽然不是这个类的成员函数,但被允许直接访问类的私有(
private)和保护(protected)成员。
语法很简单: 在类定义中,加上friend关键字和函数声明,可以放在public、private或protected任何位置(通常放在开头,以便阅读)。
代码分了3个部分实现:
头文件 Date.h:
cpp
#include<iostream>
using namespace std;
#include<assert.h>
// Date 日期类
class Date
{
// 声明友元函数,使全局的 << 和 >> 能访问私有成员
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
public:
bool CheckDate() const; // 检查日期合法性
Date(int year = 1900, int month = 1, int day = 1); // 构造函数(带默认值)
void Print()const; // 打印日期
// 静态成员函数:获取某年某月的天数
static int GetMonthDay(int year,int month)
{
int MonthDay[13] = { -1,31,28,31,30,31,30,31,31,30,31,30,31 };
// 判断闰年:闰年2月返回29天
if (month == 2 && ( (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) ))
{
return 29;
}
return MonthDay[month];
}
// 比较运算符重载
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) const; // 日期减天数
Date& operator-=(int day);
// 后置自增 d++ (int参数用于区分前置后置,无实际意义)
Date operator++(int);
// 前置自增 ++d
Date& operator++();
// 后置自减 d--
Date operator--(int);
// 前置自减 --d
Date& operator--();
// 日期减日期,返回相差天数
int operator-(const Date& d) const;
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
// 全局运算符重载函数声明
ostream& operator<<(ostream& out, const Date& d);
istream& operator>>(istream& in, Date& d);
代码实现部分 Date.cpp:
cpp
#include"Date.h"
// 检查日期合法性(年、月、日是否有效)
bool Date::CheckDate() const
{
if (_month < 1 || _month>12 || _day<1
|| _day>GetMonthDay(_year, _month))
{
return false; // 非法日期
}
else {
return true; // 合法日期
}
}
// 构造函数:初始化并验证日期
Date::Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
if (!CheckDate()) // 若日期非法,则提示
{
cout << "日期非法"<<endl;
Print();
}
}
// 打印日期
void Date::Print() const
{
cout << _year << "/" << _month << "/" << _day << endl;
}
// 比较运算符重载:依次比较年、月、日
bool Date::operator<(const Date& d) const
{
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;
}
// 其他比较运算符基于 operator<和 operator==实现
bool Date::operator<=(const Date& d) const
{
return *this < d || *this == d;
}
bool Date::operator>(const Date& d) const
{
return !(*this >= d);
}
bool Date::operator>=(const Date & d) const
{
return !(*this < d);
}
bool Date::operator==(const Date& d) const
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
bool Date::operator!=(const Date& d) const
{
return !(*this == d);
}
// 重载 +=:当前日期加上天数,处理跨月、跨年
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) // 月份满12进位到1月,年份加1
{
_year++;
_month = 1;
}
}
return *this;
}
// 重载 +:不改变原对象,返回一个新对象
Date Date::operator+(int day)const
{
Date tmp = *this; // 创建副本
tmp += day; // 复用 += 实现
return tmp;
}
// 重载 -=:当前日期减去天数,处理借位
Date& Date::operator-=(int day)
{
if (day < 0) // 若天数为负,转换为 +=
{
return *this += (-day);
}
_day -= day;
// 处理天数变为0或负数,循环向月、年借位
while (_day <= 0)
{
--_month;
if (_month == 0) // 月份借位到上年12月
{
_month = 12;
--_year;
}
_day += GetMonthDay(_year, _month); // 加上上个月的天数
}
return *this;
}
// 重载 - (日期-天数):不改变原对象
Date Date::operator-(int day)const
{
Date tmp = *this;
tmp -= day;
return tmp;
}
// 后置自增 d++:返回自增前的值
Date Date::operator++(int)
{
Date tmp = *this; // 保存原值
*this += 1; // 当前对象加1天
return tmp; // 返回旧值
}
// 前置自增 ++d:返回自增后的值
Date& Date::operator++()
{
*this += 1;
return *this;
}
// 后置自减 d--
Date Date::operator--(int)
{
Date tmp = *this;
*this -= 1;
return tmp;
}
// 前置自减 --d
Date& Date::operator--()
{
*this -= 1;
return *this;
}
// 重载 - (日期-日期):计算两个日期间的天数差
int Date::operator-(const Date& d) const
{
int flag = 1; // 结果符号,1表示this>=d,-1表示this<d
Date max = *this, min = d;
if (*this < d) // 确保max是较晚的日期
{
max = d;
min = *this;
flag = -1;
}
int 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)
{
while (1)
{
cout << "请输入 年 月 日 >";
in >> d._year >> d._month >> d._day;
if (!d.CheckDate())
{
cout << "日期非法"<< endl;
d.Print();
cout << "请重新输入" << endl;
}
else {
break; // 输入合法,退出循环
}
}
return in;
}
测试部分 DateTest.cpp:
cpp
#include"Date.h"
// 测试 + 和 += 运算符
void Test01()
{
Date d1(2026, 3, 25);
Date d2 = d1 + 365; // d2是新对象,d1不变
d1.Print();
d2.Print();
d1 += 365; // d1自身改变
d1.Print();
Date d3(2026, 3, 28);
d3 += -1; // 加负数,相当于减一天
d3.Print();// 预期输出: 2026/3/27
}
// 测试 - 和 -= 运算符
void Test02()
{
Date d1(2026, 3, 28);
d1 -= 30; // d1自身减去30天
d1.Print();
Date d3(2026, 3, 28);
Date d2 = d3 - 30; // d2是新对象
d2.Print();
Date d4(2026, 3, 28);
d4 -= -1; // 减负数,相当于加一天
d4.Print();// 预期输出: 2026/3/29
}
// 测试前置和后置 ++
void Test03()
{
Date d1(2026, 3, 28);
Date ret1 = d1++; // 后置++,ret1得到旧值
ret1.Print();// 输出: 2026/3/28
d1.Print(); // 输出: 2026/3/29
Date d2(2026, 3, 28);
Date ret2 = ++d2; // 前置++,ret2得到新值
ret2.Print();// 输出: 2026/3/29
d2.Print(); // 输出: 2026/3/29
}
// 测试前置和后置 --
void Test04()
{
Date d1(2026, 3, 28);
Date ret1 = d1--; // 后置--
ret1.Print();// 2026/3/28
d1.Print(); // 2026/3/27
Date d2(2026, 3, 28);
Date ret2 = --d2; // 前置--
ret2.Print();// 2026/3/27
d2.Print(); // 2026/3/27
}
// 测试 日期-日期 运算符
void Test05()
{
Date d1(2026, 3, 28);
Date d2(2026, 3, 1);
cout << d1 - d2 << endl; // 预期输出正数天数差
Date d3(2026, 3, 28);
Date d4(2026, 4, 28);
cout << d3 - d4 << endl; // 预期输出负数天数差
}
// 测试 >> 和 << 运算符
void Test06()
{
Date d1;
Date d2;
cin >> d1 >> d2; // 从控制台输入两个日期
cout << d1 << d2; // 输出两个日期
cout << d1 - d2 << endl; // 输出天数差
}
int main()
{
//Test01(); // 测试 + +=
//Test02(); // 测试 - -=
//Test03(); // 测试 ++
//Test04(); // 测试 --
//Test05(); // 测试 日期-日期
Test06(); // 测试 输入输出
return 0;
}
结语
今天的内容到这里就结束了,希望你能有所收获~
代码无bug,学习不迷路,我们下篇再见!(•̀ᴗ•́)و