运算符重载:operator 函数、五个不能重载的符号,与 << 的真相
我的github:(https://github.com/xcx55/ubuntu-linux-project)
感谢各位大佬参观我的github!!
类和对象"中"的下一段:运算符重载。它是 C++ 里"语法糖浓度"最高的机制------q1 == q2、cout << d1、q[i] 这些写法背后全是函数调用。这篇从"为什么需要它"讲到"哪些不能重载",最后拆掉 cout 的神秘感。
一、为什么内置类型不需要它,自定义类型离不开它
内置类型的比较,硬件直接支持------汇编里有现成的 cmp 指令 比较数据大小。所以 3 < 5 天经地义。
自定义类型呢?写个日期类,q1 < q2 直接编译报错:
error C2676: 二进制"<": "date"不定义该运算符...
E0349: 没有与这些操作数匹配的 "<" 运算符
编译器不知道两个 date 怎么比------按年比?按月比?这个"语义"只有类的作者知道。运算符重载就是让类的作者把运算符的语义写出来:当运算符被用于类类型对象时,C++ 规定必须转换成调用对应的运算符重载函数,没有对应重载就编译报错。
形式上它就是个函数:名字 = operator + 运算符,有返回类型、参数列表、函数体:
cpp
bool operator==(const Date& x1, const Date& x2)
{
return x1._year == x2._year
&& x1._month == x2._month
&& x1._day == x2._day;
}
二、参数个数的两条规则
- 参数个数 = 运算对象的数量:一元运算符一个参数,二元运算符两个------左操作数传第一个参数,右操作数传第二个;
- 成员函数版本参数少一个:作为成员函数时,第一个运算对象默认传给隐式的 this 指针,所以参数从左边少一个。
两种调用方式完全等价:
cpp
bool a = q1 == q2; // 隐式调用,编译器转成下面的
bool b = operator==(q1, q2); // 全局函数版:显式调用
bool c = q1.operator==(q2); // 成员函数版:显式调用
q1 == q2 不是什么魔法,就是一次函数调用------重载之后运算符的优先级和结合性跟内置版本保持一致,所以写起来跟原生类型没差别。
private 成员怎么访问?
全局的 operator 函数要访问对象的私有成员,绕不开这个问题。三种思路:
- 提供 Getxxx 函数(把成员读出来);
- 友元(friend,后面细讲);
- 直接把运算符重载函数写进类里------最常用。
写进类里还有个附带的好处:成员函数自带 this 指针,this == a.year 的比较直接写在函数体里;参数也顺势少一个。参数建议写成 const Date&:引用避免传值传参触发拷贝构造,const 保证不改被比较的对象。
三、重载的边界:五个不能碰的符号
自由是有边界的,选择题常考:
- 不能创造语言里没有的运算符 :
operator@不行,运算符得是语法中存在的; - 五个运算符不能重载 :
.*::sizeof?:.; - 至少要有一个类类型参数 :不能改内置类型的含义------
int operator+(int x, int y)编译器直接拒绝,3 + 5的语义谁都别想动; - 重载要重载得有意义 :看运算符对这类对象有没有自然语义------Date 类重载
operator-(算两个日期差几天)有意义,重载operator+就没意义(两个日期相加是什么?)。一个类需要哪些运算符,是看重载后有意义的那些。
四、cout 的真面目:一堆 operator<< 的重载
之前入门篇说过 cout 能"自动识别类型",现在能拆穿了:cout 是 ostream 类创建的对象 ,"自动识别"就是函数重载------ostream 里把 operator<< 按所有内置类型重载了个遍(bool、short、int、unsigned、long、float、double、void*......),cout << i 就是 cout.operator<<(i),一次输出就是一次函数调用,连续输出就是多次链式调用。
那给自定义类型重载 << 呢?这里有个反直觉的规定:重载 << 和 >> 必须写成全局函数,不能是成员函数。
原因是 this 指针的位置:成员函数版第一个参数永远是 this(左操作数),写成成员函数的话,调用 d1 << cout 才能匹配------变成"对象 << cout",反人类 。想要 cout << d1 这个自然顺序,就必须把 ostream 放在第一个参数位置,只能全局函数:
cpp
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out; // 返回流对象本身,支持连续输出
}
返回 ostream& 是为了 cout << d1 << d2 这种链式写法------第一次调用返回 cout,接着当第二个调用的左操作数。
五、operator\[\]:让自定义类型像数组一样用
再补一个实用的:operator[]。给 Stack 加上它:
cpp
int& operator[](int i)
{
return _a[i];
}
stack q;
for (int i = 0; i < 10; i++)
q[i] = i; // 像 C 数组一样读写
q[i] 就是 q.operator[](i),返回引用才能既读又写。C 语言里"数组下标"是内置类型的特权,重载之后自定义类型也享受同款语法------STL 的 vector、string 能用 [],全是这套机制的产物。
串一下:内置类型有 cmp 硬件支持 → 自定义类型没有 → 运算符重载把语义写成函数(operator+运算符)→ 成员函数版 this 占掉第一个参数 → 全局版要解决 private 访问(三种思路)→ 五个符号不能重载、内置类型含义不能改 → cout 的"自动识别"就是 ostream 里的一排 operator<< 重载 。运算符重载讲完,下一篇写其中最特殊的那个------赋值运算符 =,六大默认成员函数的最后一块拼图。