第5章函数 函数是C++最基本的程序模块。本章介绍如何定义并使用函数。 5.1定义和调用函数 5.1.1函数的定义 如果在一个程序中不同的地方,需要经常执行一组相同的语句来完成一种相对独立的功能,则可以把这组语句定义为一个独立的程序模块——函数,需要执行时,只要用函数名对其进行调用即可。 使用函数是为了达到代码重用的目的,避免重复存储完全相同的代码,节省存储器空间; 同时也可以减少编程时的重复劳动,提高编程效率; 并使程序结构清晰、易读,易于修改,符合结构化程序设计原理。 定义函数的语法形式如下: 类型说明符 函数名(形式参数列表) { 语句序列; } 例如: int max(int j1, int j2) { int r; r= j1>=j2? j1:j2; return r; } 上面的程序语句定义了一个函数max,功能是返回两个整型参数中较大的一个。 以下是对函数定义的几点说明。 (1) 有的函数执行结束时,要向调用它的程序返回一个值,这种函数称为有返回值的函数。函数定义中的类型说明符就是函数返回值的数据类型。如上面定义的max函数的返回值类型为int型。函数的返回值可以是除数组外的其他任何类型,如整型、浮点型、字符型、布尔型、指针型,甚至可以是结构和类的对象。如果一个函数有返回值,则在函数内部必须使用返回语句(return)把值返回到程序中调用函数的地方。 (2) 如果一个函数没有返回值,则可以用关键字void作为类型说明符。这样的函数称为无返回值函数或void型函数。C++程序中的main函数通常就是一个void型函数。 (3) 函数名是一个标识符。一个程序中,除main函数和C++库函数外,其他的函数都是由编程者命名的,如上面定义的函数max。 (4) 函数名后的小括号中是函数的形式参数列表。函数的参数是函数被调用时,由调用它的程序传递给函数的数据。通常把出现在函数定义中的参数称为形式参数(简称形参),如max函数的参数j1和j2。形式参数列表可以为空,表示函数没有参数。非空的形参列表的语法形式如下: 数据类型说明符1参数1,数据类型说明符2参数2,…,数据类型说明符n参数n 数据类型说明符指定了形参的数据类型。形参可以是C++中的任何数据类型。在函数内部可以像使用变量一样来使用形式参数。 (5) 函数定义中包含函数名的第一行语句称为函数头; 函数名后面由花括号括住的语句序列叫作函数体。 5.1.2函数的调用 函数被定义后,就可以对函数进行调用了。调用函数的语法形式如下: 函数名(实际参数列表) 函数名后面的小括号中是实际参数列表,把调用函数时向函数传递的参数称为实际参数(简称实参)。实参可以是常量、变量或表达式。 函数调用既可以是一条单独的语句,也可以是表达式中的一部分。 在其内部发生函数调用的函数称为主调函数; 被调用的函数称为被调函数。 例如: void main() { int i1,i2,lar; cout<<"请输入两个正整数: "; cin>>i1>>i2; lar=max(i1,i2); cout<<"整数"< using namespace std; int max(int,int); void main() { int i1,i2,lar; cout<<"请输入两个正整数: "; cin>>i1>>i2; lar=max(i1,i2); cout<<"整数"<=j2? j1:j2; return r; } 例5.1程序的运行结果如图5.1所示。 图5.1例5.1程序的运行结果 例5.2编写一个函数求出并返回一个整数各位数字的和,在main函数中输入一系列整数,分别输出它们各位数字的和,直到输入0时,程序结束。 #include using namespace std; int digsum(int val) { int s=0; while(val!=0) { s+=val%10; val/=10; } return s; } void main() { int in; cout<<"请输入一个正整数: "; cin>>in; while(in!=0) { cout<<"整数"<>in; } } 例5.2程序的运行结果如图5.2所示。 图5.2例5.2程序的运行结果 例5.3创建一个函数,用辗转取余法求两个整数的最大公约数和最小公倍数。 算法思想为: 若i和j为两个正整数,则令i1=i,j1=j。 (1) 令res=i1%j1; (2) 若res等于0,则i和j的最大公约数是j1,最小公倍数是i×j÷j1。 否则,令i1=j1; j1=res; 返回第1步继续执行。 程序实现如下。 #include using namespace std; void divandmul(int,int); void main() { int i,j; cout<<"请输入两个正整数: "; cin>>i>>j; if(i>0 && j>0) divandmul(i,j); else cout<<"输入错误\n"; } void divandmul(int i,int j) { int i1=i,j1=j,res=i1%j1; while(res!=0) { i1=j1; j1=res; res=i1%j1; } cout< using namespace std; const double PI = 3.14159; double cylinderArea(double, double); double circleArea(double); double power(double, int); void main() { double radius, height, vol; cout << "请输入圆柱体的底面半径和高: "; cin >> radius >> height; vol = cylinderArea(radius, height); cout << "圆柱体的体积为: " << vol << endl; } double cylinderArea(double radius, double height) { return circleArea(radius)*height; } double circleArea(double radius) { return power(radius, 2)*PI; } double power(double x, int i) { double prod = 1.0; for (int j = 0;j using namespace std; void swap(int par1, int par2); void main() { int i{ 10 }, j{ 20 }; cout << "调用函数swap前: i=" << i << " j=" << j << endl; swap(i, j); cout << "调用函数swap后: i=" << i << " j=" << j << endl; } void swap(int par1, int par2)//传值传递参数 { cout << "swap函数内,形参值交换前: par1=" << par1 << " par2=" << par2 << endl; int temp = par1; par1 = par2; par2 = temp; cout << "swap函数内,形参值交换后: par1=" << par1 << " par2=" << par2 << endl; } 函数swap的功能是交换两个形式参数par1和par2的值,并在交换前后分别输出两个参数的值。main函数中,定义并初始化了两个整型变量i和j,然后以i和j作为实际参数调用函数swap,并在调用前后分别输出i和j的值。 程序的运行结果如图5.5所示。 图5.5例5.5程序的运行结果 从程序的运行结果可以看到,虽然在函数swap内部交换了两个形参par1和par2的值,但main函数中的两个实参的值并没有被交换。正如“传值传递”方式的名字,实参只是把值传递给相应的形参,而后它们就互不相干了,形参的改变不会影响到实参。传值传递是信息从实参到形参的单向传递(不包括指针)。 5.2.2引用传递 在函数的定义中,如果形式参数为引用类型,则参数的传递方式就是引用传递。正如我们所知道的: 一个引用类型的变量是另一个变量的别名(4.3.1节),引用类型的形式参数也是相应的实际参数的别名,它们其实是同一个变量,即在内存中占用同一块存储空间。所以如果在被调函数中形参的值发生了改变,则主调函数中实参的值也会随之改变。 例5.6参数的引用传递。 #include using namespace std; void swap(int &par1,int &par2); void main() { int i=10,j=20; cout<<"调用函数swap前: i="< using namespace std; int fun(); void main() { int i,j=0; cout<<"请输入一个整数: "; cin>>i; while(i!=0) { j=fun(); cout<<"请输入一个整数: "; cin>>i; } cout<<"函数fun被调用了"<num2)return num1; elsereturn num2; } C++编译器处理内联函数调用的方法是: 把内联函数的函数体直接嵌入到发生函数调用的地方,以取代函数调用语句,而不进行常规的函数调用。 使用内联函数既可以达到代码重用的目的,也可以避免函数调用时的额外开销,提高程序的执行速度。但是需要注意,不要把较长的函数、包含循环语句和switch语句的函数定义为内联函数,这样的函数即使被定义为内联函数,C++编译器也不会把它们作为内联函数来处理。 5.6递归函数 递归是一种解决问题的数学方法。例如,可以用递归方法求正整数n的阶乘n!。n!的递归定义如下: n!= n×(n-1)!(当n>0时) 1(当n=0时)(5.1) (5.2) 以上两式给出了n!的递归定义。当n大于0时,n!=n×(n-1)!; 同理(n-1)!=(n-1)×(n-2)!; ……这是一个回溯的过程,目的是把规模较大的问题逐步化简为相同类型的规模较小的问题。当n等于0时,n!=0,这时就回到了源头,这是回溯终止的条件,也就是说,当问题足够小时,可以容易地得到问题的解,从而终止回溯。根据式(5.1)可以从0!递推出1!=1×0!=1,再从1!推出2!,……,最后求出n!。这是一个递推的过程,从小问题的解逐步推出规模较大的问题的解,最后得到原始问题的解。可以看到,用递归的方法解决问题主要包含回溯和递推两个过程。 C++使用递归函数解决递归问题。如果一个函数直接或间接地调用了函数自己,则这个函数就是递归函数。例如,可以定义一个求n!的递归函数fac。 long fac ( int n) { if(n==0) return 1; elsereturn n*fac (n-1);//递归调用函数本身 } 图5.11中以n=4为例,模拟函数fac的调用过程,揭示递归函数的执行原理。 图5.11递归函数fac的调用过程 图5.11中的矩形表示一次函数调用,矩形间向下的箭头从主调函数指向被调函数,矩形右侧水平方向的箭头表示一次函数调用结束后返回到主调函数。从图中可以看出,每一次递归函数调用执行结束后,总是返回到函数中调用它的地方继续向后执行。 递归函数的逐级调用就是从繁到简的回溯过程; 而到达回溯终止条件后,函数开始逐级返回,这是由简到繁的递推过程。 例5.9编写一个函数判断一个字符串的子串是否是回文字符串,回文字符串是指从左向右读和从右向左读都一样的不含空格的字符串。函数有三个参数,一个字符数组用来存储字符串,两个整型参数分别表示子串的起始位置和结束位置。 分析: 设函数int pal(char[] ch,int s,int e)是判断回文的函数。每次执行时,首先判断ch[s]和ch[e]是否相等,若相等,则以字符数组ch、整数s+1和e-1为参数进行递归调用; 若不相等,说明子串不是回文字符串,返回0。递归调用的终止条件 是s≥e,说明子串是回文字符串,返回1。 源程序如下。 #include using namespace std; int pal(char a[], int s,int e); int main() { char ch[100]; int s,e; cout<<"请输入一行字符串: "; cin>>ch; cout<<"请输入两个不大于字符串长度的整数: "; cin>>s>>e; if(s>=e) { cout<<"输入错误\n"<=e)return 1; else if(a[s]==a[e]) return pal(a,s+1,e-1); else return 0; } 例5.9程序的运行结果如图5.12所示。 图5.12例5.9程序的运行结果 例5.10编写一个递归函数,输出一个单字节正整数n的二进制值。 分析: 根据求整数二进制值的方法,可以得出此问题的递归定义: 正整数n的二进制值=正整数n/2的二进制值×2+n%2 上式中的乘2表示将二进制数左移1位。根据递归定义可以很快地得出这个问题的递归算法。 源程序如下。 #include using namespace std; void printBinary(unsigned char); void main() { unsigned char ch; int i; cout<<"请输入一个大于0并且小于256的正整数: "; cin>>i; if(i>0&&i<256) { ch=i; cout<<"正整数"<<(int)ch<<"的二进制值为:"; printBinary(ch); cout< using namespace std; void hanoi(int n,char a,char b,char c); void main() { int n; cout<<"请输入盘子的个数: "; cin>>n; cout<<"移动盘子的步骤如下: \n"; hanoi(n,'A','B','C'); 图5.16例5.11程序的运行结果 } void hanoi(int n,char a,char b,char c) { if(n==1) cout< using namespace std; const float PI=(float)3.14159; float area(float radius=1.0); void main() { float radius; cout<<"请输入圆的半径: "; cin>>radius; if(radius==0.0) cout<<"半径为1的圆的面积为: "< using namespace std; int *fun(); void main() { int *ptr; ptr=fun(); cout<<"*ptr= "<<*ptr<>worh>>height; fptr=areaofRectangle; //指向函数areaofRectangle area=fptr(worh,height);//使用函数指针调用函数areaofRectangle cout<<"矩形面积为: "<>worh>>height; fptr=areaofTriangle;//指向函数areaofTriangle area=fptr(worh,height); //使用函数指针调用函数areaofTriangle cout<<"三角形面积为: "< using namespace std; double forSale(int num) { double salary; salary = 10.0*num+2000; return salary; } double forWork(int num) { double salary; salary=50.0*num; return salary; } double forSc(int num) { double salary; salary = num * 15.0 + 3000; return salary; } double calculate(int num, double(*ptf)(int)) { return ptf(num); } void main() { double salary; int num,choice; double(*ptf)(int num); while (true) { cout << "1.销售人员\n"; cout << "2.工人\n"; cout << "3.研发人员\n"; cout << "0.退出程序\n"; cout << "请输入您的选择: "; cin >> choice; switch (choice) { case 1: cout << "请输入销售员销售的产品个数: "; cin >> num; ptf = forSale; salary=calculate(num, ptf); cout << "您的薪水是: " << salary << endl; break; case 2: cout << "请输入工人的工作时数: "; cin >> num; ptf = forWork; salary = calculate(num, ptf); cout << "您的薪水是: " << salary << endl; break; case 3: cout << "请输入研发人员的工作时数: "; cin >> num; ptf = forSc; salary = calculate(num, ptf); cout << "您的薪水是: " << salary << endl; break; case 0: cout << "退出程序!\n"; break; } if (choice == 0) break; 图5.20例5.16程序的运行结果 } } 程序主函数main用一个while循环输出一个菜单,程序的用户根据自己的类型做出选择。例如,如果用户是工人类型的员工,则输入整数2; 如果是销售人员,则输入1; 如果要退出程序,则输入0。程序根据用户的输入,使用函数指针ptf指向不同的计算工资函数,然后再把该函数指针作为参数传递给函数calculate。函数calculate使用传入的函数指针和int型参数num调用不同的函数来计算不同类型员工的薪水。例5.16程序的运行结果如图5.20所示。 5.9函数重载 在例5.1中定义了一个函数max,用于比较两个int型数的大小。现在如果要创建一组函数,分别找出两个short型整数、两个long型整数、两个float型实数、两个double型实数、三个int型数、三个float型实数、……、中的最大值。可以看出,它们是一组功能相近的函数。为了区分它们,可以给它们取各不相同的函数名,例如,比较两个int型数的函数取名为imax2、比较三个int型数的函数取名为imax3、比较两个float型数的函数取名为fmax2、……但这样做会给编程带来不便,因为编程者必须记忆大量的函数名。解决问题的方法是函数重载。 C++允许在相同的作用域中定义函数名相同但参数形式不同的多个函数,参数形式不同是指: 要么参数的类型不同,要么参数的个数不同。这样的编程技术称为函数重载,这组同名的函数称为重载的函数。例如,下面是三个重载的max函数,分别找出两个int型数、两个float型数和三个int型数中最大的一个。 int max(int num1,int num2) { return (num1>=num2)? num1:num2; } float max(float num1,float num2) { return (num1>=num2)? num1:num2; } int max(int num1,int num2,int num3) { return (max(num1,num2)>=num3)? max(num1,num2):num3; } 调用重载函数时,C++编译器根据实参的类型和实参的个数找到匹配的重载函数进行调用。例如: float fmax,f1,f2; … fm=max(f1,f2); 上边的语句fm=max(f1,f2);调用重载函数max,由于f1和f2是两个float型变量,所以编译器选择函数float max(float num1,float num2)进行调用。 使用重载函数时应注意以下几点。 (1) 重载的函数应具有类似的功能,例如,上边定义的一组重载函数max都返回两个或三个数中较大的一个。如果两个函数的功能区别很大,则不应定义为重载函数。 (2) 只能以参数的类型来重载函数,而不能以函数返回值的类型来重载函数。例如,若按下面的方式重载max函数,则会产生编译错误。 int max(int num1,int num2); float max(int num1,int num2); (3) 不能用形参的名字来重载函数。例如,下边的重载声明也是错误的。 int max(int num1,int num2); int max(int n1,int n2); (4) 如果形参为引用类型或指针类型,则可以使用关键字const来重载函数。例如,下边的重载声明是正确的。 int max(int &num1,int &num2); int max(const int &num1,const int &num2); 原因请读者思考。 例5.17使用重载函数。 #include using namespace std; int max(int num1,int num2); float max(float num1,float num2); int max(int num1,int num2,int num3); void main() { int i1,i2,i3; float f1,f2; cout<<"请输入两个整数: "; cin>>i1>>i2; cout<<"两个整数中较大的一个是: "<>f1>>f2; cout<<"两个实数中较大的一个是: "<>i1>>i2>>i3; cout<<"三个整数中最大的一个是: "<=num2)? num1:num2; } float max(float num1,float num2) { return (num1>=num2)? num1:num2; } int max(int num1,int num2,int num3) { return (max(num1,num2)>=num3)? max(num1,num2):num3; } 例5.17程序的运行结果如图5.21所示。 图5.21例5.17程序的运行结果 5.10函数模板 5.9节中学习了函数重载技术,以下利用重载技术定义几个函数。 int max(int num1,int num2)//返回两个int型参数中较大的一个 { int themax; themax = (num1>=num2)? num1: num2; return themax; } float max(float num1,float num2) //返回两个float型参数中较大的一个 { float themax; themax = (num1>=num2)? num1: num2; return themax; } double max(double num1,double num2) //返回两个double型参数中较大的一个 { double themax; themax = (num1>=num2)? num1: num2; return themax; } 仔细观察这组重载函数,不难发现,函数中除了函数返回值、形参和局部变量的数据类型不同外,其他的语句内容完全相同。 也就是说,这组函数是将相同的算法应用于不同的数据类型。但在编程时,却不得不给出每一个函数的实现,把相同的语句书写了多遍,降低了编程的效率。那么能否用一个函数来取代这一组函数呢?回答是能。C++提供了函数模板技术来解决这类问题。 函数模板就是将具体函数中的数据类型参数化,即用通用的参数取代函数中具体的数据类型,从而形成一个通用模板来代表数据类型不同的一组函数。定义函数模板的语法如下: template 返回值类型 函数名(用模板参数取代具体类型的形参列表) { 用模板参数取代具体数据类型的函数体; } 其中,template和class是定义模板函数时必须使用的C++关键字,class也可以用关键字typename代替。template后的尖括号称为模板参数列表; 其中的T1,T2,…,Tn称为模板参数——参数化的数据类型。函数的返回值类型、形参的数据类型和局部变量的数据类型可以是具体数据类型,也可以是模板参数类型。每一个模板参数在函数定义中应至少使用一次。 现在在程序中,就可以用下面的函数模板取代上面的一组max函数。 template T max(T num1, T num2) { T themax; themax = (num1>=num2)? num1: num2; return themax; } 在这个函数模板中,用模板参数T取代具体数据类型创建了一个通用的函数模板。 可以看到,一个函数模板可以代表一组函数,这就极大地增强了程序代码的重用性,提高了编程效率。 那么,函数模板是怎样转换成具体函数的呢?结合下面的例子来进行说明。 例5.18定义和使用函数模板。 #include using namespace std; template T max(T num1, T num2);//声明函数模板 void main() { int i1,i2,imax; float f1,f2,fmax; cout<<"请输入两个整数: "; cin>>i1>>i2; imax=max(i1,i2); cout<<"两个整数中较大的一个是: "<>f1>>f2; fmax=max(f1,f2); cout<<"两个实数中较大的一个是: "< T max(T num1, T num2) { T themax; themax = (num1>=num2)? num1: num2; return themax; } C++编译器在编译函数调用语句时,用实参的数据类型取代函数模板中的模板参数创建一个具体的函数,并对其进行编译。例如,在上面的程序中,当编译器编译语句imax=max(i1,i2);时,就用实参i1和i2的数据类型——int取代函数模板中的参数T,创建一个具体的函数用来返回两个整型参数中较大的一个: int max(int num1,int num2){ int themax; themax = (num1>=num2)? num1: num2; return themax; } 编译器编译这个函数并实现调用。当编译语句fmax=max(f1,f2);时,由于实参f1和f2为float型变量,所以编译器用float取代函数模板中的模板参数T,再次创建了一个具体的函数来返回两个float型参数中较大的一个: float max(float num1,float num2){ float themax; themax = (num1>=num2)? num1: num2; return themax; } 编译器编译这个函数并实现本次调用。 例5.18程序的运行结果如图5.22所示。 图5.22例5.18程序的运行结果 5.11lambda函数 lambda函数又称为lambda表达式,是C++11/ C++14引入的一种新的元素。它本质上是一种匿名函数。使用lambda函数的好处是可以用简洁的程序语句完成强大的函数功能。 5.11.1定义和使用lambda函数 定义一个lambda函数的语法形式如下: [捕获列表](参数列表)->返回值类型{函数体;}; 其中: (1) 捕获列表: 是一个可能为空的列表,作用是指明定义lambda函数的作用域中的哪些对象(包括变量、常量等)可以在lambda函数中使用,以及它们在lambda函数中的存在方式是值的拷贝还是变量的引用。 (2) 参数列表: 是lambda函数的参数列表,形式和普通函数的参数列表相同。如果一个lambda函数没有参数,则小括号中为空,也可以省略小括号。 (3) ->返回值类型: lambda函数定义中的这部分内容表示函数的返回值类型,是可以省略的。因为编译器可以根据函数的内容自动推定函数的返回值类型。 (4) {函数体;}: 花括号中是lambda函数的函数体。 例如: auto f1=[](int i,int j)->int{ return i>j? i:j;}; auto f2=[](int i,int j){ return i>j? i:j;}; 上面两条语句定义了两个lambda函数f1和f2,它们的功能是相同的,都是返回两个整型参数中较大的一个。必须使用关键字auto来定义lambda函数。 这样定义之后,可以使用下面的语句来调用它们。 int max=f1(10,15); int max=f2(10,15); 例5.19定义一个lambda函数,返回两个整型参数中较大的一个。 ` #include using namespace std; void main() { int max, one, tow; cout << "请输入两个整数: "; cin >> one >> tow; auto f = [](int i, int j)->int { return i > j ? i : j;}; max = f(one, tow); cout << "两个整数中比较大的是" << max << endl; } 上面程序中先定义了一个lambda函数f,然后用两个整型变量作为参数调用该函数。例5.19程序的运行结果如图5.23所示。 图5.23例5.19程序的运行结果 5.11.2lambda函数的捕获列表 定义lambda函数时,方括号[]中的内容称为捕获列表,它的作用是确定lambda函数所在的作用域中的哪些对象(包括变量或常量)能在lambda函数中使用,以及它们在lambda函数中是值的拷贝还是变量的引用。以下是lambda函数捕获列表的使用方式。 (1) []: 捕获列表为空,意味着在lambda函数中不能使用其外层作用域中的任何对象。 (2) [&]: 表示在lambda函数中可以以引用的方式使用其外层作用域中的所有局部对象。 (3) [=]: 表示在lambda函数中可以以值的拷贝的方式使用其外层作用域中的所有局部对象。 (4) [a,&b]: 显式地捕获a和b,表示在lambda函数中可以以值的拷贝(传值)的方式使用其外层作用域中的对象(变量)a,可以以引用的方式使用其外层作用域中的对象(变量)b。 (5) [this]: 表示在lambda函数中可以以值传递的方式使用this指针。关于this指针的相关内容,将在后续章节中介绍。 (6) [&,a,b]: 表示在lambda函数中可以以值传递的方式使用其外层作用域中的变量a和b,以引用的方式使用其外层作用域中其他所有对象。 (7) [=,&a,&b,&c]: 表示在lambda函数中可以以引用的方式使用其外层作用域中的变量a、b和c,以传值的方式使用其外层作用域中其他所有对象。 例如,对于例5.19中的程序,可按如下几种方式对定义和调用lambda函数的两条语句进行修改,修改后程序的功能不变。 (1) auto f = [&]() {return one > tow ? one : tow;}; max = f(); (2) auto f = [=]() {return one > tow ? one : tow;}; max = f(); (3) auto f = [&one, tow]() {return one > tow ? one : tow;}; max = f(); 第(1)种修改方式的捕获列表为&,表示在lambda函数中可以以引用的方式访问其外层作用域中定义的局部变量one和tow。 第(2)种修改方式的捕获列表为=,表示在lambda函数中可以以值的拷贝的方式访问其外层作用域中定义的局部变量one和tow。即在lambda函数中存在变量one和tow的值的拷贝。 第(3)种修改方式的捕获列表为[&one, tow],它显式地指出,在lambda函数中可以以引用的方式访问其外层作用域中定义的局部变量one,且lambda函数中存在其外层作用域中定义的局部变量tow的拷贝。 上面这三种修改方式都使得可以不用为lambda函数传递参数,故lambda函数的参数列表为空。例5.20是使用上述第(2)种方式对例5.19的程序所做的修改。 例5.20使用lambda函数的捕获列表。 #include using namespace std; void main() { int max, one, tow; cout << "请输入两个整数: "; cin >> one >> tow; auto f = [=]() {return one > tow ? one : tow;}; max = f(); cout << "两个整数中比较大的是" << max << endl ; } 5.11.3lambda函数作为函数的参数 lambda函数本身可以作为其他函数的参数,这种功能类似于函数指针(5.8.3节)。但是用lambda函数实现比用函数指针更加简洁,效率更高。 使用lambda函数作函数参数的一个关键问题是: 被调函数的参数形式。即什么类型的形式参数可以接收一个作为实参的lambda函数。常用的解决方法有以下两种。 (1) 把被调函数设计成模板函数,用模板参数作为形式参数接收作为实参的lambda函数。这种方法就是让编译器去自行推定参数类型,比较简单。 对于例5.16中的计算员工薪水的程序,可做下述修改。 首先把例5.16程序中的通用函数calculate设计成一个模板函数。在原来的程序中,它的一个形参是函数指针。下面是修改前和修改后的calculate函数的代码。 修改前的calculate函数代码: double calculate(int num, double(*ptf)(int)) { return ptf(num); } 修改后的calculate函数代码: template double calculate(int num, F f) { return f(num); } 下一步把原程序中分别计算三种不同类型员工薪水的三个函数forSale、forWork和forSc设计成三个lambda函数,并把它们作为参数直接传递给函数calculate。 修改后程序的完整代码如例5.21所示。 例5.21修改例5.16中的程序,使用lambda作为参数,实现计算员工薪水的功能。 #include using namespace std; template double calculate(int num, F f)// .............................................................(1) { return f(num); //............................................................ (2) } void main() { double salary; int num, choice; double(*ptf)(int num); while (true) { cout << "1.销售人员\n"; cout << "2.工人\n"; cout << "3.研发人员\n"; cout << "0.退出程序\n"; cout << "请输入您的选择: "; cin >> choice; switch (choice) { case 1: cout << "请输入销售员销售的产品个数: "; cin >> num; salary = calculate(num, [](int n) ->double { return 10.0*n + 2000; });// ....(3) cout << "您的薪水是: " << salary << endl; break; case 2: cout << "请输入工人的工作时数: "; cin >> num; salary = calculate(num, [](int n) ->double { return 50.0*n;}); / /............(4) cout << "您的薪水是: " << salary << endl; break; case 3: cout << "请输入研发人员的工作时数: "; cin >> num; salary = calculate(num, [](int n) { return n * 15.0 + 3000; }); //.............(5) cout << "您的薪水是: " << salary << endl; break; case 0: cout << "退出程序!\n"; break; } if (choice == 0) break; } } 上面程序中注释为(3)(4)和(5)的三条语句用lambda函数作为实参,把它们传递给注释为(1)的模板函数calculate。函数calculate使用参数f接收lambda函数,并调用它们计算不同类型员工的工资,再把计算结果作为函数的返回值返回给main函数。调用lambda函数的语句注释为(2)。程序的功能和例5.16中程序的功能完全相同。但和原来的程序相比,修改后的程序更加简洁,执行效率更高。 (2) 第二种在函数中接收作为实参的lambda函数的方法是使用C++11新引进的function类。function类是一个模板类,本书到目前为止还没有涉及有关类的内容,在此读者只需了解怎样使用function类的对象引用lambda函数和作为函数的形参接收lambda函数,以及使用function类的对象调用lambda函数的方法。有关模板类的相关内容将会在后续章节介绍。 类是由程序员自己定义的一种抽象数据类型,类的对象相当于这种类型的一个变量。类和对象的关系类似于结构体类型和结构体变量之间的关系。 C++11新引入了一个类function,它是一个在名字空间std中定义的模板类。该类的对象称为可调用对象,一个可调用对象可以被绑定到任意一个特定的函数,只要这个函数的声明形式符合可调用对象的声明形式,通过该对象即可调用它绑定到的函数。例如: functionf1; 上面的语句定义了一个function类的对象f1,该对象可以被绑定到的函数都必须具有以下的形式: 函数的返回值为double类型,函数有一个int型的形式参数。再如: functionf2; 上面的语句定义了一个function类的对象f2,该对象可以被绑定到的函数都必须具有以下的形式: 函数没有返回值(void型),函数有两个形式参数,第一个形参的类型为double,第二个形参的类型为int。 定义了function对象之后,就可以把该对象绑定到某个特定的函数,假设程序中声明了两个函数fun1和fun2,下面是这两个函数的原型声明: double fun1(int); void fun2(double,int); 则可以把前面定义的function类的对象f1和f2绑定到这两个函数: f1=fun1; f2=fun2; 接下来,就可以使用对象f1和f2去调用函数fun1和fun2了: double re=f1(100); f2(12.34,100); 可以将function类的对象绑定到一个lambda函数,然后再使用该对象调用该lambda函数。例如: f1=[](int n)->double{ return 10.5*n;}; double lr=f1(10); function类的对象还可以作为函数的形参接收作为实参的lambda函数。 例5.22修改例5.21中的程序,使用function类的对象作为函数的形式参数接收作为实参的lambda函数。 本程序只需对例5.21中的程序稍做修改,在例5.21程序中,函数calculate被定义为一个模板函数,其第二个参数的数据类型由模板参数指定。在本例程序中,函数calculate不是模板函数,且它的第二个参数是一个function类的对象,用来接收从主调函数传递来的lambda函数,并在函数calculate中,使用这个function类的对象去调用作为实参的lambda函数。完整的程序代码如下 。 #include #include using namespace std; double calculate(int num, function f)// ....................................(1) { return f(num); //............................................................ (2) } void main() { double salary; int num, choice; while (true) { cout << "1.销售人员\n"; cout << "2.工人\n"; cout << "3.研发人员\n"; cout << "0.退出程序\n"; cout << "请输入您的选择: "; cin >> choice; switch (choice) { case 1: cout << "请输入销售员销售的产品个数: "; cin >> num; salary = calculate(num, [](int n) ->double { return 10.0*n + 2000; }); //....(3) cout << "您的薪水是: " << salary << endl; break; case 2: cout << "请输入工人的工作时数: "; cin >> num; salary = calculate(num, [](int n) ->double { return 50.0*n;}); //............(4) cout << "您的薪水是: " << salary << endl; break; case 3: cout << "请输入研发人员的工作时数: "; cin >> num; salary = calculate(num, [](int n) { return n * 15.0 + 3000; }); //.............(5) cout << "您的薪水是: " << salary << endl; break; case 0: cout << "退出程序!\n"; break; } if (choice == 0) break; } } 和例5.21中的程序相比,本例中的程序只做了两点细微的修改。 (1) 把函数calculate从模板函数修改为普通函数。 (2) 例5.21程序中,函数calculate的第二个形式参数的数据类型是模板参数F,而本例的程序中,函数calculate的第二个形式参数是function类的一个对象。 例5.16、例5.21和例5.22这三个程序使用三种不同的技术,实现完全相同的功能。例5.16使用函数指针来传递函数型参数,并实现函数回调; 例5.21使用模板函数结合lambda函数来实现同样的功能; 而例5.22则使用lambda函数结合function类来实现。相比于函数指针,后两种方法更加简单明了,逻辑清晰。 5.12具有可变长参数的函数 C++11引入了定义和调用具有可变长参数函数的方法,这种函数可以接收可变数量的参数,且不同参数的数据类型也可以不同。C++11使用模板函数来定义这种具有可变长参数的函数,为了定义这种函数,C++11还引入了一个新的操作符和两个新的程序元素。下面分别介绍。 (1) 元操作符: 元操作符用一个省略号…来表示,表示具有0或多个参数的参数包。 (2) 模板参数包: 是由0个或多个模板参数组成的参数包。 (3) 函数参数包: 是由0个或多个参数组成的参数包。 C++11用函数参数包来表示可变长的参数,用模板参数包表示这些参数的相应的数据类型。例如: template void display(Args… args) { //函数要显示每个参数的值,但是怎么得到这些参数呢? } 上面的程序语句定义了一个模板函数,该函数的所有参数的数据类型都被包含在模板参数包Args中,而该函数的所有参数都包含在函数参数包args中。该函数就是一个具有可变长参数的模板函数。 定义了具有可变长参数的函数后,面临的一个问题是: 在函数内部怎样展开函数参数包,并获取其中的每个参数呢? 函数递归调用是解决这个问题的一个有效方法。可把上面的函数定义形式稍做变形,得到下面的定义形式。 template void display(T t, typename… args) { cout< void display(T t) { cout << t << endl; } 例5.23设计具有可变长参数的函数,输出所有参数的值。 #include using namespace std; template void display(T t, Args… args) { cout << t << ","; display(args…); } template void display(T t) { cout << t << endl; } void main() { display(12.5, 50, 'a', "Hello"); } 程序在main函数中使用了4个不同类型的参数调用了模板函数display。 例5.23程序的运行结果如图5.24所示。 图5.24例5.23程序的运行结果 例5.24要求设计一个具有可变长参数的函数,所有的参数都具有相同的数据类型,该函数返回这些参数的累加和。 算法思想: 根据本节前面介绍的设计具有可变长参数函数的方法可知: (1) 本函数应该是一个模板函数。 (2) 由于要使用可变长参数,所以在函数中要使用模板参数包和函数参数包。 (3) 为了能用递归调用的方法展开函数参数包,函数的模板参数列表和函数参数列表都应由两个部分组成: 第一部分是调用函数时,传递给函数的最左边的参数; 第二部分是代表可变长参数的模板参数包和函数参数包。 (4) 所有参数的累加和=第一个参数(最左边的参数)+其他参数的累加和。 (5) 当调用函数的参数个数只剩一个的时候,参数的累加和就是该参数本身。 根据以上5步分析,可以设计出如下的两个重载函数。 第一个重载函数: 用参数包表示可变长参数的递归函数sum,代码如下。 template T sum(T t, Args… args) { return t + sum(args…); //对自己的递归调用,累加和=第一个参数+其他参数的累加和 } 第二个重载函数: 是只有一个模板参数的重载函数sum,它是第一个函数递归调用的终点。代码如下。 T sum(T t) { return t; } 下面是程序的完整代码。 #include using namespace std; template T sum(T t, Args… args) { return t + sum(args…); } template T sum(T t) { return t; } void main() { int sui; sui = sum(1, 2, 3); cout <<"三个整数的和为: "<< sui << endl; sui = sum(1, 2, 3, 4, 5); cout << "五个整数的和为: "<