C++运算符函数
一、运算符函数
在C++中会把运算符当做函数处理,一个表达式,其实可能调用了很多运算符函数来完成计算,这种特性对内建类型没有用,但是对于自建类型而言,通过设计运算符函数能够进行个性化运算,以此提高代码的可读性、易用性
例如string类:
string str,str1;
str += "hehe"; strcat(str,"hehe");
str == str1; strcmp(str,str1);
运算符函数的格式:
表示运算符 O表示运算符对象
单目运算符: #O O#
- 成员函数:
[返回值类型] O::operator#(void)
{
}
返回值不确定,唯一的参数就是调用者本身
- 全局函数:
[] operator#(O& o)
{
}
⭐ :某种运算符成员函数、全局函数只能同时实现一个,不能一起实现
实例:
#include <iostream>
using namespace std;
class Test
{
public:
/*
int operator!(void)
{
cout << "我是!运算符成员函数" << endl;
return 0;
}
int operator+(int num)
{
cout << "我是+运算符成员函数" << endl;
return 88;
}
*/
};
int operator!(Test& t)
{
cout << "我是!运算符全局函数" << endl;
}
int operator+(Test& t,int num)
{
cout << "我是+运算符全局函数" << endl;
return 188;
}
int main(int argc,const char* argv[])
{
Test t;
!t;
cout << t + 10 << endl;
}
双目运算符: a # b
⭐ :左操作数是运算符函数的发起者
- 成员函数:
[] A::operator#(B& b)
{
}
- 全局函数:
[] operator#(A& a,B& b)
{
}
二、友元、哑元
友元
在实现类的全局运算符函数时,可能会使用到类内的私有成员,此时全局函数是没有访问权限,如果改变私有为公开会破坏类的封装性,如果提供公开的访问函数又非常麻烦,最好的方式是给该全局函数给与独家授权让其能够访问类内私有成员,这种行为称把该全局函数设置为友元函数
方式:在类内对全局函数声明,并在声明前加 friend
friend const O operator+(const O& a,const O& b);
哑元
在参数列表末尾增加一个不使用且无形参名的int哑元类型,唯一目的就是用于区分是前自变还是后自变
三、运算类的双目运算符 +
Point
{
int x;
int y;
}
Point p1(1,1),p2(2,2);
p1 + p2;
p1 - p2;
O是类名
- 成员函数:a + b
const O O::operator+(const O& b)const
{
return O(x+b.x,y+b.y);
}
- 全局函数: a + b
const O operator+(const O& a,const O& b)
{
}
四、运算类的单目运算符 ~
单目运算符有:++/-- ! ~(按位取反) -(取负值) *(指针) &(引用) sizeof
- 成员函数:
O O::operator~(void)const
{
return O(~x,~y);
}
- 全局函数:
O operator~(const O& a)
{
return O(~a.x,~a.y);
}
五、输入输出运算符>> <<
在C++中 << 、>> 运算符不光是按位左移、按位右移,同时还是cout该类的输出运算符 cin该类的输入运算符
由于 << 、>> 运算符的调用者是cout对象,我们是无法在该对象的类中去设计一个输出运算符的成员函数,所以只能实现 << 运算的全局函数
- 输出运算符<<
ostream& operator<<(ostream& os,const O& t)
{
return os << t.x << t.y;
}
- 输入运算符>>
istream& operator>>(istream& is,O& t)
{
return is >> t.x >> t.y;
}
⭐ 由于输出、输入是可以连续进行的,所以返回值还应该是ostream、istream引用
⭐ 因为无法在ostream、istream中重载运算符成员函数,所以<< >>只能重载成全局函数
⭐ 如果在重载全局函数中使用到自己类中私有的成员变量,需要声明为友元函数
⭐ 输出运算符函数中,第二个参数一定要加const,而输入运算符函数中不能加
六、自变运算符 ++
左值与右值:
能位于赋值运算符= 左边的就是左值,反之为右值
有名称、可以获取到存储地址的称为左值,反之为右值
- C++前自变:
++num = 10; // 成功 num=10
直接修改原对象,在原对象基础上实现自变,然后将原对象的引用返回,所以操作和返回的一直是原对象,是左值
- C++后自变:
num++ = 10; // 报错
先将原对象的数据存储到临时变量中,接着在原对象基础上自变,然后把临时变量以只读方式返回,并且该临时变量执行语句结束后立即销毁了,无法访问,因此结果是右值
++num++ // 后自变优先级更高,报错
(++num)++ // 先前自变为左值,成功
注意:C语言中,无论前后自变,结果都是右值
前自变运算符: ++a/--a
- 成员函数:
O& O::operator++(void)
{
x++,y++;
return *this;
}
- 全局函数:
O& operator++(O& a)
{
a.x++,a.y++;
return a;
}
后自变运算符:a++/a--
哑元:在参数列表末尾增加一个不使用且无形参名的int哑元类型,唯一目的就是用于区分是前自变还是后自变
- 成员函数:
O O::operator++(int)
{
return O(x++,y++);
}
- 全局函数:
O operator++(O& a,int)
{
return O(a.x++,a.y++);
}
七、特殊的运算符重载函数
-> () [] new delete *
1、[]下标运算符
想让一个类对象当成数组一样使用,可以考虑重载下标运算符,例如:vector等
可以考虑在下标重载函数中做非法下标的判断,让下标的使用更安全
实例:
#include <iostream>
using namespace std;
#define TYPE int
class Array
{
TYPE* ptr;
int cal;
public:
Array(int cal):cal(cal)
{
ptr = new TYPE[cal];
}
TYPE& operator[](int i)
{
if(i >= cal || 0 > i)
throw("Array Over Cal"); // 调用[]时抛
return ptr[i];
}
};
int main(int argc,const char* argv[])
{
Array arr(10);
for(int i=0; i<11; i++)
{
//对非法坐标进行判断,进行抛异常,catch接throw的内容
try{
arr[i] = i;
}catch(const char* msg){
cout << msg << endl;
}
cout << arr[i] << endl;
}
}
2、()函数运算符
重载此运算符可以让一个类对象当做函数一样使用
实例:
#include <iostream>
using namespace std;
class Func
{
int num;
public:
Func(int num=0):num(num){} //有参构造
int operator()(int num)const
{
cout << "()" << endl;
}
};
/* 不能定全局
int operator()(const Func& f,int num)
{
cout << "quanju ()" << endl;
}
*/
int main(int argc,const char* argv[])
{
const Func func(10);
//Func func(10);
func(100);
}
注意:()\[] 均不能实现为全局运算符函数,只能实现成员函数(C++全局中已经有类似的函数实现,所以不让实现)
= 赋值运算符函数也不能实现为全局函数,因为类内本身一定有一个=赋值运算符成员函数
3、解引用* 和访问成员运算符 - >
重载这两个运算符可以让类对象像指针一样使用,智能指针就是通过重载这俩运算符从而像使用指针一样的类
4、new/delete运算符重载
- new
void* operator new(size_t size)
C++语法要求重载new运算符的参数必须为size_t,编译器会帮助计算出要申请的字节数并传递,返回值必须为void*,编译器会帮助转换成对应的类型指针返回
- delete
void operator delete(void* ptr)
C++语法要求重载delete的参数必须为void,编译器帮助转换成void传递
实例:
#include <iostream>
using namespace std;
class Test
{
int num;
public:
Test(int num=0):num(num){}
// 如果显式地实现析构函数,则会多申请4字节存储要析构的次数
// 如果不显式实现析构,则不会多申请
~Test(void){}
void* operator new[](size_t size)
{
cout << size << endl;
cout << "new []" << endl;
return malloc(size);
}
void* operator new(size_t size)
{
cout << "new" << endl;
return malloc(size);
}
void operator delete(void* ptr)
{
cout << "delete" << endl;
free(ptr);
}
void operator delete[](void* ptr)
{
cout << "delete []" << endl;
}
};
int main(int argc,const char* argv[])
{
Test* t = new Test[10];
delete[] t;
int* p = (int*)t;
cout << p[-1] << endl;
int* num = new int;
delete num;
}
注意: new、delete的成员函数、全局函数格式一样
如果只是针对某个类想要重载它的new\delete时,则写为成员函数
如果想要所有类型都执行重载版本,则实现为全局函数
为什么要重载new\delete?
1、可以在重载函数中记录每次分配、释放内存的地址、代码情况、次数情况等到日志中,从而方便检查是否出现内存泄漏,以及泄漏位置
2、对于字节少、且频繁申请、释放的对象,可以在重载函数中给他多分配点内存从而减少产生碎片的可能
八、重载运算符的限制
1、不能重载的运算符
:: 域限定符
. 直接访问成员的运算符
?: 三目运算符
sizeof 计算字节数
typeid 获取类型信息的运算符
2、只能重载为全局函数的运算符
<< 输出运算符
>> 输入运算符
3、只能重载为成员函数的运算符
[]
()
=
->
4、运算符重载可以自定义运算符执行过程,但是无法改变运算符的优先级
5、运算符的操作数量也不能改变
6、不能发明新的运算符
建议:
1、重载运算符要遵循一致性原则,不要随意改变运算符本身的含义
2、不要忘记实现运算符重载函数的初衷,为了提高可读性,不要随意炫技
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!