类和对象(中):六大默认成员函数与运算符重载全解析


✨ 把代码写进星轨,
用逻辑丈量宇宙。

导航 链接
个人主页 🏠 星轨初途
基础语言专栏 💻 C语言📚 数据结构
C++ 进阶专栏 🏆 C++学习(竞赛类)⚙️ C++专栏(开发类)
刷题实战专栏 🚀 算法及编程题分享

文章目录

  • 前言
  • 一、类的默认成员函数
  • 二、构造函数(Constructor):对象的"出生证明"
    • [1. 构造函数的基础语法](#1. 构造函数的基础语法)
    • [2. 编译器的"偏心眼"与 MyQueue 经典实战](#2. 编译器的“偏心眼”与 MyQueue 经典实战)
  • 三、析构函数(Destructor):对象的"遗嘱"
    • [1. 析构函数的特征](#1. 析构函数的特征)
    • [2. 什么时候必须写析构函数?](#2. 什么时候必须写析构函数?)
  • 四、拷贝构造函数:克隆的艺术与陷阱
    • [1. 致命的语法陷阱与"参数迷局"](#1. 致命的语法陷阱与“参数迷局”)
    • [2. 深拷贝与浅拷贝(核心考点)](#2. 深拷贝与浅拷贝(核心考点))
  • 五、运算符重载:让自定义类型"活"起来
    • [1. 规则](#1. 规则)
    • [2. 赋值运算符重载(`operator=`)](#2. 赋值运算符重载(operator=))
    • [3. 前置++ 与 后置++ 的"参数迷局"](#3. 前置++ 与 后置++ 的“参数迷局”)
    • [4. 极致避坑:`<<` 和 `>>` 为什么不能写在类里面?](#4. 极致避坑:<<>> 为什么不能写在类里面?)
    • [5. 重载 `+` 和 `+=`,谁复用谁?](#5. 重载 ++=,谁复用谁?)
    • 6.用日期类进行举例
  • [六、const 成员函数:安全的最后一道防线](#六、const 成员函数:安全的最后一道防线)
  • [七、取地址及 const 取地址操作符重载:最没存在感的"卧龙凤雏"](#七、取地址及 const 取地址操作符重载:最没存在感的“卧龙凤雏”)
    • [1. 它们长什么样?](#1. 它们长什么样?)
    • [2. 工程开发指导意见:千万别手写!](#2. 工程开发指导意见:千万别手写!)
  • 结束语

前言

嗨(。◕ˇ∀ˇ◕)!我们又见面啦!在上一篇博客类和对象(上)中,我们从底层探索了C++类与对象的初貌,了解了封装与隐藏的 this 指针。

在实际的软件开发中,内存泄漏、野指针、对象未初始化等问题是C/C++程序员最头疼的"终极BOSS"。试想一下,如果你写了一个复杂的系统,每次创建一个对象都要手动调用一次 Init 函数,稍微忘了一次程序就直接崩溃,这谁受得了?

为了解决这些痛点,C++的设计者在类中引入了默认成员函数机制,让对象的生命周期管理(初始化与清理)变得自动化、智能化。今天,我们就来扒一扒C++编译器在背后偷偷帮我们写的代码,深度抠透类的"六大天王"!准备好接受硬核干货了吗?起飞!


一、类的默认成员函数

如果我们在C++中写一个空类,里面什么都不写,它真的是空的吗?

并不是!一个类,就算我们什么都不写,编译器也会默认生成 6 个成员函数 。这就是所谓的"默认成员函数"。最重要的是前4个,后面两个了解即可,其次就是C++11以后还会增加两个默认成员函数,移动构造和移动赋值,这个我们后面再进行讲解。

这 6 个函数分别是:

  1. 构造函数:负责对象的初始化。
  2. 析构函数:负责对象的资源清理。
  3. 拷贝构造函数:负责用同类对象初始化创建新对象。
  4. 赋值运算符重载:负责把一个对象赋值给另一个已经存在的对象。
  5. 取地址重载:返回普通对象的地址。
  6. const 取地址重载 :返回 const 对象的地址。

对于这些核心函数,我们在工程开发中必须掌握两个灵魂拷问:

  • 编译器默认生成的能满足需求吗?
  • 如果不满足,我们该怎么手写?

二、构造函数(Constructor):对象的"出生证明"

在以前写 C 语言的 Stack(栈)时,我们每次定义完变量,都必须手动调用一次 Init() 函数。C++ 的构造函数 就是为了完美替代 Init() 而生的!

构造函数虽然名字叫"构造",但它的主要任务并不是开辟空间创建对象,而是初始化对象(空间在进入函数栈帧时就已经分配好了)。

1. 构造函数的基础语法

  1. 函数名与类名完全相同
  2. 没有返回值 (连 void 都不需要写)。
  3. 对象实例化时,编译器自动调用对应的构造函数。
  4. 构造函数可以重载(意味着可以提供多种初始化的方式)。
  5. 默认构造函数的自动生成规则 :如果类中没有显式定义 构造函数,则C++编译器会自动生成 一个无参的默认构造函数;一旦用户显式定义 构造函数,编译器将不再自动生成
  6. 默认构造函数的定义与约束 :无参构造函数、全缺省构造函数、未手动编写构造函数时编译器默认生成的构造函数,都属于默认构造函数 。需满足以下约束:
    • 这三类函数有且只能存在一个,不能同时存在。
    • 无参构造函数与全缺省构造函数虽构成函数重载 ,但调用时会产生歧义
    • 核心总结:不传实参即可调用的构造函数,都称为默认构造函数。
  7. 编译器默认生成构造函数的初始化行为
    • 对于内置类型成员变量 :初始化行为由编译器决定,不保证会被初始化 ,值是不确定的
    • 对于自定义类型成员变量 :会自动调用 该成员变量的默认构造函数进行初始化;若该成员变量无默认构造函数 ,则编译报错,需通过初始化列表完成初始化。
cpp 复制代码
class Date 
{
public:
    // 1. 无参构造函数
    Date()
     {
        _year = 1; 
        _month = 1; 
        _day = 1;
    }
    // 2. 带参构造函数 (函数重载)
    Date(int year, int month, int day)
     {
        _year = year;
         _month = month;
          _day = day;
    }
private:
    int _year;
    int _month;
    int _day;
};
int main()
 {
    Date d1; // 调用无参构造。注意:千万不能写成 Date d1(); 编译器会把它当成函数声明!
    Date d2(2026, 3, 21); // 调用带参构造
    return 0;
}

2. 编译器的"偏心眼"与 MyQueue 经典实战

如果我们不写构造函数,编译器会生成一个默认的。但这个默认构造函数有个极其坑爹又极其巧妙的特性:

  • 内置类型int, char, 指针等):默认不处理,里面的值是随机的垃圾值!
  • 自定义类型 (我们自己写的类):会去自动调用它们自己的默认构造函数

这个特性初看很坑,但在实际开发中其实非常强大。我们来看一个极其经典的例子:用两个栈模拟实现一个队列(MyQueue)

假设我们已经写好了一个 Stack 类,并且为它提供了一个全缺省的构造函数:

cpp 复制代码
class Stack
 {
public:
    // Stack 的全缺省构造函数
    Stack(int capacity = 4)
     {
        _a = (int*)malloc(sizeof(int) * capacity);
        _top = 0;
        _capacity = capacity;
    }
    // ... 其他 Push/Pop 方法 ...
private:
    int* _a;
    int _top;
    int _capacity;
};

现在我们要写一个 MyQueue 类,里面包含两个栈对象:

cpp 复制代码
class MyQueue
 {
public:
    // 这里我们什么构造函数都不写
    void push(int x) { /*...*/ }
    // ...
private:
    Stack _pushST; // 自定义类型
    Stack _popST;  // 自定义类型
};
int main() 
{
    MyQueue mq; // 实例化队列
    return 0;
}

震撼的来了 :当我们执行 MyQueue mq; 时,尽管我们没有给 MyQueue 写哪怕一行的构造代码,程序依然能完美运行!

因为编译器发现 _pushST_popST 是自定义类型,于是编译器在自动生成的 MyQueue 默认构造函数中,主动跑去调用了 Stack 的构造函数。这就相当于编译器在背后默默帮我们完成了两个底层栈的空间申请和初始化!这就是面向对象自动化的魅力。

🛠️ 注意:

对于全是内置类型的类(比如 Date),编译器不处理怎么办?C++11 引入了新补丁:支持在类成员声明时直接给缺省值

cpp 复制代码
class Date
 {
private:
    // 这不是初始化,这是给编译器默认构造函数提供的"缺省值"
    int _year = 1;  
    int _month = 1;
    int _day = 1;
};

💡 建议 :在开发中,强烈建议直接写一个全缺省的构造函数。它不仅好用,而且同时兼具无参和带参的功能,是工程中最主流的写法!


三、析构函数(Destructor):对象的"遗嘱"

有生就有死,有构造就有析构。析构函数完美替代了以前 C 语言里的 Destroy() 函数,专门用来做资源清理

1. 析构函数的特征

  • 函数名是类名前面加个波浪号 ~
  • 无参数、无返回值
  • 不能重载(一个类只能有一个析构函数)。
  • 对象生命周期结束时(比如函数执行完毕出作用域),编译器自动调用
  • 跟构造函数类似,我们不写编译器自动生成的析构函数:对内置类型成员不做处理,自定义类型成员会调用其析构函数
  • 还需要注意的是我们显式写析构函数,对于自定义类型成员也会调用其析构 ;也就是说自定义类型成员无论什么情况都会自动调用析构函数
  • 如果类中没有申请资源时,析构函数可以不写 ,直接使用编译器生成的默认析构函数,如Date;如果默认生成的析构就可以用,也就不需要显式写析构,如MyQueue;但是有资源申请时,一定要自己写析构,否则会造成资源泄漏,如Stack。
  • 一个局部域的多个对象,C++规定后定义的先析构

2. 什么时候必须写析构函数?

和构造函数一样,编译器默认生成的析构函数同样偏心 :对内置类型不处理(普通变量出作用域本来就会自动销毁),对自定义类型会去调用它的析构函数

这决定了我们在开发中的策略:

  1. 不需要写析构(情况一) :像 Date 这样的类,里面全是一般的 int 变量,没有去堆区(Heap)动态 malloc 申请内存,不需要清理。
  2. 不需要写析构(情况二) :像刚才的 MyQueue !因为它的成员全是自定义类型 Stack。当 MyQueue 销毁时,编译器会自动去调用 _pushST_popST 的析构函数,底层资源的释放全由 Stack 内部搞定了,外面完全不用操心!
  3. 必须手写析构 :像 Stack(栈)这样的底层类。因为它的 _a 指针 malloc 了一大块堆内存。编译器默认的析构可不会帮你 free 掉指针指向的堆空间,如果不手写析构函数,必定导致内存泄漏
cpp 复制代码
class Stack 
{
public:
    // ...
    // 必须手写析构,释放堆内存!
    ~Stack()
     {
        if(_a) 
        {
            free(_a);
            _a = nullptr;
            _top = _capacity = 0;
        }
    }
private:
    int* _a;
    // ...
};

四、拷贝构造函数:克隆的艺术与陷阱

假如我们现在有一个对象 d1,想直接克隆一个一模一样的 d2 出来,这就需要用到拷贝构造函数

1. 致命的语法陷阱与"参数迷局"

拷贝构造函数其实是构造函数的一个重载 。但它的参数有着极其严格的规定:第一个参数必须是同类型对象的引用 (通常加 const 修饰以防被修改)。

💡 进阶冷知识:

很多初级教程会告诉你"拷贝构造只能有一个参数",这其实是不严谨的

实际上,拷贝构造函数可以有多个参数 ,前提是除了第一个传引用的参数外,后面的所有参数都必须有缺省值(默认值)

例如:Date(const Date& d, int extra = 0) 这也是一个完全合法的拷贝构造函数!只不过在日常绝大多数的工程开发中,我们只需要且只写第一个核心参数。
🚨 连环报错警告:为什么第一个参数必须传引用?

如果你觉得传引用麻烦,写成传值调用 Date(const Date d),编译器会直接翻脸报错!

因为 C++ 规定:自定义类型传值传参,必须调用拷贝构造函数

如果你的拷贝构造本身又是传值,就会引发一个恐怖的链式反应:调用拷贝构造 -> 需要传参 -> 传值传参又触发拷贝构造 -> 又需要传参...... 从而形成无穷无尽的死循环递归

所以,第一个参数必须传引用 &,以此来打破递归死循环!

拷贝构造本身是传值效果图

2. 深拷贝与浅拷贝(核心考点)

如果不手写拷贝构造,编译器默认生成的拷贝构造会按照内存的字节序进行浅拷贝(值拷贝)->(⼀个字节⼀个字节的拷贝)

  • 对于 Date 类,浅拷贝完美适用,因为把年月日直接按字节复制过去毫无问题,不需要手写。
  • 对于 Stack 类,浅拷贝是灾难 !如果直接把栈 d1 的指针地址原封不动复制给栈 d2,会导致两个对象的指针指向同一块物理内存。
    后果极其严重:修改 d1 的数据会直接覆盖 d2;更要命的是程序结束时,这两个对象都会去调用析构函数,导致同一块内存被 free 两次,程序直接崩溃!

工程开发结论: 只要类里面包含指向堆区的指针并且管理了底层资源,就必须手动实现深拷贝 (即重新 malloc 一块一样大的空间,再把数据用 memcpy 拷过去)。


五、运算符重载:让自定义类型"活"起来

在 C语言中,如果我们要对比两个结构体的内容是否相等,或者想把它们的某个值相加,只能去写类似于 IsEqual(&d1, &d2) 或者 Add(&d1, &d2) 这样的函数,代码极其反人类。

C++ 引入了运算符重载 (关键字 operator),使得自定义类型的对象也能像普通整型一样,直接使用 +, -, =, ==, << 等符号,代码可读性迎来了史诗级革命!

1. 规则

  1. 运算符重载的核心作用 :当运算符被用于类类型的对象时,C++允许通过运算符重载为其指定新的含义;C++规定类类型对象使用运算符时,必须转换为调用对应运算符重载,没有对应重载则编译报错
  2. 运算符重载的函数本质 :运算符重载是具有特殊名字的函数,函数名由 operator 和要定义的运算符共同构成;和普通函数一致,具备返回类型、参数列表和函数体。
  3. 运算符重载的参数数量规则
    • 重载运算符函数的参数个数,和该运算符作用的运算对象数量一致:一元运算符有1个参数,二元运算符有2个参数(二元运算符左侧运算对象传给第一个参数,右侧传给第二个参数)。
    • 若重载运算符函数是类的成员函数 ,第一个运算对象会默认传给隐式的this指针,因此参数个数比运算对象数量少1个
  4. 运算符重载的优先级与结合性 :运算符重载以后,其优先级和结合性与对应的内置类型运算符保持一致,不可修改。
  5. 运算符重载的核心限制
    • 不能通过拼接语法中不存在的符号创建新的运算符,例如operator@是非法写法。
    • 5个不可重载的运算符(选择题高频考点).*::sizeof?:.
    • 重载运算符至少有一个类类型参数 ,不能通过重载改变内置类型对象的运算符含义,例如int operator+(int x, int y)是非法写法。
    • 运算符重载需符合逻辑意义:仅当运算符重载后对该类有业务意义时才重载,例如Date类重载operator-(计算日期差)有意义,重载operator+无意义。
  6. 自增运算符++的重载规则
    • 前置++和后置++的重载函数名均为operator++,无法直接区分。
    • C++规定:后置++重载时,必须增加一个int类型的形参 ,以此和前置++构成函数重载,实现区分。
  7. 流运算符<<>>的重载规则
    • 必须重载为全局函数,不可重载为类的成员函数。
    • 原因:若重载为成员函数,this指针会默认抢占第一个形参位置(左侧运算对象),调用时会变成对象<<cout,不符合使用习惯和可读性。
    • 正确写法:重载为全局函数时,将ostream/istream放在第一个形参位置,第二个形参放置类类型对象。

2. 赋值运算符重载(operator=

这是默认成员函数之一,用于把一个对象的值,拷贝赋值给另一个已经存在 的对象。为了支持连续赋值(a = b = c),它必须返回当前对象的引用 *this

cpp 复制代码
class Date
 {
public:
    // 赋值运算符重载
    Date& operator=(const Date& d)
     {
        if (this != &d) 
        { // 防止自己给自己赋值
            _year = d._year;
            _month = d._month;
            _day = d._day;
        }
        return *this; 
    }
};

注意:如果不手写,编译器默认生成的是浅拷贝。如果类内部管理了堆区指针,必须手写深拷贝!

3. 前置++ 与 后置++ 的"参数迷局"

cpp 复制代码
class Date 
{
public:
    // 前置++:返回改变后的对象,用引用返回提高效率
    Date& operator++() 
    {
        _day += 1; // 假设简单的加一天
        return *this;
    }

    // 后置++:多加一个 int 参数区分。
    // 注意:只能返回局部变量 tmp 的值(拷贝),绝对不能返回引用!
    Date operator++(int) 
    {
        Date tmp = *this; // 先把旧状态保存下来
        _day += 1;        // 自己偷偷加1
        return tmp;       // 返回加之前的旧状态
    }
};

4. 极致避坑:<<>> 为什么不能写在类里面?

我们想实现 cout << d1 直接打印日期,很自然地会想到在 Date 类里写一个成员函数:

cpp 复制代码
// 如果写在类里面:
void operator<<(ostream& out)
 {
    out << _year << "/" << _month << "/" << _day;
}

但是!你会发现 cout << d1 根本编译不过! 必须反过来写成 d1 << cout 才能运行。
底层原因剖析 :如果是类的成员函数,隐含的 this 指针永远抢占**第一个参数(左操作数)**的位置。这就会导致调用形式变成了 对象 << cout,彻底违背了我们的使用直觉。

标准解法 :必须把流插入 << 和流提取 >> 重载成全局函数 !这样我们就可以自由决定参数顺序,让 ostream& 霸占第一个位置。同时为了能访问类内部的私有成员,我们需要在类中声明它为 友元函数(friend)

cpp 复制代码
class Date
 {
    // 声明全局函数为友元,允许它访问私有成员
    friend ostream& operator<<(ostream& out, const Date& d);
    friend istream& operator>>(istream& in, Date& d);
// ...
};

// 全局函数实现
ostream& operator<<(ostream& out, const Date& d)
 {
    out << d._year << "/" << d._month << "/" << d._day;
    return out; // 返回 out 以支持连续输出:cout << d1 << d2;
}

istream& operator>>(istream& in, Date& d)
 {
    in >> d._year >> d._month >> d._day;
    return in;
}

5. 重载 ++=,谁复用谁?

在工程开发中,日期加天数(d1 + 100)和日期自加天数(d1 += 100)是非常常见的操作。这两个操作逻辑几乎一模一样。为了不写重复代码,我们通常让其中一个去**复用(调用)**另一个。

到底是用 + 实现 +=,还是用 += 实现 +
标准答案:先实现 +=,再让 + 去复用 += 且看效率对比:

  • 方案A:复用 += 实现 +(最优解🏆)

    cpp 复制代码
    // 1. 先踏踏实实实现 +=(改变自身,无需开辟新对象,返回引用即可)
    Date& operator+=(int day) 
    {
        _day += day;
        // ... 处理进位逻辑 ...
        return *this;
    }
    
    // 2. 让 + 复用 +=
    Date operator+(int day) const 
    {
        Date tmp = *this; // 拷贝构造出一个副本
        tmp += day;       // 副本自己加上去
        return tmp;       // 返回副本
    }

    性能分析+= 没有创建任何临时对象。+ 创建了 1 个临时对象 tmp总共产生 1 次拷贝。

  • 方案B:反过来,复用 + 实现 +=(极度低效❌)

    cpp 复制代码
    // 1. 先实现 + (+本来的语义就是不改变自己,所以必须产生临时对象)
    Date operator+(int day) const 
    {
        Date tmp = *this; 
        tmp._day += day;
        // ... 处理进位 ...
        return tmp; 
    }
    // 2. 让 += 复用 +
    Date& operator+=(int day)
     {
        *this = *this + day; // 这里调用了 + 和赋值重载
        return *this;
    }

    性能分析+ 里创建了 1 个临时对象。而在 += 中,*this + day 会产生临时对象,再赋值给 *this 又会产生拷贝。无端多出了大量的拷贝和赋值开销! 工程箴言: 改变自身的操作(如 +=, -=),自己踏踏实实实现;不改变自身的操作(如 +, -),通过拷贝一个副本然后复用前者来实现,这是最极致的性能压榨!

6.用日期类进行举例

了解了这么多,我们来对日期进行各种操作来更加熟悉吧

test.h

cpp 复制代码
#pragma once

#include<iostream>
using namespace std;
#include<assert.h>

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; // 打印日期

	// 获取某年某月的天数(频繁调用,定义在类内默认为 inline)
	int GetMonthDay(int year, int month) const
	{
		assert(month > 0 && month < 13);
        // 静态数组缓存每月天数,避免每次创建。下标对应月份(1-12)
		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;
		}

		return monthDayArray[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) const; // 返回新对象,原对象不变
	Date& operator+=(int day);     // 修改原对象,返回自身引用

	Date operator-(int day) const;
	Date& operator-=(int day);

    // --- 自增/自减运算符重载 ---
	// d1++; (int 为占位符,用于区分前置和后置)
	Date operator++(int);

	// ++d1; 
	Date& operator++();

	// d1--;
	Date operator--(int);

	// --d1;
	Date& operator--();

    // --- 日期减日期 ---
	// d1 - d2:返回两个日期相差的天数
	int operator-(const Date& d) const;

    // --- 取地址运算符重载(特殊演示/防黑客用途) ---
    // 返回伪造的地址,隐藏真实地址
	Date* operator&()
	{
		return (Date*)0x2673FF40; 
	}
	
	const Date* operator&() const
	{
		return (Date*)0x2673FE30;
	}
private:
	int _year;
	int _month;
	int _day;
};

// 全局函数声明
ostream& operator<<(ostream& out, const Date& d);
istream& operator>>(istream& in, Date& d);

test.c

cpp 复制代码
#include"test.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 << "非法日期:";
		Print();
	}
}

// 打印函数
void Date::Print() const
{
	cout << _year << "/" << _month << "/" << _day << endl;
}

// --- 比较运算符:基于 < 和 == 实现所有其他逻辑 ---

// d1 < d2
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;
}

// d1 <= d2 (复用 < 和 ==)
bool Date::operator<=(const Date& d) const
{
	return *this < d || *this == d;
}

// d1 > d2 (对 <= 取反)
bool Date::operator>(const Date& d) const
{
	return !(*this <= d);
}

// d1 >= d2 (对 < 取反)
bool Date::operator>=(const Date& d) const
{
	return !(*this < d);
}

// d1 == d2
bool Date::operator==(const Date& d) const
{
	return _year == d._year && _month == d._month && _day == d._day;
}

// d1 != d2 (对 == 取反)
bool Date::operator!=(const Date& d) const
{
	return !(*this == d);
}

// --- 加减运算逻辑 ---

// d1 += day:进位法
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;
}

// d1 + day:复用 += (通过拷贝构造临时对象,不改变自身)
Date Date::operator+(int day) const
{
	Date tmp = *this;
	tmp += day;
	return tmp;
}

// d1 - day:复用 -=
Date Date::operator-(int day) const
{
	Date tmp = *this;
	tmp -= day;
	return tmp;
}

// d1 -= day:借位法
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;
}

// --- 自增/自减 ---

// 后置++:返回改变之前的旧值 (拷贝)
Date Date::operator++(int)
{
	Date tmp = *this;
	*this += 1; // 复用 +=
	return tmp;
}

// 前置++:返回改变之后的新值 (引用,效率更高)
Date& Date::operator++()
{
	*this += 1;
	return *this;
}

// --- 日期差值计算 ---

// 日期 - 日期:利用计数器思想,让小日期不断++直到等于大日期
int Date::operator-(const Date& d) const
{
	int flag = 1;      // 符号位:默认 *this 大于 d
	Date max = *this;
	Date min = d;
	
    // 如果 *this 小于 d,交换位置并翻转符号位
	if (*this < d)
	{
		max = d;
		min = *this;
		flag = -1;
	}

	int n = 0; // 记录天数差
	while (min != max)
	{
		++min;
		++n;
	}

	return n * flag;
}

// --- 流输入/输出重载 ---

// 流输出 (支持 cout << d1 << d2)
ostream& operator<<(ostream& out, const Date& d)
{
	out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
	return out; // 返回 out 用于连续输出
}

// 流输入 (支持 cin >> d1 >> d2,并带有循环查错机制)
istream& operator>>(istream& in, Date& d)
{
	while (1)
	{
		cout << "请依次输入年月日:>";
		in >> d._year >> d._month >> d._day;

		if (!d.CheckDate())
		{
			cout << "输入日期非法:";
			d.Print();
			cout << "请重新输入!!!" << endl;
		}
		else
		{
			break; // 输入合法,跳出循环
		}
	}

	return in; // 返回 in 用于连续输入
}

六、const 成员函数:安全的最后一道防线

有时候,我们会用 const 来修饰一个对象(代表该对象只能读,不能修改)。

cpp 复制代码
const Date d1(2026, 3, 21);
d1.Print(); // 编译报错!

为什么普通打印函数 Print() 会报错呢?

因为 Print() 隐藏的参数是 Date* const this,而传进来的 d1const Date*。把只读对象传给可读可写的指针,这导致了权限的放大,编译器绝不允许!

为了解决这个问题,我们需要在成员函数的参数列表后面加上 const 修饰符:

cpp 复制代码
class Date 
{
public:
    // 这个 const 实际上是修饰隐藏的 this 指针!
    // 让 this 指针从 Date* const this 变成了 const Date* const this
    void Print() const
     { 
        cout << _year << "/" << _month << "/" << _day << endl;
    }
};

🛠️ 开发原则: 只要成员函数内部不需要修改 成员变量(比如各种打印、获取属性的 Get 方法),都强烈建议在后面加上 const。这样无论是普通对象还是 const 对象,都能安全调用,大大增强了代码的健壮性。


七、取地址及 const 取地址操作符重载:最没存在感的"卧龙凤雏"

在编译器默认生成的 6 个成员函数中,最后两个分别是 普通对象的取地址重载(operator&const 对象的取地址重载(operator& const

1. 它们长什么样?

如果我们强行手写,代码是这样的:

cpp 复制代码
class Date
 {
public:
    // 普通对象取地址
    Date* operator&() 
    {
        return this;
    }

    // const 对象取地址
    const Date* operator&() const 
    {
        return this;
    }
};

2. 工程开发指导意见:千万别手写!

这两个函数在实际的工程开发中,存在感几乎为零

因为编译器默认生成的取地址重载,就已经能完美返回对象的真实地址了。我们 99.99% 的情况下都不需要自己去重载它。

那什么时候需要手写?

只有在一种极其奇葩的场景下:你不想让别人获取到这个对象的真实地址

比如,别人执行 &d1 时,你偏偏重载让他拿到一个 nullptr,或者一个伪造的假地址(通常用于某些极度变态的安全防御或底层黑客级操作)。除了这种特殊情况,大家日常开发完全可以把这俩函数当透明人!


结束语

嗨ヾ(o´∀`o)ノ!今天的内容就到这里啦!

我们不仅干掉了C++类中最重要、最核心的六大默认成员函数,还通过 MyQueue 的实战场景见证了编译器对自定义类型的"偏爱"。梳理了令人头疼的深浅拷贝问题,挖掘了运算符重载的底层性能密码,以及 const 的权限法则。

大家在写代码时一定要多留心底层资源的管理,如果类里面有指针申请空间,千万别忘了手写深拷贝和析构!

下一篇我们将讲解类和对象最后一部分,相信大家会有所收获,感谢大家的支持啦!

相关推荐
骇客野人2 小时前
用python实现一个查询当天天气的MCP服务器
服务器·开发语言·python
天空属于哈夫克32 小时前
拒绝被动响应:企业微信主动调用接口高阶方案
开发语言·python
cccyi72 小时前
【C++ 脚手架】gtest 单元测试库的介绍与使用
c++·单元测试·gtest
2501_941982052 小时前
Go 语言实现企业微信外部群消息主动推送方案
开发语言·golang·企业微信
南山love2 小时前
spring-boot多线程并发执行任务
java·开发语言
似水明俊德2 小时前
13-C#.Net-设计模式六大原则-学习笔记
笔记·学习·设计模式·c#·.net
Flittly2 小时前
【从零手写 ClaudeCode:learn-claude-code 项目实战笔记】(11)Autonomous Agents (自治智能体)
笔记·python·ai·ai编程
dmlcq2 小时前
一文读懂 PageQueryUtil:分页查询的优雅打开方式
开发语言·windows·python
不会写DN2 小时前
JS 最常用的性能优化 防抖和节流
开发语言·javascript·ecmascript