5.1.3 函数参数的传递
我们知道,函数是用来完成某个功能的相对独立的一段代码。函数在完成这个功能的时候,往往需要外部数据的支持,这时就需要在调用这个函数时向它传递所需要的数据它才能完成这个功能获得结果。例如,当调用一个加法函数时,需要向它传递两个数作为加数和被加数,然后在它内部才能对这两个数进行计算获得加和结果。在定义一个函数的时候,如果这个函数需要跟外部进行数据交换,就需要在函数定义中加入形式参数表,以确定函数的调用者向函数传递的数据个数和具体类型。例如,可以这样定义需要两个int类型加数的加法函数:
// 声明并定义Add()函数 // 形式参数表确定了这个函数需要两个int类型的参数 int Add( int a, int b ) { return a + b; }
从Add()函数的声明中可以知道这个函数有两个int类型数作为参数,所以在调用的时候,就需要用两个int类型数作为实际的参数对其进行调用:
// 以1和2作为实际参数调用Add()函数 // 也就是向这个函数传递1和2这两个数据 int nRes = Add(1, 2);
我们将在定义函数时形式参数表中给出的参数称为形式参数,例如这里的a和b;而将在函数调用时函数名之后括号中给出的参数称为实际参数,例如这里的1和2。在执行函数调用的时候,系统会使用调用函数时给出的实际参数一一对应地对函数声明中的各个形式参数进行赋值。例如,在执行“Add(1,2)”函数调用时,这里1和2两个实际参数会分别被赋值给Add()函数的两个形式参数a和b。也就是说,在程序进入Add()函数内部执行时,a和b两个变量的值一开始就是1和2,这也就意味着,通过形式参数,我们将数据1和2 从函数的调用者传递进入了Add()函数内部。如图5-6所示。
图5-6 函数调用过程中的参数传递
在执行函数调用时,系统需要将实际参数复制给形式参数以实现数据的传递。可是如果要向函数内部传递一些比较大的数据,比如拥有多个数据元素的数组,这个复制过程会非常耗费时间而显著地降低程序的效率。为了提高效率,更多时候,我们用传递指向这个大体积数据的指针来代替传递这个数据本身。在函数内部,可以通过指针来访问它所指向的外部数据,同样可以达到向函数内部传递数据的目的。例如,在前面的工资程序中,当所有工资数据都输入到arrSalary数组后,需要将这些工资数据传递给GetAverage()函数来计算平均工资。这时,我们就可以向函数传递指向这个数组的指针,也就是数组名,来实现向函数传递整个数组的目的:
// 定义计算数组平均值的函数 float GetAverage(int* pArr,int nCount) { // 判断数据是否合法 if(nCount <= 0 || nullptr == pArr) { return 0; // 如果数据不合法,返回默认值 } // 计算平均值 int nTotal = 0; // 用for循环遍历数组,统计工资总数 for(int i = 0; i < nCount; ++i) { // 通过传递进来的数组首地址指针访问数组元素 nTotal += pArr[i]; } // 返回工资总数与数据个数的商,就是平均工资 return (float)nTotal/nCount; }
int main() { // 定义保存工资数据的数组 const int NUM = 100000; int arrSalary[NUM] = {0}; // 输入工资数据到数组… // 以数组名(数组首地址)和数据元素个数为实际参数调用函数 float fAver = GetAverage(arrSalary,NUM); cout<<"平均工资是:"<<fAver<<endl; return 0; }
在定义GetAverage()函数时,我们定义了两个形式参数,第一个“int*”类型的pArr表示指向数组首地址的指针。只要有了这个指针,在函数内部,我们就可以把它当作一个数组名直接通过它访问数组中的各个数据元素。例如在for循环中用“pArr[i]”的形式就可以访问到数组中的各个元素。因为第一个参数只是数组的首地址,并没有包含数组元素个数的信息,所以要想访问整个数组,还需要用第二个int类型的形式参数nCount来表示这个数组中数据元素的个数。这样,我们在函数内部就可以利用传递进来的数组首地址和元素个数,用for循环对整个数组进行遍历访问统计工资总数,进而计算工资平均值。在调用GetAverage()函数时,按照函数的声明要求,我们将数组名arrSalary,也就是指向数组首地址的指针,以及数组元素个数NUM作为实际参数对其进行调用。
// 以数组名和数据元素个数为实际参数调用函数 float fAver = GetAverage(arrSalary,NUM);
在执行这个函数调用的时候,实际参数arrSalary和NUM会分别被复制给形式参数pArr和nCount。这样,在GetAverage()函数内部就可以通过pArr和nCount访问arrSalary数组,也就是通过一个小小的指针向函数传递了一个大大的数组。整个过程不需要复制arrSalary数据的100000个int类型数据,取而代之的是4个字节的数组首地址指针,从而避免了大量数据的复制,提高了函数调用的效率。
知道更多:访问主函数的参数,接收命令行传递的数据
要向普通函数传递数据,我们可以在调用函数时通过函数参数的形式来完成。但是主函数不会被我们调用,如果我们想要向主函数传递数据,则要借助在执行程序时的命令行参数来完成。例如,我们想要传递两个加数给一个加法计算程序add.exe并让它计算结果,就可以以如下的命令形式来执行这个程序,它就会接收命令行中的两个加数并计算得到结果:
F:code>add.exe 3 4(回车) 3 + 4 = 7 (输出结果)
要想做到这一点,我们需要给main()主函数添加两个参数:int类型的argc和字符串指针数组类型的argv。当我们在命令行执行程序时,操作系统会根据我们的命令行指令对这两个参数分别赋值。其中,第一个参数argc就是命令行中指令的个数,包括程序名本身在内。在这里,对于“add.exe 3 4”这个命令行指令而言,argc的值就应该是3。而第二个参数argv实际上是一个字符串指针数组,其中的各个字符串指针,依次指向命令行中各个指令字符串。当然,也包括程序名在内。这样,argv[0]指向的就是“add.exe”这个字符串,而argv[1]指向的就是“3”这个字符串,依次类推。明白这些规则后,我们就可以在主函数中通过访问这两个参数,从而接收从命令行传递进来的数据:
#include <iostream> using namespace std; int main(int argc, char* argv[]) { // 根据指令的个数(argc),判断指令是否正确 // 如果不正确,则提示正确的使用方法 if(3 != argc) // 通过argc得到指令个数 { // 通过argv[0]得到程序的名字 cout<<"用法: "<<argv[0]<<" num1 num2"<<endl; return -1; // 命令行指令不合法,返回一个错误值 } // 如果指令正确,则通过argv访问命令行传递的数 // 通过atoi()函数, // 分别将argv[1]和argv[2]指向的字符串“3”和“4”转换为数字3和4 int a = atoi(argv[1]); int b = atoi(argv[2]); // 利用转换后的数据计算结果 int res = a + b; // 输出结果 // 这里,将命令行指令当作字符串来访问,直接输出 cout<<argv[1]<<" + "<<argv[2]<<" = "<<res<<endl; return 0; }
在主函数中,我们首先利用argc对命令行指令的个数进行了判断,以此来判断程序的执行方式是否正确。然后,就是从argv字符串指针数组中获取程序执行时的各个指令了。因为argv提供给我们的是命令行指令的字符串,如果是数字指令,我们还需要利用atoi()等转换函数,将字符串转换成对应的数值数据。在完成命令行指令从字符串到数字的转换之后,我们就可以将其用于计算并输出结果。在输出的时候,我们又将argv数组中的字符串指针用作字符串直接输出命令行的指令。
通过主函数的argc和argv参数,让我们可以接收来自命令行指令的数据,从而在执行程序的时候,对程序的行为进行控制(提供选项或者数据等),极大地增加了程序执行的灵活性。
5.1.4 函数的返回值
到这里我们已经知道了,函数就像一个具有某种功能的箱子,把原材料数据通过函数参数放进去,函数箱子经过一定的处理后,就得到了我们想要的结果数据。比如,把两个整数放到Add()函数箱子中,经过加和处理后得到的就是这两个整数的和。通过函数参数,我们可以把原材料数据放到函数箱子中去,那么我们又如何从函数箱子中取出结果数据呢?
还记得在声明函数的时候,需要指明这个函数的返回值类型吗?只要一个函数的返回值类型不是void,那它就具有返回值,而我们就是通过函数的返回值来从函数中取得结果数据的。下面还是以Add()函数为例:
int Add( int a, int b ) { // 计算结果数据 int res = a + b; // 利用return关键字返回结果数据 return res; } // 调用函数,获得计算结果 int nRes = Add(2,3);
在函数内部,我们首先对通过参数传递进来的原材料数据2和3进行加和计算,得到结果数据5,然后使用return关键字结束函数的执行并将结果数据(5)返回,而从调用这个函数的外部来看,这个结果数据也就是整个函数调用表达式“Add(2,3)”的值,进而,我们可以把这个值赋值给nRes变量,nRes变量的值变为5。这也就是说,通过返回值我们从Add()函数中取回了结果数据5。换句话说,函数调用表达式的值就是从函数箱子中取出的结果数据,这个数据的类型就是函数的返回值类型。
既然整个函数调用表达式可以看成是从函数得到的结果数据,拥有特定的数据类型,那么除了可以用它对变量进行赋值之外,还可以将其应用在任何可以使用此类型数值的地方直接参与计算。例如,函数调用表达式可以用在条件语句中表示某个复杂条件是否成立:
// IsPassed()函数的返回值是bool类型 // 它的调用表达式可以看成是一个bool类型的数据,可以直接与true进行逻辑运算 if( true == IsFinished()) { // … }
另外,对于返回值为bool类型的函数调用表达式,可以被看作是一个bool类型的数据,从而上面的代码还可以改写成下面这种更简洁的形式:
// 直接判断IsFinished()函数的返回值是否是true, // 如果我们要判断IsFinished()函数的返回值是否是false, // 则可以用if( !IsFinished() )的形式 if( IsFinished() ) { // … }
除此之外,函数调用表达式还可以应用在另一个函数调用表达式中,直接作为参数参与另一个函数调用。例如:
// 函数调用表达式Power(2)和Power(3)是整型数值, // 直接用做Add()函数的整型参数参与其调用 int nRes = Add( Power(2), Power(3) );
在执行计算的时候,会先分别计算Power(2)和Power(3)这两个函数调用表达式的值得到4和9,然后再以这两个数据作为参数调用Add()函数,得到最终结果13。这里值的提醒的是,这种把一个函数调用表达式当作某个数据直接参与计算的方式,虽然可以让代码更加简洁,但是却在一定程度上降低了代码的可读性,所以应该有选择地使用,避免形成过于复杂的表达式,达到代码简洁与可读性之间的平衡。
从以上代码可以注意到,每个函数只有唯一的返回值,使用函数返回值只能从函数中取出一个数据,如果想要从函数中取出多个数据又该怎么办呢?
回想一下,我们是如何将一个大体积的数据传入函数的?是的,我们使用了指针。利用指针的指代特性,可以在函数内部通过指针访问它所指向外部内存,读取其中的数据,从而间接地实现将函数外的数据传入函数。同样地,我们在通过指针访问它所指向的外部内存时,也可以将函数内的数据写入这个内存位置,从而间接地实现将函数内的数据传出函数。不好理解吗?没关系,来看一个实际的例子。在我们前面的工资程序中,我们需要用一个InputSalary()函数来负责工资数据的输入,这时就需要利用指针将函数内输入的工资数据传出函数:
// 输入员工的工资数据 int InputSalary(int* pArr, const int MAX_NUM ) { // 参数有效性检查… = 0; // 临时变量,暂存用户输入的数据 int nIndex = 0; // 输入的序号 do { cout<<"请输入第"<<nIndex<<"号员工的工资:"<<endl; cin>>nTemp; // 如果输入的是负数或零,表示输入工作结束,跳出输入循环 if ( nTemp <= 0 ) { break; } // 将合法的数据保存到数组中,开始下一次输入 // 通过指针将数据写入它指向的外部数组,实现数据的传出 pArr[nIndex] = nTemp; ++nIndex; } while ( nIndex < MAX_NUM ); // 返回输入的数据总个数 return nIndex; }
InputSalary()函数的第一个参数pArr指向的是函数外部用于保存工资数据的数组。这样在函数内部,我们就可以通过这个指针将用户输入的工资数据保存到它所指向的外部数组,从而间接实现了函数内部多个数据的传出。另外在这个函数中,还利用函数返回值从函数中得到了输入的数据总个数。这也表明,函数返回值和函数指针参数这两种方式都可以从函数中传出数据。它们可以单独使用,也可以混合使用。一般而言,函数返回值多用于从函数内返回单个小体积数据,比如某个基本数据类型的结果数据,而函数指针参数多用于从函数内返回多个或大体积数据,比如包含多个数据的数组或大体积的结构体。
现在,我们就可以利用InputSalary()函数将工资数据输入到arrSalary数组,然后再利用前面的GetAverage()函数来统计平均工资,实现工资程序对平均工资的统计功能:
int main() { // 定义保存工资数据的数组 t int NUM = 100000; int arrSalary[NUM] = {0}; // 输入工资数据到数组,用指针实现传出数据 int nCount = InputSalary(arrSalary,NUM); // 统计平均工资,用指针实现传入数据 float fAver = GetAverage(arrSalary,nCount); cout<<"平均工资是:"<<fAver<<endl; return 0; }
在这里,我们用数组名arrSalary作为InputSalary()函数的参数, 用于从函数内传出数据,而同样的arrSalary用作GetAverage()函数的参数,则是用于向函数内传入数据。这是因为,通过指针,既可以对它所指向的内存做写操作,从而将函数内的数据传出函数,同时也可以做读操作,从而将函数外的数据传入函数。指针参数既可以传出数据也可以传入数据,而至于到底是传出还是传入,取决于函数内部对它所指向内存的访问是写还是读,如图5-7所示。
图 5-7 通过指针实现函数中数据的传出与传入
从这个例子也可以看到,经过“自顶向下,逐步求精”的功能分解,我们将主函数中相对独立的输入功能和统计功能分别封装到了InputSalary()函数和GetAverage()函数中,实现了将复杂程序分解装箱的工作。经过这样的分解封装,之前比较复杂臃肿的主函数,现在只需要简单地调用这两个子函数就完成了所有功能。将程序装箱成函数,整个程序结构变得更加清晰,实现和维护都更加容易。