521 lines
16 KiB
Markdown
521 lines
16 KiB
Markdown
---
|
||
id: 函数
|
||
title: 函数
|
||
sidebar_position: 8
|
||
data: 2022年3月30日
|
||
---
|
||
|
||
## 简介
|
||
|
||
函数是一段可以重复执行的代码。它可以接受不同的参数,完成对应的操作。下面的例子就是一个函数。
|
||
|
||
```c
|
||
int plus_one(int n) {
|
||
return n + 1;
|
||
}
|
||
```
|
||
|
||
上面的代码声明了一个函数`plus_one()`。
|
||
|
||
函数声明的语法有以下几点,需要注意。
|
||
|
||
(1)返回值类型。函数声明时,首先需要给出返回值的类型,上例是`int`,表示函数`plus_one()`返回一个整数。
|
||
|
||
(2)参数。函数名后面的圆括号里面,需要声明参数的类型和参数名,`plus_one(int n)`表示这个函数有一个整数参数`n`。
|
||
|
||
(3)函数体。函数体要写在大括号里面,后面(即大括号外面)不需要加分号。大括号的起始位置,可以跟函数名在同一行,也可以另起一行,本书采用同一行的写法。
|
||
|
||
(4)`return`语句。`return`语句给出函数的返回值,程序运行到这一行,就会跳出函数体,结束函数的调用。如果函数没有返回值,可以省略`return`语句,或者写成`return;`。
|
||
|
||
调用函数时,只要在函数名后面加上圆括号就可以了,实际的参数放在圆括号里面,就像下面这样。
|
||
|
||
```c
|
||
int a = plus_one(13);
|
||
// a 等于 14
|
||
```
|
||
|
||
函数调用时,参数个数必须与定义里面的参数个数一致,参数过多或过少都会报错。
|
||
|
||
```c
|
||
int plus_one(int n) {
|
||
return n + 1;
|
||
}
|
||
|
||
plus_one(2, 2); // 报错
|
||
plus_one(); // 报错
|
||
```
|
||
|
||
上面示例中,函数`plus_one()`只能接受一个参数,传入两个参数或不传参数,都会报错。
|
||
|
||
函数必须声明后使用,否则会报错。也就是说,一定要在使用`plus_one()`之前,声明这个函数。如果像下面这样写,编译时会报错。
|
||
|
||
```c
|
||
int a = plus_one(13);
|
||
|
||
int plus_one(int n) {
|
||
return n + 1;
|
||
}
|
||
```
|
||
|
||
上面示例中,在调用`plus_one()`之后,才声明这个函数,编译就会报错。
|
||
|
||
C 语言标准规定,函数只能声明在源码文件的顶层,不能声明在其他函数内部。
|
||
|
||
不返回值的函数,使用`void`关键字表示返回值的类型。没有参数的函数,声明时要用`void`关键字表示参数类型。
|
||
|
||
```c
|
||
void myFunc(void) {
|
||
// ...
|
||
}
|
||
```
|
||
|
||
上面的`myFunc()`函数,既没有返回值,调用时也不需要参数。
|
||
|
||
函数可以调用自身,这就叫做递归(recursion)。下面是斐波那契数列的例子。
|
||
|
||
```c
|
||
unsigned long Fibonacci(unsigned n) {
|
||
if (n > 2)
|
||
return Fibonacci(n - 1) + Fibonacci(n - 2);
|
||
else
|
||
return 1;
|
||
}
|
||
```
|
||
|
||
上面示例中,函数`Fibonacci()`调用了自身,大大简化了算法。
|
||
|
||
## main()
|
||
|
||
C 语言规定,`main()`是程序的入口函数,即所有的程序一定要包含一个`main()`函数。程序总是从这个函数开始执行,如果没有该函数,程序就无法启动。其他函数都是通过它引入程序的。
|
||
|
||
`main()`的写法与其他函数一样,要给出返回值的类型和参数的类型,就像下面这样。
|
||
|
||
```c
|
||
int main(void) {
|
||
printf("Hello World\n");
|
||
return 0;
|
||
}
|
||
```
|
||
|
||
上面示例中,最后的`return 0;`表示函数结束运行,返回`0`。
|
||
|
||
C 语言约定,返回值`0`表示函数运行成功,如果返回其他非零整数,就表示运行失败,代码出了问题。系统根据`main()`的返回值,作为整个程序的返回值,确定程序是否运行成功。
|
||
|
||
正常情况下,如果`main()`里面省略`return 0`这一行,编译器会自动加上,即`main()`的默认返回值为0。所以,写成下面这样,效果完全一样。
|
||
|
||
```c
|
||
int main(void) {
|
||
printf("Hello World\n");
|
||
}
|
||
```
|
||
|
||
由于 C 语言只会对`main()`函数默认添加返回值,对其他函数不会这样做,所以建议总是保留`return`语句,以便形成统一的代码风格。
|
||
|
||
## 参数的传值引用
|
||
|
||
如果函数的参数是一个变量,那么调用时,传入的是这个变量的值的拷贝,而不是变量本身。
|
||
|
||
```c
|
||
void increment(int a) {
|
||
a++;
|
||
}
|
||
|
||
int i = 10;
|
||
increment(i);
|
||
|
||
printf("%d\n", i); // 10
|
||
```
|
||
|
||
上面示例中,调用`increment(i)`以后,变量`i`本身不会发生变化,还是等于`10`。因为传入函数的是`i`的拷贝,而不是`i`本身,拷贝的变化,影响不到原始变量。这就叫做“传值引用”。
|
||
|
||
所以,如果参数变量发生变化,最好把它作为返回值传出来。
|
||
|
||
```c
|
||
int increment(int a) {
|
||
a++;
|
||
return a;
|
||
}
|
||
|
||
int i = 10;
|
||
i = increment(i);
|
||
|
||
printf("%d\n", i); // 11
|
||
```
|
||
|
||
再看下面的例子,`Swap()`函数用来交换两个变量的值,由于传值引用,下面的写法不会生效。
|
||
|
||
```c
|
||
void Swap(int x, int y) {
|
||
int temp;
|
||
temp = x;
|
||
x = y;
|
||
y = temp;
|
||
}
|
||
|
||
int a = 1;
|
||
int b = 2;
|
||
Swap(a, b); // 无效
|
||
```
|
||
|
||
上面的写法不会产生交换变量值的效果,因为传入的变量是原始变量`a`和`b`的拷贝,不管函数内部怎么操作,都影响不了原始变量。
|
||
|
||
如果想要传入变量本身,只有一个办法,就是传入变量的地址。
|
||
|
||
```c
|
||
void Swap(int* x, int* y) {
|
||
int temp;
|
||
temp = *x;
|
||
*x = *y;
|
||
*y = temp;
|
||
}
|
||
|
||
int a = 1;
|
||
int b = 2;
|
||
Swap(&a, &b);
|
||
```
|
||
|
||
上面示例中,通过传入变量`x`和`y`的地址,函数内部就可以直接操作该地址,从而实现交换两个变量的值。
|
||
|
||
虽然跟传参无关,这里特别提一下,函数不要返回内部变量的指针。
|
||
|
||
```c
|
||
int* f(void) {
|
||
int i;
|
||
// ...
|
||
return &i;
|
||
}
|
||
```
|
||
|
||
上面示例中,函数返回内部变量`i`的指针,这种写法是错的。因为当函数结束运行时,内部变量就消失了,这时指向内部变量`i`的内存地址就是无效的,再去使用这个地址是非常危险的。
|
||
|
||
## 函数指针
|
||
|
||
函数本身就是一段内存里面的代码,C 语言允许通过指针获取函数。
|
||
|
||
```c
|
||
void print(int a) {
|
||
printf("%d\n", a);
|
||
}
|
||
|
||
void (*print_ptr)(int) = &print;
|
||
```
|
||
|
||
上面示例中,变量`print_ptr`是一个函数指针,它指向函数`print()`的地址。函数`print()`的地址可以用`&print`获得。注意,`(*print_ptr)`一定要写在圆括号里面,否则函数参数`(int)`的优先级高于`*`,整个式子就会变成`void* print_ptr(int)`。
|
||
|
||
有了函数指针,通过它也可以调用函数。
|
||
|
||
```c
|
||
(*print_ptr)(10);
|
||
// 等同于
|
||
print(10);
|
||
```
|
||
|
||
比较特殊的是,C 语言还规定,函数名本身就是指向函数代码的指针,通过函数名就能获取函数地址。也就是说,`print`和`&print`是一回事。
|
||
|
||
```c
|
||
if (print == &print) // true
|
||
```
|
||
|
||
因此,上面代码的`print_ptr`等同于`print`。
|
||
|
||
```c
|
||
void (*print_ptr)(int) = &print;
|
||
// 或
|
||
void (*print_ptr)(int) = print;
|
||
|
||
if (print_ptr == print) // true
|
||
```
|
||
|
||
所以,对于任意函数,都有五种调用函数的写法。
|
||
|
||
```c
|
||
// 写法一
|
||
print(10)
|
||
|
||
// 写法二
|
||
(*print)(10)
|
||
|
||
// 写法三
|
||
(&print)(10)
|
||
|
||
// 写法四
|
||
(*print_ptr)(10)
|
||
|
||
// 写法五
|
||
print_ptr(10)
|
||
```
|
||
|
||
为了简洁易读,一般情况下,函数名前面都不加`*`和`&`。
|
||
|
||
这种特性的一个应用是,如果一个函数的参数或返回值,也是一个函数,那么函数原型可以写成下面这样。
|
||
|
||
```c
|
||
int compute(int (*myfunc)(int), int, int);
|
||
```
|
||
|
||
上面示例可以清晰地表明,函数`compute()`的第一个参数也是一个函数。
|
||
|
||
## 函数原型
|
||
|
||
前面说过,函数必须先声明,后使用。由于程序总是先运行`main()`函数,导致所有其他函数都必须在`main()`函数之前声明。
|
||
|
||
```c
|
||
void func1(void) {
|
||
}
|
||
|
||
void func2(void) {
|
||
}
|
||
|
||
int main(void) {
|
||
func1();
|
||
func2();
|
||
return 0;
|
||
}
|
||
```
|
||
|
||
上面代码中,`main()`函数必须在最后声明,否则编译时会产生警告,找不到`func1()`或`func2()`的声明。
|
||
|
||
但是,`main()`是整个程序的入口,也是主要逻辑,放在最前面比较好。另一方面,对于函数较多的程序,保证每个函数的顺序正确,会变得很麻烦。
|
||
|
||
C 语言提供的解决方法是,只要在程序开头处给出函数原型,函数就可以先使用、后声明。所谓函数原型,就是提前告诉编译器,每个函数的返回类型和参数类型。其他信息都不需要,也不用包括函数体,具体的函数实现可以后面再补上。
|
||
|
||
```c
|
||
int twice(int);
|
||
|
||
int main(int num) {
|
||
return twice(num);
|
||
}
|
||
|
||
int twice(int num) {
|
||
return 2 * num;
|
||
}
|
||
```
|
||
|
||
上面示例中,函数`twice()`的实现是放在`main()`后面,但是代码头部先给出了函数原型,所以可以正确编译。只要提前给出函数原型,函数具体的实现放在哪里,就不重要了。
|
||
|
||
函数原型包括参数名也可以,虽然这样对于编译器是多余的,但是阅读代码的时候,可能有助于理解函数的意图。
|
||
|
||
```c
|
||
int twice(int);
|
||
|
||
// 等同于
|
||
int twice(int num);
|
||
```
|
||
|
||
上面示例中,`twice`函数的参数名`num`,无论是否出现在原型里面,都是可以的。
|
||
|
||
注意,函数原型必须以分号结尾。
|
||
|
||
一般来说,每个源码文件的头部,都会给出当前脚本使用的所有函数的原型。
|
||
|
||
## exit()
|
||
|
||
`exit()`函数用来终止整个程序的运行。一旦执行到该函数,程序就会立即结束。该函数的原型定义在头文件`stdlib.h`里面。
|
||
|
||
`exit()`可以向程序外部返回一个值,它的参数就是程序的返回值。一般来说,使用两个常量作为它的参数:`EXIT_SUCCESS`(相当于 0)表示程序运行成功,`EXIT_FAILURE`(相当于 1)表示程序异常中止。这两个常数也是定义在`stdlib.h`里面。
|
||
|
||
```c
|
||
// 程序运行成功
|
||
// 等同于 exit(0);
|
||
exit(EXIT_SUCCESS);
|
||
|
||
// 程序异常中止
|
||
// 等同于 exit(1);
|
||
exit(EXIT_FAILURE);
|
||
```
|
||
|
||
在`main()`函数里面,`exit()`等价于使用`return`语句。其他函数使用`exit()`,就是终止整个程序的运行,没有其他作用。
|
||
|
||
C 语言还提供了一个`atexit()`函数,用来登记`exit()`执行时额外执行的函数,用来做一些退出程序时的收尾工作。该函数的原型也是定义在头文件`stdlib.h`。
|
||
|
||
```c
|
||
int atexit(void (*func)(void));
|
||
```
|
||
|
||
`atexit()`的参数是一个函数指针。注意,它的参数函数(下例的`print`)不能接受参数,也不能有返回值。
|
||
|
||
```c
|
||
void print(void) {
|
||
printf("something wrong!\n");
|
||
}
|
||
|
||
atexit(print);
|
||
exit(EXIT_FAILURE);
|
||
```
|
||
|
||
上面示例中,`exit()`执行时会先自动调用`atexit()`注册的`print()`函数,然后再终止程序。
|
||
|
||
## 函数说明符
|
||
|
||
C 语言提供了一些函数说明符,让函数用法更加明确。
|
||
|
||
### extern 说明符
|
||
|
||
对于多文件的项目,源码文件会用到其他文件声明的函数。这时,当前文件里面,需要给出外部函数的原型,并用`extern`说明该函数的定义来自其他文件。
|
||
|
||
```c
|
||
extern int foo(int arg1, char arg2);
|
||
|
||
int main(void) {
|
||
int a = foo(2, 3);
|
||
// ...
|
||
return 0;
|
||
}
|
||
```
|
||
|
||
上面示例中,函数`foo()`定义在其他文件,`extern`告诉编译器当前文件不包含该函数的定义。
|
||
|
||
不过,由于函数原型默认就是`extern`,所以这里不加`extern`,效果是一样的。
|
||
|
||
### static 说明符
|
||
|
||
默认情况下,每次调用函数时,函数的内部变量都会重新初始化,不会保留上一次运行的值。`static`说明符可以改变这种行为。
|
||
|
||
`static`用于函数内部声明变量时,表示该变量只需要初始化一次,不需要在每次调用时都进行初始化。也就是说,它的值在两次调用之间保持不变。
|
||
|
||
```c
|
||
#include <stdio.h>
|
||
|
||
void counter(void) {
|
||
static int count = 1; // 只初始化一次
|
||
printf("%d\n", count);
|
||
count++;
|
||
}
|
||
|
||
int main(void) {
|
||
counter(); // 1
|
||
counter(); // 2
|
||
counter(); // 3
|
||
counter(); // 4
|
||
}
|
||
```
|
||
|
||
上面示例中,函数`counter()`的内部变量`count`,使用`static`说明符修饰,表明这个变量只初始化一次,以后每次调用时都会使用上一次的值,造成递增的效果。
|
||
|
||
注意,`static`修饰的变量初始化时,只能赋值为常量,不能赋值为变量。
|
||
|
||
```c
|
||
int i = 3;
|
||
static int j = i; // 错误
|
||
```
|
||
|
||
上面示例中,`j`属于静态变量,初始化时不能赋值为另一个变量`i`。
|
||
|
||
另外,在块作用域中,`static`声明的变量有默认值`0`。
|
||
|
||
```c
|
||
static int foo;
|
||
// 等同于
|
||
static int foo = 0;
|
||
```
|
||
|
||
`static`可以用来修饰函数本身。
|
||
|
||
```c
|
||
static int Twice(int num) {
|
||
int result = num * 2;
|
||
return(result);
|
||
}
|
||
```
|
||
|
||
上面示例中,`static`关键字表示该函数只能在当前文件里使用,如果没有这个关键字,其他文件也可以使用这个函数(通过声明函数原型)。
|
||
|
||
`static`也可以用在参数里面,修饰参数数组。
|
||
|
||
```c
|
||
int sum_array(int a[static 3], int n) {
|
||
// ...
|
||
}
|
||
```
|
||
|
||
上面示例中,`static`对程序行为不会有任何影响,只是用来告诉编译器,该数组长度至少为3,某些情况下可以加快程序运行速度。另外,需要注意的是,对于多维数组的参数,`static`仅可用于第一维的说明。
|
||
|
||
### const 说明符
|
||
|
||
函数参数里面的`const`说明符,表示函数内部不得修改该参数变量。
|
||
|
||
```c
|
||
void f(int* p) {
|
||
// ...
|
||
}
|
||
```
|
||
|
||
上面示例中,函数`f()`的参数是一个指针`p`,函数内部可能会改掉它所指向的值`*p`,从而影响到函数外部。
|
||
|
||
为了避免这种情况,可以在声明函数时,在指针参数前面加上`const`说明符,告诉编译器,函数内部不能修改该参数所指向的值。
|
||
|
||
```c
|
||
void f(const int* p) {
|
||
*p = 0; // 该行报错
|
||
}
|
||
```
|
||
|
||
上面示例中,声明函数时,`const`指定不能修改指针`p`指向的值,所以`*p = 0`就会报错。
|
||
|
||
但是上面这种写法,只限制修改`p`所指向的值,而`p`本身的地址是可以修改的。
|
||
|
||
```c
|
||
void f(const int* p) {
|
||
int x = 13;
|
||
p = &x; // 允许修改
|
||
}
|
||
```
|
||
|
||
上面示例中,`p`本身是可以修改,`const`只限定`*p`不能修改。
|
||
|
||
如果想限制修改`p`,可以把`const`放在`p`前面。
|
||
|
||
```c
|
||
void f(int* const p) {
|
||
int x = 13;
|
||
p = &x; // 该行报错
|
||
}
|
||
```
|
||
|
||
如果想同时限制修改`p`和`*p`,需要使用两个`const`。
|
||
|
||
```c
|
||
void f(const int* const p) {
|
||
// ...
|
||
}
|
||
```
|
||
|
||
## 可变参数
|
||
|
||
有些函数的参数数量是不确定的,声明函数的时候,可以使用省略号`...`表示可变数量的参数。
|
||
|
||
```c
|
||
int printf(const char* format, ...);
|
||
```
|
||
|
||
上面示例是`printf()`函数的原型,除了第一个参数,其他参数的数量是可变的,与格式字符串里面的占位符数量有关。这时,就可以用`...`表示可变数量的参数。
|
||
|
||
注意,`...`符号必须放在参数序列的结尾,否则会报错。
|
||
|
||
头文件`stdarg.h`定义了一些宏,可以操作可变参数。
|
||
|
||
(1)`va_list`:一个数据类型,用来定义一个可变参数对象。它必须在操作可变参数时,首先使用。
|
||
|
||
(2)`va_start`:一个函数,用来初始化可变参数对象。它接受两个参数,第一个参数是可变参数对象,第二个参数是原始函数里面,可变参数之前的那个参数,用来为可变参数定位。
|
||
|
||
(3)`va_arg`:一个函数,用来取出当前那个可变参数,每次调用后,内部指针就会指向下一个可变参数。它接受两个参数,第一个是可变参数对象,第二个是当前可变参数的类型。
|
||
|
||
(4)`va_end`:一个函数,用来清理可变参数对象。
|
||
|
||
下面是一个例子。
|
||
|
||
```c
|
||
double average(int i, ...) {
|
||
double total = 0;
|
||
va_list ap;
|
||
va_start(ap, i);
|
||
for (int j = 1; j <= i; ++j) {
|
||
total += va_arg(ap, double);
|
||
}
|
||
va_end(ap);
|
||
return total / i;
|
||
}
|
||
```
|
||
|
||
上面示例中,`va_list ap`定义`ap`为可变参数对象,`va_start(ap, i)`将参数`i`后面的参数统一放入`ap`,`va_arg(ap, double)`用来从`ap`依次取出一个参数,并且指定该参数为 double 类型,`va_end(ap)`用来清理可变参数对象。
|