【C++ 面试真题】13. 聊聊 C++ 的运算符重载

【C++ 面试真题】聊聊 C++ 的运算符重载

运算符重载是 C++ 面试里特别能考察功底的一道题。会用 operator+ 只是入门,真正考你的是"哪些能重载、怎么写、为什么有些只能写成成员函数、自增自减的前后置怎么区分"。一道题能从语法一路问到设计哲学。本文用问答的方式,把运算符重载一次讲透。


一、先说结论:什么是运算符重载

❓ 什么是运算符重载?为什么 C++ 要提供这个特性?

✅ 运算符重载,就是给已有的运算符赋予新的含义,让它能作用于自定义类型。

比如内置的 + 只能算 int、double。但你想让两个"复数"对象也能相加:

struct Complex {
    double r, i;
    // 重载 + 号
    Complex operator+(const Complex& o)
        const {
        return {r + o.r, i + o.i};
    }
};

Complex a{1, 2}, b{3, 4};
Complex c = a + b;  // c == {4, 6}

a + b 实际调用了 a.operator+(b),读起来却像内置类型一样自然。

💡 设计哲学(加分点):运算符重载的初衷是让自定义类型用起来像内置类型,提高代码可读性。C++ 之父 Bjarne 的忠告是:“运算符重载应该让代码更清晰,而不是用来炫技。” 如果一个 + 让读者看不出在干嘛,那就该老老实实写个函数。

一个反面教材:有人给字符串类重载 / 表示"拼接",看起来很酷,但读到 s1 / s2 时谁也猜不出它是啥意思。运算符的语义要符合直觉——+ 就是合并、== 就是相等、< 就是有序。一旦违背常识,可读性反而下降。


二、哪些运算符能重载?哪些不能?

❓ 是不是所有运算符都能重载?

✅ 不是。C++ 里绝大多数运算符可重载,但有 5 个硬性不能重载,外加几个几乎不该重载的。

绝对不能重载的 5 个:

运算符含义为什么不能
::作用域解析基础语法,无法重定义
.成员访问基础语法
.*成员指针访问基础语法
?:三目条件含 3 个操作数,语义难定义
sizeof取大小编译期求值,无运行期调用

⚠️ 注意(C++20):C++20 引入了 operator<=>(飞船运算符),但 . 仍不能重载——C++20 给的是"默认比较",不是真重载 .

能重载,但强烈不建议重载的:

  • &&||:重载后会失去短路求值(两个操作数都会算),几乎总是个坑;
  • ,(逗号):重载后求值顺序的保证丢失,语义混乱。

🎯 口诀:: . .* ?: sizeof 五个不能动;&& || , 三个别去动。


三、两种写法:成员函数 vs 全局函数

❓ 运算符重载写成成员函数和全局函数,有什么区别?

✅ 这是最高频的追问点。同一个运算符有两种写法,区别在于左操作数是谁

写法一:成员函数——左操作数必须是 this 对象:

struct Complex {
    double r, i;
    // 成员:左操作数是 *this
    Complex operator+(const Complex& o)
        const {
        return {r + o.r, i + o.i};
    }
};

写法二:全局(友元)函数——左操作数可以是任意类型:

struct Complex {
    double r, i;
    // 开放访问,让全局函数读私有
    friend Complex operator+(
        const Complex&, const Complex&);
};
// 左右都是 Complex,全局实现
Complex operator+(const Complex& a,
                  const Complex& b) {
    return {a.r + b.r, a.i + b.i};
}

为什么要全局函数? 当左操作数不是你的类时,成员函数写不出来。最经典的例子——cout << obj

struct Point { int x, y; };
// 左操作数是 ostream,不是 Point
// 只能用全局函数
std::ostream& operator<<(
    std::ostream& os, const Point& p) {
    return os << "(" << p.x
              << "," << p.y << ")";
}
// 用法
Point p{1, 2};
std::cout << p;  // 打印 (1,2)

cout 是标准库的类,你没法给它加成员函数,所以 << 只能写成全局。

💡 加分点:有几个运算符必须写成成员函数(见下一节),其余的二者皆可。经验法则:二元运算符(+ - * 等)优先写全局,能保证左右操作数都支持隐式转换;尤其当构造函数未加 explicit 时差别明显。

举个具体差别:成员函数写 a + b 时,a 必须是目标类型,隐式转换只在 b 上发生;全局函数则左右都能隐式转换。如果你的类能从 int 隐式构造,那 1 + obj 用成员函数写不出来(左操作数 1 不是对象),全局函数却可以。


四、哪些运算符必须写成成员函数?

❓ 有没有运算符强制要求写成成员函数?

✅ 有。下面这 4 个必须是成员函数,全局写法编译直接报错:

运算符原因
=赋值与对象身份绑定
()函数调用,依赖对象
[]下标,依赖对象
->成员访问,依赖对象
struct Vec {
    int data[4];
    // [] 必须是成员
    int& operator[](int i) {
        return data[i];
    }
};

⚠️ 高频考点:赋值运算符 = 若你没自己写,编译器会自动生成一个默认的(逐成员拷贝)。这一点和构造/析构一样,是"Rule of"系列(三五法则)的起点。


五、自增自减:前置 vs 后置怎么区分?

++aa++ 的重载函数长得一样,怎么区分?

✅ 这是最经典的设计巧思。C++ 用一个多余的 int 参数来区分:

  • 前置 ++a:无参,返回引用(修改后的对象);
  • 后置 a++:多一个 int 哑参数,返回(修改前的副本)。
struct Counter {
    int n = 0;
    // 前置 ++n:无参,返回引用
    Counter& operator++() {
        ++n;
        return *this;
    }
    // 后置 n++:int 哑参,返回旧值
    Counter operator++(int) {
        Counter old = *this;  // 存旧值
        ++n;                   // 先自增
        return old;            // 返回旧值
    }
};
Counter c;
++c;   // 调 operator++()
c++;   // 调 operator++(int)

那个 int 参数根本不用,纯粹是个标记——调用时编译器自动塞个 0 进去,你写 c++ 实际是 c.operator++(0)

🎯 性能提示(加分点):后置 ++先拷贝一份旧值再自增,多一次拷贝。所以对自定义类型,优先用前置 ++c。这也是为什么迭代器习惯写 ++it 而不是 it++-- 同理。

对内置类型(int 等)前置后置性能没差,但养成写前置的习惯,到了自定义类型就自动受益——这是"前置优先"能成为社区共识的原因。


六、赋值运算符:三句话法则

❓ 重载 = 要注意什么?

✅ 赋值运算符重载有三条铁律,面试必背:

  1. 参数:常量引用 const T&(避免拷贝,不改源);
  2. 返回:引用 T&(支持 a = b = c 链式赋值);
  3. 处理自赋值a = a 不能出错(尤其涉及动态内存)。
struct Buffer {
    char* p = nullptr;
    // 规范的赋值重载
    Buffer& operator=(const Buffer& o) {
        if (this == &o) return *this; // 自赋值
        delete[] p;                    // 释放旧的
        p = new char[strlen(o.p)+1];
        strcpy(p, o.p);
        return *this;  // 返回 *this
    }
};

💡 加分点:现代 C++ 推荐用 copy-and-swap 惯用法,一行解决自赋值和异常安全:

Buffer& operator=(Buffer o) {  // 值传参
    std::swap(p, o.p);          // 交换
    return *this;               // o 析构释放旧
}   // 异常安全 + 自动处理自赋值

七、核心规则速查表

维度规则
不能重载:: . .* ?: sizeof
不该重载&& `
必须成员函数= () [] ->
前后置区分后置多一个 int 哑参
赋值重载三铁律const 引用参、返回引用、防自赋值
全局 vs 成员左操作数非本类 → 全局

八、面试高频追问

❓ Q1:运算符重载能改变运算符的优先级和结合性吗?

不能。重载只改变"对自定义类型时怎么算",优先级、结合性、操作数个数全都钉死不动a + b * c 永远是先算 * 再算 +

❓ Q2:重载后的 + 还能算 int 吗?

。重载是"新增"不是"覆盖"——内置类型上的 + 行为不变。重载只对自定义类型生效,这叫重载解析

❓ Q3:为什么 <<>> 能用来做输入输出?

✅ 因为它们在 <iostream> 里被重载成了流插入/提取运算符。本质就是全局函数 operator<<(ostream&, T)。这是运算符重载最经典的成功案例。

❓ Q4:operator+ 该返回值还是引用?

✅ 返回a + b 产生一个新对象,它不在任何变量里。若返回引用,引用的是局部临时对象,函数返回即销毁——未定义行为。算术运算符返回值,赋值/复合赋值返回引用,这是通用法则。

❓ Q5:C++20 的 operator<=> 是什么?

✅ “飞船运算符”(三向比较)。你只需重载这一个,编译器自动生成 < <= > >= 四个,大大减少手写比较的样板代码。


九、总结速查表

场景推荐写法
算术运算符全局,返回值
赋值/复合赋值成员,返回引用
比较、流全局
= () [] ->必须成员
自增自减成员,后置加 int

一句话回顾

运算符重载的本质,是让自定义类型用起来像内置类型。记住"五个不能动、三个别去动、四个必须成员、前后置靠 int 区分",再配上赋值三铁律,这题就稳了。

如果您觉得本篇内容对你有帮助,欢迎点赞 👍、收藏 ⭐、转发 📢。下期我们聊聊 lambda 表达式的本质,敬请关注 👋

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码工许师傅

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值