作用域、链接属性和存储类型

最近在读《程序员的自我修养——链接、装载与库》,感觉自己当初学习C的时候,对extern、static等关键字了解不是特别清晰,因此重温了一遍《C和指针》中关于作用域、链接属性和存储类型的相关部分,加上了自己的理解,用博客记录一下。

作用域

当变量在程序的某个部分被声明时,它只有在程序的一定区域才能被访问

这个区域由标识符的作用域决定,标识符的作用域就是程序中该标识符可以被使用的区域。

据我所学,编译原理中有讲到,检查变量的作用域是否合乎规则,是在编译中的语义分析时查看的。

编译器可以确认4种不同类型的作用域——文件作用域、函数作用域、代码块作用域和原型作用域。标识符声明的位置决定了它的作用域。

  • 代码块作用域

    位于一堆花括号之间的所有语句称为一个代码块,任何在代码块的开始位置声明的标识符具有代码块作用域。表明他们可以被这个代码块中的所有语句访问。

    下图中的a、b、c、d和arg均具有代码块作用域。

    /* main.c */
    #include <stdio.h>
    int g;
    
    int func(int x);
    
    int main(int argc, char* argv[]) {
        int a;	
        int b;
        a = 5;
        {
            int c;
            int a;	//隐藏外部的a,外层的那个标识符将无法在内层代码块中通过名字访问。
            c = 5;
            a = 10;
            printf("%d", c + a);	//打印结果:15,而不是10
        }
        {
            int d;
            func(d);
        }
    }
    
    int func(int arg) {
        //
    }
    

    注:我们应当避免在嵌套的代码块中出现相同的变量名,因为并没有很好的理由使用这种技巧,他们只会在程序的调试或维护期间引起混淆。

  • 文件作用域

    任何在所有代码块之外声明的标识符都具有文件作用域(file scope),他表示这些标识符从他们的声明之处直到他所在的源文件结尾处都是可以访问的。g、func和main都具有文件作用域。这也就是为什么我们要将func的声明单独写在main函数前,就是为了main可以调用func函数,否则main是不可以访问到func函数的。

  • 原型作用域

    原型作用域只适用于在函数原型中声明的参数名,如func声明语句中的x。

  • 函数作用域

    只适用于语句标签,语句标签用于goto语句。

后两种作用域非常非常不常见,因此我们应当把关注点放在前两个作用域上面。

链接属性

标识符的链接属性(Linkage)决定如何处理在不同文件中出现的标识符。标识符的作用域与它的链接属性有关。但这两个属性并不相同。

/* main.c */
#include <stdio.h>
int g;

int func(int x);

int main(int argc, char* argv[]) {
    int a;	
    int b;
    a = 5;
    {
        int c;
        int a;	//隐藏外部的a,外层的那个标识符将无法在内层代码块中通过名字访问。
        c = 5;
        a = 10;
        printf("%d", c + a);	//打印结果:15,而不是10
    }
    {
        int d;
        func(d);
    }
}

int func(int arg) {
    //
}
  • external

    属于external链接属性的标识符不管声明多少次,位于几个源文件都表示同一个实体。

    缺省情况下,声明在任何代码块之外的变量或函数(即具有文件作用域)具有external链接属性,其余都为none。代码中g、func和main链接属性都是external,其余的变量链接属性均为none。

    extern关键字:

    • extern关键字为一个标识符指定external链接属性。
    • 对于文件作用域即已经是extern链接属性的变量,extern关键字是可选的
    • extern关键字用于源文件中一个标识符的第一次声明时,它指定该标识符具有extern链接属性,但是如果该标识符用于该标识符的第2次或以后的声明,他并不会更改由第一次声明所指定的链接属性。
  • internal

    具有internal链接属性的标识符在同一个源文件内的所有声明都指同一个个体,但位于不同源文件的多个声明则分属不同的实体。

    如果某个声明在正常情况下具有external链接属性,在他面前加上static关键字,可以使他的链接属性变为internal。例如如果g的声明为static int g;,那么变量g就变为源文件私有。其他源文件如果要链接g的变量,引用的是另一个不同的变量,类似的,函数声明也可以是static,如static int func(int x);

    static只有对缺省链接属性为external的声明才有改变链接属性的效果。

  • none

    没有链接属性的标识符(none)总是被当作单独的个体,也就是说该标识符的多个声明被当作独立不同的个体。

存储类型

变量的存储类型(storage class)是指存储变量值的内存类型。变量的存储类型决定变量何时创建、何时销毁以及它的值将保持多久。有三个地方可以用于存储变量:

  • 普通内存

    凡是在任何代码块之外声明的变量(具有文件作用域、external链接属性)总是存储于静态内存,这类变量称为静态变量,放在二进制文件的.data段或bss段中。

    静态变量在程序运行之前创建,在程序的整个执行期间始终存在。他始终保持原先的值,除非给他附一个不同的值或程序结束。

  • 运行时堆栈

    在代码内部声明的变量的缺省存储类型是自动的。也就是说他存储于堆栈中,称为自动变量。

    如果给他加上关键字static,可以使他的存储类型从自动变为静态(放在.data段或.bss段中)。具有静态存储类型的变量在整个程序执行过程中一直存在,而不仅仅在声明它的代码块的执行时存在。注意,修改变量的存储类型并不表示修改该变量的作用域,他虽然始终存在,但是还是只能在该代码块内声明过后,按名字访问。

  • 硬件寄存器

    用于自动变量的声明,提醒他们应该存储于机器的硬件寄存器,而不是内存中,这类变量称为寄存器变量。但是编译器并不一定要理睬register关键字,也就是说不是你在变量前加了register关键字,这个变量最后就被存储于机器的硬件寄存器里面了,还是要看编译器的”心情“的,即取决于编译器的优化方案。

    注:register变量是不提供地址的哦。

浅谈初始化

初始化静态变量不需要额外的时间和开销,变量将会得到正确的值,如果不显示地指定其初始值,静态变量将初始化为0。因为静态变量直接存在.data段或.bss段里面,在生成目标文件时已经被编译器写进去了,所以运行时肯定不花时间。

自动变量的初始化需要更多开销因为当程序链接时还无法判断自动变量的存储位置。事实上,函数的局部变量在函数的每次调用中可能占据不同的位置,因此基于这个理由,自动变量没有缺省的初始值,而显式的初始化将在代码块的起始处插入一条隐式的赋值语句。这里的隐式我认为就是代码段中插入了一条赋值语句如mov [ebp -4] , value,这样的话就造成初始化和先声明后赋值效率并无提高,只有风格之差。

Static和Extern

  • 当用于不同的上下文环境时,static关键字具有不同的意思。

    • 用于具有文件作用域的变量或函数时,Static关键字可以改变他们的链接属性,从external改为internal,但标识符的作用域和存储类型不受影响。函数照样放在.text段中,全局变量根据是否初始化放在.data段或.bss段中。
    • 用于具有代码块作用域的变量时,其链接属性为none,static不改变其链接属性,而是修改变量的存储类型,从自动变量改为静态变量,作用域也不受影响。
  • extern关键字

    • 用于具有文件作用域的变量或函数时,extern关键字是可选的,因为本身他们就具有external链接属性,然而,如果你在其中一个地方定义变量,并在使用这个变量的其他源文件的声明中添加extern关键字,可以使读者更好地了解你的意图。
    • 用于具有代码段作用域的局部变量时,extern关键字可以修改变量的链接属性从none到external,这对我们在深度嵌套代码块中引用全局变量提供了一个途径。

相关推荐

发表评论

路人甲

网友评论(0)