💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
## 2.3 函数II(Functions II) ### 参数按数值传递和按地址传递(Arguments passed by value and by reference) 到目前为止,我们看到的所有函数中,传递到函数中的参数全部是按数值传递的(by value)。也就是说,当我们调用一个带有参数的函数时,我们传递到函数中的是变量的数值而不是变量本身。 例如,假设我们用下面的代码调用我们的第一个函数addition : int x=5, y=3, z; z = addition ( x , y ); 在这个例子里我们调用函数addition 同时将**x**和**y**的值传给它,即分别为**5**和**3**,而不是两个变量: ![](https://box.kancloud.cn/2015-09-06_55ebefb5674ca.gif) 这样,当函数**addition**被调用时,它的变量**a**和**b**的值分别变为**5**和**3**,但在函数addition内对变量a 或b 所做的任何修改不会影响变量他外面的变量x 和 y 的值,因为变量**x**和**y**并没有把它们自己传递给函数,而只是传递了他们的数值。 但在某些情况下你可能需要在一个函数内控制一个函数以外的变量。要实现这种操作,我们必须使用按地址传递的参数(arguments passed by reference),就象下面例子中的函数**duplicate**: ~~~ | // passing parameters by reference #include void duplicate (int& a, int& b, int& c) { a*=2; b*=2; c*=2; } int main () { int x=1, y=3, z=7; duplicate (x, y, z); cout << "x=" << x << ", y=" << y << ", z=" << z; return 0; } | x=2, y=6, z=14 | ~~~ 第一个应该注意的事项是在函数**duplicate**的声明(declaration)中,每一个变量的类型后面跟了一个地址符ampersand sign (&),它的作用是指明变量是按地址传递的(by reference),而不是像通常一样按数值传递的(by value)。 当按地址传递(pass by reference)一个变量的时候,我们是在传递这个变量本身,我们在函数中对变量所做的任何修改将会影响到函数外面被传递的变量。 ![](https://box.kancloud.cn/2015-09-06_55ebefb686308.gif) 用另一种方式来说,我们已经把变量**a, b,c**和调用函数时使用的参数(**x, y**和 **z**)联系起来了,因此如果我们在函数内对a 进行操作,函数外面的x 值也会改变。同样,任何对b 的改变也会影响y,对c 的改变也会影响**z>**。 这就是为什么上面的程序中,主程序**main**中的三个变量**x, y**和**z**在调用函数duplicate 后打印结果显示他们的值增加了一倍。 如果在声明下面的函数: `void duplicate (int& a, int& b, int& c)` 时,我们是按这样声明的: `void duplicate (int a, int b, int c)` 也就是不写地址符 **ampersand (&)**,我们也就没有将参数的地址传递给函数,而是传递了它们的值,因此,屏幕上显示的输出结果**x, y ,z** 的值将不会改变,仍是**1,3,7**。 ![](https://box.kancloud.cn/2015-09-06_55ebefb821d40.png) 这种用地址符 ampersand (&)来声明按地址"by reference"传递参数的方式只是在C++中适用。在C 语言中,我们必须用指针(pointers)来做相同的操作。 按地址传递(Passing by reference)是一个使函数返回多个值的有效方法。例如,下面是一个函数,它可以返回第一个输入参数的前一个和后一个数值。 ~~~ | // more than one returning value #include void prevnext (int x, int& prev, int& next) { prev = x-1; next = x+1; } int main () { int x=100, y, z; prevnext (x, y, z); cout << "Previous=" << y << ", Next=" << z; return 0; } | Previous=99, Next=101 | ~~~ ### 参数的默认值(Default values in arguments) 当声明一个函数的时候我们可以给每一个参数指定一个默认值。如果当函数被调用时没有给出该参数的值,那么这个默认值将被使用。指定参数默认值只需要在函数声明时把一个数值赋给参数。如果函数被调用时没有数值传递给该参数,那么默认值将被使用。但如果有指定的数值传递给参数,那么默认值将被指定的数值取代。例如: ~~~ | // default values in functions #include int divide (int a, int b=2) { int r; r=a/b; return (r); } int main () { cout << divide (12); cout << endl; cout << divide (20,4); return 0; } | 6 5 | ~~~ 我们可以看到在程序中有两次调用函数**divide**。第一次调用: `divide (12)` 只有一个参数被指明,但函数**divide**允许有两个参数。因此函数**divide** 假设第二个参数的值为**2**,因为我们已经定义了它为该参数缺省的默认值(注意函数声明中的int b=2)。因此这次函数调用的结果是 **6** (12/2)。 在第二次调用中: `divide (20,4)` 这里有两个参数,所以默认值 (int b=2) 被传入的参数值4所取代,使得最后结果为 5 (20/4). ### 函数重载(Overloaded functions) 两个不同的函数可以用同样的名字,只要它们的参量(arguments)的原型(prototype)不同,也就是说你可以把同一个名字给多个函数,如果它们用不同数量的参数,或不同类型的参数。例如: ~~~ | // overloaded function #include int divide (int a, int b) { return (a/b); } float divide (float a, float b) { return (a/b); } int main () { int x=5,y=2; float n=5.0,m=2.0; cout << divide (x,y); cout << "\n"; cout << divide (n,m); cout << "\n"; return 0; } | 2 2.5 | ~~~ 在这个例子里,我们用同一个名字定义了两个不同函数,当它们其中一个接受两个整型(int)参数,另一个则接受两个浮点型(float)参数。编译器 (compiler)通过检查传入的参数的类型来确定是哪一个函数被调用。如果调用传入的是两个整数参数,那么是原型定义中有两个整型(int)参量的函数被调用,如果传入的是两个浮点数,那么是原型定义中有两个浮点型(float)参量的函数被调用。 为了简单起见,这里我们用的两个函数的代码相同,但这并不是必须的。你可以让两个函数用同一个名字同时完成完全不同的操作。 ### Inline 函数(inline functions) **inline** 指令可以被放在函数声明之前,要求该函数必须在被调用的地方以代码形式被编译。这相当于一个宏定义(macro)。它的好处只对短小的函数有效,这种情况下因为避免了调用函数的一些常规操作的时间(overhead),如参数堆栈操作的时间,所以编译结果的运行代码会更快一些。 它的声明形式是: `inline type name ( arguments ... ) { instructions ... }` 它的调用和其他的函数调用一样。调用函数的时候并不需要写关键字inline ,只有在函数声明前需要写。 ### 递归(Recursivity) 递归(recursivity)指函数将被自己调用的特点。它对排序(sorting)和阶乘(factorial)运算很有用。例如要获得一个数字n的阶乘,它的数学公式是: `n! = n * (n-1) * (n-2) * (n-3) ... * 1` 更具体一些,5! (factorial of 5) 是: `5! = 5 * 4 * 3 * 2 * 1 = 120` 而用一个递归函数来实现这个运算将如以下代码: ~~~ | // factorial calculator #include long factorial (long a){ if (a > 1) return (a * factorial (a-1)); else return (1); } int main () { long l; cout << "Type a number: "; cin >> l; cout << "!" << l << " = " << factorial (l); return 0; } | Type a number: 9 !9 = 362880 | ~~~ 注意我们在函数factorial中是怎样调用它自己的,但只是在参数值大于1的时候才做调用,因为否则函数会进入死循环(an infinite recursive loop),当参数到达0的时候,函数不继续用负数乘下去(最终可能导致运行时的堆栈溢出错误(stack overflow error)。 这个函数有一定的局限性,为简单起见,函数设计中使用的数据类型为长整型(long)。在实际的标准系统中,长整型long无法存储12!以上的阶乘值。 ### 函数的声明(Declaring functions) 到目前为止,我们定义的所有函数都是在它们第一次被调用(通常是在main中)之前,而把main 函数放在最后。如果重复以上几个例子,但把main 函数放在其它被它调用的函数之前,你就会遇到编译错误。原因是在调用一个函数之前,函数必须已经被定义了,就像我们前面例子中所做的。 但实际上还有一种方法来避免在main 或其它函数之前写出所有被他们调用的函数的代码,那就是在使用前先声明函数的原型定义。声明函数就是对函数在的完整定义之前做一个短小重要的声明,以便让编译器知道函数的参数和返回值类型。 它的形式是: `type name ( argument_type1, argument_type2, ...);` 它与一个函数的头定义(header definition)一样,除了: * 它不包括函数的内容, 也就是它不包括函数后面花括号{}内的所有语句。 * 它以一个分号semicolon sign (;) 结束。 * 在参数列举中只需要写出各个参数的数据类型就够了,至于每个参数的名字可以写,也可以不写,但是我们建议写上。 例如: | // 声明函数原型 #include void odd (int a); void even (int a); int main () { int i; do { cout << "Type a number: (0 to exit)"; cin >> i; odd (i); } while (i!=0); return 0; } void odd (int a) { if ((a%2)!=0) cout << "Number is odd.\n"; else even (a); } void even (int a) { if ((a%2)==0) cout << "Number is even.\n"; else odd (a); } | Type a number (0 to exit): 9 Number is odd. Type a number (0 to exit): 6 Number is even. Type a number (0 to exit): 1030 Number is even. Type a number (0 to exit): 0 Number is even. | 这个例子的确不是很有效率,我相信现在你已经可以只用一半行数的代码来完成同样的功能。但这个例子显示了函数原型(prototyping functions)是怎样工作的。并且在这个具体的例子中,两个函数中至少有一个是必须定义原型的。 这里我们首先看到的是函数odd 和even的原型: void odd (int a); void even (int a); 这样使得这两个函数可以在它们被完整定义之前就被使用,例如在main中被调用,这样main就可以被放在逻辑上更合理的位置:即程序代码的开头部分。 尽管如此,这个程序需要至少一个函数原型定义的特殊原因是因为在odd 函数里需要调用even 函数,而在even 函数里也同样需要调用odd函数。如果两个函数任何一个都没被提前定义原型的话,就会出现编译错误,因为或者odd 在even 函数中是不可见的(因为它还没有被定义),或者even 函数在odd函数中是不可见的。 很多程序员建议给所有的函数定义原型。这也是我的建议,特别是在有很多函数或函数很长的情况下。把所有函数的原型定义放在一个地方,可以使我们在决定怎样调用这些函数的时候轻松一些,同时也有助于生成头文件。