做用域、连接属性和存储类型

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


做用域

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

这个区域由标识符的做用域决定,标识符的做用域就是程序中该标识符能够被使用的区域。学习

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

编译器能够确认4种不一样类型的做用域——文件做用域、函数做用域、代码块做用域和原型做用域。标识符声明的位置决定了它的做用域。指针

  • 代码块做用域调试

    位于一堆花括号之间的全部语句称为一个代码块,任何在代码块的开始位置声明的标识符具备代码块做用域。代表他们能够被这个代码块中的全部语句访问。code

    下图中的a、b、c、d和arg均具备代码块做用域。blog

    /* 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,这对咱们在深度嵌套代码块中引用全局变量提供了一个途径。
相关文章
相关标签/搜索