C 语言的变量作用域及头文件

C语言的变量作用域及头文件

关于C语言的变量作用域和头文件的问题都是比较基础的问题,但是这些问题在实际使用过程中的概念不清和混乱会对一个多文件的项目的组织结构及文件结构造成很大的影响,使得项目本身的脉络也变的很模糊。在项目中,多人相互协作完成的项目,这个问题就更加突出。所以也就有了我写(总结)这个文档。

一.C 语言的变量作用域及相关

1.作用域:

作用域描述了程序中可以访问一个标识符的一个或多个区域。即变量的可见性。一个C变量的作用域可以是代码块作用域、函数原型作用域,和文件作用域。

函数作用域(Function Scope),标识符在整个函数中都有效。只有语句标号属于函数作用域。标号在函数中不需要先声明后使用,在前面用一个goto语句也可以跳转到后面的某个标号,但仅限于同一个函数之中。

文件作用域(File Scope),标识符从它声明的位置开始直到这个程序文件的末尾都有效。例如下例中main函数外面的sum,add,还有main也算,printf其实是在stdio.h中声明的,被包含到这个程序文件中了,所以也算文件作用域的。

块作用域(Block Scope),标识符位于一对{}括号中(函数体或语句块),从它声明的位置开始到右}括号之间有效。例如上例中main函数里的num。此外,函数定义中的形参也算块作用域的,从声明的位置开始到函数末尾之间有效。

函数原型作用域(Function Prototype Scope),标识符出现在函数原型中,这个函数原型只是一个声明而不是定义(没有函数体),那么标识符从声明的位置开始到在这个原型末尾之间有效。例如void add(int num);中的num。

下面再介绍另一种分类形式:它分为代码块作用域和文件作用域。代码块作用域和文件作用域也有另一种分类方法,局部作用域和全局作用域。

代码块作用域:代码块是指一对花括号之间的代码,函数的形参虽然是在花括号前定义但也属于代码作用域。在C99中把代码块的概念扩大到包括由for循环、while循环、do while循环、if语句所控制的代码。在代码块作用域中,从该变量被定义到代码块末尾该变量都可见。 文件作用域:一个在所有函数之外定义的变量具有文件作用域。具有文件作用域的变量从它的定义处到包含该定义的文件结尾都是可见的。

2.链接

一个C语言变量具有下列链接之一:外部链接(external linkage),内部链接(internal linkage)或空链接(no linkage)。

空链接:具有代码块作用域或者函数原型作用域的变量就具有空链接,这意味着他们是由其定义所在的代码块或函数原型所私有。 内部链接:具有文件作用域的变量可能有内部或外部链接,一个具有文件作用域的变量前使用了static标识符标识时,即为具有内部链接的变量。一个具有内部链接的变量可以在一个文件的任何地方使用。 外部链接:一个具有文件作用域的变量默认是具有外部链接的。但当起前面用static标识后即转变为内部链接。一个具有外部链接的链接的变量可以在一个多文件程序的任何地方使用。

例:

static int a;(在所有函数外定义)内部链接变量 int b; (在所有函数外定义) 外部链接变量 main() {

int b;//空链接,仅为main函数私有。

..

}

3.存储时期

一个C语言变量有以下两种存储时期之一:(未包括动态内存分配malloc和free等) 静态存储时期(static storage duration)和自动存储时期(automatic storage duration)和动态存储时期。 静态存储时期:如果一个变量具有静态存储时期,他在程序执行期间将一直存在。具有文件作用域的变量具有静态存储时期。这里注意一点:对于具有文件作用域的变量,关键词static表明链接类型,而不是存储时期。一个使用了static声明了的文件作用域的变量具有内部链接,而所有的文件作用域变量,无论他具有内部链接,是具有外部链接,都具有静态存储时期。

自动存储时期:具有代码块作用域的变量一般情况下具有自动存储时期。在程序进入定义这些变量的代码块时,将为这些变量分配内存,当退出这个代码块时,分配的内存将被释放。

举例如下:

//example_1.c

以上示例须在在C99标准下编译。(gcc支持c99的方法,编译时加入参数 –std=C99)。从上例中可以比较清楚明白代码作用域和文件作用域的概念。另外注意文件作用域不仅限于变量也包括函数。在文件作用域中函数也是以其声明开始到文件结尾结束。而且当拥有文件作用域与拥有代码作用域变量同名时,不会发生冲突,而是以最小作用域的变量可见。

4.存储类修饰符(Storage Class Specifier)

有以下几种关键字,可以修饰变量或函数声明:

static,用它修饰的变量的存储空间是静态分配的,用它修饰的文件作用域的变量或函数具有Internal Linkage(内部链接)。 auto,用它修饰的变量在函数调用时自动在栈上分配存储空间,函数返回时自动释放,例如上例中main函数里的num其实就是用auto修饰的,只不过auto可以省略不写(此处与编译器有关,参照编译器不同而有所变动),auto不能修饰文件作用域的变量。

register,编译器对于用register修饰的变量会尽可能分配一个专门的寄存器来存储,但如果实在分配不开寄存器,编译器就把它当auto变量处理了,register不能修饰文件作用域的变量。现在一般编译器的优化都做得很好了,它自己会想办法有效地利用CPU的寄存器,所以现在register关键字也用得比较少了。 extern,上面讲过,链接属性是根据一个标识符多次声明时是不是代表同一个变量或函数来分类的,extern关键字就用于多次声明同一个标识符。

c语言使用作用域,链接和存储时期来定义了5种存储类:自动,寄存器,具有代码块的作用域的静态、具有外部链接的静态,以及具有内部链接的静态。

五种存储类

存储类 时期 作用域 链接 声明方式
自动 自动 代码块 代码块内
寄存器 自动 代码块 代码块内,使用register
具有外部链接的静态 静态 文件之间 外部 所有函数之外
具有内部链接的静态 静态 文件之内 内部 所有函数之外使用关键字static
空链接的静态 静态 代码块 代码块内,使用关键字static

二.头文件的处理和书写

很多人对C语言中的 “文件包含”都不陌生了,文件包含处理在程序开发中会给我们的模块化程序设计带来很大的好处,通过文件包含的方法把程序中的各个功能模块联系起来是模块化程序设计中的一种非常有利的手段。 头文件的功能:

(1)通过头文件来调用库功能。在很多场合,源代码不便(或不准)向用户公布,只要向用户提供头文件和二进制的库即可。用户只需要按照头文件中的接口声明来调用库功能,而不必关心接口怎么实现的。编译器会从库中提取相应的代码。

(2)头文件能加强类型安全检查。如果某个接口被实现或被使用时,其方式与头文件中的声明不一致,编译器就会指出错误,这一简单的规则能大大减轻程序员调试、改错的负担。

文件包含处理是指在一个源文件中,通过文件包含命令将另一个源文件的内容全部包含在此文件中。在源文件编译时,连同被包含进来的文件一同编译,生成目标目标文件。怎么写文件件? 怎么包含才能避免重定义? 其实这个只要了解了文件包含的基本处理方法就可以对文件包含有一个很好的理解与应用了: 文件包含的处理方法:

(1) 处理时间:文件包含也是以”#”开头来写的(#include ), 那么它就是写给预处理器来看了, 也就是说文件包含是会在编译预处理阶段进行处理的。

(2) 处理方法:在预处理阶段,系统自动对#include命令进行处理,具体做法是:降包含文件的内容复制到包含语句(#include )处,得到新的文件,然后再对这个新的文件进行编译。

抓住这两点,那么这个就没有什么难的了。。。

首先,先对#include指令的作用和实际验证一下。 #include指令是预处理指令,它仅仅是将#incluce “A.h”中的A.h的内容替换它自己所在位置,和C语言中的宏的使用类似。而且A.h并不是说必须是.h的文件,只要是文本文件都可以的。下面我给出两个例子证明一下。

例1:有以下两个文件,main.c和main.n

这时我们对main.c进行编译 gcc main.c -o main.exe(我在windows系统下),你会发现能编译通过并打印出n的值。如果你使用预编译参数-E,会在预编译后的文件中发现其原因所在。使用 gcc -E main.c -o main.cpp。打开main.cpp后在文件最后会有如下内容。

以上的示例应该能比较明显解释#include的作用,和使用方法了。但是在实际开发中,这种使用方式是严重的不规范行为,强烈建议不要使用。同样下边的例子也是一样的建议。

例2:

(1)包含.c文件:

这个例子是采用 包含.c文件 的方法实现的。 在编译时,直接去编译main.c文件,预处理器会先把test.c文件中的内容复制到main.c中来,然后再对新的main.c进行编译。

编译命令:

  • gcc main.c -o main

可以看到,这里并没有对test.c进行编译,但还是生成了最终的main可执行程序。

也可以通过命令来观察一下预处理的结果, 编译命令:

  • gcc -E main.c -o main.cpp(仅预处理)

在main.cpp文件末尾可以看来下面一段代码:

可见,其实就是将test.c文件中的内容添加到了main函数之前,然后对新的文件进行编译,生成最终的可执行程序。
这次如果还是按照上面的方法只编译main.c的话就会出错,因为变量m和函数add并没有在main.c中定义,所以编译时需要将test.c一起编译,

编译命令:

  • gcc -c main.c -o main.o #编译main.c
  • gcc -c fun.c -o fun.o #编译fun.c
  • gcc main.o fun.o -o main #用main.o fun.o生成main

到这里大家应该已经理解包含#include文件和多文件程序的本质区别了。

包含文件仅仅是在c预编译时进行再次整合,最终的还是合并成一个文件编译,生成执行文件。

而多文件的编译,是多个文件分别编译,(也可能是在编译时添加必须的标识),然后通过链接器将各个文件链接后加载形成可执行文件。

这种方式会使得我们的定义和声明分开,不容易产生重定义。而且也利于模块化,仅通过头文件来给出接口,而隐藏具体的实现。

预处理时会把头文件中的内容复制到包含它的文件中去,而复制的这些内容只是声名,不是定义,所以它被复制再多份也不会出现”重定义”的错误。。。

前面说了头文件的方法也是模块化程序设计中的一种非常有利的手段。把同一类功能写到一个.c文件中,这样可以把他们划为一个模块,另外再对应的写上一

个.h文件做它的声明。这样以后再使用这个模块时只需要把这两个文件添加进工程,同时在要使用模块内函数或变量的文件中包含.h文件就可以了。

举个很实际的例子,在单片机、ARM或其他嵌入式开发中,每一个平台可能本身都有多种不同的硬件模块,使用时需要去写相应的驱动程序,

这样就可以把各个硬 件模块的驱动程序作为一个模块(比如lcd驱动对对应lcd.c和lcd.h,IIC驱动对应I2C.c和I2C.h等),当具体使用到某个模块时,

只需 要在将对应的.c和.h文件添加进工程,并在文件中包含对就的.h文件即可。

根据以上的原理理解和实际中使用的一些问题及模块化的原则,对头文件写法给出以下几点个人建议作为基础:

(1) 按相同功能或相关性组织.c和.h文件,同一文件内的聚合度要高,不同文件中的耦合度要低。接口通过.h文件给出。

(2) 对应的.c文件中写变量、函数的定义,并指定链接范围。对于变量和函数的定义时,仅本文件使用的变量和函数,要用static限定为内部链接防止外部调用。

(3) 对应的.h文件中写变量、函数的声明。仅声明外部需要的函数,和必须给出变量。有时可以通过使用设定和修改变量函数声明,来减少变量外部声明。

(4) 如果有数据类型的声明 和 宏定义 ,请写的头文件(.h)中,这时也要注意模块化问题,如果数据类型仅本文件使用则不必在写头文件中,而写在源文件(.c)中,会提高聚合度。减少不必要的格式外漏。

(5) 头文件中一定加上#ifndef…#define….#endif之类的防止重包含的语句

(6) 头文件中不要包含其他的头文件,头文件的互相包含使的程序组织结构和文件组织变得混乱,同时给会造成潜在的错误,同时给错误查找造成麻烦。如果出现,头文件中类型定义需要其他头文件时,将其提出来,单独形成全局的一个源文件和头文件。

(7)模块的.c文件中别忘包含自己的.h文件

以上几点仅是个人观点,供大家讨论,如果有意见或是认为不合理或是有更合理的方式请讨论指出。

补充1:

按照c语言的规则,变量和函数必须是先声明再使用。可以多次声明,但不可以多次定义。

补充2:变量的定义和声明。

“声明”仅仅是告诉编译器某个标识符是:变量(什么类型)还是函数(参数和返回值是什么)。要是在后面的代码中出现该标识符,编译器就知道如何处理。记住最重要的一点:声明变量不会导致编译器为这个变量分配存储空间。 C语言专门有一个关键字(keyword)用于声明变量或函数:extern。带有extern的语句出现时,编译器将只是认为你要告诉它某个标识符是什么,除此之外什么也不会做(直接变量初始化除外)。
编译器在什么情况下将语句认为是定义,什么情况下认为是声明。这里给出若干原则:

#1 带有初始化的语句是定义

例如:

int a = 1; //定义

#2 带有extern的语句是声明(除非对变量进行初始化)

例如:

extern int a; //声明

extern int b = 2; //定义

#3既没有初始化又没有extern的语句是“暂时定义”(tentative definition) C语言中,外部变量只能被(正式)定义一次:

int a = 0; int a = 0; //错误!重复定义

又或者:

int a = 0; double a = 0.1; //错误!标识符a已经被使用

暂时定义有点特殊,因为它是暂时的,我们不妨这样看: 暂时定义可以出现无数次,如果在链接时系统全局空间没有相同名字的变量定义,则暂时定义“自动升级”为(正式的)定义,这时系统会为暂时定义的变量分配存储空间,此后,这些相同的暂时定义(加起来)仍然只算作是一个(正式)定义。

例如:

1 3 收藏 评论

相关文章

可能感兴趣的话题



直接登录
跳到底部
返回顶部