ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

浅聊运算符重载

浅聊运算符重载 前言先看一段代码int a 1, b 2; int c a b; // 内置类型编译器知道怎么加 Date d1(2024, 4, 14); Date d2 d1 100; // 编译器不知道日期 天数是什么意思第二行为什么编译不过因为编译器只认识内置类型之间的。要让自定义类型也能写出这种自然代码就需要运算符重载。本文先整理运算符重载的规则再用一个完整的日期类把所有规则落地。一、什么是运算符重载C 的规定是当运算符被用于类类型的对象时C 语言允许我们通过运算符重载的形式指定新的含义。类类型对象使用运算符时必须转换成调用对应运算符重载若没有对应的运算符重载则会编译报错。翻译一下运算符重载本质就是给运算符指定一个新的函数。名字有固定格式——operator加上要重载的运算符operator // 重载加号 operator // 重载等于 operator // 重载流插入 operator // 重载自增它和其他函数一样有返回类型、参数列表和函数体。二、十一条核心规则这一节是选择题高发区建议逐条过一遍。规则1参数个数 该运算符作用的运算对象个数一元运算符有 1 个参数二元运算符有 2 个参数。规则2二元运算符左侧对象传给第一个参数右侧对象传给第二个参数bool operator(const Date d1, const Date d2) { // d1 是左侧运算对象d2 是右侧运算对象 return d1._year d2._year d1._month d2._month d1._day d2._day; } d1 d2; // 编译器转换成operator(d1, d2) operator(d1, d2); // 也可以显式调用规则3作为成员函数时第一个运算对象默认传给 this所以参数比运算对象少一个这是最需要理解的一条。对比两种写法// 写法一重载为成员函数 class Date { public: bool operator(const Date d) { ... } // 只有 1 个参数 }; d1 d2; // 编译器转换成d1.operator(d2) d1.operator(d2); // 显式调用 // 写法二重载为全局函数 bool operator(const Date d1, const Date d2) { ... } // 有 2 个参数 d1 d2; // 编译器转换成operator(d1, d2)成员函数形式的参数比运算对象少一个因为隐藏的this占了第一个参数的位置。规则4重载后优先级和结合性与对应的内置类型运算符保持一致重载不会改变运算符的优先级也不会改变结合性——只改变作用在自定义类型上时做什么。规则5不能通过连接语法中没有的符号来创建新运算符operator // ❌ 非法 不是 C 的运算符规则6以下 5 个运算符不能重载常考务必记住不能重载的运算符说明.*成员指针访问运算符::作用域解析符sizeof长度运算符?:三目运算符.成员访问运算符规则7重载运算符至少有一个类类型参数不能通过重载改变内置类型对象的含义// ❌ 编译报错operator 必须至少有一个类类型的形参 int operator(int x, int y) { return x - y; }规则8需要重载哪些运算符看哪些运算符重载后有意义不要为了重载而重载。例如Date重载operator-日期 - 日期 天数很有意义但重载operator日期 日期 就没有意义。规则9前置 和后置 靠一个 int 形参区分两者的函数名都是operator无法区分。C 的规定是后置版本增加一个int形参以此与前缀构成函数重载。Date operator(); // 前置d1 → d1.operator() Date operator(int); // 后置d1 → d1.operator(0)这个int形参不用写形参名因为它接收的值是多少不重要、也用不到——仅仅是为了跟前置 构成重载区分。规则10 和 必须重载为全局函数假设把它们重载为成员函数class Date { public: void operator(ostream out); // this 抢占了第一个参数位 }; d1 cout; // ⚠️ 调用变成了这样因为成员函数的第一个参数位置被this占走了而第一个参数位置代表左侧运算对象——于是使用方式就变成了对象 cout完全不符合使用习惯和可读性。正确做法重载为全局函数把ostream/istream放第一个参数位置ostream operator(ostream out, const Date d) { out d._year 年 d._month 月 d._day 日 endl; return out; // 必须返回流引用才能连续输出 cout d1 d2; }规则11全局函数访问私有成员有四种解法方案说明① 成员放公有破坏封装不推荐② 提供getxxx()函数可以但需要为每个成员写一个③友元函数本次日期类采用这种④ 重载为成员函数对不适用见规则10友元的用法是在类里声明一下class Date { // 友元函数声明 friend ostream operator(ostream out, const Date d); friend istream operator(istream in, Date d); public: // ... };三、为什么 operator 必须返回 ostream这是一个高频问题。看连续输出cout d1 d2;它等价于operator(operator(cout, d1), d2);也就是第一次调用的返回值要作为第二次调用的第一个参数。所以返回值必须是ostream流对象本身否则第二次调用就拿不到cout了。void operator(ostream out, const Date d) // ❌ 返回 void { out d._year 年 d._month 月 d._day 日 endl; } cout d1 d2; // 编译报错void 不能作为第一个参数同样的道理也要返回istream才能支持cin d1 d2;。四、实战完整实现一个日期类把上面的规则全部用起来手写一个Date。工程上分三个文件Date.h声明、Date.cpp实现、Test.cpp测试。4.1 头文件 Date.h#pragma once #include iostream #include cassert using namespace std; class Date { // 友元函数声明让全局的 / 能访问私有成员 friend ostream operator(ostream out, const Date d); friend istream operator(istream in, Date d); public: // 获取某年某月的天数 int GetMonthDay(int year, int month); Date(int year 1900, int month 1, int day 1); // 全缺省构造 Date(const Date d); // 拷贝构造 Date operator(const Date d); // 赋值运算符重载 ~Date(); // 析构函数 void Print(); // 打印测试辅助 bool CheckDate(); // 合法性检查测试辅助 Date operator(int day); // 日期 天数 Date operator(int day); // 日期 天数 Date operator-(int day); // 日期 - 天数 Date operator-(int day); // 日期 - 天数 Date operator(); // 前置 Date operator(int); // 后置 Date operator--(); // 前置 -- Date operator--(int); // 后置 -- bool operator(const Date d); // 六个比较运算符 bool operator(const Date d); bool operator (const Date d); bool operator (const Date d); bool operator (const Date d); bool operator ! (const Date d); int operator-(const Date d); // 日期 - 日期返回天数 private: int _year; int _month; int _day; };4.2 获取某年某月的天数int Date::GetMonthDay(int year, int month) { assert(month 0 month 13); // 用 static 数组查表避免每次调用都重建数组 static int monthDayArray[13] { -1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; // 闰年规则能被 4 整除且不能被 100 整除或者能被 400 整除 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 日期非法: _year 年 _month 月 _day 日 endl; } }注意闰年判断里那对括号(year % 4 0 year % 100 ! 0)——如果不加括号和||的优先级会让结果出错。整百年要特殊处理1900、2100 都不是闰年。4.3 六个比较运算符只认真写 2 个比较运算符的逻辑很容易写重复。推荐的策略是认真实现和其余四个全部复用它们。这样逻辑只有一份不会出现两处判断不一致的 bug。// d1 d2按 年 → 月 → 日 的优先级逐级比较 bool Date::operator(const Date d) { if (_year ! d._year) { return _year d._year; } if (_month ! d._month) { return _month d._month; } return _day d._day; } bool Date::operator(const Date d) { return _year d._year _month d._month _day d._day; } // 下面四个全部复用上面两个 bool Date::operator(const Date d) { return *this d || *this d; } bool Date::operator(const Date d) { return !(*this d); } bool Date::operator (const Date d) { return !(*this d); } bool Date::operator ! (const Date d) { return !(*this d); }4.4 日期加减 和 - 就地修改 和 - 复用它们这一组最能体现复用思想// d1 50 或 d1 -50 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) { Date tmp *this; // 拷贝一份 tmp day; return tmp; } // d1 - 100 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 day) { Date tmp *this; tmp - day; return tmp; }分工要记清负责就地修改 处理进位/借位负责拷一份出来改副本。这样d1 100不会改变d1而d1 100会——语义完全符合直觉。4.5 前置 / 后置 和 --// d1直接自增返回自身引用无拷贝 Date Date::operator() { *this 1; return *this; } // d1先保存旧值自增后返回旧值副本有一次拷贝 Date Date::operator(int) { Date tmp(*this); *this 1; return tmp; } Date Date::operator--() { *this - 1; return *this; } Date Date::operator--(int) { Date tmp(*this); *this - 1; return tmp; }为什么后置 必须返回值不能图省事返回引用因为要返回自增之前的值而那个值此刻已经从*this上消失了只能提前把旧值拷进tmp。tmp是局部对象函数结束就销毁所以只能按值返回。这份多一次拷贝正是前置 比后置 效率高的根本原因。顺带一个能验证这个结论的语法现象(d1); // ✅ 合法d1 返回 Date是左值可以再 (d1); // ❌ 非法d1 返回临时值右值不能对它用后置 4.6 日期 - 日期返回相差天数思路先确定谁大谁小用flag记录正负号然后让小的不断直到等于大的计数器就是天数差。int Date::operator-(const Date d) { 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; }4.7 流插入和流提取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; }4.8 拷贝构造、赋值重载、析构这个类为什么这么简单// 拷贝构造第一个参数必须是自身类类型的引用 Date::Date(const Date d) { _year d._year; _month d._month; _day d._day; } // 赋值运算符重载 Date Date::operator(const Date d) { if (this ! d) // 避免自己给自己赋值 { _year d._year; _month d._month; _day d._day; } return *this; // 支持连续赋值 } // 析构函数Date 没有申请任何资源空实现即可 Date::~Date() { // 无需释放资源 }为什么Date的析构是空的、拷贝构造也是最简单的逐成员拷贝因为它没有任何资源申请没有malloc/new——成员全是内置类型不指向任何堆空间。所以编译器默认生成的拷贝构造和赋值重载就已经够用这两个函数其实不写也可以。对比Stack那里_a指向malloc出来的空间默认的浅拷贝会让两个对象共用同一块内存析构时释放两次直接崩溃所以必须自己写深拷贝。判断技巧如果一个类显式实现了析构并释放资源那么它就需要显式写拷贝构造和赋值重载否则不需要。五、测试与运行结果void TestDate1() { Date d1(2024, 4, 14); Date d2 d1 30000; // 大数据加法 d1.Print(); // 2024年4月14日d1 未被改变 d2.Print(); // 2106年6月4日 Date d3(2024, 4, 14); Date d4 d3 - 5000; d3.Print(); // 2024年4月14日 d4.Print(); // 2010年8月6日 Date d5(2024, 4, 14); d5 -5000; // 负数天数 d5.Print(); // 2010年8月6日 } void TestDate2() { Date d1(2024, 4, 14); Date d2 d1; // 前置d2 是自增后的 d1 d1.Print(); // 2024年4月15日 d2.Print(); // 2024年4月15日 Date d3(2024, 4, 14); Date d4 d3; // 后置d4 保存的是旧值 d3.Print(); // 2024年4月15日 d4.Print(); // 2024年4月14日 - 前后置的区别就在这里 } void TestDate3() { Date d1(2024, 4, 14); Date d2(2034, 4, 14); cout d2 - d1 endl; // 3652 cout d1 - d2 endl; // -3652 }比较运算符的测试Date d1(2024, 4, 14); Date d2(2024, 4, 15); cout (d1 d2) endl; // 1 cout (d1 d2) endl; // 1 cout (d1 d2) endl; // 0 cout (d1 d2) endl; // 0 cout (d1 d2) endl; // 0 cout (d1 ! d2) endl; // 1几个值得注意的边界结果测试结果说明2024-03-01 - 12024年2月29日闰年退位2023-03-01 - 12023年2月28日平年退位2024-01-15 - 202023年12月26日借 2023-12 的 31 天2024-12-31 12025年1月1日跨年进位2024-02-28 12024年2月29日闰年进位2034-04-14 - 2024-04-143652⚠️ 不是 3653中途只有 2028、2032 两个闰年六、易错点汇总#坑正确结论1想重载.*::sizeof?:.这 5 个不能重载2/写成成员函数会变成对象 cout→必须写成全局函数3operator返回void无法连续输出cout d1 d2;4忘记写友元声明全局函数访问私有成员会编译报错5后置的int形参只为构成重载区分不用写形参名6operator没有类类型参数int operator(int, int)❌ 编译报错7Date d3();这是函数声明不是创建对象8d1 100改动了d1不能修改自身应Date tmp *this; tmp day; return tmp;9operator返回引用并return tmp;错误 C4172野引用局部对象只能值返回10从成员函数的角度数参数成员函数形式的参数比运算对象少一个this 占了一位七、总结这篇文章的核心是三条主线第一条运算符重载的规则本质是给运算符指定一个函数参数个数与运算对象个数对应成员函数少一个5 个运算符不能重载重载不改变优先级和结合性至少有一个类类型参数。第二条两个特殊场景前置 / 后置 靠一个int形参区分后置返回旧值副本所以效率低/必须重载为全局函数否则会变成对象 cout并返回流引用以支持连续输入输出通常配合friend访问私有成员。第三条返回值类型的选择判断标准是返回的对象在函数结束后还活不活着函数返回对象返回类型operator/-*this类前置/--*this类operator*this类operator/-天数局部tmp类值后置/--局部tmp旧值类值
返回列表