变量和函数的定义和声明
2013-04-03 16:49
441 查看
2. 定义和声明
2.1. extern
和static
关键字
在上一节我们把两个程序文件放在一起编译链接,main.c用到的函数
push、
pop和
is_empty由
stack.c提供,其实有一点小问题,我们用
-Wall选项编译
main.c可以看到:
$ gcc -c main.c -Wall main.c: In function ‘main’: main.c:8: warning: implicit declaration of function ‘push’ main.c:12: warning: implicit declaration of function ‘is_empty’ main.c:13: warning: implicit declaration of function ‘pop’
这个问题我们在第 2 节 “自定义函数”讨论过,由于编译器在处理函数调用代码时没有找到函数原型,只好根据函数调用代码做隐式声明,把这三个函数声明为:
int push(char); int pop(void); int is_empty(void);
现在你应该比学第 2 节 “自定义函数”的时候更容易理解这条规则了。为什么编译器在处理函数调用代码时需要有函数原型?因为必须知道参数的类型和个数以及返回值的类型才知道生成什么样的指令。为什么隐式声明靠不住呢?因为隐式声明是从函数调用代码推导而来的,而事实上函数定义的形参类型可能跟函数调用代码传的实参类型并不一致,如果函数定义带有可变参数(例如
printf),那么从函数调用代码也看不出来这个函数带有可变参数,另外,从函数调用代码也看不出来返回值应该是什么类型,所以隐式声明只能规定返回值都是
int型的。既然隐式声明靠不住,那编译器为什么不自己去找函数定义,而非要让我们在调用之前写函数原型呢?因为编译器往往不知道去哪里找函数定义,像上面的例子,我让编译器编译
main.c,而这几个函数的定义却在
stack.c里,编译器又怎么会知道呢?所以编译器只能通过隐式声明来猜测函数原型,这种猜测往往会出错,但在比较简单的情况下还算可用,比如上一节的例子这么编译过去了也能得到正确结果。
现在我们在
main.c中声明这几个函数的原型:
/* main.c */ #include <stdio.h> extern void push(char); extern char pop(void); extern int is_empty(void); int main(void) { push('a'); push('b'); push('c'); while(!is_empty()) putchar(pop()); putchar('\n'); return 0; }
这样编译器就不会报警告了。在这里
extern关键字表示这个标识符具有External Linkage。External Linkage的定义在上一章讲过,但现在应该更容易理解了,
push这个标识符具有External Linkage指的是:如果把
main.c和
stack.c链接在一起,如果
push在
main.c和
stack.c中都有声明(在
stack.c中的声明同时也是定义),那么这些声明指的是同一个函数,链接之后是同一个
GLOBAL符号,代表同一个地址。函数声明中的
extern也可以省略不写,不写
extern的函数声明也表示这个函数具有External Linkage。
如果用
static关键字修饰一个函数声明,则表示该标识符具有Internal Linkage,例如有以下两个程序文件:
/* foo.c */ static void foo(void) {}
/* main.c */ void foo(void); int main(void) { foo(); return 0; }
编译链接在一起会出错:
$ gcc foo.c main.c /tmp/ccRC2Yjn.o: In function `main': main.c:(.text+0x12): undefined reference to `foo' collect2: ld returned 1 exit status
虽然在
foo.c中定义了函数
foo,但这个函数只具有Internal Linkage,只有在
foo.c中多次声明才表示同一个函数,而在
main.c中声明就不表示它了。如果把
foo.c编译成目标文件,函数名
foo在其中是一个
LOCAL的符号,不参与链接过程,所以在链接时,
main.c中用到一个External Linkage的
foo函数,链接器却找不到它的定义在哪儿,无法确定它的地址,也就无法做符号解析,只好报错。凡是被多次声明的变量或函数,必须有且只有一个声明是定义,如果有多个定义,或者一个定义都没有,链接器就无法完成链接。
以上讲了用
static和
extern修饰函数声明的情况。现在来看用它们修饰变量声明的情况。仍然用
stack.c和
main.c的例子,如果我想在
main.c中直接访问
stack.c中定义的变量
top,则可以用
extern声明它:
/* main.c */ #include <stdio.h> void push(char); char pop(void); int is_empty(void); extern int top; int main(void) { push('a'); push('b'); push('c'); printf("%d\n", top); while(!is_empty()) putchar(pop()); putchar('\n'); printf("%d\n", top); return 0; }
变量
top具有External Linkage,它的存储空间是在
stack.c中分配的,所以
main.c中的变量声明
extern int top;不是变量定义,因为它不分配存储空间。以上函数和变量声明也可以写在
main函数体里面,使所声明的标识符具有块作用域:
int main(void) { void push(char); char pop(void); int is_empty(void); extern int top; push('a'); push('b'); push('c'); printf("%d\n", top); while(!is_empty()) putchar(pop()); putchar('\n'); printf("%d\n", top); return 0; }
注意,变量声明和函数声明有一点不同,函数声明的
extern可写可不写,而变量声明如果不写
extern意思就完全变了,如果上面的例子不写
extern就表示在
main函数中定义一个局部变量
top。另外要注意,
stack.c中的定义是
int top = -1;,而
main.c中的声明不能加Initializer,如果上面的例子写成
extern int top = -1;则编译器会报错。
在
main.c中可以通过变量声明来访问
stack.c中的变量
top,但是从实现
stack.c这个模块的角度来看,
top这个变量是不希望被外界访问到的,变量
top和
stack都属于这个模块的内部状态,外界应该只允许通过
push和
pop函数来改变模块的内部状态,这样才能保证堆栈的LIFO特性,如果外界可以随机访问
stack或者随便修改
top,那么堆栈的状态就乱了。那怎么才能阻止外界访问
top和
stack呢?答案就是用
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 <stdio.h> #include "stack.h" int main(void) { push('a'); push('b'); push('c'); while(!is_empty()) putchar(pop()); putchar('\n'); return 0; }
首先说为什么
#include <stdio.h>用角括号,而
#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这个模块怎么办呢?如果在
foo.c里面也
#include "stack.c",就相当于
push、
pop、
is_empty这三个函数在
main.c和
foo.c中都有定义,那么
main.c和
foo.c就不能链接在一起了。如果采用包含头文件的办法,那么这三个函数只在
stack.c中定义了一次,最后可以把
main.c、
stack.c、
foo.c链接在一起。如下图所示:
图 20.2. 为什么要包含头文件而不是
.c文件
同样道理,头文件中的变量和函数声明一定不能是定义。如果头文件中出现变量或函数定义,这个头文件又被多个
.c文件包含,那么这些
.c文件就不能链接在一起了。
2.3. 定义和声明的详细规则
以上两节关于定义和声明只介绍了最基本的规则,在写代码时掌握这些基本规则就够用了,但其实C语言关于定义和声明还有很多复杂的规则,在分析错误原因或者维护规模较大的项目时需要了解这些规则。本节的两个表格出自[Standard C]。首先看关于函数声明的规则。
表 20.1. Storage Class关键字对函数声明的作用
Storage Class | File Scope Declaration | Block Scope Declaration |
---|---|---|
none | previous linkage can define | previous linkage cannot define |
extern | previous linkage can define | previous linkage cannot define |
static | internal linkage can define | N/A |
extern关键字表示这个标识符具有External Linkage”其实是不准确的,准确地说应该是Previous Linkage。Previous Linkage的定义是:这次声明的标识符具有什么样的Linkage取决于前一次声明,这前一次声明具有相同的标识符名,而且必须是文件作用域的声明,如果在程序文件中找不到前一次声明(这次声明是第一次声明),那么这个标识符具有External Linkage。例如在一个程序文件中在文件作用域两次声明同一个函数:
static int f(void); /* internal linkage */ extern int f(void); /* previous linkage */
则这里的
extern修饰的标识符具有Interanl Linkage而不是External Linkage。从上表的前两行可以总结出我们先前所说的规则“函数声明加不加
extern关键字都一样”。上表也说明了在文件作用域允许定义函数,在块作用域不允许定义函数,或者说函数定义不能嵌套。另外,在块作用域中不允许用
static关键字声明函数。
关于变量声明的规则要复杂一些:
表 20.2. Storage Class关键字对变量声明的作用
Storage Class | File Scope Declaration | Block Scope Declaration |
---|---|---|
none | external linkage static duration static initializer tentative definition | no linkage automatic duration dynamic initializer definition |
extern | previous linkage static duration no initializer[*] not a definition | previous linkage static duration no initializer not a definition |
static | internal linkage static duration static initializer tentative definition | no linkage static duration static initializer definition |
static关键字修饰,那么如果它有Initializer则编译器认为它就是一个变量定义,如果它没有Initializer则编译器暂定它是变量定义,如果程序文件中有这个变量的明确定义就用明确定义,如果程序文件没有这个变量的明确定义,就用这个暂定的变量定义[32],这种情况下变量以0初始化。在[C99]中有一个例子:
int i1 = 1; // definition, external linkage static int i2 = 2; // definition, internal linkage extern int i3 = 3; // definition, external linkage int i4; // tentative definition, external linkage static int i5; // tentative definition, internal linkage int i1; // valid tentative definition, refers to previous int i2; // 6.2.2 renders undefined, linkage disagreement int i3; // valid tentative definition, refers to previous int i4; // valid tentative definition, refers to previous int i5; // 6.2.2 renders undefined, linkage disagreement extern int i1; // refers to previous, whose linkage is external extern int i2; // refers to previous, whose linkage is internal extern int i3; // refers to previous, whose linkage is external extern int i4; // refers to previous, whose linkage is external extern int i5; // refers to previous, whose linkage is internal
变量
i2和
i5第一次声明为Internal Linkage,第二次又声明为External Linkage,这是不允许的,编译器会报错。注意上表中标有
[*]的单元格,对于文件作用域的
extern变量声明,C99是允许带Initializer的,并且认为它是一个定义,但是
gcc对于这种写法会报警告,为了兼容性应避免这种写法。
相关文章推荐
- js 函数定义,使用,作用域,变量声明提升,遇解析.....
- VC++如何声明定义全局函数与全局变量
- Kotlin教程学习-函数定义,变量声明
- JSP声明定义网页范围的变量、函数或类
- C变量和函数的声明和定义
- 变量和函数的声明与定义(C/C++)
- 多个".h"文件中声明及定义 全局变量和函数
- 定义一个函数求字符串的长度,要求该函数体内不能声明任何变量
- C语言如何跨文件调用函数定义中声明的变量
- JS中定义函数的两种方式哪个优先级高·引出变量和函数声明的运行顺序
- 在函数内部定义的变量加与不加var的区别,匿名函数和有名函数内声明变量的区别
- C语言如何跨文件调用函数定义中声明的变量
- C语言学习笔记:13_变量和函数的声明与定义
- 递归:从尾到头输出链表& 从尾到头输出一个字符串 & 定义一个函数求字符串的长度,要求该函数体内不能声明任何变量
- 定义一个函数求字符串的长度,要求该函数体内不能声明任何变量
- PHP函数定义声明以及函数类型(回调函数,递归函数,重用函数。。)的介绍 变量的范围
- #ifdef OS_GLOBLES… 变量/函数的定义和声明
- C++中重构函数声明定义与static变量使用方法
- 变量、函数声明与定义
- 浅谈 变量以及函数的声明以及定义,以及对extern “C”的理解