菜鸟教程重载函数和运算符
(明确列出可以和不可重载的运算符,以及对可重载运算符的实例实现)
推荐去看清华大学的郑莉教授出版的《C++语言程序设计》一书,友元函数在第五章第四节,重载在第三章第五节(函数重载)和第八章前两节(多态和运算符重载)。对理解c++语言有很大帮助(如果只是会写代码打个比赛拿奖退役那就算了),下面会书的内容部分引用。
先说一下结构体struct和类class的区别。类的成员包括数据成员和函数成员,分别描述问题的属性和行为,而每个成员都有一个访问权限,权限分三种:公有类型public,私有类型private,保护类型protected。类的成员默认是私有类型,而结构体默认是公有类型。
私有成员只在本类的成员函数中可见,公有成员全局可见,保护成员可在本类和派生类的成员函数中可见。
所以打比赛就用结构体就够了,代码量不多也不用考虑安全问题,搞太多可见不可见反而容易坑了自己。
重载意味着多态,多态是指同样的信息被不同类型的对象接收时导致不同的行为,所谓消息是指对函数的调用,不同的行为是指不同的实现,也就是调用了不同的函数。最简单的例子就是运算符,同样使用“+”进行加法运算,整数之间,浮点数之间的加法运算是不一样的,整数调用整数的加法运算函数,浮点数调用浮点数的加法运算函数,同样调用一个函数,不同类型的对象来调用,就导致了不同的行为(也就是不同的函数实现),这就是多态。
两个以上的函数,具有相同的函数名,但是形参的个数或者类型不同,编译器根据实参和形参的类型及个数的最佳匹配,自动确定调用哪一个函数,这就是函数的重载。 这意味相同函数名的函数,它们的形参列表必须不同:个数不同或者类型不同 (或者你可以认为函数名和参数表共同组成了函数独一无二的名字)(这里可没说返回值类型,你考虑调用哪个函数的时候又不可能预测返回值类型是什么)。
重载函数只需要参数表不同即可,比如:
int add(int x,int y);
float add(float x,float y);
这样就实现了函数的重载,如果你给add函数传递了两个int类型的数据,就会自动调用第一个函数,如果传递了两个float类型的数据,就会自动调用第二个函数。
在结构体和类中可以加入函数成员,函数成员也是函数,也可以重载。比如
struct Box{
public:
int x,y,z;//盒子的长宽高
void add(int a){
x+=a;
}
void add(int a,int b){
x+=a;
y+=b;
}
void add(int a,int b,int c){
x+=a;
y+=b;
z+=c;
}
};
int main(){
Box x;
x.add(2024);
x.add(1,13);
x.add(1,1,4);
return 0;
}
创建一个Box类,实例化一个对象x,使用x调用它的公有成员函数add。给add传递了一个int类型的数据,就会自动调用第一个函数,如果传递了两个int数据,就会自动调用第二个函数,如果传递了三个int数据,就会自动调用第三个函数。(不过实际使用的时候可以给后面的参数设置默认值)
类的成员函数的函数名你可以看作 是包含类名的,也就是说,普通函数以及其他类的成员函数即使函数名和参数表是完全一致的,也是不冲突的。因为名字变成了:类名::函数名(参数表)
。比如说这样写是不会报错的
struct Box{
public:
int x,y,z;//盒子的长宽高
void add(int a,int b){
x+=a;
y+=b;
}
};
int add(int a,int b){
return a+b;
}
int main(){
Box x;
x.add(1,13);
add(1,1);
return 0;
}
重载运算符和重载函数其实本质上没有什么区别。拿加法举例,+是一个双目运算符(双目表示两个操作数),它的调用需要两个对象,而这两个对象正好就在它的两边,两个int类型的数进行加法运算,看似是 a+b ,实际上调用的是 operator+(a,b),返回值就是a+b,operator+就是加法的函数名。或者可以把a看作一个对象,调用operator+()成员函数,即 a.operator+(b),两种都是可以在代码中实现的。
运算符重载只能针对大部分 C++ 内置的运算符,一小部分重要的运算符不允许改变其含义,有:
.
:成员访问运算符
.*
,->*
:成员指针访问运算符
::
:域运算符
sizeof
:长度运算符
?:
:条件运算符
#
:预处理符号
上面提到的两种重载方式都是可以实现的,比如:
struct Box{
public:
int x,y,z;//盒子的长宽高
Box operator+(Box a);//类内需要声明,之后在类外进行具体实现
};
//实现1:
Box operator+(Box a,Box b){
Box t;
t.x=a.x+b.x;
t.y=a.y+b.y;
t.z=a.z+b.z;
return t;
}
//实现2:
Box Box::operator+(Box a){
Box t;
t.x=x+a.x;
t.y=y+a.y;
t.z=z+a.z;
return t;
}
两种实现各有优缺,实现1在遇到需要用到私有成员的时候比较麻烦,需要声明为友元函数。实现2本质是前一个对象调用它的成员函数,所以前一个调用者必须是类的对象。
可以看到实现2少一个参数,这是因为它是类的成员函数,到时候一定会由一个对象来调用,这个对象就是隐含的被传进去的参数,在成员函数中可以用this
指针来指向自己,比如这样:
struct Box{
public:
int x,y,z;//盒子的长宽高
Box& self_add(){
(this->x)++;
(this->y)++;
(this->z)++;
return *this;
}
};
大部分运算符都是双目运算符,依葫芦画瓢都可以写出代码,不会可以去上面给的参考网站去查。
+
和-
号可以是单目也可以是双目运算符。
这里提一下自增和自减(++和–)的实现。拿++举例,它有前置和后置的用法,那么怎么区分呢?
答案就是在参数表上做文章,后置++会在参数表中放一个int
来进行一个区分(就好像一个a.operator++(0),简化一下就是a++0,就变成后置了),举个例子:
struct Box{
public:
int x,y,z; //盒子的长宽高
Box operator++(int){ //后置++
Box t(*this);
x++;y++;z++;
return t;
}
Box& operator++(){ //前置++
x++;y++;z++;
return *this;
}
};
Box operator--(Box& a,int){//后置--
Box t(a);
a.x--;a.y--;a.z--;
return t;
}
重载运算符在竞赛中很有用,尤其是使用需要比较的系统函数的时候,比如sort。sort内部只使用了<
运算符,因此我们只要对自定义的类型重载一下小于号,就可以用sort来排序我们的类型了。比如:
struct Box{
public:
int x,y,z; //盒子的长宽高
bool operator<(Box a){ //先按x比较,再比较y,最后比较z
if(x!=a.x)return x<a.x;
if(y!=a.y)return y<a.y;
return z<a.z;
}
};
int main(){
Box a[maxn],n;
...//输入
sort(a+1,a+n+1);//先按x降序,再按y降序,再按z降序
}
除此之外,优先队列(堆),重载小于号用于自定义的排序也比较常见(优先队列里也是只用了<
)。
struct Box{
public:
int x,y,z; //盒子的长宽高
bool operator<(const Box a)const{ //比较体积
return x*y*z>a.x*a.y*a.z;
}
};
priority_queue<Box> h; //比较体积的小根堆
priority_queue
本身维护的是大根堆,这里重载的时候故意将小于的含义换成了大于,所以变成了维护小根堆,这个技巧很常用,或者你可以这样定义一个小根堆:
//这里需要预先重载好Box类的大于号
//Box可以换成其他任何类型,只要重载了大于号就行
//比如可以写int,double,pair<int,int>等等
priority_queue<Box,vector<Box>,greater<Box> > h;
另外上面在重载小于号的时候,函数体必须用const修饰。形式参数使用const修饰实际上是在向编译器保证:这个形参的值在函数中不会被改变。函数体被const修饰实际上是在向编译器保证:这个函数中 不会对 调用这个函数的对象 进行修改(因为是对对象的,所以只有类的成员函数的函数体可以被const修饰)。因此在const函数体中本对象调用的其他成员函数都需要有const属性(不然你上面刚保证完,下面反手一个函数给它改了。不是不让你用,而是你调用的这个函数得有const属性)。
priority_queue在实现的时候,有的成员函数体被const修饰了,并且函数体中本对象调用了小于号。所以你提供的重载小于号的函数也需要带上const属性,向编译器保证不会进行修改。
如果你只用public,那么友元函数是没有什么用的。友元函数的作用是让类的私有和保护成员 破例向一个 非本类的函数 开放。比如如果上面的Box类的xyz
为私有数据成员,那么用方式1来实现加号的重载就会出现问题。因为方式1是一个独立的函数,它不属于Box类,自然也没有权限去访问xyz
。当把方式1函数声明为Box的友元函数时,方式1函数就可以访问Box的私有成员了。
友元函数实际上是一个关系。方式1函数是个独立的函数,Box类是一个独立的类。在这个基础上,Box类 声明 方式1函数为友元函数,Box类就和函数产生了一个“朋友关系”。
这个关系是单向的,就好比 你把某人当朋友,允许他访问你家,但是他不把你当朋友,你就不能访问他家。如果要实现双向的,就需要两人(两个类)都做出声明。(不过友元函数是类和函数的关系,函数没有私有成员,所以没有双向,只有单向,即类对函数)。
声明友元函数需要在给出权限的类中声明一下友元函数名,再在最前面加一个friend
修饰即可。比如:
有点语法糖的感觉,感觉用的很少。在《C++语言程序设计》第十章第六节有讲。(以下是原文引用)
在10.5节中,我们介绍了几种关联容器,在设计容器时,为了使容器能够通用,容器元素可以是任何类型,这样容器并不知道元素类型的任何信息。一方面在判断键值是否相等时,除了的几种基本数据类型外,C++并未提供判断相等的方法;另一方面,为了在查询和更新时具有较高的时间效率,容器内部使用了平衡树的数据结构,这种数据结构依赖于对键值大小的比较。故需要一个比较函数,才能实现关联容器。
从以上分析可以看到,具体的容器类型被抽象成了通用的容器框架,框架会依赖于一些基本的函数,根据具体的问题替换这些函数,便能实现具体的容器。一般的函数调用只传值参和形参,要传递函数,只能借助于函数对象。这一节就介绍函数对象和函数对象适配器的概念,以及使用和设计的相关内容。
函数对象(function object or functor)是STL 提供的一类主要组件,它使得STL 的应用更加灵活方便,从而增强了算法的通用性。大多数STL算法可以用一个函数对象作为参数。所谓函数对象其实就是一个行为类似函数的对象,它可以不需参数,也可以带有若干参数,其功能是获取一个值,或者改变操作的状态。在C++程序设计中,任何普通的函数、函数指针、lambda表达式和任何重载了调用运算符operator()的类的对象都满足函数对象的特征,因此都可以作为函数对象传递给算法作为参数使用。
常用的函数对象可分为产生器(Generator)、一元函数(Unary Function)、二元函数(Binary Function)、一元谓词(Unary Predicate)和二元谓词(Binary Predicate)函数对象5大类。
下面将以数值算法 accumulate() 为例,介绍函数对象的设计及应用过程。accumulate的原型声明如下:
template<class InputIt,class T,class BinaryFunction>
Type accumulate(InputIt first,InputIt last,T val,BinaryFunction op);
它的功能是对数组元素进行累积运算,有两种重载形式:第一种形式是以“+”运算符作为运算规则,而第二种形式允许用户通过传递给算法相应的函数对象来指定计算规则。该声明是第二种形式,[first,last)为累加的区间,val为累加初始值,op为对应的累加函数。
第一段的大体意思是:很多关联容器都使用了平衡树来实现,而平衡树需要用到比较函数。如果要实现通用的容器或函数,就必须对对象的比较函数进行说明。
第二段:抽象的容器与函数只需要一些基本的关系(函数),根据具体问题去提供函数就能得到具体的容器与函数。提供的函数参数就是函数对象。
函数对象其实就是一个行为类似函数的对象,只要可以通过 函数名(参数表)
来调用的对象或是什么东西都可以叫函数对象。因此 任何普通的函数、函数指针、lambda表达式和任何重载了调用运算符operator()的类的对象 都可以叫做函数对象。
具有0个,1个和2个传入参数的函数对象,称为产生器、一元函数和二元函数。返回值为bool型,并具有1个和2个传入参数的函数对象称为一元谓词和二元谓词。
看这个函数对象是如何传递的:
template<class InputIt,class T,class BinaryFunction>
Type accumulate(InputIt first,InputIt last,T val,BinaryFunction op);
op
就是要传入函数对象的那个形式参数。
如果传入的是普通函数函数名,那么传入的是一个 函数指针(函数名本身代表函数首地址指针),比如:
bool cmp(int a,int b){return a<b;}
int f(int a,int b,bool (*op)(int,int)){ //指针类型为 bool (*)(int,int)
if(op(a,b))return a;
else return b;
}
int main(){
int a,b;
cin>>a>>b;
cout<<f(a,b,cmp);
return 0;
}
传递函数指针也是同一回事。
如果传入的是重载了运算符operator()的类对象,那么传入的实际上是一个对象。这个对象因为重载了operator(),所以可以像函数一样使用括号,比如:
class gt{
public:
int operator()(int a,int b){return a+b;}
};
int main(){
gt f;
int a,b;
cin>>a>>b;
cout<<f(a,b);
return 0;
}
其实调用的是f.operator()(a,b)。类gt的对象f调用了成员函数operator(),传入了a和b的值。
实际使用的时候可能会使用类模板:
template<class T>
class gt{
public:
T operator()(T a,T b){return a+b;}
};
int main(){
gt<int> f1;
int a,b;
cin>>a>>b;
cout<<f1(a,b)<<endl;
gt<double> f2;
double c,d;
cin>>c>>d;
cout<<f2(c,d)<<endl;
return 0;
}
lambda表达式本质是个重载了运算符operator()的类对象,因此lambda表达式也是函数对象。
虽然函数指针和重载了operator()的对象完全不是一个东西,但是它们在向模板传参的时候都可以成功传递,而且都可以像使用函数一样使用。所以它们都叫函数对象。
实际上,sort传入函数对象实现升序排序:
sort(a+1,a+n+1,greater<int>());
和实现小根堆:
priority_queue<int,vector<int>,greater<int> > h;
都用到了这个函数对象。
这个真是语法糖了,虽然学会了会方便一些,但是也不是不可替代的。
这篇lambda表达式讲的特别好,我就不多赘述了。
使用例:
struct point{
int x,y;
}a[maxn];
//对point数组排序,方法是先按x降序,再按y降序
sort(a+1,a+n+1,[](point a,point b){return (a.x==b.x)?a.y<b.y:a.x<b.x;});