函数调用与函数调用运算符
先写一个简单的函数,如下:
/*函数的定义*/
int func(int i)
{
cout<<"这是一个函数\t"<<i<<endl;
}
void test()
{
func(1);//函数的调用
}
通过这个普通的函数可以看到,调用一个函数很简单,首先是函数名,后面跟一对圆括号,如果这个函数有参数,就在函数调用位置的函数名后圆括号中给出实参,在函数定义位置函数名后圆括号中给出形参
函数调用总离不开一对圆括号,这个“()”就叫函数调用运算符
特别的,如果在类中重载了这个函数调用运算符(),我们就可以像使用函数一样使用类的对象,换句话说就可以像函数调用一样来“调用”该类的对象。
这种重载了()的类对象,就叫做函数对象,如下
class biggerThanZero
{
public:
int m_i;
biggerThanZero(int i):m_i(i)
{
cout<<"构造函数调用了"<<endl;
}
int operator()(int num)
{
if(num<0) return 0;
return num;
}
};
void test()
{
biggerThanZero bz1(10);//这是对象定义的初始化,会调用类的构造函数
int num=-20;
cout<<bz1(num)<<endl;//这才是调用了类的函数调用运算符()
}
分析代码并观察运行结果可以看到,测试函数中的第一行代码是一个正常的调用构造函数对类对象进行初始化的代码,而接下来的两行代码里的bz1(num)才是对函数调用运算符的调用,观察其书写形式可以发现,其调用形式与文章开头的普通函数func调用形式是相同的。
这就是可调用对象,即
- func函数
- 重载了函数调用运算符“()”的biggerthanzero类所生成的对象bz1,我们调用bz1时可以像调用函数那样进行调用
问题的引出
继续观察这两个可调用对象,可以发现他们的调用形式都是相同的,也就是int (int),同时这也是func的函数类型
注:函数类型就是返回值类型和参数类型的组合。
如func函数中的参数类型是int,而返回值类型int,因此func的函数类型就是int (int)。
同理,假如现在有一个函数的声明为 void fun(double);则这个fun函数的函数类型就是
void (double)
而我们又知道,我们可以使用一个函数指针来保存函数地址,那么操作这个函数指针就等同于操作这个函数指针所指向的函数,如下所示
void test()
{
int (*fp)(int);//定义一个函数指针,也就是可以保存函数地址的指针
fp=func;//在c++中,函数名就是这个函数的地址,也可以使用fp=&func
fp(1);//由于fp现在指向func,因此调用fp(1)就等同于调用func(1)
}
但是对于相同的调用形式的函数对象,我们却不能使用函数指针进行统一调用
void test()
{
int (*fp)(int,int);
biggerThanZero bz(0);//类对象初始化
fp=bz;//报错
}
这说明系统并没有把类biggerthanzero的对象bz看成一个函数指针,当然系统更不可能把类biggerthanzero看成一个函数指针
那么问题来了,对于具有相同调用形式的不同可调用对象,是否可以使用一个统一的调用呢?
function类模板的引出
暂时先搁置一下上边提到的概念,我们暂时已经知道以下三点:
- 普通函数和重载了函数调用运算符()的对象都是可调用对象
- 可以使用函数指针统一调用普通函数这个可调用对象
- 但是同为可调用对象的函数对象却无法使用函数指针进行统一调用
接下来,我们做两个实验来引出function类模板
观察以下代码
int max(int a,int b)
{
return (a>b)?a:b;
}
int min(int a,int b)
{
return (a<b)?a:b;
}
void test()
{
cout<<max(1,2)<<endl;
cout<<min(1,2)<<endl;
}
在这段代码里,我们定义了两个函数,分别是max函数和min函数,并且我们在测试函数中调用了他们。
实验一
仔细观察这段代码,可以发现max函数和min函数的函数类型是相同的,都是int(int ,int ),因此我们可以使用一个函数指针来对他们进行统一形式的调用,如下
void test()
{
int (*fp)(int,int);//定义一个函数指针
fp=max;//将max函数的函数地址赋给函数指针fp
cout<<fp(1,2)<<endl;//现在fp指向的是max函数,调用fp(1,2)等同于调用max(1,2)
fp=min;//将min函数的函数地址赋给函数指针fp
cout<<fp(1,2)<<endl;//现在fp指向的是min函数,调用fp(1,2)等同于调用min(1,2)
}
运行结果如下:
其实,这就是函数指针最开始的作用,但是后来面向对象诞生之后这种写法就被替代了。
实验二
再进一步,我们可以使用一个map来对这段逻辑进行重写,如下:
void test()
{
map<string,int(*)(int,int)> mp;
mp.insert({"max",max});
mp.insert({"min",min});
cout<<mp["max"](1,2)<<endl;//等同于调用max(1,2)
cout<<mp["min"](1,2)<<endl;//等同于调用min(1,2)
}
在上述代码里,我们使用一个map对上述调用形式进行了统一的改写,map键值对的键类型是string,而值类型是函数类型为int(int,int)的函数指针
综上观察,我们可以说
- 同为可调用对象,且函数对象和普通函数的调用形式是相同的,但是我们只能使用函数指针对调用形式相同(函数类型名相同)的普通函数进行统一调用
- 但是我们却无法再进一步对相同调用形式的函数对象进行统一调用
为了解决以上问题,c++引入了function模板的概念,我们直接看代码
int func(int i)
{
cout<<"这是一个函数\t"<<i<<endl;
}
class biggerThanZero
{
public:
int m_i;
biggerThanZero(int i):m_i(i)
{
cout<<"构造函数调用了"<<endl;
}
int operator()(const int& num)
{
if(num<0) return 0;
return num;
}
};
void test()
{
function<int(int)> fp2;
fp2=func;
cout<<fp2(10)<<endl;
biggerThanZero bz(0);
fp2=bz;
cout<<fp2(-1)<<endl;
}
现在我们可以拿出function类模板的定义和作用了
function的类模板,就是用来包装一个可调用对象的,换句话说,它可以像函数指针那样,将不同的可调用对象包装成一个相同的东西,以方便我们使用
正如我们使用函数指针那样,对于同一个函数指针,我们可以赋给它不同的函数地址,只要这些函数的调用形式是相同的,我们就可以使用这一个指针实现不同的函数调用,而function类模板实现的就是类似的功能
现在,我们就可以进一步使用map来对可调用对象进行统一封装了
void test()
{
biggerThanZero bz(0);
map<string,function<int(int)>> mp={
{"func",func},
{"biggerThanZero",bz}
};
mp["func"](20);
cout<<mp["biggerThanZero"](-10)<<endl;
}
因此,我们可以把函数、可调用对象等都包装成function<int(int)>对象。
但是,值得说明的是,无法对重载的函数包装进function中
我们只能这样写
void test() { function<int(int,int)> fc; int(*fp)(int,int)=max; fc=fp; cout<<fc(1,2)<<endl; }
可调用对象
在上边我们已经说了有关可调用对象的概念,这里再强调一下,普通函数和重载了函数调用运算符的类对象(也就是函数对象)都是可调用对象,他们都可以像使用普通函数调用那样进行调用
除了上述两个可调用对象外,其余还有一些可调用对象,我们一并进行介绍
1.函数指针
普通函数的函数名就是一个函数地址,是一个可调用对象,我们已经介绍过了
void func()
{
cout<<"func运行了"<<endl;
}
void test()
{
void(*fp)()=func;//定义函数指针,并使用func的函数地址进行初始化
fp();//调用函数,这是一个可调用对象
}
2.函数对象(仿函数)
具有operator()成员函数的类对象也是一个可调用对象,我们也已经介绍过了。在这里我们给出函数对象(仿函数)的定义
仿函数的定义:仿函数(functors)又称为函数对象(function objects),是一个能行使函数功能的类所定义的对象。仿函数的语法几乎和普通的函数调用一样
class TC
{
public:
void operator()(int tv)
{
cout<<"TC::operator()执行了\t"<<tv<<endl;
}
};
void test()
{
TC tc;
tc(10);//像调用函数那样调用()操作符,等价于tc.operator(10)
}
3.可转换为函数指针的类对象
其实,函数指针都是可调用对象,可以在代码中像函数一样使用。
这在我们之前探讨函数指针的时候已经见到过了,当时我们定义了两个函数类型为int(int,int)的max函数与min函数,并使用同一个函数指针分别指向max函数和min函数的地址进行调用,使用函数指针进行调用的时候我们也观察到其调用与普通函数调用无异
现在我们说一种特殊的函数指针,如下
可转换为函数指针的类对象也叫做函数对象或者仿函数
在说这个问题之前,先说一下有关类型转换的问题
我们知道
- 一个类对象的类型就是定义这个对象的类本身,比如上边的对象tc的类型就是TC。
- 常用的普通类型之间是可以进行类型转换的,如int可以转换成double,而double可以转换成int
- 再进一步,我们又知道,我们可以将一个int类型的变量转换为一个类类型,只要这个类提供了对应的构造函数,如下:
class TS { public: int m_i; TS(){ cout<<"无参构造函数调用了"<<endl; }; TS(int a):m_i(a) { cout<<"带参数的构造函数调用了"<<endl; } }; void test() { TS ts; ts=10;//通过构造一个临时对象,将int类型的10转换为一个TS类型的变量 }
当然,上述这种类型转换是隐式,而如果我们不希望编译器为我们使用这种int到类型之间的隐式类型转换,使用显示类型转换也是可以的,如下:
class TS { public: int m_i; TS(){ cout<<"无参构造函数调用了"<<endl; }; explicit TS(int a):m_i(a)//声明explicit,不允许编译器使用隐式类型转换 { cout<<"带参数的构造函数调用了"<<endl; } }; void test() { TS ts; //由于构造函数使用了explicit关键字,不允许编译器使用隐式类型转换,因此我们只能使用显示类型转换 ts=TS(10); }
那么问题来了,是否可以将一个类类型转换为一个普通类型呢,如将上述的TS类类型转换成一个int类型?
答案当然也是肯定的。
类型转换运算符
类型转换运算符,也叫做类型转换函数,是类的一种特殊成员函数,它能将一个类类型对象转成某个其他类型数据
其一般形式为:
先说以下几点说明,我们再使用代码进行实验
- 末尾的const是可选项,表示不应该改变待转换对象的内容,但不是必须有const
- “类型名”表示要转换成的某种类型,一般只要是能作为函数返回类型的类型都可以。所以一般不可以转成数组类型或者函数类型(把一个函数声明去掉函数名剩余的部分就是函数类型,如void(inta,intb)),但是转换成数组指针、函数指针、引用等都是可以的。
- 类型转换运算符,没有形参(形参列表必须为空),因为类型转换运算符是隐式执行的,所以无法给这些函数传递参数。同时,也不能指定返回类型,但是却会返回一个对应类型(“类型名”所指定的类型)的值。
- 必须定义为类的成员函数
接下来,我们做实验以做验证
class TS
{
public:
int m_i;
TS(){
cout<<"无参构造函数调用了"<<endl;
};
TS(int a):m_i(a)//声明explicit,不允许编译器使用隐式类型转换
{
cout<<"带参数的构造函数调用了"<<endl;
}
//类型转换运算符,必须定义为一个成员函数
//表示编译器可以调用这个函数隐式的将TS类型的变量转换为一个int类型的变量
operator int() const
{
return m_i;
}
};
void test()
{
TS ts1;//普通构造
ts1=10;//调用带参构造,将int类型的10隐式转换为TS类型的变量
int res=ts1+20;//隐式调用operator int()成员函数将对象ts1转换为int类型变量之后再做加法运算
cout<<res<<endl;
int res2=ts1.operator int()+30;//也可以显示的调用operator int()成员函数将对象ts1转换为int类型变量
cout<<res2<<endl;
}
上述代码执行过程为
- 调用无参构造函数构造ts1对象
- 调用有参构造函数并生成一个临时对象,隐式的将int类型变量的10转换为一个TS类型变量,再把这个临时对象赋给ts1
- 隐式的调用operator int()成员函数将ts1转换为int类型,再进行加法运算,执行10+20
- 显示的调用operator int()成员函数将ts1转换为int类型,再进行加法运算,执行10+30
同样的,如果我们想拒绝隐式类型转换,也可以使用显示类型转换运算,如下:
class TS
{
public:
int m_i;
TS(){
cout<<"无参构造函数调用了"<<endl;
};
TS(int a):m_i(a)//声明explicit,不允许编译器使用隐式类型转换
{
cout<<"带参数的构造函数调用了"<<endl;
}
//类型转换运算符,必须定义为一个成员函数
//表示编译器可以调用这个函数隐式的将TS类型的变量转换为一个int类型的变量
explicit operator int() const//拒绝编译器使用隐式类型转换
{
return m_i;
}
};
void test()
{
TS ts1;//普通构造
ts1=10;//调用带参构造,将int类型的10隐式转换为TS类型的变量
int res=static_cast<int>(ts1)+20;//显示类型转换
cout<<res<<endl;
}
现在我们转回正题,我们现在已经知道了,类类型也可以转换为其他类型,如int类型,而类类型也同样可以转换为函数指针类型,而这种可以转换为函数指针的类对象,也是一种可调用对象
为了说明问题,我们继续写一个测试代码
class TS
{
public:
int m_i;
TS(){
cout<<"无参构造函数调用了"<<endl;
};
TS(int a):m_i(a)//声明explicit,不允许编译器使用隐式类型转换
{
cout<<"带参数的构造函数调用了"<<endl;
}
static void myfunc(int v1)
{
cout<<"static myfunc函数执行了\t"<<v1<<endl;
}
//类型定义,void(*)(int)是一个函数指针类型,使用tfpoint替换这个类型
//等价于typedef void(*tfpoint)(int)
using tfpoint=void(*)(int);
operator tfpoint()
{
cout<<"类型转换运算符调用了"<<endl;
return myfunc;
}
};
void test()
{
TS ts;
//执行operator tfpoint(),之后再执行myfunc(123)
ts(123);
}
4.lambda表达式
lambda表达式也是一种可调用对象,参见
可调用对象总结
其实,可调用对象首先被看作一个对象,程序员可以对其使用函数调用运算符“()”,那就可以称其为“可调用的”
function类模板
如果找通用性,上述提到的这几种可调用对象的调用形式都比较统一,那么,有没有什么方法能够把这些可调用对象的调用形式统一一下呢?有,那就是使用std::function把这些可调用对象包装起来。这在我们之前也已经实验过了
但有一点需要注意,function类模板用来往里装各种可调用对象,但是它不能装类成员函数指针,因为类成员函数指针是需要类对象参与才能完成调用的。
std::function类模板的特点是:通过指定模板参数,它能够用统一的方式来处理各种可调用对象。
1.绑定普通函数
void func1(int num)
{
cout<<"这是一个普通函数func1:"<<num<<endl;
}
void test()
{
function<void(int)> fp1=func1;//绑定一个普通函数
func1(10);
}
2.绑定类的静态成员函数
class TC
{
public:
static void stcfunc(int num)
{
cout<<"TC类的静态成员函数执行了:"<<num<<endl;
}
};
void test()
{
function<void(int)> fp2=TC::stcfunc;
fp2(20);
}
3.绑定仿函数
class TC
{
public:
int m_i;
TC(int i=0):m_i(i)
{
cout<<"构造函数运行了"<<endl;
}
void operator()(int num)
{
cout<<"函数调用运算符运行了:"<<num<<endl;
}
static void stcfunc(int num)
{
cout<<"TC类的静态成员函数执行了:"<<num<<endl;
}
};
void test()
{
// function<void(int)> fp1=func1;//绑定一个普通函数
// func1(10);
function<void(int)> fp2=TC::stcfunc;
fp2(20);
TC tc;
function<void(int)> fp3=tc;//TC类声明了函数调用运算符,因此对象tc是一个函数对象
fp3(30);
}
std::bind绑定器
std::bind能将对象以及相关的参数绑定到一起,绑定完后可以直接调用,也可以用std::function进行保存,在需要的时候调用。
std::bind有两个意思:·
- 将可调用对象和参数绑定到一起,构成一个仿函数,所以可以直接调用。·
- 如果函数有多个参数,可以绑定部分参数,其他的参数在调用的时候指定。
绑定普通函数
#include<iostream>
#include<functional>
using namespace std;
void func1(int x,int y,int z)
{
cout<<"x="<<x<<",y="<<",z="<<z<<endl;
}
void test()
{
function<void(int,int,int)> bf1=bind(func1,10,20,30);
bf1(10,20,30);
auto bf2=bind(func1,10,20,30);
bf2();
}
int main()
{
test();
// system("pause");
return 0;
}
参数占位符
#include<iostream>
#include<functional>
using namespace std;
using namespace placeholders;
void func1(int x,int y,int z)
{
cout<<"x="<<x<<",y="<<y<<",z="<<z<<endl;
}
void test()
{
//_1和_2分别表示func1的第一个参数和第二个参数暂时不定,需要自己传入
auto bf2=bind(func1,_1,_2,30);
bf2(10,20);
auto bf3=bind(func1,_2,_1,30);
bf3(10,20);
}
int main()
{
test();
// system("pause");
return 0;
}
可以看到,分别调整参数占位符的位置后,输出的结果是不一样的
绑定函数对象
class CQ
{
public:
void operator()(int x,int y)
{
cout<<x<<"\t"<<y<<endl;
}
};
void test()
{
CQ cq;
bind(cq,10,20)();
bind(cq,_1,20)(5);
}
绑定类成员函数
#include<iostream>
#include<functional>
using namespace std;
using namespace placeholders;
void func1(int x,int y,int z)
{
cout<<"x="<<x<<",y="<<y<<",z="<<z<<endl;
}
class CQ
{
public:
int m_a=0;
void operator()(int x,int y)
{
cout<<x<<"\t"<<y<<endl;
}
void classFunc(int num)
{
cout<<"this is a class func:"<<num<<endl;
m_a=num;
}
};
void test()
{
CQ cq;
// bind(cq,10,20)();
// bind(cq,_1,20)(5);
bind(&CQ::classFunc,cq,55)();
cout<<"m_a="<<cq.m_a<<endl;
bind(&CQ::classFunc,&cq,_1)(23);
cout<<"m_a="<<cq.m_a<<endl;
}
int main()
{
test();
// system("pause");
return 0;
}
注意,上面的代码中
第一个std::bind的调用中,第二个参数cq会导致生成一个临时的CQ对象,std::bind是将该临时对象和相关的成员函数以及多个参数绑定到一起,后续对myfunpt成员函数的调用修改的是这个临时的CQ对象的m_a值,并不影响真实的cq对象的m_a值。
如果将std::bind的第二个参数cq前增加&,这样就不会导致生成一个临时的CQ对象,后续的myfunpt调用修改的就会是cq对象的m_a值。
这就是为什么第二个std::bind模板调用后m_a的值改变的原因
绑定类成员变量
#include<iostream>
#include<functional>
using namespace std;
using namespace placeholders;
void func1(int x,int y,int z)
{
cout<<"x="<<x<<",y="<<y<<",z="<<z<<endl;
}
class CQ
{
public:
int m_a;
CQ(int num=0):m_a(num)
{
cout<<"构造函数调用了"<<endl;
}
CQ(const CQ& cq):m_a(cq.m_a)
{
cout<<"拷贝构造函数调用了"<<endl;
}
void operator()(int x,int y)
{
cout<<x<<"\t"<<y<<endl;
}
void classFunc(int num)
{
cout<<"this is a class func:"<<num<<endl;
m_a=num;
}
virtual ~CQ()
{
cout<<"析构函数执行了"<<endl;
}
};
void test()
{
CQ cq;
function<int&(void)> fp=bind(&CQ::m_a,cq);
fp()=66;
cout<<cq.m_a<<endl;
}
int main()
{
test();
// system("pause");
return 0;
}
注意观察输出
把成员变量地址当函数一样绑定,绑定的结果放在std::function<int &(void)>里保存。换句话说,就是用一个可调用对象的方式来表示这个变量,bind这个能力还是比较神奇的。
重点分析一下代码行“bf7()=60;”,因为其上面的那行代码用了&cq,所以这里等价于cq.m_a=60。如果cq前不用&,发现会调用两次CQ类的拷贝构造函数。
为什么调用两次拷贝构造函数呢?
第一次是因为第一个参数为cq,所以利用cq产生一个临时的CQ对象
第二次是因为std::bind要返回一个CQ对象(确切地说是经过std::bind包装起来的CQ对象),所以要返回的这个CQ对象(仿函数)复制自这个临时CQ对象,但bind这行执行完毕后,临时CQ对象被释放,返回的这个CQ对象(仿函数)放到了bf7里。
所以上述std::bind代码行中,一般都应该用&cq,否则最终会多调用两次拷贝构造函数和两次析构函数,用了&cq后这4次调用全省了,提高了程序运行效率。
void test()
{
CQ cq;
function<int&(void)> fp=bind(&CQ::m_a,&cq);
fp()=66;
cout<<cq.m_a<<endl;
}
总结
- 因为有了占位符(placeholder)这种概念,所以std::bind的使用就变得非常灵活。可以直接绑定函数的所有参数,也可以仅绑定部分参数。绑定部分参数时,就需要通过std::placeholders来决定bind所在位置的参数将会属于调用发生时的第几个参数。
- std::bind的思想实际上是一种延迟计算的思想,将可调用对象保存起来,然后在需要的时候再调用。
- std::function一般要绑定一个可调用对象,类成员函数不能被绑定。而std::bind更加强大,成员函数、成员变量等都能绑定。
现在通过std::function和std::bind的配合,所有的可调用对象都有了统一的操作方法。
应用
至此,我们已经详细的讲解了function类模板与std::bind,接下来具体看看他们的使用场景