C++ 运算符重载

📅 2026/8/5 14:52:34
C++ 运算符重载
运算符重载核心知识点讲解1 什么是运算符重载运算符重载是 C 的核心特性本质是函数重载的特殊形式—— 它允许你为自定义类型类 / 结构体重新定义运算符的行为让自定义类型能像int、float等内置类型一样使用、-、、等运算符。举个例子默认情况下MyClass a, b; a b;是非法的编译器不知道如何 “加” 两个自定义对象但通过运算符重载你可以定义a b的逻辑比如把两个对象的成员变量相加。2 运算符重载的核心规则不可违反这是重载的 “红线”违反会导致编译错误或逻辑混乱只能重载已有运算符不能发明新运算符如、#支持重载的运算符包括、-、*、、、[]等。不可改变运算符的基本特性优先级不变如*始终比优先结合性不变如是左结合abc等价于(ab)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.发现c1c2;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 3i3. 赋值运算符必须是成员函数编译器会生成默认版本但涉及动态内存时需手动实现深拷贝避免内存泄漏 / 野指针。#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 (以便继续 其他东西)总结调用方式运算符重载后既可以用「运算符形式」如ab、cina也可以用「函数形式」如a.operator(b)、operator(cin,a)两者等价。实现规则双目运算符成员函数版参数少 1 个左操作数是this全局 / 友元版参数个数等于操作数个数特殊运算符/[]//--必须是成员函数/必须是全局 / 友元函数前置返回引用后置带int标记且返回值。核心目的让自定义类型像内置类型int/float一样使用运算符提升代码可读性和直观性。