【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 后置怎么区分?
❓ ++a 和 a++ 的重载函数长得一样,怎么区分?
✅ 这是最经典的设计巧思。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 等)前置后置性能没差,但养成写前置的习惯,到了自定义类型就自动受益——这是"前置优先"能成为社区共识的原因。
六、赋值运算符:三句话法则
❓ 重载 = 要注意什么?
✅ 赋值运算符重载有三条铁律,面试必背:
- 参数:常量引用
const T&(避免拷贝,不改源); - 返回:引用
T&(支持a = b = c链式赋值); - 处理自赋值:
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 表达式的本质,敬请关注 👋

618

被折叠的 条评论
为什么被折叠?



