C 函数

C 语言提供了子程序,它可以将我们的程序分割成不同的部分。为了编写函数,我们至少给出函数的定义。通常,我们还需要给出函数的声明;但是,函数的声明不是必须的,如果不给出函数声明,那么编译器将给出一个匹配该函数的隐式声明,而且我们将得到编译时警告。

每个程序都至少需要一个名为 main 的函数,这是程序执行的入口函数。本文主要分为以下几部分:

  • 函数声明
  • 函数定义
  • 函数调用
  • 函数参数
  • 变长参数列表
  • 函数指针调用
  • main 函数
  • 递归函数
  • 静态函数
  • 嵌套函数

函数声明

我们编写函数声明时需要指定函数名称、参数列表以及函数返回类型。函数声明以分号结尾,其形式如下:

1
return-type function-name(parameter-list);
  • return-type 表明了函数返回值的数据类型。通过给定 void 的返回类型,我们可以声明函数不返回任何值。
  • function-name 可以是任何有效的标识符(见标示符)。
  • parameter-list 由零个或多个参数组成,通过逗号隔开。参数由数据类型和可选的参数名组成。我们可以声明变长的参数,或者无参数(void)。如果不给定参数列表,那么就意味着该函数没有参数,但是最好显示的使用 void 给出。

例如,下面是一个包含两个参数的函数声明:

1
int foo(int, double);

如果我们需要包含参数名称,我们可以在参数类型后给出,例如:

1
int foot(int x, double y);

参数名同样可以为任何标识符,如果我们包含多个参数,那么在同一个声明中参数名不能重复。函数声明中的参数名称可以不必与函数定义时的参数名称一致。

我们应当在第一次使用函数之前声明该函数。我们可以将其放在头文件中,随后我们便可以在任何 C 源文件中通过 #include 指令来包含函数声明,从而使用该函数。

函数定义

函数定义给出了函数实际执行的操作。函数定义包含函数名称、返回类型、参数列表(数据类型和参数名)以及函数主体。函数主体由一系列包含在大括号内的语句组成;实际上它就是一个块语句(见 块语句)。下面是函数定义的通用形式:

1
2
3
4
5
return-type
function-name(parameter-list)
{
function-body
}
  • return-typefunction-name 与函数声明中的相同(见函数声明)。

  • parameter-list 与函数声明中的参数列表相同(见函数声明),但是,在函数定义时我们必须显示的给出参数名。

例如,下面的函数定义包含两个整型参数并且返回它们的和作为返回值:

1
2
3
4
5
int
add_values(int x, int y)
{
return x + y;
}

为了兼容最初的 C 语言设计,我们还可以在参数列表的右括号后指定函数参数的类型,如下所示:

1
2
3
4
5
6
int
add_values(x, y)
int x, int y;
{
return x + y;
}

但是,我们强烈建议不要采用这种编码格式;它可能导致类型转换的细微问题,以及其他问题。

函数调用

我们可以通过函数名称以及提供该函数必要的参数来调用函数,其一般形式如下:

1
function-name(parameters)

函数调用可以是独立的语句,也可被用作子表达式。例如,下面是一个独立的函数调用示例:

1
foo(5);

在上述的示例中,函数 foo 的参数为 5

下面是用作子表达式的函数调用的示例:

1
a = square(5);

假设函数 square 将其参数平方,则上面的示例将值 25 赋值给 a

如果一个函数接受多个参数,我们需要使用逗号将其分隔开。

1
a = quux(5, 10);

函数参数

函数参数可以是任何表达式 – 字面值、变量存储的值、内存地址或者通过组合这些值而构建的更复杂的表达式。

在函数体内,参数是传递给函数的值的本地副本;您不能通过更改本地副本来更改传入的值。

1
2
3
4
5
6
7
8
9
int x = 23;
foo(x);
...
/* 定义函数 foo */
int foo(int a)
{
a = 2 * a;
return a;
}

在上面的示例总,即使传递给函数 foo 得到参数被修改了,传递给函数的变量 x 也不会发生改变。如果希望使用该函数来更改 x 的原始值,则必须将函数调用合并到赋值语句中:

1
x = foo(x);

如果传递给函数的值是内存地址(即指针),则可以访问(并更改)存储在该内存地址的数据。这样可以达到与其他语言中的按引用传递相似的效果,但是效果不一样:内存地址只是一个值,就像其他任何值一样,并且本身不能更改。传递指针和传递整数之间的区别在于您可以使用函数中的值进行何种操作。

下面是使用指针参数的函数调用示例:

1
2
3
4
5
6
7
void foo(int *x)
{
*x = *x + 42;
}
...
int a = 15;
foo(&a);

该函数的形式参数是 int 类型的指针,我们通过向其传递 int 类型变量的地址来调用该函数。通过在函数体内对指针进行解引用操作,我们可以获取内存地址中的值并对其进行修改。上述示例将修改变量 a 的值为 57

即使您不想改变存储在地址中的值,如果变量类型很大并且您需要节省内存空间或限制参数复制对性能的影响,则传递变量的地址而不是变量本身可能会很有用。例如:

1
2
3
4
5
6
7
8
struct foo
{
int x;
float y;
double z;
};

void bar(const struct foo *a);

在这种情况下,除非您在内存地址非常大的计算机上工作,否则传递结构体的指针要比传递结构实例花费更少的内存。

任何类型的数组总是以指针的形式进行传递:

1
2
3
4
void foo(int a[]);
...
int x[100];
foo(x);

在上述示例中,使用参数 a 调用函数 foo 不会将整个数组复制到 foo 函数的新局部参数中;而是将 x 作为指针传递给 foo 函数的第一个参数。但是要小心:在函数内,您不能使用 sizeof 来确定数组 x 的大小,而 sizeof 却告诉您指针 x 的大小。实际上,以上代码等效于:

1
2
3
4
void foo(int *a);
...
int x[100];
foo(x);

在参数声明中明确指定数组的长度无济于事。如果您确实需要按值传递数组,则可以将其包装在结构中,尽管这样做几乎没有用(传递 const 限定的指针通常足以表明调用者不应该修改数组)。

变长参数列表

我们可以编写一个带有可变数量参数的函数。这些称为可变函数(_variadic functions_)。为此,该函数至少需要具有一个已知数据类型的参数,但是其余参数是可选的,并且在数量和数据类型上都可以不同。

您可以像平常一样列出初始参数,但之后再使用省略号:...。下面是一个示例函数原型:

1
int add_multiple_values(int number, ...);

要使用函数定义中的可变参数,我们需要使用库头文件 <stdarg.h> 中定义的宏,因此必须使用 #include 包含该文件。有关这些宏的详细说明,请参见 GNU C 库手册中有关可变参数功能的部分。

下面是一个可变参数函数的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int
add_multiple_values (int number, ...)
{
int counter, total = 0;

/* 声明 'va_list' 类型变量 */
va_list parameters;

/* 调用 'va_start' 函数 */
va_start(parameters, number);

for (counter = 0; counter < number; counter++)
{
/* 获取可选参数的值 */
total += va_arg(parameters, int);
}

/* 结束 'parameters' 变量的使用,释放资源 */
va_end(parameters);

return total;
}

要使用可选参数,您需要一种方法来知道有多少个参数。这可能会有所不同,因此无法进行硬编码,但是如果您不知道有多少个可选参数,则可能很难知道何时停止使用 va_arg 函数。在上面的示例中,函数 add_multiple_values 的第一个参数 number 是实际传递的可选参数的数量。因此,我们可以这样调用函数:

1
sum = add_multiple_values(3, 12, 34, 190);

第一个参数指示跟随多少个可选参数。另外,请注意,您实际上并不需要使用 va_end 功能。实际上,对于GCC 而言,它什么也没做。但是,您可能要包括它以最大程度地与其他编译器兼容。

函数指针调用

我们也可以调用由指针标识的函数。间接操作符 * 在执行此操作时是可选的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>

void foo(int i)
{
printf("foo %d!\n", i);
}
void bar(int i)
{
printf("%d bar!\n", i);
}

void message(void (*func)(int), int times)
{
int j;
for (j = 0; j < times; ++j)
func(j); /* 与 (*func) (j); 作用相同 */
}

void example(int want_foo)
{
void (*pf)(int) = &bar; /* 运算符 & 是可选的 */
if (want_foo)
pf = foo;
message (pf, 5);
}

main 函数

每个程序都至少需要一个名为 main 的函数。这是程序开始执行的地方。我们不需要为 main 函数提供声明或原型,我们只需要定义它即可。

main 函数的返回值总是 int 类型。我们不必为 main 函数指定返回类型,但是我们可以这样做。需要注意的是,我们不能指定它的返回类型不是 int

通常,main 函数的返回值表明了程序退出状态。返回 0 或者 EXIT_SUCCESS 表明成功,EXIT_FAILURE 则表示错误。否则,返回的值由实现定义。

main 函数的末尾到达 } 而没有返回值,或者执行没有值的 return 语句(即 return;)都是等效的。在 C89 标准中,这是未定义的,然而在 C99 中明确定义这种情况返回 0

我们可以编写没有任何参数的 main 函数(也就是 int main(void)),或者从命令行接受参数。这是一个非常简单的没有参数的主函数:

1
2
3
4
5
6
int
main(void)
{
puts ("Hi there!");
return 0;
}

为了接受来自命令行的参数,我们需要 main 函数提供两个参数,argc*argv[]。我们可以改变参数的名称,但是它们数据类型不能改变 – int 类型和 char 类型的数组指针。argc 是命令行参数的数量,包括程序本身的名称。argv 是参数的数组,以字符串的形式给出。argv[0] 数组的第一个元素是在命令行中键入的程序名称;之后任何数组元素都是程序名称的参数。

下面的示例定义了 main 函数接受命名行参数,并且将其打印出来:

1
2
3
4
5
6
7
8
9
10
int
main(int argc, char *argv[])
{
int counter;

for (counter = 0; counter < argc; counter++)
printf ("%s\n", argv[counter]);

return 0;
}

递归函数

我们可以编写一个递归函数 – 调用自身的函数。如一个计算整数的阶乘的示例:

1
2
3
4
5
6
7
8
int
factorial(int x)
{
if (x < 1)
return 1;
else
return (x * factorial(x - 1));
}

注意不要编写无限递归的函数。在上面的示例中,一旦 x1,则递归停止。但是,在以下示例中,递归不会停止,直到程序被中断或内存不足为止:

1
2
3
4
5
int
watermelon(int x)
{
return (watermelon(x));
}

当然,函数也可以是间接递归的。

静态函数

如果希望仅可在定义该函数的源文件中调用该函数,则可以将其定义为静态函数:

1
2
3
4
5
static int
foo(int x)
{
return x + 42;
}

如果您正在构建可重用的函数库,并且需要包含一些最终用户不应该调用的子函数,则这很有用。

以这种方式定义的函数被称为具有静态链接;不幸的是 static 关键字具有多重含义,见 存储类型说明符

嵌套函数

作为 GNU C 扩展,我们可以在其他函数中定义函数,这种技术称为嵌套函数。

下面是使用嵌套函数定义的尾递归阶乘函数的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int
factorial(int x)
{
int
factorial_helper(int a, int b)
{
if (a < 1)
{
return b;
}
else
{
return factorial_helper((a - 1), (a * b));
}
}

return factorial_helper(x, 1);
}

请注意,必须在函数的开头定义嵌套函数以及变量声明,然后再声明所有其他语句。

实际上,我在 gcc -std=c99 的时候也能通过编译,我使用的是 GCC 7.4,而使用 clang -std=gnu99 不能通过编译(clang-1001.0.46.4),具体的可能是编译器实现的问题。

参考

[1] https://www.gnu.org/software/gnu-c-manual/gnu-c-manual.html#Functions