无论学习哪一种语言,都免不了要讨论这些问题。而且这些问题,深究起来有时也让我们很迷惑。

标识符的定义无需多讲,只需注意不仅仅是指变量,还有函数标签等。

1. 标识符的作用域

作用域是指允许对标识符进行访问的位置范围。按照C99(章节6.2.1),C语言的作用域共有 4 种类型:文件作用域、代码块作用域、函数作用域、函数原型作用域。

类型

位置

说明

文件作用域  (file) 在所有 代码块和参数列表 之外 整个文件内都可以访问
代码块作用域 ( block) 在“代码块”或者“函数的参数列表”内部 只有所在的代码块内可以访问
函数作用域 (function) 函数体内 具有此作用域的只有一种语句:只有goto语句要使用的“语句标签”。简化为一条规则:一个函数中的语句标签(即label)不可相同。
函数原型作用域  (function prototype) 声明的函数原型的参数列表中(注意与“函数定义”不同) 由于函数原型的参数名称可以省略,即使不省略,也不要求和“函数定义”中的形参列表中名称相同。 
只有一种情况会发生冲突:参数列表中的有重复的变量名。(这时编译报错: redefinition of parameter )

说明:当出现两个标识符名称相同的情况,而且都属于同一个命名空间,那么在内层代码块,内层的那个标识符会隐藏外层的那个标识符。

举例说明并分析

  1. int my_func(int a, int b);  /* myfunc是“文件作用域”;a,b是 “函数原型作用域” */
  2. int a;/* a是文件作用域。 注意:虽然上面的函数原型中将参数名称声明为a, 但是由于作用域不同,是合法的。下一行的b也是这种情况 */
  3. static int b; /* b是文件作用域 */
  4. int d( int n ){ /* d是“文件作用域”。因为这是函数定义,而不是函数原型,所以形式参数n 是“代码块作用域” */
  5. /* 由于形式参数中已经声明n,那么在函数体内的最外层变量的名称就不能再为n,因为同一个作用域内不允许对同一个变量进行多次声明。
  6. 如果声明,编译器会提示重复声明变量。(在某些较老版本的编译器是允许的,但是C99标准是不允许的)
  7. 在不同的作用域内可以 */
  8. int f;  /* f是代码块作用域 */
  9. int g(int k);   /* 函数原型,位于函数体代码块内。声明的函数名称g是“代码块作用域”,参数k是“函数原型作用域” */
  10. my_label:  /* 定义一个label,是“函数作用域” */
  11. ...  /*  下面的代码块可以是while循环、for循环或if语言等等*/
  12. {
  13. int f, g, i; /* 都是代码块作用域,而且只是在内层代码块,在外层代码块不可见 */
  14. /* 对于f,外层已经存在f,这里会隐藏掉外层的f,即在这个内层代码块中无法访问外层的f */
  15. int n;     /* 代码块作用域,由于这里已经不是函数体内的最外层,所以可以声明与函数的形式参数同名的变量,
  16. 同样会隐藏掉外层的变量n   */
  17. }
  18. ...  /* 另外一个 代码块 */
  19. {
  20. int i;  /* 代码块作用域,虽然上面的一个内层代码块中已经存在i,但是由于这两个代码块不存在嵌套关系,所以也不存在隐藏现象 */
  21. }
  22. }

注意事项:

1.  注意函数原型中的参数是“函数原型作用域”,而函数定义中的参数是“代码块作用域”。例如上面代码中第一行的a,b和函数定义中的 n

2.  由于函数定义中参数是“代码块作用域”,所以在函数体内的最外层的变量名称不能再为n,但是内层嵌套的代码块变量名称可以为n。虽然这条特性在某些较老版本的编译器中是可以的,但是在ANSI C中师不允许的。

3.  变量的隐藏只是针对嵌套的作用域,对于不嵌套的作用域就没有这个说法。例如上面例子中的变量 f 是嵌套的,而 i 是不嵌套的,所以内层的 f 会隐藏掉外层的 f ,但是 i 不会相互隐藏。

2. 标识符的命名空间

命名空间是为了解决 “在相同作用域内如何区分 相同的标识符”。 
        说明:①只有在相同作用域的情况下才能使用到命名空间去区分标识符,在嵌套的作用域不同的作用域区分标识符都用不到命名空间的概念。 
                  相同的作用域内,如果命名空间不同,标识符可以使用相同的名称。否则,即如果命名空间不同,编译器会报错,提示重复定义。

按照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的成员 等等。 
                  注意:如果标识符出现重名的情况,宏定义覆盖所有其它标识符,这是因为它在预处理阶段而不是编译阶段处理。除了宏定义之外其它类别的标识符,处理规则是:内层作用域会隐藏掉外层作用域的标识符。

举例说明并分析

  1. ">#include <stdio.h>
  2. #include <stdlib.h>
  3. int main(){
  4. struct A{   /* “结构体的tag”和“结构体成员”不在同一个命名空间,所以名称可以相同 */
  5. int A;
  6. };
  7. union B{  /* 根据第二条,这个union的tag不能是A,但是根据第三条,其成员的名称可以与struct A的成员名称相同 */
  8. int A;
  9. };
  10. struct A A; /* “结构体的tag”和“普通变量”不在同一个命名空间,所以名称可以相同 */
  11. union B B;  /* 上面的“结构体变量”和 这行的“联合体变量”属于同一个命名空间,名称不能相同,即不能是 union B A */
  12. int my_label = 1; /* “普通变量”和“标签”不属于同一个命名空间,所以名称可以相同 */
  13. A.A = 1;
  14. B.A = 20;
  15. printf("B.A == %d  /n/n", B.A);
  16. my_label:     /* 这里label 的名称与上面变量的名称 相同 */
  17. printf("A.A == %d  /n", A.A);
  18. A.A +=1;
  19. if(A.A <= 5){
  20. goto my_label;
  21. }
  22. system("pause");
  23. return EXIT_SUCCESS;
  24. }

运行结果为:

  1. B.A == 20
  2. A.A == 1
  3. A.A == 2
  4. A.A == 3
  5. A.A == 4
  6. A.A == 5

3. 标识符的链接属性

主要用于处理多次声明相同的标识符名称后,如何判断这些标识符是否是同一个。  
        原文对链接属性(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。  
             举例说明并分析:(注意所有文件都在同一个工程中)

  1. /* 文件《test1.c》 */
  2. int a=1 ;   /* 这里的a为external */
  3. int b=1;    /* 这里的b为external */
  4. void print_in_test1(){
  5. static int a;   /* 这里是重新声明一个变量a, 并且会隐藏掉外层的a。由于是static静态类型,其默认初始化为0,所以下面的打印结果应为 0*/
  6. extern int b;   /* 虽然这里将b用extern声明,但是由于文件前面声明的b是external,所以b的链接属性也没有改变,依然是external,所以下面的打印结果应为 1 */
  7. printf("test1.c:  a == %d  /n", a);
  8. printf("test1.c:  b == %d  /n", b);
  9. }
  10. /*文件《test2.c》 */
  11. static int a=2; /* 这里的a为internal */
  12. void print_in_test2(){
  13. extern int a;  /* 虽然这里将a用extern声明,但是由于文件前面声明的a是internal,所以a的链接属性并没有改变,依然是internal */
  14. int b =2;  /* 这里b为none,不会链接到test1.c中的 b,所以下面的打印结果应为 2 */
  15. printf("test2.c:  a == %d  /n", a);     /* 所以下面的打印结果应为 2 */
  16. printf("test2.c print_in_test2() :  b == %d  /n", b);
  17. }
  18. void print2_in_test2(){
  19. extern int b;   /* b会链接到test1.c中的 b,而不是上面的函数中的 b,所以下面的打印结果应为 1 */
  20. printf("test2.c:  b == %d  /n", b);
  21. }
  22. /* 文件《main.c》 */
  23. #include
  24. #include
  25. extern int a; /* 会链接到test1.c中的 a,所以下面的打印结果应该为 1 */
  26. void print_in_test1();  /* 函数原型,会链接到test1.c中的 print_in_test1()*/
  27. int main(int argc, char *argv[])
  28. {
  29. void print_in_test2();  /* 函数原型,会链接到test2.c中的 print_in_test2()*/
  30. void print2_in_test2(); /* 函数原型,会链接到test2.c中的 print2_in_test2()*/
  31. printf("main.c:  a == %d  /n", a);
  32. print_in_test1();
  33. print_in_test2();
  34. print2_in_test2();
  35. system("PAUSE");
  36. return 0;
  37. }

        运行结果:

  1. main.c:  a == 1
  2. test1.c:  a == 0
  3. test1.c:  b == 1
  4. test2.c:  a == 2
  5. test2.c print_in_test2() :  b == 2
  6. test2.c:  b == 1

3.3  如果不使用static和extern: 
                1.对于函数声明:一定是external,无论是否在代码块内部。 
                2.对于变量声明:如果在   代码块外,则是 external;否则是none

例子可以参照上面的程序代码,《main.c》中声明函数原型时,print_in_test1()在main函数外,print_in_test2()和print2_in_test2()在main函数内,虽然位置不同,但都是external的,都会正确链接到相应的函数。

4. 变量的生命周期、存储类型

变量的生存期(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修饰的。

5. 总结

下图为一个变量声明,在 不同的作用域 对应的其他属性:

作用域

声明位置

链接属性

存储类型

默认初始化值

使用static修饰

file 在所有“代码块”和“参数列表”之外 external static   0 internal
block 在“代码块”或者“函数的参数列表”内部 none automatic 形式参数 调用时被初始化;代码块内部的不自动初始化 none
function 函数体内 --------- -------- 标签,不需要初始化 ---------
function prototype 声明的函数原型的参数列表中(注意与“函数定义”不同) --------- -------- 不需要初始化 ---------

最新文章

  1. [bzoj4726]Sabota
  2. jvm性能监控与故障处理工具
  3. Isometric terrain
  4. PHP 文章实现内链
  5. NSXMLParser解析本地.xml数据(由于like7xiaoben写的太好了,我从她那里粘贴过来的)
  6. 优化特性(Attribute)性能
  7. ycsb-命令及参数-与生成的负载类型相关
  8. Introduction to Project Management(I)
  9. Websocket协议数据帧传输和关闭连接
  10. iOS多线程——同步异步串行并行
  11. 初识 JShell
  12. AngularJS学习篇(五)
  13. linux下如何查询未知库所依赖的包
  14. 日志管理中获取浏览器、操作系统、IP等信息。。。
  15. Centos7 下SVN迁移
  16. MySQL 深入浅出数据库索引原理(转)
  17. 基于TCP(面向连接)的Socket编程
  18. 机械臂运动学逆解(Analytical solution)
  19. java详解内部类
  20. Logger级别和输出的地方

热门文章

  1. 【vuejs深入三】vue源码解析之二 htmlParse解析器的实现
  2. C++框架_之Qt的开始部分_概述_安装_创建项目_快捷键等一系列注意细节
  3. FJUT第三周寒假作业《第九集,离间计》栈
  4. Go 语言类型转换
  5. Docker常见仓库Ubuntu
  6. python3+django2 开发易语言网络验证(上)
  7. Dynamics 365 你所期待的子网格编辑终于来了
  8. Programming In Scala笔记-第六章、函数式对象
  9. SSH 之 Spring的源码(一)——Bean加载过程
  10. Findbugs异常总汇