首页 / 资讯中心 / 文章详情

C++ 运算符重载完全指南:从基础概念到双目、单目与赋值运算符实战

C++ 运算符重载完全指南:从基础概念到双目、单目与赋值运算符实战 ★ FEATURED ARTICLE
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 面向对象编程中极具表现力的特性它让自定义类型的使用体验更接近内置类型。掌握运算符重载的关键在于理解运算符重载本质上是定义特殊命名的函数。双目运算符、-、*、/既可以用友元函数也可以用成员函数重载。输入输出运算符只能重载为友元函数。赋值运算符只能重载为成员函数且当类包含动态资源时必须自定义以实现深拷贝。单目运算符需要区分前置和后置形式。在实际开发中应根据类的设计需求合理选择重载形式并始终关注资源管理和自赋值等边界情况才能写出健壮、易用的代码。
阅读完成 · 觉得有帮助?
咨询建站