1
0
wiki/dev/C/函数.md

521 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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)`用来清理可变参数对象。