深挖C++:类和对象(中)

目录

一、类的默认成员函数

二、构造函数

三、析构函数

四、拷贝构造函数

五、赋值运算符重载

[5.1 运算符重载](#5.1 运算符重载)

[5.2 赋值运算符重载](#5.2 赋值运算符重载)

[5.3 日期类实现](#5.3 日期类实现)

六、取地址运算符重载

[6.1 const成员函数](#6.1 const成员函数)

[6.2 取地址运算符重载](#6.2 取地址运算符重载)


一、类的默认成员函数

类的六个默认成员函数------不主动写,编译器自动生成的函数。

表格如下:

| 分类 | 函数 | 作用 | 需要自己写吗 |
| 初始化和清理 | 构造函数 | 对象创建时初始化 | 一般都自己写 |
| | 析构函数 | 对象销毁时清理资源 | 一般都自己写 |
| 拷贝复制 | 拷贝构造 | 用同类对象初始化新对象 | 涉及深拷贝时必须写 |
| | 赋值重载 | 把一个对象赋值给另一个已有对象 | 几乎不用写 |
| 取地址 | operator& | 普通对象取地址 | 几乎不用写 |

const operator& const 对象取地址 几乎不用写

C++又加了两个:移动构造、移动赋值(后续会讲)。

学习思路:

1、不写时编译器生成的版本做了什么?------是否满足需求。

2、不满足怎么自己写?------这是重点


二、构造函数

作用:替代以前手写的初始化函数,对象创建时自动调用,不用手动调用。

5个特点:

1、函数名 = 类名

cpp 复制代码
class Date{
    Date(){ } // 函数名 = 类名
};

2、无返回值(不用写 void)

不写返回值类型,也不写void

不是"没有返回值",是C++语法规定构造函数就不能写返回值类型

3、对象实例化时自动调用

cpp 复制代码
Date d1;  // 自动调用Date()
Dare d2(2026,11,7); // 自动调用Date(2026,11,7)

4、可以重载

cpp 复制代码
class Date{
public:
   Date(){ };  // 无参版本
   Date(int year,int m,int d){ } // 带参版本
};

5、不主动写,编译器自动生成一个无参的;主动写,编译器就不生成了

一旦显示定义了任何构造函数,编译器就认为"你知道自己在干嘛",不再自动生成。

2个问题:

什么是默认构造函数?

不传实参就能够调用的构造函数,都叫默认构造函数,包括三种:

(1) 无参构造函数

(2) 全缺省构造函数

(3) 编译器自动生成的那个构造函数

这三个只能有一个存在,不能同时存在,为什么?

cpp 复制代码
class Date {
public:
	// 无参构造
	Date() {
		_year = 1;
		_month = 1;
		_day = 1;
	}
	// 全缺省构造
	Date(int year = 1, int month = 1, int day = 1) {
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main() {
	Date d1; // 编译器根本不知道调用哪个默认构造函数,因为两个都不需要传参
	return 0;
}

编译器自动生成的构造函数,做了什么?

| 成员类型 | 编译器自动初始化吗 |
| 内置类型(int/char/double/ 指针等 | 不初始化,值是随机的 |

自定义类型(class/struct 等) 自动调用它的默认构造函数初始化

如果自定义类型成员没有默认构造函数,编译报错------这时需要用初始化列表解决(下章讲解)

代码示例:

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;
	}

	void Print() {
		cout << _year << '/' << _month << '/' << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main() {
	Date d1; // 自动调用无参构造
	Date d2(2026, 11, 7); // 自动调用带参构造

	d1.Print();
	d2.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 (_a = nullptr) {
			perror("malloc申请空间失败");
			return;
		}
		_capacity = n;
		_top = 0;
	}
	// ......Push/Pop/Top/Destory等
private:
	STDataType* _a;
	size_t _capacity;
	size_t _top;
};

// 两个Stack实现队列
class MyQueue {
public:
	// 你不写构造函数,编译器自动生成MyQueue的默认构造函数
	// 它会自动调用两个Stack成员的构造函数,完成初始化
	// 相当于:MyQueue() : pushst(4), popst(4) {}
private:
	Stack pushst; // 编译器自动调 Stack(4) 初始化
	Stack popst;  // 编译器自动调 Stack(4) 初始化
};

int main() {
	MyQueue mq; // 自动调MyQueue默认构造,里面自动调两个Stack的构造
	return 0;
}

三、析构函数

对象销毁时自动调用,负责清理资源(释放malloc的空间等),代替之前手写的Destory。

注意一个关键区分:析构函数不是销毁对象本身。局部对象在栈上,函数结束栈帧一回收,对象空间就释放了,不用管,析构函数只是在销毁对象前,帮你清理对象里申请的资源------比如malloc的内存、打开的文件句柄等。

上文讲的 Date 类不需要析构函数------它没有malloc动态资源,成员都是栈上的int,对象销毁自动就没了,没东西需要清理。

6个特点:

1、析构函数名 = ~ + 类名

cpp 复制代码
class Stack{
  ~Stack(){ } // 类名前加 ~
};

2、无参数,无返回值

不写返回值类型,也不写void;不能重载(因为没有参数,无法重载)

3、一个类只能有一个析构函数

不主动写,编译器自动生成一个默认的;主动写。编译器就不生成了。

4、对象生命周期结束时,系统自动调用

cpp 复制代码
int main(){
  Stack st; // 自动调构造函数
    // ......
} // 函数结束,st自动销毁,自动调用析构函数

5、自己手写析构函数,自定义类型成员还是会自动调用析构函数

不管手不手写析构,只要自定义类型成员存在,它的析构函数一定会被调用;你自己手写的析构只负责你自己类里的资源,成员的析构编译器自动帮你调用。

6、多个对象的析构顺序

一个局部域里定义了多个对象,后定义的先析构(先进后出)

cpp 复制代码
int main(){
  Stack st1; // 先构造st1
  Stack st2; // 再构造st2
    // 函数结束:
    // 先析构st2(后定义的先析构)
    // 再析构st1
}

2个问题:

7、编译器自动生成的析构函数做了什么 ?

| 成员类型 | 是否自动处理 |
| 内置类型(int/char/ 指针等) | 不处理,什么都不做 |

自定义类型(class/struct 等) 自动调用它的析构函数

8、什么时候必须自己写析构函数?

| 情况 | 是否需要自己写 |
| 类里没有申请资源(如 Date) | 不用写,编译器默认生成的就行 |
| 类里有自定义类型成员,但没有动态资源(如 MyQueue) | 不用写,默认生成的就行 |

类里有 malloc/new 等动态资源(如 Stack) 必须自己写,否则内存泄漏

代码示例:

Stack类------构造+析构

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;
    }

    // 析构函数:清理资源(释放malloc的空间)
    ~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内部的资源
    // 相当于:~MyQueue() { pushst.~Stack(); popst.~Stack(); }
private:
    Stack pushst;   // 编译器自动调pushst的析构
    Stack popst;    // 编译器自动调popst的析构
};

int main() {
    Stack st;       // 自动调Stack构造
    MyQueue mq;     // 自动调MyQueue默认构造,里面自动调两个Stack构造

    // 函数结束:
    // 先析构mq(后定义的先析构)
    //   → MyQueue析构自动调pushst析构、popst析构
    // 再析构st(先定义的后析构)
    //   → 自动调~Stack()

    return 0;
}

总结:析构函数就是类里类名前加~的无参无返回值函数,对象销毁时自动调用,替代你以前手写的 Destroy;有动态资源(malloc/new)的类必须自己写析构,否则内存泄漏。


四、拷贝构造函数

定义:用一个已存在的同类对象,初始化一个新对象时,自动调用的构造函数。

它是构造函数的一个重载------函数名还是类名,参数是同类对象的引用。

cpp 复制代码
Date d1(2025,11,7);
Date d2(d1); // 用d1初始化d2,自动调拷贝构造

4个特点:

1、拷贝构造是构造函数的一个重载

函数名 = 类名;

第一个参数是自身类类型的引用

后面的参数可以有,但必须有缺省值

2、第一个参数必须是类类型对象的引用,不能传值

cpp 复制代码
// 错误写法:传值方式,编译报错
Date(Date d){......}

为什么不能传值?会导致无穷递归

cpp 复制代码
Date d1;
Date d2(d1); // 调用拷贝构造 Date(Date d)
             // 传参d1给d,又要拷贝构造一次 Date(d1)
             // 又传参......无限递归,栈溢出

所以必须用引用:

cpp 复制代码
// 正确写法:const引用
Date(const Date& d){
     ......
}

3、自定义类型对象进行拷贝行为,必须调用拷贝构造

传值传参:Func1(Date d1)传 d1 进去,会调用拷贝构造生成临时对象

传值返回:Date Func2() 返回 tmp,会调用拷贝构造生成临时对象

4、不手动写,编译器会自动生成拷贝构造

编译器自动生成的拷贝构造做什么:

内置类型成员:值拷贝(浅拷贝),一个字节一个字节复制

自定义类型成员:自动调用它的拷贝构造

1个问题:

什么时候需要自己写拷贝构造?

| 类的情况 | 是否需要自己写 | 原因 |
| 成员全是内置类型,没有动态资源(如 Date) | 不用写 | 编译器自动生成的浅拷贝就够用 |
| 成员有 malloc/new 动态资源(如 Stack) | 必须自己写 | 浅拷贝只复制指针,两个对象指向同一块资源,析构两次崩溃 |

成员主要是自定义类型(如 MyQueue) 不用写 自动生成的拷贝构造会自动调 Stack 的拷贝构造

小技巧:如果一个类自己写了析构函数释放资源,那它需要自己写拷贝构造(深拷贝)。

一个对比:

传值返回 vs 传引用返回

传值返回:会产生一个临时对象,调用拷贝构造(有拷贝开销)

传引用返回:返回的是原对象的别名,没有拷贝(减少开销)

注意:如果返回的是函数内的局部对象,函数结束就销毁了,传引用返回会变成野引用(类似野指针)

代码示例:

1、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;
    }

    // 拷贝构造函数:第一个参数必须是const引用
    // 编译报错: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(2024, 7, 5);
    tmp.Print();
    return tmp;  // tmp是局部对象,函数结束就销毁了
                 // 返回的是野引用,外面用就是野指针
}

int main() {
    Date d1(2024, 7, 5);

    // 传值传参:自动调拷贝构造,把d1拷贝一份给d
    Func1(d1);
    cout << &d1 << endl;  // 和Func1里的&d不一样,证明是两个对象

    // 用指针构造:不是拷贝构造,是普通构造
    Date d2(&d1);
    d1.Print();
    d2.Print();

    // 拷贝构造:通过同类对象初始化新对象
    Date d3(d1);
    d3.Print();

    // 也是拷贝构造:Date d4 = d1;
    Date d4 = d1;
    d4.Print();

    // Func2返回局部对象的引用,是野引用,用了就崩
    Date ret = Func2();
    ret.Print();

    return 0;
}

2、Stack类------深拷贝(必须自己写)

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;
    }

    // 拷贝构造:深拷贝(必须自己写)
    // 如果不写,编译器自动生成的浅拷贝会导致两个对象的_a指向同一块资源
    // 析构时会free两次,程序崩溃
    Stack(const Stack& st) {
        // 重新malloc一块同样大小的空间
        _a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);
        if (nullptr == _a) {
            perror("malloc申请空间失败!!!");
            return;
        }
        // 把数据从st._a拷贝到新的_a
        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 {
    // 不写拷贝构造,编译器自动生成
    // 自动生成的拷贝构造会自动调用Stack的拷贝构造
    // 只要Stack实现了深拷贝,MyQueue就没问题
private:
    Stack pushst;
    Stack popst;
};

int main() {
    Stack st1;
    st1.Push(1);
    st1.Push(2);

    // 拷贝构造st2 = st1
    // 如果不写Stack的拷贝构造,编译器自动生成的浅拷贝会导致:
    // st1._a 和 st2._a 指向同一块资源
    // 析构时free两次,程序崩溃
    Stack st2 = st1;

    MyQueue mq;
    // MyQueue自动生成的拷贝构造会自动调用Stack的拷贝构造
    // 只要Stack实现了深拷贝,就没问题
    MyQueue mq2 = mq;

    return 0;
}

总结:拷贝构造就是用同类对象初始化新对象时自动调用的构造函数;第一个参数必须是 const 引用(传值会无穷递归);有动态资源(malloc/new)的类必须自己写深拷贝,否则两个对象指向同一块资源,析构两次崩溃。


五、赋值运算符重载

5.1 运算符重载

定义:让运算符(+、-、==、<等)也能用于自定义类型的对象,并且具有意义。

本质是一个特殊的函数,函数名称是 operator + 运算符

cpp 复制代码
d1 == d2; // 编译器自动转换成 d1.operator==(d2);

4个特点:

1、函数名 = operator + 运算符

cpp 复制代码
bool operator==(const Date& d1, const Date& d2){ 
          ...... 
}

2、参数个数 = 运算对象数量

一元运算符(如 ++):1个参数

二元运算符(如+、==):2个参数

注意:如果是成员函数,第一个参数默认传给this指针,所以参数比运算对象少一个

3、至少有一个类类型参数

不能重载内置类型的运算符,比如不能写 int operator+(int x, int y)

4、重载后优先级和结合性不变

a + b * c 还是先算乘后算加

不能重载的五个运算符

| 运算符 | 说明 |
| .* | 成员指针访问 |
| :: | 作用域解析 |
| sizeof | 取大小 |
| ?: | 三目运算符 |

. 成员访问

也不能创造新运算符,比如 operator@ 不行。

两个特殊点:

1、前置++ 和 后置++ 怎么区分?

函数名都叫 operator++,编译器用一个额外的 int 形参区分:

cpp 复制代码
Date& operator++();   // 前置++:无额外参数
Date operator++(int); // 后置++:多一个int形参(不用传值,编译器自动传0)

2、为什么 << 和 >> 要重载为全局函数?

如果重载为成员函数,this指针抢占第一个参数位置:

cpp 复制代码
// 成员函数:cout << d 会变成 cout.operator <(d),不对!
// 因为cout是ostream类,不是我们的Date类

所以必须重载为全局函数,把 ostream 放在第一个参数:

cpp 复制代码
ostream& operator<<(ostream& out, const Date& d) {
    out << d._year << "-" << d._month << "-" << d._day;
    return out;
}

代码示例:

1、错误示例:不能重载内置类型

cpp 复制代码
#include<iostream>
using namespace std;

// 编译报错:"operator +"必须至少有一个类类型的形参
// int operator+(int x, int y) {
//     return x - y;
// }

2、Date类------全局 operator==

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() {
        cout << _year << "-" << _month << "-" << _day << endl;
    }

// private:  // 先注释掉,方便全局函数访问
    int _year;
    int _month;
    int _day;
};

// 全局函数重载==
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(2024, 7, 5);
    Date d2(2024, 7, 6);

    // 运算符重载函数可以显式调用
    operator==(d1, d2);

    // 编译器自动转换成 operator==(d1, d2);
    d1 == d2;

    return 0;
}

Date类------成员函数operator== 和 前置/后置 ++

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() {
        cout << _year << "-" << _month << "-" << _day << endl;
    }

    // 成员函数重载==
    // 第一个参数是this(隐含),所以只有一个参数d
    bool operator==(const Date& d) {
        return _year == d._year
            && _month == d._month
            && _day == d._day;
    }

    // 前置++:返回引用,先++再返回
    Date& operator++() {
        cout << "前置++" << endl;
        // ... 年月日加1
        return *this;
    }

    // 后置++:多一个int形参区分,返回临时对象
    Date operator++(int) {
        Date tmp = *this;  // 先保存原来的值
        cout << "后置++" << endl;
        // ... 年月日加1
        return tmp;  // 返回原来的值
    }

private:
    int _year;
    int _month;
    int _day;
};

int main() {
    Date d1(2024, 7, 5);
    Date d2(2024, 7, 6);

    // 显式调用
    d1.operator==(d2);

    // 编译器自动转换成 d1.operator==(d2);
    d1 == d2;

    // 前置++:编译器转换成 d1.operator++();
    ++d1;

    // 后置++:编译器转换成 d1.operator++(0);
    d1++;

    return 0;
}

5.2 赋值运算符重载

含义:赋值运算符重载是默认成员函数,用于两个已存在对象之间的拷贝赋值。

| | 特点 | 说明 |
| 1 | 必须重载为成员函数 | 参数建议写成 const **类类型& ,**避免传值拷贝 |
| 2 | 有返回值,建议返回 类类型& | 支持连续赋值 d1 = d2 = d3 |
| 3 | 不写编译器自动生成 | 内置类型值拷贝,自定义类型调它的赋值重载 |

4 有动态资源必须自己写 否则浅拷贝导致两个对象指向同一块资源

拷贝构造 vs 赋值重载

| | 拷贝构造 | 赋值重载 |
| 作用对象 | 用一个对象初始化新对象 | 把一个对象赋值给已存在对象 |

例子 Date d2(d1); d2 = d1;

代码示例:

cpp 复制代码
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;
    }

    // 赋值运算符重载
    Date& operator=(const Date& d) {
        // 检查自己给自己赋值的情况
        if (this != &d) {
            _year = d._year;
            _month = d._month;
            _day = d._day;
        }
        // 返回*this,支持连续赋值 d1 = d2 = d3
        return *this;
    }

    void Print() {
        cout << _year << "-" << _month << "-" << _day << endl;
    }

private:
    int _year, _month, _day;
};

int main() {
    Date d1(2024, 7, 5);
    Date d2(d1);        // 拷贝构造

    Date d3(2024, 7, 6);
    d1 = d3;            // 赋值重载(d1已经存在)

    // 注意:Date d4 = d1; 这是拷贝构造,不是赋值重载!
    // 因为d4是新创建的对象
    Date d4 = d1;

    return 0;
}

5.3 日期类实现

整体设计:

cpp 复制代码
// Date.h
#pragma once
#include<iostream>
using namespace std;
#include<assert.h>

class Date {
    // 友元函数声明:operator<<和operator>>
    friend ostream& operator<<(ostream& out, const Date& d);
    friend istream& operator>>(istream& in, Date& d);

public:
    Date(int year = 1900, int month = 1, int day = 1);
    void Print() const;

    // 获取某月天数(静态成员函数,不依赖对象)
    int GetMonthDay(int year, int 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;

    // 前置++ / 后置++
    Date& operator++();
    Date operator++(int);

    // 前置-- / 后置--
    Date& operator--();
    Date operator--(int);

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

private:
    int _year, _month, _day;
};

关键函数实现

cpp 复制代码
// Date.cpp
#include "Date.h"

// 获取某月天数
int Date::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月29天
    if (month == 2 && (year % 4 == 0 && year % 100 != 0) || year % 400 == 0) {
        return 29;
    }
    return monthDayArray[month];
}

// 检查日期合法性
bool Date::CheckDate() {
    if (_month < 1 || _month > 12
        || _day < 1 || _day > GetMonthDay(_year, _month)) {
        return false;
    }
    return true;
}

// 构造函数
Date::Date(int year, int month, int day) {
    _year = year;
    _month = month;
    _day = day;
    if (!CheckDate()) {
        cout << "日期非法" << 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;
}

// 其他比较运算符复用<和==
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) {
            ++_year;
            _month = 1;
        }
    }
    return *this;
}

// 日期 + 天数(返回新对象,不修改自己)
Date Date::operator+(int day) const {
    Date tmp = *this;
    tmp += day;
    return tmp;
}

// 前置++
Date& Date::operator++() {
    *this += 1;
    return *this;
}

// 后置++
Date Date::operator++(int) {
    Date tmp = *this;
    *this += 1;
    return tmp;  // 返回原来的值
}

// 两个日期相减,返回天数差
int Date::operator-(const Date& d) const {
    Date max = *this;
    Date min = d;
    int flag = 1;
    if (*this < d) {
        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) {
    cout << "请依次输入年月日:";
    in >> d._year >> d._month >> d._day;
    if (!d.CheckDate()) {
        cout << "日期非法" << endl;
    }
    return in;
}

测试代码:

cpp 复制代码
// Test.cpp
#include "Date.h"

void TestDate1() {
    Date d1(2024, 4, 14);
    Date d2 = d1 + 30000;  // 加30000天
    d1.Print();
    d2.Print();
}

void TestDate2() {
    Date d1(2024, 4, 14);
    Date d2 = ++d1;   // 前置++
    d1.Print();
    d2.Print();

    Date d3 = d1++;   // 后置++
    d1.Print();
    d3.Print();
}

void TestDate3() {
    Date d1(2024, 4, 14);
    Date d2(2034, 4, 14);
    int n = d1 - d2;  // 两个日期相差多少天
    cout << n << endl;
}

void TestDate4() {
    Date d1(2024, 4, 14);
    Date d2 = d1 + 30000;
    cout << d1;   // 自动调 operator<<(cout, d1)
    cout << d2;
    cin >> d1 >> d2;
    cout << d1 << d2;
}

int main() {
    TestDate1();
    TestDate2();
    TestDate3();
    TestDate4();
    return 0;
}

总结:赋值运算符重载是两个已存在对象之间的拷贝赋值,必须返回引用支持连续赋值;日期类实现把所有比较、加减、++、--、流插入都重载成运算符,用起来像内置类型一样方便。


六、取地址运算符重载

6.1 const成员函数

const 修饰的是什么?

const修饰的是成员函数隐含的this指针,不是函数本身。

cpp 复制代码
void Print() const;
// 编译器看到的真实样子:
// void Print(const Date* const this) const;

原来的 this 是 Date* const this(指针本身不能改指向,但指向的内容能改)

加了 const 后变成 const Date* const this(指针不能改指向,指向的内容也不能改)

作用:

在 const 成员函数里,不能修改类的任何成员变量。

cpp 复制代码
void Print() const {
    _year = 2025;  // ✗ 编译错误!const成员函数不能改成员
}

权限规则:

| 对象类型 | 能调普通成员函数 | 能调 const 成员函数 |
| 非 const 对象 | ✅ 能调 | ✅ 能调(权限缩小) |

const 对象 ❌ 不能调 ✅ 只能调 const 成员函数

为什么要加const?

1、const 对象必须通过 const 成员函数访问,否则 const 对象什么都干不了。

2、只读函数加 const,明确告诉调用者:这个函数不会修改对象。

3、权限可以缩小不能放大:非 const 对象调 const 函数,相当于把 "能读能写" 缩小成 "只读",允许;反过来不行。

代码示例:

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;
    }

    // const成员函数:this指针被const修饰,不能修改成员变量
    // 编译器真实原型:void Print(const Date* const this) const
    void Print() const {
        cout << _year << "-" << _month << "-" << _day << endl;
        // _year = 2025;  // ✗ 编译错误!const成员函数不能改成员
    }

private:
    int _year;
    int _month;
    int _day;
};

int main() {
    // 非const对象:能调普通函数,也能调const函数(权限缩小)
    Date d1(2024, 7, 5);
    d1.Print();   // ✓ 非const对象调const成员函数,允许

    // const对象:只能调const成员函数
    const Date d2(2024, 8, 5);
    d2.Print();   // ✓ const对象只能调const成员函数

    return 0;
}

总结:const 成员函数就是在函数后面加 const,本质是修饰 this 指针,让函数里不能改成员变量;const 对象只能调 const 成员函数,非 const 对象两种都能调。


6.2 取地址运算符重载

什么是取地址运算符重载?

就是重载 & 运算符,控制对类对象取地址时返回什么。

有两种形式:

**1、**普通版本:非 const 对象取地址

**2、**const 版本:const对象取地址

一般不用自己写,编译器自动生成的版本就够用了------就是正常返回对象的地址。

什么时候需要自己写?

特殊场景:不想让别人取到真实地址,就自己实现一份,返回一个假地址(比如nullptr)。

代码示例:

cpp 复制代码
class Date {
public:
    // 普通版本:非const对象取地址
    Date* operator&() {
        return this;           // 返回对象真实地址
        // return nullptr;     // 特殊场景:不想暴露真实地址,返回空
    }

    // const版本:const对象取地址
    const Date* operator&() const {
        return this;           // 返回const对象地址
        // return nullptr;     // 特殊场景:不想暴露真实地址
    }

private:
    int _year;    // 年
    int _month;   // 月
    int _day;     // 日
};

总结:取地址运算符重载就是重载 &,控制取地址时返回什么;一般不用自己写,编译器自动生成的就够用,只有特殊场景(不想暴露真实地址)才需要自己实现。


最后,感谢你的阅读,欢迎交流与指正!

下一篇我们再见🔜,Bye bye 👋

相关推荐
灿飞屋23 分钟前
Rust 编程其实很简单
开发语言·rust
MOONICK2 小时前
QT元数据
开发语言·qt
D e re k ZH3 小时前
C++ 系统学习手记:从语法细节到工程实践
开发语言·数据结构·c++·算法
灿飞屋4 小时前
Kotlin与Java虚拟机三:控制流
android·java·开发语言·后端·python·kotlin·安卓
xiaoye-duck4 小时前
《C++ 项目:手写大模型接入 SDK》Ollama接口设计(上):初始化、全量响应实现和测试
开发语言·网络·c++·语言模型
zaizai10074 小时前
【Effective Modern C++】学习第一章
c++
Fruiticecake5 小时前
C 程序员速通现代 C++(一):lambda、std::function、移动语义与智能指针
c语言·c++
总要学点什么5 小时前
第98篇:三维特效-材质线效果——给一条线换十种材质,虚线的图案只填了一半
开发语言·前端·javascript·ecmascript·gis·webgl·材质
田甲5 小时前
做一款 Type-C 母座读卡器:GL3224 双卡方案实测
c语言·开发语言