变量和函数的定义和声明 (二)

2014-11-24 03:18:34 · 作者: · 浏览: 1
就是用static关键字把它们声明为Internal Linkage的:

/* stack.c */
static char stack[512];
static int top = -1;

void push(char c)
{
stack[++top] = c;
}

char pop(void)
{
return stack[top--];
}

int is_empty(void)
{
return top == -1;
}这样,即使在main.c中用extern声明也访问不到stack.c的变量top和stack。从而保护了stack.c模块的内部状态,这也是一种封装(Encapsulation)的思想。

用static关键字声明具有Internal Linkage的函数也是出于这个目的。在一个模块中,有些函数是提供给外界使用的,也称为导出(Export)给外界使用,这些函数声明为External Linkage的。有些函数只在模块内部使用而不希望被外界访问到,则声明为Internal Linkage的。

2.2. 头文件
我们继续前面关于stack.c和main.c的讨论。stack.c这个模块封装了top和stack两个变量,导出了push、pop、is_empty三个函数接口,已经设计得比较完善了。但是使用这个模块的每个程序文件都要写三个函数声明也是很麻烦的,假设又有一个foo.c也使用这个模块,main.c和foo.c中各自要写三个函数声明。重复的代码总是应该尽量避免的,以前我们通过各种办法把重复的代码提取出来,比如在第 2 节 “数组应用实例:统计随机数”讲过用宏定义避免硬编码的问题,这次有什么办法呢?答案就是可以自己写一个头文件stack.h:

/* stack.h */
#ifndef STACK_H
#define STACK_H
extern void push(char);
extern char pop(void);
extern int is_empty(void);
#endif这样在main.c中只需包含这个头文件就可以了,而不需要写三个函数声明:

/* main.c */
#include
#include "stack.h"

int main(void)
{
push('a');
push('b');
push('c');

while(!is_empty())
putchar(pop());
putchar('\n');

return 0;
}首先说为什么#include 用角括号,而#include "stack.h"用引号。对于用角括号包含的头文件,gcc首先查找-I选项指定的目录,然后查找系统的头文件目录(通常是/usr/include,在我的系统上还包括/usr/lib/gcc/i486-linux-gnu/4.3.2/include);而对于用引号包含的头文件,gcc首先查找包含头文件的.c文件所在的目录,然后查找-I选项指定的目录,然后查找系统的头文件目录。

假如三个代码文件都放在当前目录下:

$ tree
.
|-- main.c
|-- stack.c
`-- stack.h

0 directories, 3 files则可以用gcc -c main.c编译,gcc会自动在main.c所在的目录中找到stack.h。假如把stack.h移到一个子目录下:

$ tree
.
|-- main.c
`-- stack
|-- stack.c
`-- stack.h

1 directory, 3 files则需要用gcc -c main.c -Istack编译。用-I选项告诉gcc头文件要到子目录stack里找。

在#include预处理指示中可以使用相对路径,例如把上面的代码改成#include "stack/stack.h",那么编译时就不需要加-Istack选项了,因为gcc会自动在main.c所在的目录中查找,而头文件相对于main.c所在目录的相对路径正是stack/stack.h。

在stack.h中我们又看到两个新的预处理指示#ifndef STACK_H和#endif,意思是说,如果STACK_H这个宏没有定义过,那么从#ifndef到#endif之间的代码就包含在预处理的输出结果中,否则这一段代码就不出现在预处理的输出结果中。stack.h这个头文件的内容整个被#ifndef和#endif括起来了,如果在包含这个头文件时STACK_H这个宏已经定义过了,则相当于这个头文件里什么都没有,包含了一个空文件。这有什么用呢?假如main.c包含了两次stack.h:

...
#include "stack.h"
#include "stack.h"

int main(void)
{
...则第一次包含stack.h时并没有定义STACK_H这个宏,因此头文件的内容包含在预处理的输出结果中:

...
#define STACK_H
extern void push(char);
extern char pop(void);
extern int is_empty(void);
#include "stack.h"

int main(void)
{
...其中已经定义了STACK_H这个宏,因此第二次再包含stack.h就相当于包含了一个空文件,这就避免了头文件的内容被重复包含。这种保护头文件的写法称为Header Guard,以后我们每写一个头文件都要加上Header Guard,宏定义名就用头文件名的大写形式,这是规范的做法。

那为什么需要防止重复包含呢?谁会把一个头文件包含两次呢?像上面那么明显的错误没人会犯,但有时候重复包含的错误并不是那么明显的。比如:

#include "stack.h"
#include "foo.h"然而foo.h里又包含了bar.h,bar.h里又包含了stack.h。在规模较大的项目中头文件包含头文件的情况很常见,经常会包含四五层,这时候重复包含的问题就很难发现了。比如在我的系统头文件目录/usr/include中,errno.h包含了bits/errno.h,后者又包含了linux/errno.h,后者又包含了asm/errno.h,后者又包含了asm-generic/errno.h。

另外一个问题是,就算我是重复包含了头文件,那有什么危害么?像上面的三个函数声明,在程序中声明两次也没有问题,对于具有External Linkage的函数,声明任意多次也都代表同一个函数。重复包含头文件有以下问题:

一是使预处理的速度变慢了,要处理很多本来不需要处理的头文件。

二是如果有foo.h包含bar.h,bar.h又包含foo.h的情况,预处理器就陷入死循环了(其实编译器都会规定一个包含层数的上限)。

三是头文件里有些代码不允许重复出现,虽然变量和函数允许多次声明(只要不是多次定义就行),但头文件里有些代码是不允许多次出现的,比如typedef类型定义和结构体Tag定义等,在一个程序文件中只允许出现一次。

还有一个问题,既然要#include头文件,那我不如直接在main.c中#include "stack.c"得了。这样把stack.c和main.c合并为同一个程序文件,相当于又回到最初的例 12.1 “用堆栈实现倒序打印”了。当然这样也能编译通过,但是在一个规模较大的项目中不能这么做,假如又有一个foo.c也要使用stack.c这