运算符重载核心知识点讲解
1 什么是运算符重载?
运算符重载是 C++ 的核心特性,本质是函数重载的特殊形式—— 它允许你为自定义类型(类 / 结构体)重新定义运算符的行为,让自定义类型能像
int、float等内置类型一样使用+、-、<<、=等运算符。举个例子:默认情况下
MyClass a, b; a + b;是非法的(编译器不知道如何 “加” 两个自定义对象),但通过运算符重载,你可以定义a + b的逻辑(比如把两个对象的成员变量相加)。
2 运算符重载的核心规则(不可违反)
这是重载的 “红线”,违反会导致编译错误或逻辑混乱:
- 只能重载已有运算符:不能发明新运算符(如
@、#),支持重载的运算符包括+、-、*、<<、=、[]等。- 不可改变运算符的基本特性:
- 优先级不变(如
*始终比+优先);- 结合性不变(如
+是左结合,a+b+c等价于(a+b)+c);- 操作数个数不变(如
+是双目运算符,重载后仍需两个操作数)。- 部分运算符绝对不能重载:
.(成员访问)、.*(成员指针访问)、::(作用域解析)、?:(三目运算符)、sizeof、typeid。- 语义一致性:重载后的运算符功能应符合直觉(比如
+重载为 “加法”,而非 “减法”),否则代码可读性极差。
实例 + 调用
1. 基础:双目运算符重载(以+为例)
双目运算符(需要两个操作数)有两种重载方式:成员函数(左操作数是this)和全局 / 友元函数(两个操作数都是参数)。
方式 1:成员函数重载+
#include <iostream>
using namespace std;
// 自定义复数类
class Complex {
private:
double real; // 实部
double imag; // 虚部
public:
// 构造函数
Complex(double r = 0, double i = 0) : real(r), imag(i) {}
// 成员函数重载+:左操作数是this(当前对象),右操作数是other
// const:保证不修改当前对象;返回值:返回新对象(避免修改原对象)
Complex operator+(const Complex& other) const {
return Complex(real + other.real, imag + other.imag);
}
// 辅助打印
void print() const {
cout << real << " + " << imag << "i" << endl;
}
};
int main() {
// 1. 创建对象
Complex c1(1.5, 2.5), c2(3.5, 4.5);
// 2. 调用方式1:运算符形式(推荐,直观)
Complex c3 = c1 + c2;
cout << "c1 + c2 = ";
c3.print(); // 输出:5 + 7i
// 3. 调用方式2:等价的函数形式(底层本质)
Complex c4 = c1.operator+(c2);
cout << "c1.operator+(c2) = ";
c4.print(); // 输出:5 + 7i
return 0;
}
编译器逆向破译的过程:
1.发现c1+c2;
2.解读为c1.operator+(c2);
3.调用c1的+运算符重载函数
方式 2:全局 / 友元函数重载+
适合左操作数不是类对象的场景(如10 + c1):
#include <iostream>
using namespace std;
class Complex {
private:
double real;
double imag;
public:
Complex(double r = 0, double i = 0) : real(r), imag(i) {}
// 声明友元函数(突破私有成员访问限制)
friend Complex operator+(const Complex& c1, const Complex& c2);
void print() const { cout << real << " + " << imag << "i" << endl; }
};
// 全局函数实现+重载:两个操作数都是参数
Complex operator+(const Complex& c1, const Complex& c2) {
return Complex(c1.real + c2.real, c1.imag + c2.imag);
}
int main() {
Complex c1(1, 2), c2(3, 4);
// 调用方式1:运算符形式
Complex c3 = c1 + c2;
c3.print(); // 5 + 7i
// 调用方式2:函数形式
Complex c4 = operator+(c1, c2);
c4.print(); // 5 + 7i
// 支持交换操作数(如果重载了int和Complex的+,还能支持10 + c1)
return 0;
}
2. 输入输出运算符(<</>>)
注意: 类的成员函数会隐含一个
this指针(指向当前类对象),且this会强制绑定到运算符的左操作数;同时,成员函数的参数个数 = 运算符操作数总数 - 1(因为左操作数被this占用)。因此对于左移操作符<<重载: 我们逻辑上是想让类对象向外输出(ostream<<类对象), 但是如果作为成员函数,this指针会强行绑定左操作数(调用时必须:类对象<<ostream), 所以只能写成: 写在类外面, 又由于对象的成员变量大多是私有的, 所以需要写成友元方便访问;
必须用全局 / 友元函数重载(因为左操作数是cout/cin,不是自定义类对象),且返回流对象以支持链式调用。
#include <iostream>
using namespace std;
class Complex {
private:
double real;
double imag;
public:
Complex(double r = 0, double i = 0) : real(r), imag(i) {}
// 声明友元:重载<<(输出)和>>(输入)
friend ostream& operator<<(ostream& os, const Complex& c);
friend istream& operator>>(istream& is, Complex& c);
};
// 重载<<:输出自定义对象
ostream& operator<<(ostream& os, const Complex& c) {
os << c.real << " + " << c.imag << "i";
return os; // 返回os,支持链式输出(cout << c1 << c2)
}
// 重载>>:输入自定义对象
istream& operator>>(istream& is, Complex& c) {
cout << "请输入实部:";
is >> c.real;
cout << "请输入虚部:";
is >> c.imag;
return is; // 返回is,支持链式输入(cin >> c1 >> c2)
}
int main() {
Complex c;
// 1. 调用>>:输入对象
cin >> c; // 等价于 operator>>(cin, c)
// 2. 调用<<:输出对象(链式调用)
cout << "你输入的复数是:" << c << endl; // 等价于 operator<<(cout, c)
return 0;
}
调用示例输入输出:
请输入实部:2
请输入虚部:3
你输入的复数是:2 + 3i
3. 赋值运算符(=)
必须是成员函数,编译器会生成默认版本,但涉及动态内存时需手动实现深拷贝(避免内存泄漏 / 野指针)。
#include <iostream>
#include <cstring>
using namespace std;
class MyString {
private:
char* str; // 动态分配内存
int len;
public:
// 构造函数
MyString(const char* s = "") {
len = strlen(s);
str = new char[len + 1]; // 分配内存(+1存'\0')
strcpy(str, s);
}
// 重载=:深拷贝(必须成员函数)
MyString& operator=(const MyString& other) {
// 1. 防止自赋值(a = a)
if (this == &other) return *this;
// 2. 释放当前对象的内存
delete[] str;
// 3. 深拷贝
len = other.len;
str = new char[len + 1];
strcpy(str, other.str);
// 4. 返回自身,支持链式赋值(a = b = c)
return *this;
}
// 析构函数:释放内存
~MyString() { delete[] str; }
// 辅助打印
void print() const { cout << str << endl; }
};
int main() {
MyString s1("Hello"), s2("World");
// 1. 调用=:运算符形式
s1 = s2; // 等价于 s1.operator=(s2)
s1.print(); // 输出:World
// 2. 链式调用
MyString s3;
s3 = s1 = s2;
s3.print(); // 输出:World
return 0;
}
4. 自增 / 自减运算符(++/--)
区分前置(++a)和后置(a++):
- 前置:无参数,返回引用(支持连续
++); - 后置:加
int形参(仅作标记),返回值(返回自增前的状态)。
#include <iostream>
using namespace std;
class Counter {
private:
int count;
public:
Counter(int c = 0) : count(c) {}
// 1. 前置++:无参数,返回引用
Counter& operator++() {
count++;
return *this; // 返回自身,支持 ++(++a)
}
// 2. 后置++:int是标记(无实际意义),返回值
Counter operator++(int) {
Counter temp = *this; // 保存自增前的状态
count++;
return temp; // 返回旧值
}
int getCount() const { return count; }
};
int main() {
Counter c(5);
// 1. 前置++调用
++c; // 等价于 c.operator++()
cout << "前置++后:" << c.getCount() << endl; // 6
// 2. 后置++调用
Counter c2 = c++; // 等价于 c.operator++(0)
cout << "后置++后c:" << c.getCount() << endl; // 7
cout << "后置++返回值c2:" << c2.getCount() << endl; // 6
return 0;
}
5. 下标运算符([])
必须是成员函数,通常重载const(只读)和非const(可修改)两个版本,支持数组式访问。
#include <iostream>
using namespace std;
class MyArray {
private:
int arr[5] = {10, 20, 30, 40, 50}; // 固定数组
public:
// 1. 非const版:可修改元素
int& operator[](int index) {
// 边界检查(避免越界)
if (index < 0 || index >= 5) {
cerr << "下标越界!" << endl;
exit(1);
}
return arr[index]; // 返回引用,支持修改
}
// 2. const版:只读(const对象调用)
const int& operator[](int index) const {
if (index < 0 || index >= 5) {
cerr << "下标越界!" << endl;
exit(1);
}
return arr[index];
}
};
int main() {
MyArray arr;
// 1. 调用[]修改元素(非const版)
arr[2] = 300; // 等价于 arr.operator[](2) = 300
cout << "修改后arr[2]:" << arr[2] << endl; // 300
// 2. const对象调用const版
const MyArray arr2;
cout << "arr2[0]:" << arr2[0] << endl; // 10(只读,无法修改)
return 0;
}
实战练习:
// User.h
class User {
public:
std::string name = "ZhangSan";
friend std::ostream& operator<<(std::ostream& os, const User& u);
};
// 任何支持标准流的地方都能用!
inline std::ostream& operator<<(std::ostream& os, const User& u) {
os << "User: " << u.name;
return os;
}
//Logger.h
// Logger 里的模板函数
template <typename T>
LogMessage &operator<<(const T &info)
{
std::stringstream ss;
ss << info; // 【关键】:这里调用了 User 定义的标准重载
_loginfo += ss.str();
return *this;
}
完整的调用链条(Step-by-Step)
假设你在 main.cpp 里写了这样一行代码:
User u;
LOG(INFO) << u;
//LOG(INFO)是一个宏替换,执行一个函数返回一个临时的LogMessage类实例
这里有两个 operator<< 在起作用:
外层:LogMessage 里的 operator<<(模板函数)。
内层:User 里的 operator<<(也就是你刚刚贴的这段代码)。
这行代码执行时,发生了以下 4 个步骤的连环调用:
第 1 步:宏展开与对象创建
LOG(INFO) 展开并创建了一个 LogMessage 临时对象。
此时代码变成了:LogMessage_Object << u;第 2 步:调用 LogMessage 的模板函数 (入口)
编译器看到左边是 LogMessage 对象,右边是 User 对象。
它去 Logger.hpp 里找匹配的函数,找到了这个泛型模板:// Logger.hpp 中的 LogMessage 类 template <typename T> LogMessage &operator<<(const T &info) // 此时 T 被推导为 User { std::stringstream ss; // 【关键点!】 // 这里执行了 ss << u; ss << info; _loginfo += ss.str(); return *this; }第 3 步:调用 User 的重载函数 (核心)
现在程序执行到了 ss << info 这一行(即 ss << u)。
左边:ss 是 std::stringstream 类型。
右边:info 是 User 类型。
重点来了: std::stringstream 是 std::ostream 的子类(儿子)。
所以编译器会去寻找一个函数,签名满足 (std::ostream&, const User&)。它正好找到了你在 User.h 里定义的那个函数!
// User.h inline std::ostream& operator<<(std::ostream& os, const User& u) { // 此时 os 就是上面的 ss // 此时 u 就是上面的 info os << "User: " << u.name; // 把字符串写入流中 return os; }注意: 此operator<<函数不是写在User类中,只是在User.h中, 因为类中
this会强制绑定到运算符的左操作数,强制改变参数规则和操作数顺序;第 4 步:数据回流
User 的重载函数把 "User: ZhangSan" 写进了 ss 的缓冲区。
函数返回,回到 LogMessage 的代码中。
LogMessage 调用 ss.str(),把刚刚写入的字符串取出来。
拼接到 _loginfo 后面。
为什么 stringstream 能传给 ostream?
你可能会问:“我在 User.h 里定义的参数是 std::ostream&,但在 Logger 里用的是 std::stringstream,这能行吗?”
完全没问题!这是 C++ 面向对象的多态特性。
继承关系:std::stringstream 继承自 std::iostream,而 std::iostream 继承自 std::ostream。
里氏替换原则:任何需要父类引用(std::ostream&)的地方,都可以传入子类对象(std::stringstream);你可以理解为子类是父类的加强版, 满足一切父类的要求;
图解
调用方: LOG(INFO) << u | v 1. 进入: LogMessage::operator<<(User u) | | 内部创建 std::stringstream ss | 执行 ss << u | v 2. 跳转: User::operator<<(ostream& os, User u) <--- 你的代码在这里被调用 | (此时 os 引用的是 ss) | | 执行 os << "User: " << u.name | (数据写入 ss 的缓冲区) | v 3. 返回: 回到 LogMessage | | _loginfo += ss.str() (从 ss 里取出 "User: ZhangSan") | v 4. 结束: 返回 LogMessage& (以便继续 << 其他东西)
总结
- 调用方式:运算符重载后,既可以用「运算符形式」(如
a+b、cin>>a),也可以用「函数形式」(如a.operator+(b)、operator>>(cin,a)),两者等价。 - 实现规则:
- 双目运算符:成员函数版参数少 1 个(左操作数是
this),全局 / 友元版参数个数等于操作数个数; - 特殊运算符(
=/[]/++/--)必须是成员函数,<</>>必须是全局 / 友元函数; - 前置
++返回引用,后置++带int标记且返回值。
- 双目运算符:成员函数版参数少 1 个(左操作数是
- 核心目的:让自定义类型像内置类型(
int/float)一样使用运算符,提升代码可读性和直观性。

1万+

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



