无论学习哪一种语言,都免不了要讨论这些问题。而且这些问题,深究起来有时也让我们很迷惑。
标识符的定义无需多讲,只需注意不仅仅是指变量,还有函数,标签等。
作用域是指允许对标识符进行访问的位置范围。按照C99(章节6.2.1),C语言的作用域共有 4 种类型:文件作用域、代码块作用域、函数作用域、函数原型作用域。
类型 |
位置 |
说明 |
文件作用域 (file) | 在所有 代码块和参数列表 之外 | 整个文件内都可以访问 |
代码块作用域 ( block) | 在“代码块”或者“函数的参数列表”内部 | 只有所在的代码块内可以访问 |
函数作用域 (function) | 函数体内 | 具有此作用域的只有一种语句:只有goto语句要使用的“语句标签”。简化为一条规则:一个函数中的语句标签(即label)不可相同。 |
函数原型作用域 (function prototype) | 声明的函数原型的参数列表中(注意与“函数定义”不同) | 由于函数原型的参数名称可以省略,即使不省略,也不要求和“函数定义”中的形参列表中名称相同。 只有一种情况会发生冲突:参数列表中的有重复的变量名。(这时编译报错: redefinition of parameter ) |
说明:当出现两个标识符名称相同的情况,而且都属于同一个命名空间,那么在内层代码块,内层的那个标识符会隐藏外层的那个标识符。
举例说明并分析:
注意事项:
1. 注意函数原型中的参数是“函数原型作用域”,而函数定义中的参数是“代码块作用域”。例如上面代码中第一行的a,b和函数定义中的 n
2. 由于函数定义中参数是“代码块作用域”,所以在函数体内的最外层的变量名称不能再为n,但是内层嵌套的代码块变量名称可以为n。虽然这条特性在某些较老版本的编译器中是可以的,但是在ANSI C中师不允许的。
3. 变量的隐藏只是针对嵌套的作用域,对于不嵌套的作用域就没有这个说法。例如上面例子中的变量 f 是嵌套的,而 i 是不嵌套的,所以内层的 f 会隐藏掉外层的 f ,但是 i 不会相互隐藏。
命名空间是为了解决 “在相同作用域内如何区分 相同的标识符”。
说明:①只有在相同作用域的情况下才能使用到命名空间去区分标识符,在嵌套的作用域、不同的作用域区分标识符都用不到命名空间的概念。
②在相同的作用域内,如果命名空间不同,标识符可以使用相同的名称。否则,即如果命名空间不同,编译器会报错,提示重复定义。
按照C99(章节6.2.3),命名空间可以分为四种:
2.1 所有的标签(label)都属于同一个命名空间。
说明:①在同一个函数内,你的标签不能相同。②在同一个函数内,标签可以和其他变量名称相同。因为它们所属的命名空间不同。
2.2 struct、enum和union的名称,在C99中称之为tag,所有的tag属于同一个命名空间。
也就是说,如果你已经声明struct A { int a }; 就不能在声明 union A{ int a };
说明:之所以让所有的tag组成一个命名空间,由于Tag前面总是带struct,enum或union关键字,所以编译器可以将它们与其他的标识符区分开。
2.3 struct和union的成员属于一个命名空间,而且是相互独立的。例如:如果你已经声明struct A { int a };
其成员的名称为a,你仍然可以声明 struct B{ int a };或者union B{ int a };
说明:之所以让struct和union的成员各自成为一个命名空间,是因为它们的成员访问时,需要通过 "."或"->"运算符,而不会单独使用,所以编译器可以将它们与其他的标识符区分开。由于枚举类型enum的成员可以单独使用,所以枚举类型的成员不在这一名称空间内。
2.4 其他所有的标识符,属于同一个名称空间。包括变量名、函数名、函数参数,宏定义、typedef
的类型名、enum的
成员 等等。
注意:如果标识符出现重名的情况,宏定义覆盖所有其它标识符,这是因为它在预处理阶段而不是编译阶段处理。除了宏定义之外其它类别的标识符,处理规则是:内层作用域会隐藏掉外层作用域的标识符。
举例说明并分析:
运行结果为:
主要用于处理多次声明相同的标识符名称后,如何判断这些标识符是否是同一个。
原文对链接属性(linkage)的定义如下:An identifier declared in different scopes or in the same scope more than once can be made to refer to the same object or function by a process called linkage.
注意:链接属性(linkage)是相对于相同的标识符名称来说的,对于不同的标识符,没有链接属性。
按照C99(章节6.2.2),链接属性分为三种:external(外部的), internal(内部的), none(无)。
类型 |
说明 |
默认(即不使用extern和static) |
外部 external | 同一个标识符,即使在不同的文件中,也表示同一个实体。 | ①具有文件作用域的变量和函数。 ②代码块作用域内部的函数声明 |
内部 internal | 同一个标识符,仅仅在同一个文件中才表示同一个实体。 | 无(如果不使用static,那么默认没有内部链接属性的标识符。只有被static修饰的具有文件作用域的标识符,才具有internal链接属性) |
无 none | 表示不同的实体 | 所有其他的标识符。如:函数的参数、代码块作用域的变量、标签等 |
extern和static的使用:
3.1 文件作用域的变量和函数定义,即在所有 代码块和参数列表之外的标识符,使用static修饰,则具有 内部链接属性。
3.2 一个标识符声明为extern,并且前面已经对同一个标识符进行了声明,那么
①如果前一个声明时internal或者external,那么后一个声明与前一个相同。(即尽管后一个使用了extern,但其链接属性由前一个决定)。
②如果前一个声明为none,或者前一个声明在当前作用域不可见,那么这个标识符的链接属性为external。
举例说明并分析:(注意所有文件都在同一个工程中)
运行结果:
3.3 如果不使用static和extern:
1.对于函数声明:一定是external,无论是否在代码块内部。
2.对于变量声明:如果在 代码块外,则是 external;否则是none
例子可以参照上面的程序代码,《main.c》中声明函数原型时,print_in_test1()在main函数外,print_in_test2()和print2_in_test2()在main函数内,虽然位置不同,但都是external的,都会正确链接到相应的函数。
变量的生存期(Storage durations),也就是变量的生命周期(lifetime),可以理解为:程序运行期间,变量从分配到地址 到 地址被释放 这一过程。
更具C99描述,变量的生存期分为三种类型:static(静态), automatic(自动), and allocated(动态分配)。
1. 属于文件作用域(即external或internal链接属性)、以及被static修饰的变量,具有static静态生存期。
2. 链接属性为none,并且没有static修饰 的变量,具有automatic自动生存期。
3. allocated动态分配生存期,是指使用malloc函数,在进程的堆空间分配内存的变量。
说明:
4.1 生命周期、存数类型 都是针对变量,对于函数等其他标识符没有这个说法。
因为在程序运行期间,只有变量才需要分配内存和释放内存,其他的诸如函数等都不需要。
4.2 变量的生命周期和存储类型密切相关。
① 静态生存期的变量存储在静态内存中。其中使用static修饰的变量,在C语言书籍中也被称为“静态变量”。静态存储的变量,在程序运行之前就已经创建,在程序整个执行期间一直存在,如果声明时没有被显式的初始化,就会被自动初始化为0。 注意:静态变量当然是属于静态存储方式,但是属于静态存储方式的变量不一定就是静态变量, 例如外部变量虽属于静态存储方式,但不一定是静态变量,必须由 static加以定义后才能成为静态变量。
② 自动生存期的变量存储于栈或寄存器中。其中在代码块内部声明的变量,在C语言书籍中也被称为“自动变量”,使用auto修饰符,默认可以省略。对于自动存储的变量当程序执行到含有自动变量的代码段时,自动变量才被创建,并且不会被自动初始化,代码段执行结束,自动变量就自动销毁,释放掉内存。如果代码段被反复执行,那么自动变量就会反复被创建和销毁。注意这一点和静态变量不同,静态变量只创建一次,到程序结束才销毁。
③ 动态分配生存期的变量存储于堆中,也不会被自动初始化,使用free函数释放内存。
4.3 修改变量的存储类型(如用static将自动变量变为静态变量),并不会修改变量的作用域,变量的作用域仍然有其声明的位置决定。
4.4 变量的存储类型修饰符一共有五个:static、auto、register、extern、typedef。
4.5 函数的形式参数,如果使用修饰符,只能使用register修饰,表示运行时参数存储在寄存器上。注意:形式参数是不能用auto修饰的。
下图为一个变量声明,在 不同的作用域 对应的其他属性:
作用域 |
声明位置 |
链接属性 |
存储类型 |
默认初始化值 |
使用static修饰 |
file | 在所有“代码块”和“参数列表”之外 | external | static | 0 | internal |
block | 在“代码块”或者“函数的参数列表”内部 | none | automatic | 形式参数 调用时被初始化;代码块内部的不自动初始化 | none |
function | 函数体内 | --------- | -------- | 标签,不需要初始化 | --------- |
function prototype | 声明的函数原型的参数列表中(注意与“函数定义”不同) | --------- | -------- | 不需要初始化 | --------- |