📌 阅读时长:25分钟 | 关键词:C++、运算符重载、operator、友元重载、<<重载、String类
引言
你有没有想过:为什么 int 可以 a + b、string 可以 s1 + s2,而自己写的 MyVector 类只能用 v.add(w) 这种丑陋的语法?答案是运算符重载 ------它允许你为自定义类型定义 +、-、<< 等运算符的行为,让代码像操作内置类型一样自然。
一、运算符重载基础
1.1 概念与语法
运算符重载本质上就是给自定义类写一个特殊的函数 ,函数名是 operator + 运算符:
cpp
返回类型 operator运算符(参数列表) {
// 实现逻辑
}
cpp
class Complex {
public:
double real, imag;
Complex(double r = 0, double i = 0) : real(r), imag(i) {}
// 重载 + 运算符(成员函数)
Complex operator+(const Complex &other) const {
return Complex(real + other.real, imag + other.imag);
}
};
int main() {
Complex c1(2, 3), c2(4, 5);
Complex c3 = c1 + c2; // 等价于 c1.operator+(c2)
// c3 = (6, 8i)
}
1.2 可重载 vs 不可重载
| 可重载 | 不可重载 |
|---|---|
+ - * / % |
. 成员访问 |
== != < > <= >= |
.* 成员指针访问 |
= += -= *= /= |
:: 作用域解析 |
++ -- (前后缀) |
sizeof |
<< >> (流) |
?: 三元条件 |
[] () (下标/函数调用) |
typeid |
new delete |
# ## 预处理 |
1.3 两种重载方式
| 方式 | 语法 | 左操作数 | 使用场景 |
|---|---|---|---|
| 成员函数 | Ret operatorX(Para) |
必须是本类对象 | 单目运算符、赋值类 |
| 友元函数 | friend Ret operatorX(L,R) |
可以是其他类型 | 双目运算符、流运算符 |
cpp
// 成员函数版本
Complex operator+(const Complex &other) const;
// 等价于 c1.operator+(c2)
// 友元函数版本
friend Complex operator+(const Complex &a, const Complex &b);
// 等价于 operator+(c1, c2)
💡 流运算符
<<和>>必须 用友元函数,因为左操作数是std::ostream而非本类。
二、各类运算符重载实例
2.1 算术运算符 (+, -, *, /)
cpp
class Complex {
public:
double real, imag;
Complex(double r = 0, double i = 0) : real(r), imag(i) {}
Complex operator+(const Complex &o) const {
return Complex(real + o.real, imag + o.imag);
}
Complex operator-(const Complex &o) const {
return Complex(real - o.real, imag - o.imag);
}
Complex operator*(const Complex &o) const {
return Complex(real * o.real - imag * o.imag,
real * o.imag + imag * o.real);
}
};
2.2 关系运算符 (==, !=, <, >)
cpp
class Point {
public:
int x, y;
Point(int x, int y) : x(x), y(y) {}
bool operator==(const Point &o) const {
return x == o.x && y == o.y;
}
bool operator!=(const Point &o) const {
return !(*this == o); // 复用 ==
}
};
2.3 赋值运算符与复合赋值 (+=, -=)
cpp
class MyNumber {
public:
int value;
MyNumber(int v) : value(v) {}
MyNumber &operator=(const MyNumber &o) { // 返回引用,支持链式赋值
if (this != &o) value = o.value;
return *this;
}
MyNumber &operator+=(const MyNumber &o) {
value += o.value;
return *this;
}
};
// a = b = c; // 链式赋值,依赖返回引用
2.4 自增自减 (++, --)
区分前缀和后缀的秘诀:后缀版本多一个不用的 int 参数:
cpp
class Counter {
public:
int value;
Counter &operator++() { // 前缀 ++
++value;
return *this;
}
Counter operator++(int) { // 后缀 ++ (int 是标记)
Counter temp = *this;
++value;
return temp; // 返回旧值
}
};
Counter c{5};
++c; // c.value = 6, 返回 c 自己
c++; // c.value = 7, 返回 Counter(6)
2.5 流运算符 (<<, >>):必须用友元
cpp
class Complex {
public:
double real, imag;
Complex(double r = 0, double i = 0) : real(r), imag(i) {}
friend std::ostream &operator<<(std::ostream &os, const Complex &c);
friend std::istream &operator>>(std::istream &is, Complex &c);
};
std::ostream &operator<<(std::ostream &os, const Complex &c) {
os << c.real << " + " << c.imag << "i";
return os; // 必须返回 os,支持链式调用
}
std::istream &operator>>(std::istream &is, Complex &c) {
is >> c.real >> c.imag;
return is;
}
// std::cout << c1 << " and " << c2 << std::endl; ✅ 链式
2.6 下标运算符 \[\]
cpp
class IntArray {
private:
int *data;
int size;
public:
IntArray(int s) : size(s), data(new int[s]) {}
int &operator[](int index) { // 返回引用,允许修改
return data[index];
}
const int &operator[](int index) const { // const 版本,只读
return data[index];
}
~IntArray() { delete[] data; }
};
IntArray arr(5);
arr[2] = 100; // 调用非 const 版本
2.7 函数调用运算符 () --- 仿函数
cpp
class Adder {
public:
int operator()(int a, int b) const {
return a + b;
}
};
Adder add;
std::cout << add(3, 4) << std::endl; // 7,像函数一样调用对象!
三、运算符重载最佳实践
| 原则 | 说明 |
|---|---|
| 保持语义一致 | + 不应该做减法 |
| 不要滥用 | 只有提高可读性时才重载 |
| 算术运算返回新对象 | 不返回引用(临时对象) |
| 赋值运算返回引用 | return *this,支持链式 |
| const 正确性 | 不修改对象的函数加 const |
| 不可改变优先级 | 重载不改变运算符优先级和结合性 |
四、std::string 类:运算符重载的教科书范本
std::string 大量使用运算符重载,让字符串操作如内置类型般自然:
cpp
#include <string>
std::string s1 = "Hello";
std::string s2 = " World";
std::string s3 = s1 + s2; // + 重载:连接字符串
s3 += "!"; // += 重载:追加
if (s1 == s2) {} // == 重载:比较内容
char c = s3[0]; // [] 重载:下标访问
std::cout << s3; // << 重载:输出
常用 string 操作速查
| 方法 | 功能 | 示例 |
|---|---|---|
+ / += |
连接/追加 | s1 + s2 |
find(s) |
查找子串位置 | s.find("He") → 0 |
substr(pos, n) |
截取子串 | s.substr(0, 3) → "Hel" |
replace(pos, n, s) |
替换 | s.replace(0,2,"Hi") |
length()/size() |
长度 | s.length() |
c_str() |
转 C 串 | s.c_str() → const char * |
at(i) |
安全下标访问(越界抛异常) | s.at(100) |
string vs C 风格字符串
std::string |
char[] / char * |
|
|---|---|---|
| 内存管理 | 自动 | 手动 |
| 安全性 | 不易溢出 | 容易缓冲区溢出 |
| 操作便利 | 丰富的成员函数 | 需 <cstring> 函数 |
| 推荐度 | ⭐⭐⭐⭐⭐ | ⭐⭐(仅在 C 接口需要) |
小结
| 序号 | 知识点 | 一句话总结 |
|---|---|---|
| 1 | 运算符重载 | operatorX 特殊函数,让类支持运算符操作 |
| 2 | 成员 vs 友元 | 成员用于单目/赋值,友元用于双目/流运算符 |
| 3 | 不可重载运算符 | . .* :: sizeof typeid ?: # ## |
| 4 | 流运算符 | 必须用友元,返回 ostream& 支持链式 |
| 5 | 前后缀 ++ | 前缀无参,后缀有废弃 int 标记 |
| 6 | 赋值 vs 算术 | 赋值返回 *this 引用;算术返回新对象 |
| 7 | std::string | 运算符重载的典范,优先于 C 风格字符串 |
下一篇文章,我们将学习友元与设计模式初探 ------如何用 friend 打破封装边界,以及如何用 Singleton 模式用静态成员打造全局唯一实例。
本文是「C++ 从基础到项目实战」系列的第 8 篇。关注我,不错过后续更新。