~Linux C_8/14_作用域
====extern and static====
/* stack.c */ static void push(char c); // 其他文件无法调用该函数,不提供连接支持 extern char pop(void); // 其他文件可以调用该文件的这个函数定义 int is_empty(void); // 同上,默认是extern char stack[512]; int top = -1; void push(char c) { stack[++top] = c; } char pop(void) { return stack[top--]; } int is_empty(void) { return top == -1; } 编译链接在一起会出错: $ gcc stack.c main.c /tmp/ccRC2Yjn.o: In function `main': main.c:(.text+0x12): undefined reference to `stack' collect2: ld returned 1 exit status
虽然在stack.c中定义了函数push,但这个函数只具有Internal Linkage,只有在stack.c中多次声明才表示同一个函数,而在main.c中声明就不表示它了。
如果把stack.c编译成目标文件,函数名push在其中是一个LOCAL的符号,不参与链接过程,所以在链接时,main.c中用到一个External Linkage的push函数,链接器却找不到它的定义在哪儿,无法确定它的地址,也就无法做符号解析,只好报错。
凡是被多次声明的变量或函数,必须有且只有一个声明是定义,如果有多个定义,或者一个定义都没有,链接器就无法完成链接。
修饰变量声明的情况。仍然用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的。
==== 头文件 ====
为什么#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
则可以用gcc -c main.c编译,gcc会自动在main.c所在的目录中找到stack.h。
假如把stack.h移到一个子目录下:
$ tree . |-- main.c `-- stack //#include "stack/stack.h" 无须用 -I的小技巧 |-- stack.c `-- stack.h
则需要用gcc -c main.c -Istack编译。
===
提醒:用-I选项告诉gcc头文件要到子目录stack里找。
===
Header Guard :
#ifndef STACK_H #define 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,宏定义名就用头文件名的大写形式,这是规范的做法。
避免头文件重复的原因是:
1. 一是使预处理的速度变慢了,要处理很多本来不需要处理的头文件。
2. 二是如果有foo.h包含bar.h,bar.h又包含foo.h的情况,预处理器就陷入死循环了(其实编译器都会规定一个包含层数的上限)。
3. 三是头文件里有些代码不允许重复出现,虽然变量和函数允许多次声明(只要不是多次定义就行),但头文件里有些代码是不允许多次出现的,比如typedef类型定义和结构体Tag定义等,在一个程序文件中只允许出现一次。
#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就不能链接在一起了。
===
警告:
===
头文件中的变量和函数声明一定不能是定义。如果头文件中出现变量或函数定义,这个头文件又被多个.c文件包含,那么这些.c文件就不能链接在一起了。
让我对上述的内容做一个小总结
对函数相对简单
=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关键字声明函数。
对变量比较复杂
=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 |
上表的每个单元格里分成四行,分别描述变量的链接属性、生存期,以及这种变量如何初始化,是否算变量定义。
链接属性: External Linkage Internal Linkage No Linkage Previous Linkage 生存期: Static Duration Automatic Duration 初始化: Static Initializer // 表示Initializer中只能使用常量表达式,表达式的值必须在编译时就能确定 Dynamic Initializer // 表示 Initializer中可以使用任意的右值表达式,表达式的值可以在运行时计算 是否算变量定义: Definition(算变量定义) Not a Definition(不算变量定义) Tentative Definition(暂定的变量定义) // 没有Storage Class关键字修饰,或者用static关键字修饰,那么如果它有Initializer则编译器认为它就是一个变量定义 // 如果它没有Initializer则编译器暂定它是变量定义,如果程序文件中有这个变量的明确定义就用明确定义, // 如果程序文件没有这个变量的明确定义,就用这个暂定的变量定义,这种情况下变量以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对于这种写法会报警告,为了兼容性应避免这种写法。
==== 作用域 ====
我们总结一下相关的C语法:
作用域(Scope)这个概念适用于所有标识符,而不仅仅是变量,C语言的作用域分为以下几类:
*函数作用域(Function Scope),标识符在整个函数中都有效。只有语句标号属于函数作用域。标号在函数中不需要先声明后使用,在前面用一个goto语句也可以跳转到后面的某个标号,但仅限于同一个函数之中。
*块作用域(Block Scope),标识符位于一对{}括号中(函数体或语句块),从它声明的位置开始到右}括号之间有效。例如上例中main函数里的a、b、c。此外,函数定义中的形参也算块作用域的,从声明的位置开始到函数末尾之间有效。
*函数原型作用域(Function Prototype Scope),标识符出现在函数原型中,这个函数原型只是一个声明而不是定义(没有函数体),那么标识符从声明的位置开始到在这个原型末尾之间有效。例如int foo(int a, int b);中的a和b。
*文件作用域(File Scope),标识符从它声明的位置开始直到这个程序文件[30]的末尾都有效。例如上例中main函数外面的A、a、b、c,还有main也算,printf其实是在stdio.h中声明的,被包含到这个程序文件中了,所以也算文件作用域的。
==== 命名空间 ====
对属于同一命名空间(Name Space)的重名标识符,内层作用域的标识符将覆盖外层作用域的标识符,例如局部变量名在它的函数中将覆盖重名的全局变量。命名空间可分为以下几类:
*语句标号单独属于一个命名空间。例如在函数中局部变量和语句标号可以重名,互不影响。由于使用标号的语法和使用其它标识符的语法都不一样,编译器不会把它和别的标识符弄混。
*struct,enum和union(下一节介绍union)的类型Tag属于一个命名空间。由于Tag前面总是带struct,enum或union关键字,所以编译器不会把它和别的标识符弄混。
*struct和union的成员名属于一个命名空间。由于成员名总是通过.或->运算符来访问而不会单独使用,所以编译器不会把它和别的标识符弄混。
*所有其它标识符,例如变量名、函数名、宏定义、typedef的类型名、enum成员等等都属于同一个命名空间。如果有重名的话,宏定义覆盖所有其它标识符,因为它在预处理阶段而不是编译阶段处理,除了宏定义之外其它几类标识符按上面所说的规则处理,内层作用域覆盖外层作用域。
// 当冲突时,是否被覆盖。
====链接属性(Linkage)====
标识符的链接属性(Linkage)有三种:
*外部链接(External Linkage),如果最终的可执行文件由多个程序文件链接而成,一个标识符在任意程序文件中即使声明多次也都代表同一个变量或函数,则这个标识符具有External Linkage。具有External Linkage的标识符编译后在符号表中是GLOBAL的符号。例如上例中main函数外面的a和c,main和printf也算。
*内部链接(Internal Linkage),如果一个标识符在某个程序文件中即使声明多次也都代表同一个变量或函数,则这个标识符具有Internal Linkage。例如上例中main函数外面的b。如果有另一个foo.c程序和main.c链接在一起,在foo.c中也声明一个static int b;,则那个b和这个b不代表同一个变量。具有Internal Linkage的标识符编译后在符号表中是LOCAL的符号,但main函数里面那个a不能算Internal Linkage的,因为即使在同一个程序文件中,在不同的函数中声明多次,也不代表同一个变量。
*无链接(No Linkage)。除以上情况之外的标识符都属于No Linkage的,例如函数的局部变量,以及不表示变量和函数的其它标识符。
====存储类修饰符====
存储类修饰符(Storage Class Specifier)有以下几种关键字,可以修饰变量或函数声明:
*static,用它修饰的变量的存储空间是静态分配的,用它修饰的文件作用域的变量或函数具有Internal Linkage。
*auto,用它修饰的变量在函数调用时自动在栈上分配存储空间,函数返回时自动释放,例如上例中main函数里的b其实就是用auto修饰的,只不过auto可以省略不写,auto不能修饰文件作用域的变量。
*register,编译器对于用register修饰的变量会尽可能分配一个专门的寄存器来存储,但如果实在分配不开寄存器,编译器就把它当auto变量处理了,register不能修饰文件作用域的变量。现在一般编译器的优化都做得很好了,它自己会想办法有效地利用CPU的寄存器,所以现在register关键字也用得比较少了。
*extern,上面讲过,链接属性是根据一个标识符多次声明时是不是代表同一个变量或函数来分类的,extern关键字就用于多次声明同一个标识符,下一章再详细介绍它的用法。
*typedef,在第 2.4 节 “sizeof运算符与typedef类型声明”讲过这个关键字,它并不是用来修饰变量的,而是定义一个类型名。在那一节也讲过,看typedef声明怎么看呢,首先去掉typedef把它看成变量声明,看这个变量是什么类型的,那么typedef就定义了一个什么类型,也就是说,typedef在语法结构中出现的位置和前面几个关键字一样,也是修饰变量声明的,所以从语法(而不是语义)的角度把它和前面几个关键字归类到一起。
注意,上面介绍的const关键字不是一个Storage Class Specifier,虽然看起来它也修饰一个变量声明,但是在以后介绍的更复杂的声明中const在语法结构中允许出现的位置和Storage Class Specifier是不完全相同的。const和以后要介绍的restrict和volatile关键字属于同一类语法元素,称为类型限定符(Type Qualifier)。
// 注意区别 “存储类修饰符” 与 “类型限定符”。
====生存期====
变量的生存期(Storage Duration,或者Lifetime)分为以下几类:
*静态生存期(Static Storage Duration),具有外部或内部链接属性,或者被static修饰的变量,在程序开始执行时分配和初始化一次,此后便一直存在直到程序结束。这种变量通常位于.rodata, .data 或 .bss段,例如上例中main函数外的A,a,b,c,以及main函数里的a。
*自动生存期(Automatic Storage Duration),链接属性为无链接并且没有被static修饰的变量,这种变量在进入块作用域时在栈上或寄存器中分配,在退出块作用域时释放。例如上例中main函数里的b和c。
*动态分配生存期(Allocated Storage Duration),以后会讲到调用malloc函数在进程的堆空间中分配内存,调用free函数可以释放这种存储空间。