运算符重载
友元
使用形式
-
友元的形式可以分为友元函数和友元类
class 类名1{ friend 函数名; friend class 类名2; };
-
函数大致可以分为两种:成员函数和非成员函数(全局函数/自由函数/普通函数)
代码示例
- 在math.h头文件中有个hypot(x,y)函数,得到√(x²+y²)
class Point;//类的前向声明
class Line(){
//不能在这里写函数实现,因为程序执行顺序_ix_iy尚未定义
float distance(const Point &lhs, const Point &rhs);
};
class Point(){
//第2种,友元之成员函数
friend float Line::distance(const Point &lhs,const Point &rhs);
friend void Line::setPoint(Point &lhs,int _ix,int _iy);
//第3种,类,两种效果一样,因为上面已经定义
friend class Line;
friend Line;
public:
Point(int ix=0,int iy=0)
:_ix(ix),_iy(iy){
cout<<"构造"<<endl;
}
~Point(){
cout<<"析构"<<endl;
}
private:
int _ix;
int _iy;
};
//第1种,普通函数
float Line::distance(const Point &lhs,const Point &rhs){
return hypot(lhs._ix-rhs._ix, lhs._iy-rhs._iy);
}
-
构造函数加点也可以调用成员函数
(构造函数调用的时候会创建一个临时对象,只存在于某一行,之后立即析构声明周期结束)
友元性质
-
友元不受public、private、protected访问权限的控制
-
友元函数可以重载,但其中一个设置为另外一个类的友元时,其他重载形式并不是另外一个类的友元
-
友元关系是单方向的,不可传递,不可被继承
运算符重载
- C++ 预定义中的运算符的操作对象只局限于基本的内置数据类型,对于我们自定义的类型没有办法操作。但是大多时候我们需要对我们定义的类型进行类似的运算,这个时候就需要我们对这么运算符进行重新定义,赋予其新的功能,以满足自身的需求。
- 实质是函数重载
使用形式
返回类型 operator运算符(const 类型 &rhs){
//重载形式,加引用提高效率,返回类型后面是否加引用可以先尝试,注意非const左值引用不能绑定到右值
return 表达式;
//表达式在成员函数里可以*this
}
-
普通函数形式重载
(操作类的私有成员的时候,需要在类里额外写get成员函数获取)
-
成员函数形式重载
(用成员函数重载双目运算符时,左操作数无须用参数输入,而是通过隐含的this指针传入,否则违背重载规则)
-
友元函数形式重载
(在类内使用友元,可以直接操作私有成员了,形式保持与预定义的运算符一致)
-
原则:尽量使重载的运算符与其内置的、广为接受的语义保持一致
重载规则
- 不可重载
-
成员访问符 .
(重载无法确定是重载了.的对象还是通过.来调用一个成员)
-
成员指针运算符 .*
-
域运算符 ::
(重载之后运算符左边可能不是命名空间或者类,右边可能不是成员变量或者函数)
-
条件运算符 ?:
(没有歧义无需重载)
-
长度运算符 sizeof
(内部许多指针操作都依赖sizeof)
-
- 可以重载
- 为了防止用户对标准类型进行运算符重载, C++ 规定重载的运算符的操作对象必须至少有一个是自定义类型或枚举类型
- 重载运算符之后,其优先级和结合性(执行顺序)还是固定不变的。
- 重载不会改变运算符的用法,原来有几个操作数、操作数在左边还是在右边,这些都不会改变
- 重载运算符函数不能有默认参数,否则就改变了运算符操作数的个数
- 重载逻辑运算符( && , || )后,不再具备短路求值特性
- 不能臆造一个并不存在的运算符,如@、$等
特殊重载
复合赋值运算符
- 如+=,对象本身发生了改变,推荐使用成员函数形式
自增自减运算符
-
如++,对象本身发生了改变,推荐使用成员函数进行重载,分前置和后置两种
- 前置++比后置++执行效率更高
- 前置运算符重载返回类型可以带&
//前置++ Complex &operator++(){ ++_dreal; ++_dimag; return *this; }
- 后置运算符重载返回类型不能带&
//后置++,为了与前置++不同,所以在参数列表中加入int用来与前置++进行区分,没有参数传递的意思 Complex operator++(int){ Complex tmp(*this); ++_dreal; ++_dimag; return tmp; }
赋值运算符
- 如=,对象本身发生了改变,推荐使用成员函数进行重载,见类的赋值运算符构造函数
输出输出流运算符
- 如<<,不能使用成员函数(成员函数有this指针,不能改变操作数位置,左边是一种输出流,右边是对象),推荐使用友元函数进行重载
#include<limits>
#include<iostream>
friend std::ostream &operator<<(std::ostream &os, const Complex &rhs){//返回流对象os就是原来的cout,流的拷贝构造函数被delete了,所以返回类型必须引用,换句话说资源不能直接复制只能再去创建申请
if(0==rhs._dreal&&0==rhs._dimag){
os<<0<<endl;
}//...
return os;
}
void test(){
//链式编程
cout<<"c1 = "<<c1<<endl;
//将运算符函数还原,第一个cout<<是ostream给的,第二个<<是我们重载的,第三个是流成员可以调用传入endl函数
operator<<(cout,"c1 = ");
operator<<(operator(cout,"c1 = "),c1).operator<<(endl);
}
void readDouble(std::istream &is, double &num){
while(is>>num,!is.eof()){
if(is.bad()){
std::cerr<<"The istream is bad"<<endl;
return;
}else if(is.fail()){
is.clear();//重置流状态
is.ignore(std::numeric limits<std::streamsize>::max(),'\n');//清空缓冲区
cout<<"pls input a double data"<<endl;
}else{
cout<<"number = "<<num<<endl;
break;
}
}
}
friend std::istream &operator>>(std::istream &is,Complex &rhs){
is>>rhs._dreal>>rhs._dimag;//不够健壮,如果不是数字会出错
readDouble(is,rhs._dreal);
readDouble(is,rhs._dimag);
return is;
}
函数调用运算符
- 函数调用运算符的重载形式只能是成员函数形式
- 第一对小括号总是空的,因为它代表着我们定义的运算符名,第二对小括号就是函数参数列表了,它与普通函数的参数列表完全相同。对于其他能够重载的运算符而言,操作数个数都是固定的,但函数调用运算符不同,它的参数是根据需要来确定的,并不固定。
//重载了函数调用运算符的类,所创建的对象称为函数对象
class Func{
public:
Func():_cnt(0){ }
int operator()(int x,int y){
return x+y;
}
int operator()(int x,int y,int z){
return x*y*z;
}
private:
int _cnt;//函数对象的状态,被调用多少次
};
int main(){
int a=3,b=4,c=5;
Func fo;
cout<<fo(a,b)<<endl;
fo.operator()(a,b);//还原
cout<<fo(a,b,c)<<endl;
fo.operator()(a,b,c);//还原
typedef int (*pfunc)(int,int);//函数指针在使用上与普通函数形式类似
pfunc pf = &add;//&可加可不加
cout<<pf(a,b)<<endl;
}
下标访问运算符
-
下标访问运算符[]通常用于访问数组元素,它是一个二元运算符
(如arr[idx] 可以理解成arr 是左操作数, idx 是右操作数)
-
对下标访问运算符进行重载时,只能以成员函数形式进行
(size_t是跨平台的无符号整数,32位相当于unsigned int,64位相当于unsigned long)
-
相比于C数组不会访问非法位置,增加了安全性
-
只要不是返回局部变量+引用,链式编程也要+引用
class CharArray
{
public:
CharArray(size_t sz=10)
: _sz(sz),_data(new char[_sz]())
{
cout<<"构造"<<endl;
}
char &operator[](size_t idx){//此处引用不能去掉,引用传回左值,不能用右值给右值赋值,第2种解释_data[idx]的值仍然存在生命周期还没结束,换句话说idx=0的时候仍然是一个数组可以赋值给数组而不是给一个char
if(idx<size()){
return _data[idx];//不加引用有拷贝的操作开销
}else{
static char nullchar = '\0';
return nullchar;
}
}
size_t size()const{
size_t _sz;
}
~CharArray()
{
cout<<"析构"<<endl;
if(_data){
delete [] _data;
_data=nullptr;
}
}
private:
size_t _sz;
char* _data;
};
void test(){
const char *pstr = "hello,world";//内置类型
CharArray ca(strlen(pstr)+1);//对象
for(size_t idx=0;idx!=ca.size();++idx){
ca[idx]=pstr[idx];//运算符重载
ca.operator[](idx)=pstr[idx];//还原
}
for(size_t idx=0;idx!=ca.size();++idx){
cout<<ca[idx]<<" ";
}
cout<<endl;
}
成员访问运算符
- 包括->和*
- 形式上和指针相似了,智能指针的雏形,只需要new不用管delete
class Data{
public:
Data(int data=20):_data(data){
cout<<"构造1"<<endl;
}
int getData()const{
return _data;
}
~Data(){
cout<<"析构1"<<endl;
}
private:
int _data;
};
class SecondLayer{//第二层
public:
SecondLayer(Data *pdata):_pdata(pdata){
cout<<"构造2"<<endl;
}
~SecondLayer(){
cout<<"析构2"<<endl;
if(_pdata){
delete _pdata;
_pdata=nullptr;
}
}
Data *operator->(){
return _pdata;
}
Date &operator*(){
return *_pdata;
}
private:
Data* _pdata;
};
class ThirdLayer{//第三层
public:
ThirdLayer(SecondLayer *_pdata):_psl(psl){
cout<<"构造3"<<endl;
}
SecondLayer &operator->(){
return *_psl;
}
~Thirdayer(){
cout<<"析构3"<<endl;
if(_psl){
delete _psl;
_psl=nullptr;
}
}
private:
SecondLayer* _psl;
};
int main(){
//new一个空参的Data对象传入SecondLayer用来构造sl
SecondLayer sl(new Data());
//直接通过sl的->调用Data的成员函数并打印
cout<<sl->getData()<<endl;
sl.operator->()->getdata();//还原
//直接通过sl的*调用Data的成员函数并打印
(*sl).getData();
sl.operator*().getData();//还原
cout<<endl;
//new一个空参的Data对象用来构造new出来的SecondLayer最终构造出tl
ThirdLayer tl(new SecondLayer (new Data(10)));
//通过tl的->调用Data的数据成员并打印
cout<<tl->getData()<<endl;
cout<<tl.operator->().operator->()->getData()<<endl;//还原
return 0;
}
总结
- 所有的一元运算符,建议以成员函数重载
- 运算符 = () [] -> ->* ,必须以成员函数重载
- 运算符 += -= /= *= %= ^= &= != >>= <<= 建议以成员函数形式重载
- 其它二元运算符,建议以友员函数重载