C语言变量作用域、链接属性与static关键字
1.不同变量的作用域
1.1 局部变量的代码块作用域
一个代码块可以理解为一对大括号{}
括起来的部分。代码块不等于函数,因为if
、while
、for
都有{}
,所以代码块<=函数。
局部变量的作用域是代码块作用域,也就是说一个局部变量可以被访问和使用的范围仅限于定义这个局部变量的代码块中定义式之后的部分。
1.2 函数名和全局变量的文件作用域
文件作用域的意思就是全局的访问权限,也就是说整个.c
文件中都可以访问这些东西,这就是平时所说的局部和全局,全局就是文件作用域。详细准确的说:在没有声明地情况下,函数和全局变量的作用域是定义所在的整个.c
文件之内、函数或全局变量定义式之后的部分。
1.3 总结
不管是局部变量、全局变量、函数,都要先定义才能使用,准确地说:全局变量/函数的作用域都是自己所在的文件,但是定义式之前的部分因为缺少声明所以没法用,解决方案有两个:
- 1.直接把它定义到文件前面部分去;
- 2.对于全局变量和函数,可以定义到文件后面任意位置,但是在前面加声明。而局部变量因为没法声明,所以只能定义在前面去。
在c89标准的编译器中,现在很多编译器还延续使用c89标准,例如在51单片机编程环境中,所有的局部变量必须先定义在最前面,在变量定义之前不能有一句执行代码。在c99标准的编译器中,Ubuntu
中gcc
兼容c99标准,可以允许在代码块内任意地方定义变量,但是允许定义的变量还是只能使用在定义了之后,定义之前还是不能用的。
1.4 同名变量的掩蔽规则
编程时不可避免会出现同名变量,变量同名后不一定会出错。因为首先如果两个同名变量作用域不同且没有交叠,这种情况下同名没有任何影响;其次如果两个同名变量作用域有交叠,C
语言规定在作用域交叠范围内,作用域小的一个变量会掩蔽掉作用域大的那个。
2.变量的生命周期
研究变量生命周期,有助于理解变量的行为特征。
2.1 栈变量的生命周期
局部变量(栈变量)存储在栈上,生命周期是临时的,也就是说局部变量随代码执行过程按照需要去创建、使用、消亡。
比如一个函数内定义的局部变量,在这个函数每一次被调用时都会创建一次,然后使用,最后在函数返回的时候消亡。这也是一个函数内的局部变量在函数外不能使用地原因。
2.2 堆变量的生命周期
堆内存空间是客观存在的,是由操作系统维护的。我们程序只是去申请然后使用然后释放。我们只关心我们程序使用堆内存的这一段时间,因此堆变量也有了自己的生命周期,就是:从malloc()
申请时诞生,然后使用,直到free()
时消亡。所以堆内存在malloc()
之前和free()
之后不能去访问,因此堆内存在实践编程时都是被反复的malloc()
和free()
的。
2.3 数据段、bss段变量的生命周期
全局变量的生命周期是在程序被执行时诞生,在程序终止时消亡。全局变量所占用的内存是不能被程序自己释放的,是程序结束之后由父进程或者操作系统释放的。所以程序如果申请了过多的全局变量会导致这个程序一直占用大量内存。
如果说堆内存类比成图书馆借的书,那么全局变量就可以类比成自己买的书。
2.4 代码段、只读段的生命周期
代码段其实就是程序执行的代码,其实就是函数,它的生命周期也是在程序被执行时诞生,在程序终止时消亡地。不过一般代码的生命周期我们并不关注。
3.链接属性
3.1 一个C语言程序的组织架构
一个庞大、完整的一个C
语言项目,比如Linux
内核、FFMPEG
,是由多个.c
文件和多个.h
文件组成的。程序的生成过程就是:编译+链接,编译是为了将函数/变量等变成.o
二进制的机器码格式,链接是为了将各个独立分开的二进制的函数链接起来形成一个整体的二进制可执行程序。
编译器工作时是将所有源文件依次读进来,单个为单位进行编译的。链接的时候实际上是把第一步编译生成地单个的.o
文件整体的输入,然后处理链接成一个可执行程序。
3.2 三种链接属性:外链接、内链接、无链接
外链接的意思就是外部链接属性,也就是说这家伙可以在整个程序范围内(言下之意就是可以跨文件,被本工程下其他的源文件访问)进行链接,比如普通的函数和全局变量属于外链接属性。
内链接的意思就是当前.c
文件内部的链接属性,也就是说这家伙可以在当前.c
文件内部范围内进行链接,不能在当前.c
文件外面的其他.c
文件中进行访问、链接。static
修饰的函数/全局变量属于内链接属性。
无链接的意思就是这个符号本身不参与链接,它跟链接没关系,所有的局部变量(auto
的、static
的)都是无链接的。
3.3 函数和全局变量的同名冲突
因为函数和全局变量是外部链接属性,就是说每一个函数和全局变量将来在整个工程中所有的.c
文件都能被访问,因此在一个程序中的任意两个.c
文件中不能出现同名的函数/同名的全局变量。
最简单的解决方案就是起名字不要重复,但是很难做到。主要原因是一个很大的工程中函数和全局变量名字太多了,而且一个大工程不是一个人完成的,是很多人协作完成,所以很难保证不会重名。
现代高级语言,比如C++
中完美解决这个问题的方法是命名空间namespace
,其实就是给一个变量带上各个级别的前缀,但是C
语言不是这么解决的。
C
语言比较早碰到这个问题,当时还没发明namespace
概念,当时C
语言就发明了一种不是很完美但是凑活能用的解决方案,就是三种链接属性的方法。C
语言的链接属性解决重名问题思路是这样的:我们将明显不会在其他.c
文件中引用,只在当前.c
文件中引用的函数/全局变量,使用static
修饰使其成为内链接属性,这样在将来链接时即使2个.c
文件中有重名的函数/全局变量,只要其中一个为内链接属性就没事。这种解决方案在一定程度上解决了问题。但是没有从根本上解决问题,留下了很多麻烦。所以这个就导致了C
语言写很大型的项目难度很大。
4.static的第二种用法
static
的第二种用法:修饰全局变量和函数。普通的(非静态)的函数/全局变量,默认的链接属性是外部的,经过static
修饰后的函数/全局变量,链接属性就变成了内部链接属性。
5.最后的总结
- 普通局部变量分配在栈上,作用域为代码块作用域,生命周期是临时,链接属性为无连接。定义时如果未显式初始化则其值随机,变量地址由运行时在栈上分配得到,多次执行时地址不一定相同,函数不能返回该类变量的地址(指针)作为返回值。
- 静态局部变量分配在数据段/bss段,显式初始化为非0则在数据段,显式初始化为0或未显示初始化则在bss段,作用域为代码块作用域,这是人为规定的,生命周期为永久,链接属性为无链接。定义时如果未显式初始化则其值为0,变量地址由运行时环境在加载程序时确定,整个程序运行过程中唯一不变;静态局部变量其实就是作用域为代码块作用域,同时链接属性为无链接的全局变量。静态局部变量可以改为用全局变量实现,程序中尽量避免用全局变量,因为会破坏结构性。
- 静态全局变量/静态函数和普通全局变量/普通函数的唯一差别是:
static
使全局变量/函数的链接属性由外部链接(整个程序所有文件范围)转为内部链接(当前.c
文件内)。这是为了解决全局变量/函数的重名问题,但是仍未彻底解决。
一些值得注意的点:
- 写程序尽量避免使用全局变量,尤其是非
static
类型的全局变量。能确定不会被其他文件引用的全局变量一定要static
修饰。 - 注意区分全局变量的定义和声明。一般规律如下:如果定义的同时有初始化则一定会被认为是定义;如果只是定义而没有初始化则有可能被编译器认为是定义,也可能被认为是声明,要具体分析;如果使用
extern
则肯定会被认为是声明,实际上使用extern
也可以有定义,实际上加extern
就是明确声明这个变量为外部链接属性。 - 全局变量应该定义在
.c
文件中并且在头文件中声明,而不要定义在头文件中,因为如果定义在头文件中,则该头文件被多个.c
文件包含时该全局变量会重复定义。 - 在
b.c
中引用a.c
中定义的全局变量/函数有2种方法:一是在a.h
中声明该函数/全局变量,然后在b.c
中#include "a.h"
,二是在b.c
中使用extern
显式声明要引用的函数/全局变量,其中第一种方法比较正式。 - 存储类决定生命周期,作用域决定链接属性,宏和内联函数的链接属性为无链接。