文章目录
- 助记提要
- 9章 函数
- 9.1 函数定义
- 返回类型
- 形式参数
- 函数体
- 9.2 函数调用
- 9.3 函数声明
- 定义前调用函数的问题
- 函数声明
- 9.4 实际参数
- 实际参数的转换
- 数组型实参
- 变长数组做形式参数
- 数组参数声明时使用static
- 复合字面量
- 9.5 return语句
- 9.6 程序终止
- 9.7 泛型选择
- 9.8 递归
助记提要
- 函数定义的4个组成;
- 函数声明和函数原型;
- 实参提升;
- 数组型实参的特点;
- 星号表示数组长度;
- static声明数组参数;
- 使用复合字面量避免定义变量;
- return和exit;
- 泛型选择;
9章 函数
函数是C程序的构建块,是自带声明和语句的小程序。
可以用函数把程序分为很多小块,便于理解和修改程序。
函数可以复用。
9.1 函数定义
函数定义的格式:
返回值类型 函数名(形式参数列表)
函数体
返回类型
- 返回类型不能是数组类型;
void
类型返回表示函数没有返回值;- C89可以省略返回类型,默认返回
int
;C99不允许省略返回类型。
返回类型过长的话,可以放在单独的一行:
unsigned long int
func(...)
{
...
return ...;
}
形式参数
- 形式参数可以有多个,逗号分开;
- 每个形式参数前面必须说明其类型;
- 如果没有形式参数,需要在括号里写
void
;
函数体
- 函数体是一堆花括号包起来的复合语句;
- 函数体内声明的变量专属于此函数,其他函数无法调用或修改;
- C89中变量声明必须在语句之前,C99中声明和语句可以混一起;
- 返回类型是
void
的函数,函数体可以只有一对花括号;这样的操作可以在开发中预留空间,后续再来编写。 - 非
void
函数必须在函数体中使用return
语句指定返回值。
9.2 函数调用
函数调用语法:
函数名(实际参数列表);
注意 调用时只写函数名,不写圆括号和实参,也是合法语句,但是不起任何作用。
void
函数调用没有返回值。非void
函数调用会产生一个值,可以存储在变量中。
这个返回值在不需要时,允许被丢弃。可以在函数前加强制转换操作(void)
,让别人知道这个函数的返回值是故意被丢弃的。
// 函数返回打印的字符数
n = printf("Hi!\n");
// 程序用不到打印字符数,丢弃这个返回值
(void) printf("Hi\n");
9.3 函数声明
定义前调用函数的问题
编译器在main函数中遇到未知的函数调用时,不知道这个函数的形参数目和类型、返回值类型。编译器会先假设这个函数返回int类型,即为该函数做一个隐式声明。
函数的实参个数和类型仍无法确定,编译器只能进行默认实参提升。
在后面遇到该函数的定义时,如果返回类型不符合,就会产生错误。
函数声明
有时无法在调用之前把函数定义好。提前定义也会使程序顺序不自然,难以阅读。
函数声明可以让编译器在调用前先浏览函数的信息,具体的函数定义可以后面再给。
函数声明的格式类似于函数定义的第一行,但是结尾要写分号:
返回类型 函数名(形式参数);
函数声明必须和函数的定义一致。
函数声明中形式参数只写类型就够了,参数名可以省略不写。
但是最好写上,因为名字可以说明形参的作用,并提醒程序员实参的传递顺序。
C99中在调用函数前必须声明或定义,否则会出错。
老式的函数声明不提供形式参数的信息,为了作区分,将提供返回类型、形参数目和类型的的函数声明称为函数原型。
有时会出于防御性目的在函数原型中省略参数名字。因为在很多人编写的大型应用程序中,可能刚好有一个宏的名字和参数名一样,在预处理时参数名会被换掉。
注意 函数声明放在函数体外会更好。如果放在函数体内,那另一个函数要调用这个函数时,还需要再次声明。后续修改过程中想添加或移除函数声明会很麻烦。
注意 返回类型相同的函数可以放在一起声明,甚至函数声明可以和变量声明放在一起。但是这样做会让程序显得乱。
// 函数声明合并
void print_num(void), print_count(int n);
// 函数声明和变量声明混合
double x, y, average(double a, double b);
9.4 实际参数
形式参数出现在函数定义当中,使用假名字表示函数执行时需要的值。
形式参数的改变不会影响实际参数的值,所以可以把形式参数做为函数内的变量使用。
实际参数是出现在函数调用中的表达式。
实际参数是值传递的,函数调用时先计算出实际参数的值,然后将值赋给相应的形式参数。
实际参数的转换
实际参数的类型与形式参数的类型不匹配时,也能进行函数调用。编译器会对实际参数进行转换:
- 若编译器调用前遇到原型,实参的值会被隐式转换为形参的类型。
- 若编译器调用前没有遇到原型,编译器执行默认实参提升:把float类型的实参转为double;非浮点型实参执行整值提升(C99为整数提升)。
注意 默认实参提升可能无法产生期望的结果。
int main(void) {
double x = 3.0;
printf("%d\n", square(x));
return 0;
}
int square(int n)
{
return n * n;
}
上述程序打印结果为1。函数square需要int
类型的实参,拿到double
类型后会产出无效的结果。
数组型实参
形式参数是一维数组时,一般不说明数组的长度。
// 一维数组形参,不写长度
int f(a[])
{ ... }
即使数组形参写了长度,也会被编译器忽略。
该函数的实际参数可以是类型对应的任意的一维数组。
函数执行时,无法确定数组的元素数目。sizeof(a) / sizeof(a[0])
只能计算数组总长度,而不是数组的元素数。
一维数组型的实际参数,如果函数需要知道数组元素数,就必须把元素数做为第二个参数提供出来。这个参数可以告知函数,数组的长度比实际长度小。
注意 元素数不要填写大于实际情况的值。访问超出末尾的元素,会导致未定义的行为。
注意 函数改变数组型形式参数的元素时,相应的实际参数也会被改变。
如果形式参数是多维数组,声明参数时只能省略第一维的长度,后续的维度必须指定。
C语言是按行主序存储数组的。
二维数组的行数可以由总长度除以行长推断,而行长是由这一行的列数决定的。
// 多维数组形参,第二维之后的长度必须写
int f2(a[][10])
{ ... }
变长数组做形式参数
C99允许使用非常量表达式指定数组长度,这样的变长数组也可以做参数。
int f(int n, int a[n])
{ ... }
显式地说明数组a的长度是n。
注意 参数要先写n,后写数组a。
函数声明时,参数名是可以省略的,如果第一个参数定义被省略了,就无法把a的长度和n联系起来。这时用星号来表示数组的长度与前面的参数相关:
// 省略参数名
int f(int, int [*]);
声明时方括号为空也是可以的:
int f(int, int []);
形式参数是多维变长数组时,可以指定任意的列数。
// 二维数组的全部元素相加
int sum(int n, int m, int a[n][m])
{
int i, j, sum = 0;
for (i = 0; i < n; i++)
for (j = 0; j < n; j++)
sum += a[i][j];
return sum;
}
这个函数的声明可以是以下几种:
int sum(int n, int m, int a[n][m]);
int sum(int n, int m, int a[*][*]);
int sum(int n, int m, int a[][m]);
int sum(int n, int m, int a[][*]);
数组参数声明时使用static
C99可以在数组参数声明时使用static
:
int f(int a[static 3], int n)
{ ... }
static 3
表示这个数组至少有3个元素。
使用static
可以使编译器在函数调用时预先取出最小数目的元素值,而不是在函数内部需要用到元素时才去取。
对于多维数组参数,static
仅可用于第一维。
复合字面量
调用函数时,需要传递数组的名字:
int a[] = {1, 2, 3, 4};
s = sum(a, 4);
数组a是一个变量,调用前需要初始化。如果后续不再用到它,可以使用符合字面量避免定义变量a。
复合字面量是使用给定的元素创建的没有名字的数组。
s = sum((int []){1, 2, 3, 4}, 4);
复合字面量的格式:
(数组类型 [数组长度]){数组初始化}
- 数组长度可以不写。写长度时,初始化列表不足的部分会补0。
- 初始化时也可以使用指示器。
- 函数内部创建的复合字面量可以包含任意的表达式,不只是常量。
- 复合字面量中元素的值可以被改变。如果不允许改变,可以在类型前面加
const
。如(const int []){2, 3}
9.5 return语句
- 非
void
函数必须使用return
语句指定返回值。 renturn
语句的类型和函数的返回类型不符合时,会把表达式的类型隐式转换为返回类型。void
函数中也可以使用return
语句,但是后面不能跟表达式。否则会编译错误。
return
语句不是必须的,因为执行完最后一条语句之后,函数会自动返回。C89中非void
函数不写return
,程序在尝试使用函数返回值时,会出现未定义的行为。C99中不写return
,编译器会报错。
9.6 程序终止
一些操作系统在程序终止时可以检测到状态码。
main函数的返回值就是状态码,是int类型。程序正常终止,返回0,异常终止返回非0值。
C89中不允许把main函数的返回类型定义为void。
C99中main函数返回值可以不是int类型,但是这么做降低了程序的可移植性。
想要终止程序,除了在main函数中使用return
语句外,还可以调用exit
函数。
exit
函数是<stdlib.h>
头中的函数。
// 正常终止程序
exit(0);
为了语义清晰,<stdlib.h>
提供了两个宏EXIT_SUCCESS
和EXIT_FAILURE
,通常分别是0和1。
// 正常终止程序
exit(EXIT_SUCCESS);
// 异常终止
exit(EXIT_FAILURE);
return
函数只在被main函数调用时才终止程序。exit
函数不论被哪个函数调用,都会终止程序。
9.7 泛型选择
有时希望使用同一个名字编写多个函数。这些函数实现的功能相似,只是参数和返回值的类型不相同。
C99使用泛型宏来统一数学函数的各个版本。如正弦函数,标准库针对不同的参数类型定义了多种相应的函数:sin、sinf、sinl、csin、csinf、csinl。使用泛型宏之后,可以直接用sin
来调用它们。泛型宏可以根据传入的参数选择对应版本。
C11提供了泛型选择表达式:
_Generic(表达式, 泛型关联列表)
使用泛型表达式:
#define sin(x) _Generic(x, \
float: sinf, \
double: sin, \
long double: sinl, \
float _Complex: csinf, \
double _Complex: csin, \
long double _Complex: csinl))(x)
泛型表达式中的第一个表达式是控制表达式,它并不求值,C语言只会提取它的类型信息。后续的某个泛型的类型名和控制表达式的类型兼容,则将其作为泛型选择的结果表达式。
- 泛型表达式的控制表达式不允许匹配多个泛型关联的类型名。
- 可以使用一个default泛型关联,做为控制表达式都不匹配时的默认选项。
- 泛型表达式在预处理期间被识别和替换。
- 泛型选择不能选择数组类型,因为数组类型的表达式传过来的是指向其首元素的指针。
9.8 递归
递归是指一个函数调用它本身的情况。
C语言允许递归,但是大部分程序不需要用到递归。
函数1调用函数2,函数2又调用函数1,这种写法相当于间接的递归,也是合法的,但是确保它们可以终止。