1. 引言
运算符重载是 C++ 中一项非常实用的特性,它允许我们为自定义类型重新定义运算符的行为,使对象能够像内置类型一样使用+、-、*、/、<<、>>等运算符。本文将从基础概念出发,系统讲解运算符重载的流程、可重载的函数类型、重载形式(友元函数与成员函数),并重点剖析双目、单目和赋值运算符重载的详细实现与注意事项。
2. 运算符重载
2.1 定义
运算符重载(Operator Overloading)是指对已有的运算符赋予新的含义,使其能够作用于自定义类型的对象。例如,我们可以为复数类重载+运算符,让两个复数对象可以直接相加。
2.2 可重载运算符
const 版本的必要性:当对象被声明为const时,只能调用 const 成员函数。如果没有 const 版本的operator[],const 对象将无法通过下标访问元素。const 版本返回const int&,保证只读访问,防止通过下标修改 const 对象的内容。
C++ 中大部分运算符都可以重载,主要包括以下几类:
- 算术运算符:
+、-、*、/、%、++、-- - 关系运算符:
==、!=、<、>、<=、>= - 逻辑运算符:
&&、||、! - 赋值运算符:
=、+=、-=、*=、/= - 输入输出运算符:
<<、>> - 下标运算符:
[] - 函数调用运算符:
()
下面给出几个常见运算符重载的简单例子,帮助理解重载的基本写法。
#include <iostream> using namespace std; class Point { private: int x, y; public: Point(int a = 0, int b = 0) : x(a), y(b) {} // 重载 + :两个点相加 Point operator+(const Point& p) const { return Point(x + p.x, y + p.y); } // 重载 == :判断两个点是否相等 bool operator==(const Point& p) const { return (x == p.x) && (y == p.y); } // 重载 << :输出点的坐标 friend ostream& operator<<(ostream& os, const Point& p) { os << "(" << p.x << ", " << p.y << ")"; return os; } }; int main() { Point p1(1, 2); Point p2(3, 4); Point p3 = p1 + p2; // 调用重载的 + cout << "p1 + p2 = " << p3 << endl; cout << "p1 == p2: " << (p1 == p2) << endl; // 调用重载的 == return 0; }上面的例子中,+重载为成员函数,==重载为成员函数,<<重载为友元函数,分别对应了算术、关系和输入输出三类常见运算符的典型写法。
下标运算符[]通常用于实现类似数组的随机访问。以动态数组类为例,重载[]可以让对象像内置数组一样通过下标读写元素。
#include <iostream> using namespace std; class DynamicArray { private: int* data; int size; public: DynamicArray(int n) : size(n) { data = new int[size](); } ~DynamicArray() { delete[] data; } // 非 const 版本:可读可写 int& operator[](int index) { return data[index]; } // const 版本:只读访问 const int& operator[](int index) const { return data[index]; } int getSize() const { return size; } }; int main() { DynamicArray arr(5); // 通过重载的 [] 写入元素 for (int i = 0; i < arr.getSize(); ++i) { arr[i] = i * 10; } // 通过重载的 [] 读取元素 for (int i = 0; i < arr.getSize(); ++i) { cout << "arr[" << i << "] = " << arr[i] << endl; } // const 对象只能调用 const 版本 const DynamicArray& carr = arr; cout << "const 访问 arr[2] = " << carr[2] << endl; return 0; }2.3 不可重载运算符
以下运算符不允许重载:
- 成员访问运算符:
. - 成员指针访问运算符:
.* - 作用域解析运算符:
:: - 三目条件运算符:
?: - sizeof 运算符
这些运算符之所以不允许重载,主要基于以下几个原因:
- 保证语言基本语义不被破坏:
.、.*、::等运算符直接关联 C++ 的成员访问、指针访问和作用域解析等核心语法机制。如果允许重载,将改变这些基础操作的语义,导致代码可读性急剧下降,并可能引发难以排查的隐蔽错误。 - 避免歧义与二义性:
?:三目运算符的求值规则(只对选中的分支求值)和sizeof的编译期求值特性,都依赖语言层面的特殊处理。若允许重载,编译器将无法在编译期确定其行为,也无法保证与内置类型保持一致。 - 防止破坏类型安全:
sizeof返回的是类型或对象在内存中占用的字节数,属于编译期常量。若允许重载,将破坏这一编译期特性,使依赖sizeof进行内存分配、数组大小计算等操作的代码失去可靠性。 - 保持运算符优先级与结合性一致:C++ 规定运算符的优先级和结合性由语言本身决定,无法通过重载改变。对于上述运算符,若允许重载,其与其它运算符的组合行为将难以预测,容易产生逻辑混乱。
3. 重载流程
运算符重载本质上是通过定义特殊名称的函数来实现的,其函数名由关键字operator后跟要重载的运算符符号组成。基本流程如下:
- 确定重载形式:选择使用友元函数还是成员函数。
- 定义函数原型:确定参数列表和返回类型。
- 实现函数体:编写运算符的具体逻辑。
- 测试验证:通过实际调用验证重载效果。
4. 运算符与函数关系
运算符重载与普通函数有着密切的联系。从本质上讲,运算符重载就是定义一个函数,只是函数名比较特殊。例如,表达式a + b在编译器看来,等价于调用operator+(a, b)(友元函数形式)或a.operator+(b)(成员函数形式)。
这种关系意味着:
- 运算符重载函数可以像普通函数一样被直接调用。
- 运算符重载函数遵循函数重载的规则,可以针对不同类型定义多个版本。
- 运算符的优先级和结合性由语言本身决定,无法通过重载改变。
5. 重载形式
5.1 成员函数
将运算符重载为类的成员函数时,左操作数必须是当前类的对象,通过this指针隐式访问。其函数原型为:
class Complex { public: Complex operator+(const Complex& other) const; };下面给出一个成员函数形式重载+的简单示例:
#include <iostream> using namespace std; class Complex { private: double real; double imag; public: 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); } void display() const { cout << real << " + " << imag << "i" << endl; } }; int main() { Complex c1(1.5, 2.5); Complex c2(2.0, 3.0); Complex c3 = c1 + c2; // 等价于 c1.operator+(c2) c3.display(); return 0; }5.2 友元函数
将运算符重载为友元函数时,所有操作数都通过参数传递,可以灵活处理左操作数不是当前类对象的情况。其函数原型为:
class Complex { friend Complex operator+(const Complex& a, const Complex& b); };下面给出一个友元函数形式重载+的简单示例:
#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& a, const Complex& b); void display() const { cout << real << " + " << imag << "i" << endl; } }; Complex operator+(const Complex& a, const Complex& b) { return Complex(a.real + b.real, a.imag + b.imag); } int main() { Complex c1(1.5, 2.5); Complex c2(2.0, 3.0); Complex c3 = c1 + c2; // 等价于 operator+(c1, c2) c3.display(); return 0; }5.3 重载方式对比
成员函数和友元函数是运算符重载的两种主要形式,二者在左操作数、参数个数、访问方式和使用场景上存在明显差异。下面通过一个表格进行系统对比:
| 对比项 | 成员函数 | 友元函数 |
|---|---|---|
| 左操作数 | 必须是当前类对象,通过this隐式访问 | 可以是任意类型,所有操作数都通过参数显式传递 |
| 参数个数 | 比运算符操作数少一个(左操作数由this承担) | 与运算符操作数个数相同 |
| 访问权限 | 通过this直接访问当前对象的私有成员 | 通过参数访问对象的私有成员,需在类中声明friend |
| 调用方式 | a + b等价于a.operator+(b) | a + b等价于operator+(a, b) |
| 类型转换 | 左操作数不能发生隐式类型转换 | 左右操作数都可以发生隐式类型转换 |
| 适用场景 | 左操作数为当前类对象,且需要直接访问私有成员 | 左操作数非本类对象,或需要支持类型转换(如double + Complex) |
从上面的对比可以看出,两种形式各有优劣。成员函数写法简洁、封装性好,但要求左操作数必须是当前类对象;友元函数更加灵活,可以处理左操作数不是本类对象的情况,但需要在类中声明friend,一定程度上破坏了封装性。在实际开发中,应根据运算符的语义和类的设计需求灵活选择。
5.4 选择原则
- 双目运算符:
+、-、*、/既可以重载为友元函数,也可以重载为成员函数。 - 输入输出运算符:
<<和>>只能重载为友元函数,因为左操作数是ostream或istream对象,不是当前类对象。 - 赋值运算符:
=只能重载为成员函数。
6. 双目运算符重载
6.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) {} // 成员函数形式重载 + Complex operator+(const Complex& other) const { return Complex(real + other.real, imag + other.imag); } void display() const { cout << real << " + " << imag << "i" << endl; } }; int main() { Complex c1(1.5, 2.5); Complex c2(2.0, 3.0); Complex c3 = c1 + c2; // 等价于 c1.operator+(c2) c3.display(); return 0; }6.2 友元函数
同样的复数加法,使用友元函数重载:
#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& a, const Complex& b); void display() const { cout << real << " + " << imag << "i" << endl; } }; Complex operator+(const Complex& a, const Complex& b) { return Complex(a.real + b.real, a.imag + b.imag); } int main() { Complex c1(1.5, 2.5); Complex c2(2.0, 3.0); Complex c3 = c1 + c2; // 等价于 operator+(c1, c2) c3.display(); return 0; }6.3 关系运算符重载
关系运算符(如==和<)同样可以重载为成员函数或友元函数,返回类型为bool。下面以 Complex 类为例,分别展示两种实现方式。
6.3.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) {} // 成员函数形式重载 == bool operator==(const Complex& other) const { return (real == other.real) && (imag == other.imag); } // 成员函数形式重载 <(按模长比较) bool operator<(const Complex& other) const { return (real * real + imag * imag) < (other.real * other.real + other.imag * other.imag); } void display() const { cout << real << " + " << imag << "i" << endl; } }; int main() { Complex c1(1.0, 2.0); Complex c2(1.0, 2.0); Complex c3(3.0, 4.0); cout << "c1 == c2: " << (c1 == c2) << endl; // 等价于 c1.operator==(c2) cout << "c1 < c3: " << (c1 < c3) << endl; // 等价于 c1.operator<(c3) return 0; }6.3.2 友元函数
使用友元函数重载==和<运算符:
#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 bool operator==(const Complex& a, const Complex& b); // 友元函数形式重载 <(按模长比较) friend bool operator<(const Complex& a, const Complex& b); void display() const { cout << real << " + " << imag << "i" << endl; } }; bool operator==(const Complex& a, const Complex& b) { return (a.real == b.real) && (a.imag == b.imag); } bool operator<(const Complex& a, const Complex& b) { return (a.real * a.real + a.imag * a.imag) < (b.real * b.real + b.imag * b.imag); } int main() { Complex c1(1.0, 2.0); Complex c2(1.0, 2.0); Complex c3(3.0, 4.0); cout << "c1 == c2: " << (c1 == c2) << endl; // 等价于 operator==(c1, c2) cout << "c1 < c3: " << (c1 < c3) << endl; // 等价于 operator<(c1, c3) return 0; }6.3.3 注意事项
- 返回类型固定为 bool:关系运算符的语义是判断真假,必须返回
bool,不能返回其他类型。 - 建议声明为 const:成员函数形式应加上
const限定,保证比较操作不修改对象状态,也便于 const 对象调用。 - 浮点数比较需谨慎:直接使用
==比较浮点数可能因精度问题产生误差,实际项目中可引入误差阈值(如fabs(real - other.real) < 1e-9)进行近似比较。 - 保持语义一致:重载
==时通常也应重载!=,重载<时通常也应重载>、<=、>=,以保持运算符语义的完整性。
7. 单目运算符重载
7.1 自增运算符
单目运算符重载需要区分前置和后置形式。前置++返回引用,后置++通过一个额外的int参数区分,返回旧值。
#include <iostream> using namespace std; class Counter { private: int value; public: Counter(int v = 0) : value(v) {} // 前置 ++(成员函数) Counter& operator++() { ++value; return *this; } // 后置 ++(成员函数,带 int 占位参数) Counter operator++(int) { Counter temp = *this; ++value; return temp; } int getValue() const { return value; } }; int main() { Counter c(5); cout << "初始值: " << c.getValue() << endl; Counter d = c++; // 后置:d 得到旧值 cout << "后置 c++ 后 c: " << c.getValue() << ", d: " << d.getValue() << endl; ++c; // 前置:直接修改 cout << "前置 ++c 后 c: " << c.getValue() << endl; return 0; }7.2 一元负号运算符
一元负号-也可以重载为成员函数或友元函数:
class Complex { private: double real; double imag; public: Complex(double r = 0, double i = 0) : real(r), imag(i) {} // 成员函数形式重载一元负号 Complex operator-() const { return Complex(-real, -imag); } };8. 赋值运算符重载
8.1 自定义赋值运算符
当类中包含指针成员或动态分配的资源时,默认的赋值运算符执行的是浅拷贝,会导致多个对象指向同一块内存,从而引发重复释放、内存泄漏等问题。因此,必须自定义赋值运算符函数,实现深拷贝。
8.2 实现
赋值运算符只能重载为成员函数,其标准写法如下:
#include <iostream> #include <cstring> using namespace std; class MyString { private: char* data; int length; public: // 构造函数 MyString(const char* str = "") { length = strlen(str); data = new char[length + 1]; strcpy(data, str); } // 拷贝构造函数(深拷贝) MyString(const MyString& other) { length = other.length; data = new char[length + 1]; strcpy(data, other.data); } // 析构函数 ~MyString() { delete[] data; } // 赋值运算符重载(成员函数) MyString& operator=(const MyString& other) { if (this == &other) { return *this; // 处理自赋值 } delete[] data; // 释放原有资源 length = other.length; data = new char[length + 1]; strcpy(data, other.data); return *this; // 支持链式赋值 } void display() const { cout << data << endl; } }; int main() { MyString s1("Hello"); MyString s2("World"); MyString s3; s3 = s1; // 调用自定义赋值运算符 s3.display(); s2 = s2; // 自赋值测试 s2.display(); return 0; }8.3 特点
- 返回引用:返回
MyString&以支持链式赋值,如a = b = c。 - 自赋值检查:通过
if (this == &other)避免自赋值导致的内存错误。 - 释放旧资源:先释放原有内存,再分配新内存,防止内存泄漏。
- 深拷贝:为新对象分配独立内存并复制内容,避免浅拷贝带来的共享问题。
9. 输入输出运算符重载
输入输出运算符<<和>>只能重载为友元函数,因为左操作数是ostream或istream对象,无法通过成员函数修改标准库类。
#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) { os << c.real << " + " << c.imag << "i"; return os; } // 输入运算符重载(友元函数) friend istream& operator>>(istream& is, Complex& c) { cout << "请输入实部和虚部: "; is >> c.real >> c.imag; return is; } }; int main() { Complex c1(3.0, 4.0); cout << "c1 = " << c1 << endl; Complex c2; cin >> c2; cout << "c2 = " << c2 << endl; return 0; }10. 总结
运算符重载是 C++ 面向对象编程中极具表现力的特性,它让自定义类型的使用体验更接近内置类型。掌握运算符重载的关键在于理解:
- 运算符重载本质上是定义特殊命名的函数。
- 双目运算符
+、-、*、/既可以用友元函数也可以用成员函数重载。 - 输入输出运算符只能重载为友元函数。
- 赋值运算符只能重载为成员函数,且当类包含动态资源时必须自定义以实现深拷贝。
- 单目运算符需要区分前置和后置形式。
在实际开发中,应根据类的设计需求合理选择重载形式,并始终关注资源管理和自赋值等边界情况,才能写出健壮、易用的代码。