6. 函数
本章的完整目录:
- 6.1 函数声明:同一文件内的声明与定义,以及头文件中的声明。
- 6.2
return:返回值规则与函数结束时的控制流。 - 6.3 递归:终止条件与趋近终止的递归步骤。
1. 函数声明
简单地,函数声明可以有以下形式:返回类型 标识符 ( 形式参数列表 )
其中,如果这个函数不返回任何东西,则 返回类型 的位置填 void;如果不接受参数,形式参数列表(以下简称 形参列表)里面填 void。
例如,要编写一个函数 say_hello(),让它在屏幕上输出一段话(这个函数既不接受参数,也不返回值):
void say_hello(void) {
puts("Hello, World!");
}2
3
例如,编写一个函数 print_add(),让它在屏幕上输出两个数的和(这个函数接受两个参数,不返回值):
void print_add(double a, double b) {
printf("%g + %g = %g\n", a, b, a + b);
}2
3
例如,编写一个函数 add(),让它返回两个数的和(这个函数接受两个参数,返回一个值):
double add(double a, double b) {
return a + b;
}2
3
2. 函数调用
使用 函数名 ( 实际参数列表 ) 的形式调用函数。每个实参表达式会先被求值,再把得到的值赋给对应的形参对象。
当函数的形参列表为空的时候,实际参数列表(以下简称实参列表)什么都不填。
函数调用在程序执行时求值,这与预处理阶段对记号进行的宏替换不同。形参是被调函数内部的对象,对形参赋新值不会改变调用方传入的对象,见第 3 节。
例如,调用 say_hello():
#include <stdio.h>
void say_hello(void) {
puts("Hello, World!");
}
int main(void){
say_hello();
say_hello();
say_hello();
return 0;
}2
3
4
5
6
7
8
9
10
11
12
运行结果:
Hello, World!
Hello, World!
Hello, World!2
3
例如,调用 print_add():
#include <stdio.h>
void print_add(double a, double b) {
printf("%g + %g = %g\n", a, b, a + b);
}
int main(void){
print_add(1.2, 2.3);
return 0;
}2
3
4
5
6
7
8
9
10
运行结果:
1.2 + 2.3 = 3.5例如,调用 print_add() 和 add():
#include <stdio.h>
double add(double a, double b) {
return a + b;
}
void print_add(double a, double b) {
printf("%g + %g = %g\n", a, b, a + b);
}
int main(void){
print_add(add(3.5, 7.4), add(4.5, 4.4));
return 0;
}2
3
4
5
6
7
8
9
10
11
12
13
14
add(3.5, 7.4) 返回 10.9,add(4.5, 4.4) 返回 8.9。如同调用 print_add(10.9, 8.9),会输出:
10.9 + 8.9 = 19.83. 形参与实参的边界
下面的例子说明“形参对象得到的是实参的值”:
#include <stdio.h>
void set_to_zero(int x) {
x = 0;
}
int main(void) {
int n = 5;
set_to_zero(n);
printf("%d\n", n);
return 0;
}2
3
4
5
6
7
8
9
10
11
12
13
输出:
5
set_to_zero 内部修改的是形参对象 x,main 中的对象 n 不会因此改变。若函数需要修改调用方的对象,应传入该对象的地址(指针在第 8 章介绍):
void set_to_zero(int *p) {
*p = 0;
}2
3
调用时写成 set_to_zero(&n)。从函数的形参类型和调用处的 & 都能看出 n 可能被修改。
4. 实参求值顺序
实参的求值顺序
函数调用中,各实参的求值顺序没有规定,不一定按书写顺序;所有实参求值完成后,才进入被调函数。如果一个实参修改了某个对象,而另一个实参也修改或读取这个对象,行为未定义,例如 print_add(i++, i++) 和 print_add(i++, i)。
int i = 0;
/* 未定义行为:两个实参都修改 i */
/* print_add(i++, i++); */
print_add(i, i + 1);2
3
4
5
6
如果一个调用需要多个有副作用的步骤,先拆成多条语句,再调用函数。
5. 习题
编写函数 void fill_area(char ch, int width, int height),实现功能:打印 ch 字符 height 行 width 列。
编写四个函数,近似模拟 ++x、x++、--x、x-- 对 int 对象的效果。
要求:
- 通过指针修改调用方对象;
- 前缀形式返回修改后的值;
- 后缀形式返回修改前的值;
- 说明为什么这些函数只是近似模拟,而不是和运算符完全等价。
输入四个整数,判断它们是否满足“24 点”:可以构建出一个只含这四个整数、加减乘除和括号的表达式,且该表达式的求值结果为 24。
示例:1 5 5 5 满足,因为 (5-(1/5))*5=24。
括号生成:输入一个整数 n,生成所有可能的并且有效的 n 对括号组合。
示例:输入:3,输出:((())) (()()) (())() ()(()) ()()()。
全排列生成:输入一个整数 n,生成 1~n 这 n 个整数的全排列。
示例:输入:3,输出:123 132 213 231 312 321。
6. 函数调用运算符
函数调用表达式会先对被调函数表达式和实参求值,再进入被调函数执行。C 标准没有规定实参求值的先后顺序,因此实参之间不应依赖彼此副作用。
int add(int a, int b) {
return a + b;
}
int main(void) {
int s = add(3, 4);
return s == 7 ? 0 : 1;
}2
3
4
5
6
7
8
像 f(i++, i++) 这类写法是未定义行为,应避免。