C++运算符重载详解:让自定义类型拥有原生运算符的能力
1. 运算符重载的基本概念
运算符重载是C++一项强大的特性,它允许我们为自定义类型(类或结构体)重新定义运算符的行为。通过运算符重载,我们可以让自定义类型像内置类型一样使用标准的运算符语法,使代码更加直观和自然
从本质上讲,运算符重载是函数重载的一种特殊形式。当我们重载一个运算符时,实际上是在定义一个特殊的成员函数或全局函数,函数名由operator关键字后接要重载的运算符组成
cpp
a + b; // 等价于 a.operator+(b) 或 operator+(a, b)
a == b; // 等价于 a.operator==(b) 或 operator==(a, b)
运算符重载提供了语法糖,让代码更加直观和易读。比较以下两种写法:
cpp
// 使用运算符重载
list1 + list2;
// 不使用运算符重载
list1.concat(list2);
第二种写法显然更加符合直觉,让自定义类型与内置类型有一致的操作方式
。
2. 算术运算符重载
2.1 加法运算符重载
在代码中,复数类的加法运算符重载展示了运算符重载的基本用法:
cpp
Complex operator+(Complex& a, Complex& b)
{
Complex ret;
ret.real = a.real + b.real;
ret.image = a.image + b.image;
return ret;
}
运算符重载有两种实现方式:成员函数形式 和全局函数形式
。
成员函数形式 隐含一个this指针参数,只需要一个显式参数:
cpp
class Complex {
public:
Complex operator+(const Complex& other) {
Complex ret;
ret.real = this->real + other.real;
ret.image = this->image + other.image;
return ret;
}
};
全局函数形式需要两个显式参数,通常需要声明为类的友元函数以访问私有成员
cpp
class Complex {
friend Complex operator+(const Complex& a, const Complex& b);
// ...
};
代码中使用了全局函数形式,并将它们声明为友元函数,这样可以访问Complex类的私有成员real和image。
2.2 减法运算符重载
减法运算符的重载与加法类似,只需改变运算逻辑:
cpp
Complex operator-(Complex& a, Complex& b)
{
Complex ret;
ret.real = a.real - b.real;
ret.image = a.image - b.image;
return ret;
}
3. 流运算符重载
3.1 输出运算符<<重载
输出运算符<<的重载需要特别注意返回类型和参数类型。代码中实现了这一功能:
cpp
ostream& operator<<(ostream& cout, Complex& other)
{
cout << other.real << "+" << other.image << "i";
return cout;
}
这里有几个关键点:
- 返回类型必须是
ostream&(引用),这样才能支持链式输出如cout << a << b << c; - 第一个参数是
ostream&类型,通常是cout或其变体 - 第二个参数是要输出的对象
- 函数返回输出流对象本身,使链式操作成为可能
如果返回void类型,将无法实现链式输出。这是因为cout << c << endl会被解析为(cout << c) << endl,如果cout << c返回void,那么void << endl就是非法的。
3.2 避免不必要的拷贝
使用const Complex& other而不是Complex other的好处:避免拷贝构造。当对象较大时,传引用可以显著提高性能。正确的声明应该是:
cpp
ostream& operator<<(ostream& cout, const Complex& other);
这里的const确保不会意外修改对象状态。
4. 自增运算符重载
自增运算符有前置和后置两种形式,需要分别处理。
4.1 前置自增运算符
cpp
Complex& operator++()//前置++
{
this->real += 1;
return *this;
}
前置++返回引用 ,这是为了保持与内置类型一致的行为。这样++a本身可以作为左值使用
。
4.2 后置自增运算符
cpp
Complex operator++(int)//后置++
{
Complex a = *this;//保存原始值
this->real += 1;
return a;//返回原始值
}
后置++通过int参数与前置版本区分,这个参数仅用于区分,并不实际使用
。它返回的是值而不是引用,因为返回的是局部对象,不能返回引用。
注释中提到的,后置++返回临时对象,这个对象在函数结束后会被销毁,所以不能返回引用。
5. 赋值运算符重载
赋值运算符重载需要特别注意深拷贝 和自赋值问题。
cpp
Jeff& operator=(Jeff &a)
{
if (age)
{
delete age;
age = NULL;
}
age = new int;//分配新内存
*age = *a.age;
return *this;
}
这里有几个重要考虑:
- 检查自赋值 :虽然代码中没有显式检查,但
a = a这样的自赋值应该安全处理 - 释放旧资源:在分配新资源前释放已有资源,防止内存泄漏
- 深拷贝:创建新内存并复制内容,而不是简单复制指针
- 返回引用 :支持链式赋值
a = b = c
改进版本应该包含自赋值检查:
cpp
Jeff& operator=(const Jeff &a)
{
if (this != &a) { // 自赋值检查
if (age) {
delete age;
}
age = new int(*a.age);
}
return *this;
}
6. 关系运算符重载
关系运算符重载通常返回bool值,用于比较对象。
cpp
bool operator==(const Point& a)const
{
return this->x == a.x && this->y == a.y;
}
bool operator<(const Point& a)const
{
int c = x * x + y * y;
int d = a.x * a.x + a.y * a.y;
return c < d;
}
代码中通过比较点到原点的距离来定义<运算符,这是一种常见的做法。注意这些函数被声明为const,因为它们不应该修改对象状态。
7. 函数调用运算符重载
函数调用运算符()的重载创建了所谓的仿函数(functor)。
cpp
int operator()(int a, int b)
{
data++;
return a + b + data;
}
仿函数比普通函数更灵活,因为它们可以保持状态。如你的示例所示,每次调用都会增加data的值,这是普通函数无法做到的。
仿函数可以记录调用过程中的状态,比普通函数更加灵活,常用于STL算法中的定制行为
。
8. 运算符重载的规则与最佳实践
8.1 可重载的运算符
C++允许重载大部分运算符,包括:
- 算术运算符:
+,-,*,/,% - 关系运算符:
==,!=,<,>,<=,>= - 逻辑运算符:
&&,||,! - 赋值运算符:
=,+=,-=,*=,/= - 下标运算符:
[] - 函数调用运算符:
() - 流运算符:
<<,>> - 自增自减:
++,--
8.2 不可重载的运算符
有些运算符不能重载,包括:
- 成员访问运算符:
. - 成员指针访问运算符:
.* - 作用域解析运算符:
:: - 条件运算符:
?:(三目运算符) sizeof运算符typeid运算符
8.3 最佳实践
- 保持语义一致性:重载的运算符应该保持与内置类型相似的语义
- 考虑返回值类型 :
- 算术运算符通常返回新对象
- 复合赋值运算符通常返回引用
- 关系运算符返回bool
- 正确处理常量性 :不修改对象的函数应声明为
const - 遵循三/五法则:如果定义了拷贝构造函数、拷贝赋值运算符、析构函数中的一个,通常需要定义其他相关函数
9. 总结
运算符重载是C++面向对象编程的重要特性,它让自定义类型能够以更自然的方式集成到语言中。通过合理使用运算符重载,我们可以编写出更加直观、易维护的代码。
关键要点:
- 运算符重载的本质是函数重载,遵循函数重载的规则
- 选择成员函数还是全局函数形式取决于具体需求
- 流运算符
<<和>>通常重载为全局友元函数 - 赋值运算符需要处理自我赋值和深拷贝问题
- 前置和后置自增/自减运算符通过参数区分
- 函数调用运算符重载创建仿函数,可以保持状态
合理使用运算符重载可以极大提高代码的可读性和易用性,但也要避免滥用,保持运算符的直观语义。当你面对自定义类型需要类似内置类型的操作时,运算符重载是一个强大的工具。
完整代码(杂糅版选取复制):
cpp
#include<iostream>
using namespace std;
#include<string>
int main()
{
//1.加法运算符
int a = 520;
int b = 1314;
cout << a + b<<endl;
//2.字符串拼接
string c = "520";
string d = "1314";
cout << c + d << endl;
}
复数类:
class Complex
{
friend ostream& operator<<(ostream& cout, Complex& other);
friend Complex operator+(Complex& a, Complex& b);
friend Complex operator-(Complex& a, Complex& b);
public:
Complex() :real(0), image(0)
{
}
Complex (int real, int image)
{
this->real = real;
this->image = image;
}
//Complex operator+(Complex& other)//加了operator后可以进行a+b操作,这个是成员函数形式
//{
// Complex ret;
// ret.real = this->real + other.real;//this是指针用->表示,other是引用对象用.表示
// ret.image = this->image + other.image;
// return ret;
//}
void print()
{
cout << real << "+" << image<<"i" << endl;//cout传进来了
}
private:
int real;
int image;
};
Complex operator+(Complex& a,Complex&b)//加了operator后可以进行a+b操作,全员函数重载+改-就变-了
{
Complex ret;
ret.real = a.real+b.real;//this是指针用->表示,other是引用对象用.表示
ret.image = a.image+b.image;
return ret;
}
Complex operator-(Complex& a, Complex& b)//加了operator后可以进行a+b操作,全员函数重载+改-就变-了
{
Complex ret;
ret.real = a.real - b.real;//this是指针用->表示,other是引用对象用.表示
ret.image = a.image - b.image;
return ret;
}
//void operator<<(ostream& cout, Complex& other)//void 不行
//{
// cout << other.real << "+" << other.image<<"i" << endl;
//}
ostream& operator<<(ostream& cout, Complex& other)//我左移后需要不断的输出所以要返回ostream可以继续加,实现链式输出!!!
{
cout << other.real << "+" << other.image << "i";
return cout;
}//补充知识点为什么ostream要加&,因为cout是ostream类型的函数,全局只有一个并且不希望外部调用也不希望内部调用,调用的话会自动删除!!!可以用f12看拷贝那一块的源码
//去掉&后会自动调用拷贝函数,然后拷贝函数就会被删除就无法调用了!!!
int main()
{
Complex a(10,20);
Complex b(5,8);
Complex c = a + b; //a.add(b);
Complex d = a - b;
//c.print();
//d.print();
////左移函数重载
cout << c << endl;//因为左移函数没有返回值void类型,我们要返回ostream这个类型才可以继续进行返回操作
return 0;
}
函数的递增
class Complex
{
friend ostream& operator<<(ostream& cout, const Complex &other);//Complex other每次都会走拷贝构造,已知这个不能修改,我们加一个&就可以避免拷贝构造了
friend Complex operator+(Complex& a, Complex& b);
friend Complex operator-(Complex& a, Complex& b);
private:
int real;
int image;
public:
Complex() :real(0), image(0)
{
}
Complex(int real, int image)
{
this->real = real;
this->image = image;
}
Complex& operator++()//前置++,不加引用就会生成新的对象,加了之后就是指向自己的
{
this->real += 1;
return *this ;
}
Complex operator++(int)//后置++,加引用就会返回临时的变量但是我,不想要这个临时的我要本身的,所以不加&让编译器自己生成一个拷贝函数
{
Complex a = *this;//等于对象本身,临时的变量会被析构掉所以会返回随机值
this->real += 1;
return a;//返回原来的对象
}
};
ostream& operator<<(ostream& c,const Complex& a)
{
cout << a.real << "+" << a.image << "i";
return c;
}
int main()
{
int x = 1;
cout << ++x<<endl;
cout << ++x<<endl;
Complex a(10, 20);
/*++a;*/
//cout << ++a << endl;
//cout << ++(++a) << endl;
//cout << a << endl;//第一++成功了,再调用一次就会生成新的对象与原来的a没有关系了所以要加引用确定原本对象;
cout << a++ << endl;
cout << (a++)++ << endl;
cout << a << endl;//加了&后这边就会报错为什么因为a的返回值是一个可修改的左值,所以我们可以用const去修饰这个返回值,这样就可以避免这样的事情发生,这样也是规范的写法
}
赋值重载运算
class Jeff
{
public:
Jeff():age(NULL)
{
};
Jeff(int age)
{
this->age = new int;
*(this->age) = age;
}
int *age;
~Jeff()
{
if (age != NULL)
{
delete age;
age = NULL;
}
}
Jeff& operator=(Jeff &a)
{
if (age)
{
delete age;
age = NULL;
}
age = new int;//新的内存,析构不会析构到这一快
*age = *a.age;
return *this;
}
};
int main()
{
Jeff a(1);
Jeff b(2);
cout << *(a.age) << endl;//内存泄漏
cout << *(b.age) << endl;
a = b;//a b两个对象本来有那个内存的,第一次a调用时析构函数把a的对象销毁了,第二次b调用时由于两个都指向同一块地址,b调用完成后又一次析构,这时就造成二次析构了
cout << *(a.age) << endl;
cout << *(b.age) << endl;
Jeff c(3);
a = b = c;
return 0;
}
关系运算符重载
class Point
{
int x, y;
public:
Point():x(0), y(0)
{
};
Point(int x, int y)
{
this->x = x;
this->y = y;
}
bool operator==(const Point& a)const
{
return this->x == a.x && this->y == a.y;
}
bool operator<(const Point& a)const
{
int c = x * x + y * y;
int d = a.x * a.x + a.y * a.y;
return c < d;
}
bool operator>(const Point& a)const
{
if (*this==a)
{
return false;
}
else if (*this < a)
{
return false;
}
else {
return true;
}
}
};
int main()
{
Point a(2,6);
Point b(1,8);
if (a > b)
{
cout << "a到原点的距离大于b" << endl;
}
else if (a == b)
{
cout << "a到原点的距离等于b" << endl;
}
else {
cout << "a到原点的距离小于b" << endl;
}
return 0;
}
函数调用运算符重载
class HJM
{
private:
int data;
public:
HJM() :data(0)
{
};
int operator()(int a, int b)
{
data++;
return a + b + data;
};//仿函数调用可以记录当前的状态
};
int Add(int a, int b)
{
return a + b;
}
int main()
{
HJM add;
cout << add(10, 5) << endl;
cout << add(10, 5) << endl;
cout << add(10, 5) << endl;
cout << add(10, 5) << endl;
cout << add(10, 5) << endl;
cout << add(10, 5) << endl;
cout << add(10, 5) << endl;
cout << Add(10, 5)<< endl;
cout << Add(10, 5) << endl;
cout << Add(10, 5) << endl;
cout << Add(10, 5) << endl;
cout << Add(10, 5) << endl;
return 0;
}

