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 重载 <<>>:输入输出

这两个运算符必须重载为友元函数,因为左操作数是 ostreamistream,不是你的类对象。

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
我的个人习惯:在写类的时候,先问自己三个问题:这个运算符重载后,代码更易读吗?更易写吗?符合直觉吗?如果三个答案都是「是」,那就重载。否则,老老实实写函数。

嗯,运算符重载就讲到这里。记住,它是个工具,不是炫技的手段。用对了地方,事半功倍;用错了,代码维护起来会让你怀疑人生。


公众号:蓝海资料掘金营,微信deep3321