?第???5???章?
  
函    数
  
  
5.1  引例与概述
5.1.1  引例
  例5.1.1  处理并输出
  先输入整数n(n<100),然后再输入n个整数。请完成以下任务。
  (1)输出这些整数。
  (2)把这些整数逆置后输出。
  (3)把这些整数升序排列并输出。
  输出时,每两个数据之间留一个空格。
  输入格式:
  测试数据有多组,处理到文件尾。每组测试输入两行,第一行输入n(1<n<100),第二行输入n个整数。
  输出格式:
  对于每组测试,输出三行,第一行直接输出所输入的n个整数,第二行输出逆置后的n个整数,第三行输出升序排列后的n个整数。每行的每两个数据之间留一个空格。
  输入样例:
  
5
3 2 1 5 4
  
  输出样例:
  
3 2 1 5 4
4 5 1 2 3
1 2 3 4 5
  
  本题宜用数组处理。而题中3个任务每个都要求输出,可把输出的代码重复使用3次,但这样的代码较冗长,编码效率较低。我们应勤学善思,提高效率意识和规范意识。当一段代码需要重复多次使用时,通常会考虑能否把这段代码独立出来作为一个整体,这就需要用到自定义函数。具体代码如下:
  
#include<bits/stdc++.h>         	//万能头文件
using namespace std;
const int N=100;
void prtArray(int a[], int n) {	//函数定义,输出数组元素的函数
    for(int i=0; i<n; i++) {
        if(i!=0) cout<<" ";
        cout<<a[i];
    }
    cout<<endl;
}
int main() {
    int a[N],n,j,k;
    while(cin>>n) {
        for(j=0; j<n; j++) cin>>a[j];
        prtArray(a, n);         	//第1次调用自定义函数
        for(k=0; k<n/2; k++) {
            swap(a[k],a[n-1-k]);
        }
        prtArray(a, n);         	//第2次调用自定义函数
        sort(a, a+n);         		//调用系统函数,排序区间[&a[0], &a[n-1]+1)
        prtArray(a, n);         	//第3次调用自定义函数    
    }
    return 0;
}
  
  这里定义了一个自定义函数prtArray,用于输出包含n个元素的一维数组,数据之间留一个空格,然后调用该函数(函数必须被调用后才有效果)3次完成3个任务中的输出。这种代码实现显然更加简洁。实际上,一些常用功能即使在一个程序中不被调用多次,也经常写成一个一个自定义函数。例如,判断一个数是否是素数,求两个整数的最大公约数或最小公倍数、二分查找及排序等。
  另外,这个程序用了C++的万能头文件“bits/stdc++.h”,包含这个头文件相当于包含了C++所有的头文件,省去需包含各种头文件的麻烦。例如,本题中使用了系统函数sort,本来应该包含头文件algorithm,但有了万能头文件就不用再写该头文件了。使用万能头文件的C++程序只需要使用以下两句,而不用再包含其他头文件:
  
#include<bits/stdc++.h>       	//万能头文件
using namespace std;           	//引入std命名空间
  
  需要注意的是,虽然Dev-C++编译环境和目前很多高校的OJ都支持万能头文件,但也有些OJ和编译环境(如VC6、VC2010等)不支持万能头文件,建议使用新接触的IDE时或在线做题及程序设计竞赛之前先做测试。通用起见,本书代码一般不使用万能头文件,读者写代码时可自行决定是否使用万能头文件。
5.1.2  概述
  简言之,函数是一组相关语句组织在一起所构成的整体,并以函数名标注。
  从用户的角度而言,函数分为库函数(系统函数)和用户自定义函数。库函数有很多,例如,在math.h中的sqrt、fabs、pow、ceil、floor、round等,调用示例如下:
  
sqrt(9.0)    ?					//得到3.0,求平方根
fabs(-3.5)    					//得到3.5,求实数的绝对值
pow(2,3)    ?					//得到8.0,求幂
ceil(3.1)    ?					//得到4.0,即不小于参数的最小整数
floor(3.9)    					//得到3.0,即不大于参数的最大整数
round(3.56)    					//得到4.0,即对参数四舍五入取整
  
  因以上函数的返回类型都是double,故结果都表示为包含小数点的实数。规范起见,建议这些函数的参数也用double类型。
  又如,在stdlib.h中的abs、srand、rand、malloc、free等,调用示例如下:
  
abs(-1)                        		//得到1,整数的绝对值
srand(time(NULL));             	//设置随机数生成器的种子,time需包含头文件time.h
int a=rand();                		//产生0~RAND_MAX(32767)之间的一个随机整数
int b=20+rand()%(80-20+1);   	//产生20~80之间的一个随机整数
int *a=(int *) malloc(5*sizeof(int));     	//申请5个int类型元素的动态数组
free(a);                                       	//释放用malloc申请的动态数组
  
  注意,库函数使用时须包含相应头文件。另外,在Dev-C++下也能调用max(求两者中的大者)、min(求两者中的小者)、stoi(将数字字符串转换为整数)、to_string(将数值转换为字符串)等系统函数。其中,后两者是C++ 11标准下的函数,使用前需添加编译命令“?std=c++11”(设置路径详见例4.6.4)。读者可以自行查阅并测试感兴趣的系统函数。
  本章主要介绍用户自定义函数。
  一个大的程序一般分为若干个程序模块,每个模块用来实现一个特定的功能,每个模块一般写一个函数定义来实现(需调用)。
  函数是C/C++程序的基本构成单位,一个C/C++程序由一个主函数main及若干其他函数构成。C/C++程序从main 函数中开始执行,在main函数中结束。
  函数的作用是通过函数调用实现的。操作系统调用主函数,主函数调用其他函数,其他函数可以调用主函数之外的其他函数。同一函数可以被一个或几个函数调用任意次。
  如图5-1所示,是一个函数的调用示意图。
  

图5-1?函数的调用示意图
  由图5-1可见,main函数调用f?1、f?2函数,f?1函数调用f?3函数,f?3函数调用结束返回f?1函数中的调用点,f?1函数调用结束返回到main函数中的调用点,f?2函数调用结束返回到main函数中的调用点。


  
5.2  函数基本用法
5.2.1  函数的定义
  函数定义由函数头和函数体两部分组成。一般形式如下:
  类型 函数名([形参列表]) { 
       函数体
  }
  说明:
  (1)“函数类型函数名([形参列表])”是函数头,{}中的是函数体。
  (2)函数类型(也称返回类型)可以是各种基本数据类型、指针类型、结构体类型、void(空类型,明确指定函数不返回值)等。C语言的函数默认返回类型为int,但Dev-C++、VC2010等编译环境都不支持默认返回类型。建议明确指定函数的返回类型。
  (3)函数名必须是合法的标识符。
  (4)函数定义中的参数为形式参数,简称形参。根据是否有形参,函数可分为带参函数和无参函数。形参列表的每个参数包括参数类型和参数名,形参列表若有多个参数,则以逗号分开。C++支持参数带默认值,默认值参数须放在最右侧。
  (5)根据是否有返回值,函数可分为有返回值函数和无返回值函数。通过函数中的    return语句返回函数的返回值。return语句的一般格式如下:
  return [返回值表达式];    
其中,返回值表达式的类型一般应与返回类型一致,否则以返回类型为准。语句“return;”控制程序流程返回到调用点;若return语句后带返回值表达式,则在控制程序流程返回调用点的同时带回一个值。
  下面给出几个函数定义的例子:
  
void print() {                          	//无参函数,也没有返回值,返回类型用void
    cout<<"hello\n"<<endl;
}
int max(int a, int b) {                ?	//求两个整数的最大值
    return a>=b?a:b;
}
//重载函数:C++中参数不同的若干同名函数
string max(string a, string b) {      	//求两个字符串的最大值
    return a>=b?a:b;
}
//C++默认值参数的函数,带默认值的参数处于最右侧
int max3nums(int a, int b=2, int c=3){	//求三个整数的最大值
    int t=max(a, b);                    ?	//嵌套调用max(int, int)
    return max(t, c);
}
5.2.2  函数的声明
  若函数定义在函数调用之前,则定义时的函数头可以充当函数声明(或称函数原型);否则函数调用之前必须先进行函数声明,形式如下:
  函数类型?函数名([形参列表]);
  即以函数定义时的函数头加分号表示函数声明,其中,形参列表中的形参名可以省略。
  例如5.2.1节定义的max函数声明如下:
  
int max(int a, int b);
string max(string a, string b);
  
或
  
int max(int, int);                        	//省略形参名
string max(string, string);            ?	//省略形参名
5.2.3  函数的调用
  函数调用的形式一般如下:
  [变量=]函数名([实际参数表])[;]
  void返回类型的函数只能以语句形式调用,其他返回类型的函数一般以表达式形式调用,否则其返回值没有意义。
  调用时的参数称为实际参数,简称实参,一般不需要指定数据类型,除非是进行强制类型转换。参数的类型、顺序、个数必须与函数定义中的一致,但带默认值参数的函数调用时实参个数可以与形参个数不一致。
  函数调用时,把实参依序传递给形参,然后执行函数定义体中的语句,执行到return语句或函数结束时,程序流程返回到调用点。
  例如,调用5.2.1节定义的函数的方法如下:
  
print();                                   	//void返回类型的函数以语句形式调用
int t=max(123,99);                       	//有返回值的函数一般以表达式形式调用
cout<<max(1, 2)<<endl;                    	//调用max(int, int)
cout<<max("abc", "cdfg")<<endl;       	//调用max(string, string)
cout<<max3nums(1)                        	//实参为1,2,3,后两个参数使用默认值
     <<" "<<max3nums(4,5)               	//实参为4,5,3,后一个参数使用默认值
     <<" "<<max3nums(5,3,4)<<endl;    	//实参为5,3,4,不使用默认值
  
  max3nums函数共有3个参数,其中2个带默认值,调用时可提供1、2、3个实参。
  函数调用也可用变量作为实参,主调函数中的实参和被调函数中形参可以同名,但它们实际上是局限于各自所在函数的不同变量。
  在OJ做题或程序设计竞赛时,题目通常有多组测试数据,可以把一组测试的代码单独作为一个函数处理,然后在循环中调用该函数完成多组测试。
5.3  函数举例
  例5.3.1  逆序数的逆序和
  输入两个正整数,先将它们分别倒过来,然后再相加,最后再将结果倒过来输出。注意:前置的零将被忽略。例如,输入305和794,倒过来相加得到1000,输出时只要输出1就可以了。
  因为求逆序数的方法是一样的,可以编写一个求逆序数的函数,调用3次即可完成2个输入的整数及1个结果整数的逆置。
  思考:当x=1234,如何得到x的逆序数?
  设r为x的逆序数,可以这样考虑:r=((4×10+3)×10+2)×10+1=4321,即让r一开始为0,再不断把x的个位取出来加上r×10重新赋值给r,直到x为0(通过x=x/10不断去掉个位数)。具体代码如下:
  
#include<iostream>
using namespace std;
int reverseNum(int x) {        ?	//构成逆序数的函数,x是正整数
    int r=0;
    while(x>0) {
        r=r*10+x%10;            ?	//移位后右边加上x的个位数
        x=x/10;
    }
    return r;
}
int main() {
    int a,b;
    cin>>a>>b;
    int c=reverseNum(a)+reverseNum(b);
    cout<<reverseNum(c)<<endl;
    return 0;
}
  
  本例所写的reverseNum能够忽略前导0,原因请读者自行分析。
  例5.3.2  素数判定函数
  输入一个正整数n,判断n是否是素数,是则输出yes,否则输出no。要求写一个判断一个正整数是否是素数的函数。
  关于n是否是素数,已知可以从2至判断是否有n的因子,若有则不是素数。这里只要把相关代码作为一个整体写成一个函数。因为结果只有两种可能(是或否),所以返回类型设为bool(只有true、false两个值);而n是要被判断的数,因此需要一个整型参数。具体代码如下:
  
#include<iostream>
#include<cmath>                ?	//系统函数sqrt求开方数须包含此头文件
using namespace std;
bool isPrime(int n) {            	//判断n是否是素数,若是则返回true,否则返回false
    bool flag=true;            ?	//一开始假设是素数,标记变量初值设为true
    double limit=sqrt(n);
    for(int i=2; i<=limit; i++) {
        if(n%i==0) {            	//若有因子,则可以判断n不是素数
            flag=false;
            break;
        }
    }
    if(n==1) flag=false;    ?	//对1特判
    return flag;
}
int main() {
    int n;
    cin>>n;
    if(isPrime(n)==true)
        cout<<"yes"<<endl;
    else
        cout<<"no"<<endl;
    return 0;
}
  
  例5.3.3  最小回文数
  输入整数n,输出比该数大的最小回文数。其中,回文数指的是正读、反读一样的数,如131、1221等。要求写一个判断一个整数是否是回文数的函数。
  判断是否是回文数可以调用例5.3.1中的求逆序数的函数reverseNum,判断该数与逆序数是否相等。因为要找比n大的最小回文数,可以从n+1开始逐个尝试是否满足逆序数等于本身的条件,第一个满足条件的数即为结果。具体代码如下:
  
#include<iostream>
using namespace std;
int main() {
    bool isSymmetric(int); 		//定义在调用之后,须在调用前先声明
    int n;
    cin>>n;
    while(true) {
        n++;
        if(isSymmetric(n)==true) break;
    }
    cout<<n<<endl;
    return 0;
}
int reverseNum(int x) {     		//构成逆序数的函数,x 是正整数
    int r=0;
    while(x>0) {
        r=r*10+x%10;        		//移位后右边加上x的个位数
        x=x/10;
    }
    return r;
}
bool isSymmetric(int n) {?		//判断n是否是回文数,若是返回true,否则返回false
    if(n==reverseNum(n))    		//回文数的判断
        return true;
    else
        return false;
}
  
  实际上,函数isSymmetric可以简写为如下:
  
bool isSymmetric(int n) { 
    return n==reverseNum(n);    
}
  
  例5.3.4  大整数加法
  输入两个大正整数(长度可能达到1000位),求两者之和。
  两个大正整数加法的基本思路:两个大正整数作为字符串(用string类型变量)处理,加法根据“右对齐、逐位相加”的方法,关键在于右对齐相加及进位处理。其中,右对齐相加可以在把两个字符串逆置后从第一个字符开始相加。字符串的逆置可以写一个以字符串变量为形参的函数,需要注意的是,string类型形参的变化不会影响实参,因此通过返回值返回变化后的结果(实际上把string类型变量作为引用参数来返回结果更简单,读者可以在掌握引用之后自行修改)。在做加法时,拟用第一个字符串存放最终结果,因此需要保证其长度不小于第二个字符串,方法是判断两个字符串的长度,若前一个字符串短,则调用系统函数swap交换两个字符串。进位处理方面,可以用一个整型变量表示,其初值一开始设为0,在加法计算过程中把其加到和中,并不断更新为最新的进位。具体代码如下:
  
#include<iostream>
#include<string>
using namespace std;
string reverse(string s) {            ?	//逆置字符串
    int n=s.size(), mid=n/2;
    for(int i=0; i<mid; i++) {        	//以中间为界,两端字符交换
        swap(s[i],s[n-1-i]);
    }
    return s;
}
string bigAdd(string s, string t) {
    if(s.size()<t.size()) swap(s,t);	//若s短于t,则交换
    s=reverse(s);                        	//逆置s
    t=reverse(t);                        	//逆置t
    int carry=0;                        ?	//进位
    for(int i=0; i<s.size(); i++) {
        carry+=s[i]-'0';                	//把s[i]转换为整数加到carry中
        if(i<t.size())                ?		//若第二个字符串还没有结束
            carry+=t[i]-'0';        ?		//则把t[i]转换为整数加到carry中
        s[i]=carry%10+'0';            		//余数转换为数字字符存放在s[i]中
        carry/=10;                    ?		//保存新的进位
    }
    s=reverse(s);                        	//结果逆置
    if(carry>0) s="1"+s;            ?		//最后的进位
    return s;
}
int main() {
    string a,b;
    cin>>a>>b;
    cout<<bigAdd(a,b)<<endl;
    return 0;
}
  实际上,逆置字符串也可以调用algorithm头文件中的reverse函数实现。例如,逆置字符串s的代码如下:
  
reverse(s.begin(),s.end());
  
其中,两个参数对应的逆置区间为[s.begin(), s.end()),即此调用语句将逆置整个字符串s。
5.4  数组作为函数参数
5.4.1  数组元素作为实参
  数组元素也称下标变量,因此数组元素作为函数实参时,与普通变量作为函数实参是一致的:单向值传递,即只能把实参的值传递给形参,而不能再把形参的值传回给实参。
  例5.4.1  数组元素作为实参
  在一维数组a中存放10个整数,请输出它们的立方数。要求定义一个求立方数的函数。具体代码如下:
  
#include<iostream>
using namespace std;
int cubic(int?n) {                    	//自定义求立方数的函数
    return n*n*n;
}
int main() {
    int a[10], i, j;
    for(i=0; i<10; i++) cin>>a[i];
    for(i=0; i<10; i++) {
        cout<<cubic(a[i])<<endl;    ?		//数组元素作为实参
    }
    return 0;
}
5.4.2  数组名作为函数参数
  本小节讨论数组名作为函数的参数,即形参和实参都使用数组名。此时传递的是数组的首地址(数组名代表数组的首地址),即传地址。实际上,数组名作函数参数时参数传递依然是单向的,即由实参传递给形参,但由于在函数调用期间,形参数组与实参数组同占一段连续的存储单元,因此对形参数组的改变就是对实参数组的改变,即从效果上看,达到了双向传递的效果。
  例5.4.2?m趟选择排序
  输入n个整数构成的数列,要求利用选择排序进行排序,并输出第m趟排序后的数列状况。请把选择排序定义为一个函数。
  选择排序的思想和方法在前面的章节中已经讨论过,这里以函数的形式表达。具体代码如下:
  
#include<iostream>
using namespace std;
const int N=100;
//n个数,进行m趟排序
void selectSort(int a[], int n, int m) {
    for(int i=0; i<m; i++) {            ?	//控制0~m-1共m趟排序
        int k=i;
        for(int j=i+1; j<n; j++) {
            if(a[k]>a[j]) k=j;
        }
        if(k!=i) swap(a[k],a[i]);    ?	//直接调用系统函数swap交换
    }
}
void prt(int a[], int n) {               	//输出n个数据,每两个数据之间一个空格
    for(int i=0; i<n; i++) {
        if(i>0) cout<<" ";
        cout<<a[i];
    }
    cout<<endl;
}
int main() {
    int b[N], n, k;
    cin>>n>>k;
    for(int i=0; i<n; i++) cin>>b[i];
    selectSort(b, n, k);               	//第一个参数是数组名作为函数的实参
    prt(b, n);                             	//第一个参数是数组名作为函数的实参
    return 0;
}
  
  运行结果:
  
     6 3?
     3 5 1 2 8 6?
     1 2 3 5 8 6
  
  从运行结果可见,实参数组b在调用selectSort函数之后发生了改变,即形参数组a的改变影响到了实参数组b。因为数组名作为函数参数时,是将实参数组的首地址传给形参数组,所以,在函数调用期间?a[i]和?b[i](0≤i<n)同占一个存储单元,则对a[i]的改变就是对b[i]的改变。
  注意:
  (1)用数组名作为函数参数,应该在主调用函数和被调用函数中分别定义数组,本例中a是形参数组,b是实参数组,分别在其所在函数中定义,不能只在一方定义。
  (2)实参数组与形参数组类型应一致(本例都为int类型),如不一致,将出错。
  (3)string类型形参或者vector形参的改变不会影响实参,除非使用引用参数。
5.5  引用
  通俗地说,引用是对象(可以是变量、符号常量等)的“别名”。定义引用变量的格式如下: