第14章 运算符重载
第14章 运算符重载
- 运算符重载的基本语法与规则
- 如何重载常用运算符(+, -, ==, <<)
- 成员函数重载与友元函数重载的区别
想象一下,你设计了一个Vector2D类来表示二维向量。如果想让两个向量相加,你只能写v1.add(v2),这远不如直接写v1 + v2直观。运算符重载就是赋予C++运算符操作自定义类型的能力,让代码更简洁、更符合直觉。
运算符重载:语法与规则
运算符重载的本质是定义一个特殊的成员函数或全局函数,函数名由operator关键字后接要重载的运算符符号组成。
class Complex {
public:
double real, imag;
// 成员函数形式重载‘+’运算符
Complex operator+(const Complex& other) const {
return Complex{real + other.real, imag + other.imag};
}
};
// 使用
Complex c1{1.0, 2.0};
Complex c2{3.0, 4.0};
Complex c3 = c1 + c2; // 等价于 c1.operator+(c2)
关键点: 重载不能改变运算符的优先级和结合性,也不能创造新的运算符(如`**`)。它的操作数中至少有一个是用户自定义的类型。
并非所有运算符都能被重载。下表列出了可重载与不可重载的运算符,这是你必须遵守的语法边界。
| 可重载的运算符 | 不可重载的运算符 |
|---|---|
| 算术运算符:+ - * / % 关系运算符:== != < > <= >= 逻辑运算符:! && || 位运算符:& | ^ ~ << >> 赋值运算符:= += -= 等 自增自减:++ -- 下标与函数调用:[] () 成员访问:-> ->* 内存管理:new new[] delete delete[] 逗号:, | 作用域解析运算符::: 成员访问运算符: . 成员指针访问运算符: .* 条件运算符: ? : sizeof 运算符 typeid 运算符 |
两种实现方式:成员函数 vs. 友元函数
这是运算符重载的核心决策点。选择哪种方式,取决于运算符的语义和第一个操作数的类型。
成员函数重载: 运算符函数作为类的成员。此时,左操作数必须是该类的对象,它通过this指针隐式传递。这非常适合修改自身状态的运算符,如+=、++。
class MyArray {
public:
int& operator[](int index) { return data[index]; } // 下标运算符必须是成员函数
MyArray& operator+=(const MyArray& other); // 复合赋值通常为成员函数
private:
int data[100];
};
友元函数重载: 运算符函数作为全局函数,并在类内声明为friend。当左操作数不是类的对象(比如是内置类型),或者你需要运算符具有完全的对称性时,必须使用友元函数。
生活类比: 就像给一个定制工具箱(自定义类)增加“连接”功能。成员函数重载像是工具箱自带一个连接另一个工具箱的卡扣(`工具箱A.连接(工具箱B)`)。友元函数重载则像是一个外部的通用连接器,它不专属于任何一个工具箱,但被授权可以操作它们的内部结构,因此能实现`连接器(工具箱A, 工具箱B)`甚至`连接器(标准箱子, 工具箱A)`。
技术定义: 友元函数重载是在类外部定义的全局函数,通过`friend`关键字获得访问该类私有成员的权限,常用于实现运算符左、右操作数可互换的对称操作。
class Complex {
public:
// 声明友元函数
friend Complex operator+(const Complex& lhs, const Complex& rhs);
private:
double real, imag;
};
// 定义友元函数(注意,没有Complex::限定)
Complex operator+(const Complex& lhs, const Complex& rhs) {
return Complex{lhs.real + rhs.real, lhs.imag + rhs.imag};
}
// 现在可以写 2.5 + c1 (如果实现了`operator+(double, const Complex&)`)
常见误区
- 过度使用友元: 只在必要时(如实现对称运算符`<<`、`+`)才使用友元。滥用友元会破坏封装性。
- 返回临时对象的引用: 像`+`、`-`这类运算符应该返回一个新对象(值),而不是引用。返回局部变量的引用会导致未定义行为。
- 忽略const正确性: 对于不修改操作数的运算符(如`==`、`+`),应将运算符函数和参数声明为
const。
实战:重载输入输出运算符(<< 和 >>)
这是最经典的友元函数重载案例。因为左操作数是std::ostream&或std::istream&,而不是你的自定义类对象。
#include <iostream>
class Point {
public:
friend std::ostream& operator<<(std::ostream& os, const Point& p);
friend std::istream& operator>>(std::istream& is, Point& p);
private:
int x, y;
};
// 输出运算符
std::ostream& operator<<(std::ostream& os, const Point& p) {
os << "(" << p.x << ", " << p.y << ")";
return os; // 必须返回流引用以支持链式调用:cout << p1 << p2;
}
// 输入运算符
std::istream& operator>>(std::istream& is, Point& p) {
is >> p.x >> p.y;
return is; // 同样返回流引用
}
// 使用
Point pt;
std::cin >> pt; // 等价于 operator>>(std::cin, pt)
std::cout << "Point is: " << pt << std::endl;
我在项目中踩过一个坑:忘记返回流引用。这导致std::cout << a << b;这样的链式调用编译失败,错误信息通常是“没有找到匹配的`operator<<`...”,排查起来很费时间。记住,流插入和提取运算符必须返回流引用。
编者提示: 运算符`<<`和`>>`的重载依赖于输入输出流(`iostream`)的知识。如果你对`std::cout`、`std::cin`以及`std::ostream`类型感到陌生,建议回顾第21章《文件输入输出》中关于标准流的基础部分。
特殊运算符的重载要点
有些运算符的重载有固定范式,偏离这些范式很容易出错。
自增/自减 (++/--)
- 区分前置(++obj)与后置(obj++)。
- 后置版本接受一个额外的
int形参作为区分,该参数无实际意义。 - 前置返回引用,后置返回值。
赋值运算符 (=)
- 必须是成员函数。
- 注意处理自赋值(`a = a`)的情况。
- 通常返回`*this`的引用以支持链式赋值(`a = b = c`)。
函数调用运算符 (())
- 让对象像函数一样被调用,称为“函数对象”或“仿函数”。
- 这是STL算法(第19章)广泛使用的技术。
这里给出一个完整的自增运算符重载示例,实际开发中,对于包含动态内存的类,你还需要重载赋值运算符并遵循“三法则”。
class Counter {
public:
// 前置++:先加,后返回自身
Counter& operator++() {
++value;
return *this;
}
// 后置++:int形参仅为区分,返回加之前的值
Counter operator++(int) {
Counter temp = *this; // 保存原值
++(*this); // 调用前置++实现自增
return temp; // 返回原值
}
private:
int value = 0;
};
动手试一试
创建一个Money类,表示人民币(元、角、分)。
- 实现成员函数重载的加法运算符
+,使得Money m3 = m1 + m2;可以正确工作(注意:1元=10角=100分,运算后需进位)。 - 实现友元函数重载的相等运算符
==,用于比较两个Money对象是否相等。 - (挑战)实现友元函数重载的流输出运算符
<<,使其能按“¥元.角分”的格式输出,例如Money{12, 3, 4}输出为“¥12.34”。
数据成员建议:int yuan, jiao, fen;
检验你的理解
- 判断:重载运算符`*`时,可以将其功能改为除法运算。这种做法是否被允许?为什么?
- 如果你想实现`double d = 5.0 + myComplexObj;`(`myComplexObj`是你的复数类对象),你应该将`operator+`重载为成员函数还是友元函数?为什么?
- 下面的代码片段试图重载`<<`运算符,但有一处关键错误导致编译失败。请找出错误并修正。
class MyClass { int data; public: friend void operator<<(std::ostream& os, MyClass obj); }; void operator<<(std::ostream& os, MyClass obj) { os << obj.data; }
本章小结
- 运算符重载通过定义
operator@函数,让自定义类型支持C++内置运算符,提升代码可读性。 - 实现方式有两种:成员函数(左操作数为当前对象)和友元函数(常用于需要对称性或左操作数为非类类型的情况)。
- 流运算符
<<和>>必须重载为友元函数,且必须返回流引用以支持链式调用。 - 重载自增/自减运算符时,需用额外
int参数区分后置版本,并注意返回值类型(前置返引用,后置返回值)。 - 遵循运算符的常规语义,保持代码直觉,避免滥用和反直觉的实现。
微信扫码关注
抖音扫码关注