16、运算符重载:重载规则、常见运算符重载(+、-、<<、>>、[])、重载限制
运算符重载,说白了就是让 C++ 的运算符能「认识」你自定义的类。比如你写了个 Complex 复数类,想让两个复数直接 c1 + c2,而不是写 c1.add(c2)——这就是运算符重载的用武之地。
我个人习惯把运算符重载看作「语法糖」。它不改变运算符的本质,只是让代码更自然。你想想看,数学公式里到处都是 + - * /,如果代码里全是函数调用,读起来多别扭。
核心思想:运算符重载是函数重载的一种特殊形式,本质上是定义一个名为
operator@ 的函数(@ 代表运算符)。
16.1 重载规则:别踩这些坑
运算符重载不是你想怎么来就怎么来的。C++ 标准定了一些硬性规则,我当年刚学的时候踩过不少坑,这里给你捋一捋。
16.1.1 可重载的运算符
大部分运算符都能重载,但有几个例外:
- 不能重载:
::(作用域解析)、.(成员访问)、.*(成员指针访问)、?:(三目运算符) - 必须重载为成员函数:
=(赋值)、[](下标)、()(函数调用)、->(成员指针访问) - 建议重载为友元函数:
<<和>>(输入输出)、双目运算符(当左操作数不是本类对象时)
我曾经犯过的错:试图重载
. 运算符来实现「智能指针」的透明访问,结果编译器直接报错。后来才知道,. 是 C++ 的硬保留,永远不能重载。
16.1.2 重载的基本规则
- 不改变优先级和结合性:
*永远比+优先,重载了也一样 - 不改变操作数个数:一元运算符只能有一个参数,二元运算符只能有两个
- 不能发明新运算符:你不能造个
**出来表示幂运算 - 至少有一个用户定义类型:你不能重载
int + int的行为
16.2 常见运算符重载实战
光说理论没意思,咱们直接上代码。我以一个 MyString 类为例,把最常用的几个运算符都重载一遍。
16.2.1 重载 + 和 -:字符串拼接与减法
class MyString {
private:
char* data;
int length;
public:
// 构造函数、析构函数省略...
// 重载 +:字符串拼接
MyString operator+(const MyString& other) const {
MyString result;
result.length = this->length + other.length;
result.data = new char[result.length + 1];
strcpy(result.data, this->data);
strcat(result.data, other.data);
return result;
}
// 重载 -:删除子串(我个人觉得这个功能挺实用)
MyString operator-(const MyString& sub) const {
// 查找子串位置并删除
char* pos = strstr(this->data, sub.data);
if (pos == nullptr) return *this;
MyString result;
int posIndex = pos - this->data;
result.length = this->length - sub.length;
result.data = new char[result.length + 1];
strncpy(result.data, this->data, posIndex);
strcpy(result.data + posIndex, pos + sub.length);
return result;
}
};
小技巧:重载
+ 时,我建议返回 const 对象,防止出现 (a + b) = c 这种诡异的写法。嗯,虽然语法上允许,但逻辑上说不通。
16.2.2 重载 << 和 >>:输入输出
这两个运算符必须重载为友元函数,因为左操作数是 ostream 或 istream,不是你的类对象。
class MyString {
// ... 前面代码省略
friend ostream& operator<<(ostream& os, const MyString& str) {
os << str.data;
return os;
}
friend istream& operator>>(istream& is, MyString& str) {
// 先清空原有数据
delete[] str.data;
str.data = nullptr;
str.length = 0;
// 读取一行
char buffer[1024];
is.getline(buffer, 1024);
str.length = strlen(buffer);
str.data = new char[str.length + 1];
strcpy(str.data, buffer);
return is;
}
};
为什么返回 ostream& 而不是 void?为了支持链式调用啊!cout << a << b << c 这种写法,靠的就是返回引用。
16.2.3 重载 []:下标访问
这个必须重载为成员函数。我在项目中经常用它来实现「带边界检查的数组」。
class MyString {
// ... 前面代码省略
// 非 const 版本:可修改
char& operator[](int index) {
if (index < 0 || index >= length) {
throw std::out_of_range("Index out of range");
}
return data[index];
}
// const 版本:只读
const char& operator[](int index) const {
if (index < 0 || index >= length) {
throw std::out_of_range("Index out of range");
}
return data[index];
}
};
注意:一定要同时提供 const 和非 const 两个版本。否则,const 对象无法使用
[] 运算符。
16.3 重载限制:有些事真的不能做
除了前面提到的不能重载的运算符,还有一些「潜规则」需要你注意。
16.3.1 运算符的语义限制
- 逻辑运算符的短路求值失效:重载
&&和||后,两个操作数都会被求值,不再短路 - 逗号运算符的求值顺序不确定:重载
,后,不再保证从左到右求值 - 取地址运算符
&可以重载:但重载后&obj可能返回的不是真实地址,容易引发混淆
我曾经踩过的坑:重载了
&& 运算符来实现一个「智能条件判断」类,结果发现 if (ptr && ptr->isValid()) 这种写法在重载后,即使 ptr 为空,ptr->isValid() 也会被调用,直接崩溃。从那以后,我再也不重载逻辑运算符了。
16.3.2 自增自减的前置与后置
重载 ++ 和 -- 时,前置版本返回引用,后置版本返回值。区别在于后置版本有一个 int 占位参数。
class MyString {
// 前置 ++:返回引用
MyString& operator++() {
// 在字符串前面加一个空格
char* newData = new char[length + 2];
newData[0] = ' ';
strcpy(newData + 1, data);
delete[] data;
data = newData;
length++;
return *this;
}
// 后置 ++:返回值
MyString operator++(int) {
MyString temp = *this;
++(*this); // 调用前置版本
return temp;
}
};
16.4 知识体系总览
下面这张图帮你理清运算符重载的整个知识结构:
16.5 总结与避坑指南
运算符重载用好了,代码会非常优雅。但用不好,就是灾难。我总结几个关键点:
- 保持语义一致:
+就应该做加法或拼接,别拿来做减法 - 优先考虑成员函数:除非必须用友元(比如
<<和>>) - 不要过度重载:不是所有运算符都需要重载,只重载那些「自然」的
- 注意 const 正确性:不修改对象的成员函数要加 const
我的个人习惯:在写类的时候,先问自己三个问题:这个运算符重载后,代码更易读吗?更易写吗?符合直觉吗?如果三个答案都是「是」,那就重载。否则,老老实实写函数。
嗯,运算符重载就讲到这里。记住,它是个工具,不是炫技的手段。用对了地方,事半功倍;用错了,代码维护起来会让你怀疑人生。