main 函数

main 函数就是一个典型的函数,它是程序的入口函数。

示例

1
2
3
4
5
6
int main(int argc, char* argv[]){
int hour = 11;
int minute = 59;
printf("%d and %d hours\n", hour, minute / 60);
return 0;
}

argc 和 argv 分别存放执行这个程序时所带的命令行参数数量每个参数的字符串字面值

自定义函数

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
void newline(void)
{
printf("\n");
}
void threeline(void)
{
newline();
newline();
newline();
}
int main(void)
{
printf("Three lines:\n");
threeline();
printf("Another three lines.\n");
threeline();
return 0;
}

形式参数 和 实际参数

我们常把函数定义中圆括号内列表中出现的变量称为形式参数,而把函数调用中与形式参数对应的值称为实际参数。

函数原型 和 函数定义

函数原型是出现在main函数之前的函数声明语句,用于表明该函数的参数和返回值类型。在代码中可以单独写一个函数原型,后面加;号结束,而不写函数体,例如:

1
void threeline(void);

这种写法只能叫函数声明而不能叫函数定义,只有带函数体的声明才叫定义。

函数原型必须与函数声明的定义和用法一致。如果函数的定义、用法与函数原型不一致,将出现错误。

函数原型与函数声明中参数名不要求相同。事实上,函数原型中的参数名是可选的。但是,合适的参数名能够起到很好的说明作用,因此我们在函数原型中总是指明参数名。

函数原型也可以写在局部作用域中,例如:

1
2
3
4
5
6
int main(void)
{
void print_time(int, int);
print_time(23, 59);
return 0;
}

这样声明的标识符print_time具有局部作用域,只在main函数中是有效的函数名,出了main函数就不存在print_time这个标识符了。

虽然在一个函数体中可以声明另一个函数,但不能定义另一个函数,C语言不允许嵌套定义函数[5]。

传值调用

在C语言中,被调用函数不能直接修改主调函数中变量的值,而只能修改其私有的临时副本的值。必要时,也可以让函数能够修改主调函数中的变量。这种情况下,调用者需要向被调用函数提供待设置值的变量的地址(从技术角度看,地址就是指向变量的指针),而被调用函数则需要将对应的参数声明为指针类型,并通过它间接访问变量。

如果是数组参数,情况就有所不同了。当把数组名用作参数时,传递给函数的值是数组起始元素的位置或地址——它并不复制数组元素本身。在被调用函数中,可以通过数组下标访问或修改数组元素的值。

自动变量

函数中的每个局部变量只在函数被调用时存在,在函数执行完毕退出时消失。这也是其他语言通常把这类变量称为“自动变量”的原因。在 C 语言中,“自动变量”和“局部变量”是等效的。

由于自动变量只在函数调用执行期间存在,因此,在函数的两次调用之间,自动变量 不保留 前次调用时的赋值,且在每次进入函数时都要显式为其赋值。如果自动变量没有赋值,则其中存放的是无效值。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
void foo(void){
int i;
printf("%d\n", i);
i = 777;
}
int main(int argc, char *argv[]){
foo();
printf("hello\n");
foo();
return 0;
}

结果:

1
2
3
0
hello
32767

外部变量

另一方面,如果一个外部变量在定义之前就要使用到,或者这个外部变量定义在与所要使用它的源文件不相同的源文件中,那么要在相应的变量说明中强制性地使用关键词 extern

除自动变量外,还可以定义于所有函数外部的变量,外部变量的特点是:

  1. 可以在整个文件范围内访问,其他文件可以通过 extern 说明来访问它。
  2. 必须定义在所有函数之外,且只能定义一次。定义后编译程序将为它分配存储单元。
  3. 外部变量在程序执行期间一直存在,而不是在函数调用时产生、在函数执行完毕时消失。
  4. 即使在对外部变量赋值的函数返回后,这些变量仍将保持原来的值不变。

在每个需要访问外部变量的函数中,必须声明相应的外部变量,此时说明其类型。声明时可以用 extern语句显式声明 ,也可以 通过上下文隐式声明 。例如:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <stdio.h>
#define MAXLINE 1000 /* maximum input line size */
int getline(void);
void copy(void);
/* print longest input line; specialized version */
main(){
int len;
extern int max;
extern char longest[];
max = 0;
while ((len = getline()) > 0)
if (len > max) {
max = len;
copy();
}
if (max > 0) /* there was a line */
printf("%s", longest);
return 0;
}
int max; /* maximum length seen so far */
char longest[MAXLINE]; /* longest line saved here */
char line[MAXLINE]; /* current input line */
/* getline: specialized version */
int getline(void){
int c, i;
extern char line[];
for (i = 0; i < MAXLINE - 1
&& (c=getchar)) != EOF && c != '\n'; ++i)
line[i] = c;
if (c == '\n') {
line[i] = c;
++i;
}
line[i] = '\0';
return i;
}
/* copy: specialized version */
void copy(void){
int i;
extern char line[], longest[];
i = 0;
while ((longest[i] = line[i]) != '\0')
++i;
}

在该例子中,第30~42行定义了main、getline与copy函数使用的几个外部变量,声明了各外部变量的类型,这样编译程序将为它们分配存储单元。这种类型的声明除了在前面加上一个关键字extern外,其他方面与普通变量的声明相同。

什么时候必须使用 extern

在源文件中,如果外部变量的定义在使用它的函数之前,并且与它在同一个文件中,那么在那个函数中就没有必要使用 extern 声明。否则就必须加上 extern 声明。因此,上面例子中的 copy、getline中的几个 extern 声明可以省略。而 main 函数中的 extern 则是必需的。

如果程序包含在多个源文件中,而某个变量在 file1 文件中定义,在 file2 和 file3 文件中使用,那么在文件 file2 与 file3 中就需要使用 extern 声明来建立该变量与其定义之间的联系。人们通常把变量和函数的extern声明放在一个单独的文件中(习惯上称之为头文件),并在每个源文件的开头使用 #include 语句把所要用的头文件包含进来。后缀名 .h 约定为头文件名的扩展名。例如,标准库中的函数就是在类似于 <stdio.h> 的头文件中声明的。

静态变量

外部 static 说明

如果不想让某个文件定义的外部变量被其他文件使用,可以使用 static 说明。外部static说明最常用于说明变量,用于把这些对象的作用域限定为被编译源文件的剩余部分。它也可用于说明函数。通常情况下,函数名字是全局的,在整个程序的各个部分都可见。然而,如果把一个函数说明成静态的,那么该函数名字就不能用在除该函数说明所在的文件之外的其他文件中。

可以在通常的说明之前前缀以关键词 static 来指定静态存储。如果把上述两个函数与两个变量放在一个文件中编译,如下:

1
2
static char longest[MAXLINE]; /* longest line saved here */
static char line[MAXLINE]; /* current input line */

那么其他函数不能访问变量 longest 与 line,故这两个名字不会和同一程序中其他文件中的同名名字相冲突。

内部 static 说明

static说明也可用于说明内部变量。内部静态变量就像自动变量一样局部于某一特定函数,只能在该函数中使用,但与自动变量不同的是,不管其所在函数是否被调用,它都是一直存在的,而不像自动变量那样,随着所在函数的调用与退出而存在与消失。换而言之,内部静态变量是一种只能在某一特定函数中使用的但一直占据存储空间的变量。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
void foo(void){
static int i;
printf("%d\n", i);
i = 777;
}
int main(int argc, char *argv[]){
foo();
printf("hello\n");
foo();
return 0;
}

结果:

1
2
3
0
hello
777

寄存器变量

register说明用于提醒编译程序所说明的变量在程序中使用频率较高。其思想是,将寄存器变量放在机器的寄存器中,这样可以使程序更小、执行速度更快。但编译程序可以忽略此选项。

register说明如下所示:

1
2
register int x;
register char c;

寄存器说明只适用于自动变量以及函数的形式参数。对于后一种情况,例子如下:

1
2
3
4
5
f(register unsigned m, register long n)
{
register int i;
...
}

内联函数

C99引入一个新关键字inline,用于定义内联函数(inline function)。这种用法在内核代码中很常见,例如include/linux/rwsem.h中:

1
2
3
4
5
6
7
static inline void down_read(struct rw_semaphore *sem)
{
might_sleep();
rwsemtrace(sem,"Entering down_read");
__down_read(sem);
rwsemtrace(sem,"Leaving down_read");
}

inline关键字告诉编译器,这个函数的调用要尽可能快,可以当普通的函数调用实现,也可以用宏展开的办法实现。详见Cpp-函数

回调函数

如果参数是一个函数指针,调用者可以传递一个函数的地址给实现者,让实现者去调用它,这称为回调函数(Callback Function)。

例如标准库中的qsort(3),快速排序是一种比较型排序,为了方便处理不同类型的比较,在调用该函数时,需要调用者提供一个专用于该类型的比较函数。具体看qsort的帮助文档。

声明

回调函数要求其形式参数必须是 void * 类型。例如:

1
void func(void (*f)(void *), void *p);

在实现该函数时,再根据具体参数类型做强制类型转换。

示例

示例一:泛型算法

回调函数的一个典型应用就是实现类似C++的泛型算法(Generics Algorithm)。下面实现的max函数可以在任意一组对象中找出最大值,可以是一组int、一组char或者一组结构体,但是实现者并不知道怎样去比较两个对象的大小,调用者需要提供一个做比较操作的回调函数。

1
2
3
4
5
6
7
8
/* generics.h */
#ifndef GENERICS_H
#define GENERICS_H
typedef int (*cmp_t)(void *, void *);
extern void *max(void *data[], int num, cmp_t cmp);
#endif

1
2
3
4
5
6
7
8
9
10
11
12
13
/* generics.c */
#include "generics.h"
void *max(void *data[], int num, cmp_t cmp)
{
int i;
void *temp = data[0];
for(i=1; i<num; i++) {
if(cmp(temp, data[i])<0)
temp = data[i];
}
return temp;
}

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
26
27
28
29
/* main.c */
#include <stdio.h>
#include "generics.h"
typedef struct {
const char *name;
int score;
} student_t;
int cmp_student(void *a, void *b)
{
if(((student_t *)a)->score > ((student_t *)b)->score)
return 1;
else if(((student_t *)a)->score == ((student_t *)b)->score)
return 0;
else
return -1;
}
int main(void)
{
student_t list[4] = { {"Tom", 68}, {"Jerry", 72},
{"Moby", 60}, {"Kirby", 89} };
student_t *plist[4] = {&list[0], &list[1], &list[2], &list[3]};
student_t *pmax = max((void **)plist, 4, cmp_student);
printf("%s gets the highest score %d\n", pmax->name, pmax->score);
return 0;
}

应用二:异步调用

异步调用也是回调函数的一种典型用法,调用者首先将回调函数传给实现者,实现者记住这个函数,这称为注册一个回调函数,然后当某个事件发生时实现者再调用先前注册的函数,比如sigaction(2)注册一个信号处理函数,当信号产生时由系统调用该函数进行处理,再比如pthread_create(3)注册一个线程函数,当发生调度时系统切换到新注册的线程函数中运行,在GUI编程中异步回调函数更是有普遍的应用,例如为某个按钮注册一个回调函数,当用户点击按钮时调用它。

以下是一个代码框架。

1
2
3
4
5
6
7
8
/* registry.h */
#ifndef REGISTRY_H
#define REGISTRY_H
typedef void (*registry_t)(void);
extern void register_func(registry_t);
#endif

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* registry.c */
#include <unistd.h>
#include "registry.h"
static registry_t func;
void register_func(registry_t f)
{
func = f;
}
static void on_some_event(void)
{
...
func();
...
}

可变参数

printf 函数就是一个带有可变的参数的函数:

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

现在我们实现一个简单的myprintf函数:

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
26
27
28
29
30
31
32
33
34
#include <stdio.h>
#include <stdarg.h>
void myprintf(const char *format, ...)
{
va_list ap;
char c;
va_start(ap, format);
while (c = *format++) {
switch(c) {
case 'c': {
/* char is promoted to int when passed through '...' */
char ch = va_arg(ap, int);
putchar(ch);
break;
}
case 's': {
char *p = va_arg(ap, char *);
fputs(p, stdout);
break;
}
default:
putchar(c);
}
}
va_end(ap);
}
int main(void)
{
myprintf("c\ts\n", '1', "hello");
return 0;
}

深入阅读

  1. 定义和声明
  2. C语言inline详细讲解