在C++中会把运算符当做函数处理,一个表达式,其实是调用了对应运算符函数来完成运算,这种特性对于内建类型是没有多大意义的,但是对于自建类型的数据,可以通过该特性进行一些特性化设计,可以极大地提高代码的可读性、易用性。例如:stirng类
假设 # 是运算符 O 运算的类对象
[] O::operator#(void)
{
// 返回值不确定、唯一的参数就是调用者对象自己
}
[] operator#(O& o)
{
// 全局函数不属于任何类,因此需要把调用者类型作为参数传递
}
注意:运算符成员函数、全局函数只能实现一个,不能同时存在,否则产生歧义编译报错
[] A::operator#(B& b){}
[] operator#(A& a,B& b){}
注意:双目运算符的运算符函数的调用者一定是左操作数,因此如果实现成员函数的话,需要实现在左操作数类中,如果实现为全局函数的话,第一个参数为调用者,第二个参数为右操作数
假设 O是类名
const O operator+(const O& that)const
{
return O(x+that.x,y+that.y);
}
const O operator+(const O& o1,const O& o2)
{
return O(o1.x+o2.x,o1.y+o2.y);
}
在实现全局的运算符函数时,可能会使用类内的私有成员,此时全局函数没有访问权限
如果把私有成员改为公开的,会破坏类的封装性
如果给每个私有成员实现公开的访问接口函数,会很麻烦
最好的方式是在类内给这个全局函数进行独家授权,这个就是把全局函数声明为友元函数
方式: 在类中声明该全局函数,并在声明前加 friend 关键字
在C++中 << >> 运算符不光是按位左移、右移,同时它们还是cout、cin类对象的输出、输入运算符
cout的类名:ostream
cin的类名:istream
类内:
friend ostream& operator<<(ostream& os,const Test& t);
类外:
ostream& operator<<(ostream& os,const Test& t)
{
return os << t.x << t.y;
}
类内:
friend istream& operator>>(istream& is,Test& t);
类外:
istream& operator>>(istream& is,Test& t)
{
return is >> t.x >> t.y;
}
注意:
① 由于 << >>运算符的调用者是cout\cin类对象的类,所以我们无法在该类中去实现一个新的<< >>运算符的重载函数,因此只能实现 << >>运算符的全局函数
② 该全局函数大概率会访问要输出的类的私有成员,所以需要在类中声明为友元函数
③ 返回值为ostream\istream的引用,因为cout可以连续输出,cin可以连续输入
④ 参数 ostream& 、istream& 都不能加const
单目:++/-- ! ~ - * & sizeof
运算: ! ~ -
const Test operator~(void)const
{
Test t(~x,~y);
return t;
return Test t(~x,~y);
}
注意:运算对象可以带常属性,因此该类运算符成员函数需要是常函数,并且运算过程中不改变运算对象的值,而是产生一个临时的计算结果,并且结果是右值,所以返回的不能是引用,只能是带const的对象值
const Test operator~(const Test& t)
{
return Test(~t.x,~t.y);// 需要声明友元函数
}
直接修改原对象的内存,然后将原对象以引用的方式返回,所以操作和返回一直都是原内存,为左值
先将原对象的数据存储到临时变量中,接着修改原对象的内存,然后把临时变量的值以只读方式返回,并且结束前把临时变量销毁,因此无法访问,为右值
int num = 10;
++num++; // 后自变优先 所以报错
(++num)++; // num 12
cout << num << endl;
注意:在C语言中自变运算符的结果都是右值!
成员函数:
Test& operator++(void) //不能是常函数,返回值必须是引用
{
x++,y++;
return *this; //必须返回调用对象的引用
}
全局函数:
Test& operator++(Test& t)
{
t.x++,t.y++;
return t;
}
哑元:在参数列表中增加一个不使用的哑元类型(int),唯一目的就是为了区分前后自变运算符函数
成员函数:
const Test operator++(int)
{
return Test(x++,y++); //返回临时对象的值,不能返回引用,而且调用者的值必须后自变
}
全局函数:
const Test operator++(Test& t,int)
{
return Test(t.x++,t.y++);
}
当想让一个类对象当做数组一样使用时,可以重载[]运算符函数,可以让类对象像数组一样使用,例如:vector容器
重载小括号运算符可以让一个类对象当做函数一样使用
重载了这两个运算符可以让类对象像指针一样使用,C++智能指针就是通过重载这两个运算符来实现的
① 可以在该运算符重载中记录每次分配、释放的地址、次数等信息到日志中,以此检查是否出现了内存泄漏、以及哪里泄漏
② 对于申请字节较小内存的对象,可以在重载运算符中多分配一些内存,从而减少内存碎片的产生
void* operator new(size_t size)//size是要申请的字节数,编译器会自动计算new要申请的字节数并传递过来
void* operator new(size_t size)//size是要申请的字节数,编译器会自动计算new要申请的字节数并传递过来
{
void* ptr = malloc(size);
// 程序员自己设计
return ptr;//编译器也会把void*强制转换成对应的类型
}
void operator delete(void* ptr)//ptr会由编译器传递要释放内存的首地址
{
free(ptr);
// 程序员自己设计
}
注意:
① 如果只是针对某个类重载了它的new、delete,那么只有该类对象使用new、delete时才会调用重载函数
② 如果想要所有类型(自建、内建)都执行new、delete的重载版本,则实现成全局运算符函数
new[] delete[] 成员函数、全局函数重载格式一致:
void* operator new[](size_t size){}
void operator delete[](void* ptr){}
注意:
① new、delete可以同时存在全局、成员重载版本,编译器会让类对象优先调用类内成员函数版本
② 如果类中显式地实现了析构函数,那么new[]会在原大小基础上多申请前4个字节,用于存储要析构的次数,否则大小是多少就申请多少
① 有哪些运算符不能重载
:: 域限定符
. 直接访问成员的运算符
? : 三目运算符
sizeof 计算字节数的运算符
typeid 获取类型信息的运算符
② 只能重载成全局函数的运算符
<< 输出运算符
>> 输入运算符
③ 只能重载成成员函数的运算符
() 小括号运算符 Test t(10)
[] 中括号运算符 Test t[10]
= 赋值运算符
-> 间接访问成员的运算符 Test* t = new Test;t->
④ 运算符重载可以自定义运算过程,但是无法改变运算符本身的优先级
⑤ 运算符的操作数不能擅自改变
⑥ 不能自己发明新的运算
建议:
① 重载运算符函数的原理要与运算符本身自恰,不要改变运算符本身的规律、方向
② 不要忘记重载运算符的初衷:为了提高代码可读性,不要盲目炫技