# 函数

网道（WangDoc.com），互联网文档计划

## 简介 #

函数是一段可以重复执行的代码。它可以接受不同的参数，完成对应的操作。下面的例子就是一个函数。

    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;`。

调用函数时，只要在函数名后面加上圆括号就可以了，实际的参数放在圆括号里面，就像下面这样。

    int a = plus_one(13);
    // a 等于 14

函数调用时，参数个数必须与定义里面的参数个数一致，参数过多或过少都会报错。

    int plus_one(int n) {
      return n + 1;
    }

    plus_one(2, 2); // 报错
    plus_one();  // 报错

上面示例中，函数`plus_one()`只能接受一个参数，传入两个参数或不传参数，都会报错。

函数必须声明后使用，否则会报错。也就是说，一定要在使用`plus_one()`之前，声明这个函数。如果像下面这样写，编译时会报错。

    int a = plus_one(13);

    int plus_one(int n) {
      return n + 1;
    }

上面示例中，在调用`plus_one()`之后，才声明这个函数，编译就会报错。

C 语言标准规定，函数只能声明在源码文件的顶层，不能声明在其他函数内部。

不返回值的函数，使用`void`关键字表示返回值的类型。没有参数的函数，声明时要用`void`关键字表示参数类型。

    void myFunc(void) {
      // ...
    }

上面的`myFunc()`函数，既没有返回值，调用时也不需要参数。

函数可以调用自身，这就叫做递归（recursion）。下面是斐波那契数列的例子。

    unsigned long Fibonacci(unsigned n) {
      if (n > 2)
        return Fibonacci(n - 1) + Fibonacci(n - 2);
      else
        return 1;
    }

上面示例中，函数`Fibonacci()`调用了自身，大大简化了算法。

## main() #

C 语言规定，`main()`是程序的入口函数，即所有的程序一定要包含一个`main()`函数。程序总是从这个函数开始执行，如果没有该函数，程序就无法启动。其他函数都是通过它引入程序的。

`main()`的写法与其他函数一样，要给出返回值的类型和参数的类型，就像下面这样。

    int main(void) {
      printf("Hello World\n");
      return 0;
    }

上面示例中，最后的`return 0;`表示函数结束运行，返回`0`。

C 语言约定，返回值`0`表示函数运行成功，如果返回其他非零整数，就表示运行失败，代码出了问题。系统根据`main()`的返回值，作为整个程序的返回值，确定程序是否运行成功。

正常情况下，如果`main()`里面省略`return 0`这一行，编译器会自动加上，即`main()`的默认返回值为0。所以，写成下面这样，效果完全一样。

    int main(void) {
      printf("Hello World\n");
    }

由于 C 语言只会对`main()`函数默认添加返回值，对其他函数不会这样做，所以建议总是保留`return`语句，以便形成统一的代码风格。

## 参数的传值引用 #

如果函数的参数是一个变量，那么调用时，传入的是这个变量的值的拷贝，而不是变量本身。

    void increment(int a) {
      a++;
    }

    int i = 10;
    increment(i);

    printf("%d\n", i); // 10

上面示例中，调用`increment(i)`以后，变量`i`本身不会发生变化，还是等于`10`。因为传入函数的是`i`的拷贝，而不是`i`本身，拷贝的变化，影响不到原始变量。这就叫做“传值引用”。

所以，如果参数变量发生变化，最好把它作为返回值传出来。

    int increment(int a) {
      a++;
      return a;
    }

    int i = 10;
    i = increment(i);

    printf("%d\n", i); // 11

再看下面的例子，`Swap()`函数用来交换两个变量的值，由于传值引用，下面的写法不会生效。

    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`的拷贝，不管函数内部怎么操作，都影响不了原始变量。

如果想要传入变量本身，只有一个办法，就是传入变量的地址。

    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`的地址，函数内部就可以直接操作该地址，从而实现交换两个变量的值。

虽然跟传参无关，这里特别提一下，函数不要返回内部变量的指针。

    int* f(void) {
      int i;
      // ...
      return &i;
    }

上面示例中，函数返回内部变量`i`的指针，这种写法是错的。因为当函数结束运行时，内部变量就消失了，这时指向内部变量`i`的内存地址就是无效的，再去使用这个地址是非常危险的。

## 函数指针 #

函数本身就是一段内存里面的代码，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)`。

有了函数指针，通过它也可以调用函数。

    (*print_ptr)(10);
    // 等同于
    print(10);

比较特殊的是，C 语言还规定，函数名本身就是指向函数代码的指针，通过函数名就能获取函数地址。也就是说，`print`和`&print`是一回事。

    if (print == &print) // true

因此，上面代码的`print_ptr`等同于`print`。

    void (*print_ptr)(int) = &print;
    // 或
    void (*print_ptr)(int) = print;

    if (print_ptr == print) // true

所以，对于任意函数，都有五种调用函数的写法。

    // 写法一
    print(10)

    // 写法二
    (*print)(10)

    // 写法三
    (&print)(10)

    // 写法四
    (*print_ptr)(10)

    // 写法五
    print_ptr(10)

为了简洁易读，一般情况下，函数名前面都不加`*`和`&`。

这种特性的一个应用是，如果一个函数的参数或返回值，也是一个函数，那么函数原型可以写成下面这样。

    int compute(int (*myfunc)(int), int, int);

上面示例可以清晰地表明，函数`compute()`的第一个参数也是一个函数。

## 函数原型 #

前面说过，函数必须先声明，后使用。由于程序总是先运行`main()`函数，导致所有其他函数都必须在`main()`函数之前声明。

    void func1(void) {
    }

    void func2(void) {
    }

    int main(void) {
      func1();
      func2();
      return 0;
    }

上面代码中，`main()`函数必须在最后声明，否则编译时会产生警告，找不到`func1()`或`func2()`的声明。

但是，`main()`是整个程序的入口，也是主要逻辑，放在最前面比较好。另一方面，对于函数较多的程序，保证每个函数的顺序正确，会变得很麻烦。

C 语言提供的解决方法是，只要在程序开头处给出函数原型，函数就可以先使用、后声明。所谓函数原型，就是提前告诉编译器，每个函数的返回类型和参数类型。其他信息都不需要，也不用包括函数体，具体的函数实现可以后面再补上。

    int twice(int);

    int main(int num) {
      return twice(num);
    }

    int twice(int num) {
      return 2 * num;
    }

上面示例中，函数`twice()`的实现是放在`main()`后面，但是代码头部先给出了函数原型，所以可以正确编译。只要提前给出函数原型，函数具体的实现放在哪里，就不重要了。

函数原型包括参数名也可以，虽然这样对于编译器是多余的，但是阅读代码的时候，可能有助于理解函数的意图。

    int twice(int);

    // 等同于
    int twice(int num);

上面示例中，`twice`函数的参数名`num`，无论是否出现在原型里面，都是可以的。

注意，函数原型必须以分号结尾。

一般来说，每个源码文件的头部，都会给出当前脚本使用的所有函数的原型。

## exit() #

`exit()`函数用来终止整个程序的运行。一旦执行到该函数，程序就会立即结束。该函数的原型定义在头文件`stdlib.h`里面。

`exit()`可以向程序外部返回一个值，它的参数就是程序的返回值。一般来说，使用两个常量作为它的参数：`EXIT_SUCCESS`（相当于 0）表示程序运行成功，`EXIT_FAILURE`（相当于 1）表示程序异常中止。这两个常数也是定义在`stdlib.h`里面。

    // 程序运行成功
    // 等同于 exit(0);
    exit(EXIT_SUCCESS);

    // 程序异常中止
    // 等同于 exit(1);
    exit(EXIT_FAILURE);

在`main()`函数里面，`exit()`等价于使用`return`语句。其他函数使用`exit()`，就是终止整个程序的运行，没有其他作用。

C 语言还提供了一个`atexit()`函数，用来登记`exit()`执行时额外执行的函数，用来做一些退出程序时的收尾工作。该函数的原型也是定义在头文件`stdlib.h`。

    int atexit(void (*func)(void));

`atexit()`的参数是一个函数指针。注意，它的参数函数（下例的`print`）不能接受参数，也不能有返回值。

    void print(void) {
      printf("something wrong!\n");
    }

    atexit(print);
    exit(EXIT_FAILURE);

上面示例中，`exit()`执行时会先自动调用`atexit()`注册的`print()`函数，然后再终止程序。

## 函数说明符 #

C 语言提供了一些函数说明符，让函数用法更加明确。

### extern 说明符 #

对于多文件的项目，源码文件会用到其他文件声明的函数。这时，当前文件里面，需要给出外部函数的原型，并用`extern`说明该函数的定义来自其他文件。

    extern int foo(int arg1, char arg2);

    int main(void) {
      int a = foo(2, 3);
      // ...
      return 0;
    }

上面示例中，函数`foo()`定义在其他文件，`extern`告诉编译器当前文件不包含该函数的定义。

不过，由于函数原型默认就是`extern`，所以这里不加`extern`，效果是一样的。

### static 说明符 #

默认情况下，每次调用函数时，函数的内部变量都会重新初始化，不会保留上一次运行的值。`static`说明符可以改变这种行为。

`static`用于函数内部声明变量时，表示该变量只需要初始化一次，不需要在每次调用时都进行初始化。也就是说，它的值在两次调用之间保持不变。

    #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`修饰的变量初始化时，只能赋值为常量，不能赋值为变量。

    int i = 3;
    static int j = i; // 错误

上面示例中，`j`属于静态变量，初始化时不能赋值为另一个变量`i`。

另外，在块作用域中，`static`声明的变量有默认值`0`。

    static int foo;
    // 等同于
    static int foo = 0;

`static`可以用来修饰函数本身。

    static int Twice(int num) {
      int result = num * 2;
      return(result);
    }

上面示例中，`static`关键字表示该函数只能在当前文件里使用，如果没有这个关键字，其他文件也可以使用这个函数（通过声明函数原型）。

`static`也可以用在参数里面，修饰参数数组。

    int sum_array(int a[static 3], int n) {
      // ...
    }

上面示例中，`static`对程序行为不会有任何影响，只是用来告诉编译器，该数组长度至少为3，某些情况下可以加快程序运行速度。另外，需要注意的是，对于多维数组的参数，`static`仅可用于第一维的说明。

### const 说明符 #

函数参数里面的`const`说明符，表示函数内部不得修改该参数变量。

    void f(int* p) {
      // ...
    }

上面示例中，函数`f()`的参数是一个指针`p`，函数内部可能会改掉它所指向的值`*p`，从而影响到函数外部。

为了避免这种情况，可以在声明函数时，在指针参数前面加上`const`说明符，告诉编译器，函数内部不能修改该参数所指向的值。

    void f(const int* p) {
      *p = 0; // 该行报错
    }

上面示例中，声明函数时，`const`指定不能修改指针`p`指向的值，所以`*p = 0`就会报错。

但是上面这种写法，只限制修改`p`所指向的值，而`p`本身的地址是可以修改的。

    void f(const int* p) {
      int x = 13;
      p = &x; // 允许修改
    }

上面示例中，`p`本身是可以修改，`const`只限定`*p`不能修改。

如果想限制修改`p`，可以把`const`放在`p`前面。

    void f(int* const p) {
      int x = 13;
      p = &x; // 该行报错
    }

如果想同时限制修改`p`和`*p`，需要使用两个`const`。

    void f(const int* const p) {
      // ...
    }

## 可变参数 #

有些函数的参数数量是不确定的，声明函数的时候，可以使用省略号`...`表示可变数量的参数。

    int printf(const char* format, ...);

上面示例是`printf()`函数的原型，除了第一个参数，其他参数的数量是可变的，与格式字符串里面的占位符数量有关。这时，就可以用`...`表示可变数量的参数。

注意，`...`符号必须放在参数序列的结尾，否则会报错。

头文件`stdarg.h`定义了一些宏，可以操作可变参数。

（1）`va_list`：一个数据类型，用来定义一个可变参数对象。它必须在操作可变参数时，首先使用。

（2）`va_start`：一个函数，用来初始化可变参数对象。它接受两个参数，第一个参数是可变参数对象，第二个参数是原始函数里面，可变参数之前的那个参数，用来为可变参数定位。

（3）`va_arg`：一个函数，用来取出当前那个可变参数，每次调用后，内部指针就会指向下一个可变参数。它接受两个参数，第一个是可变参数对象，第二个是当前可变参数的类型。

（4）`va_end`：一个函数，用来清理可变参数对象。

下面是一个例子。

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

本教程采用知识共享 署名-相同方式共享 3.0协议。

分享本文