zoukankan      html  css  js  c++  java
  • 存储类、生命周期、作用域、链接域

     Linux下c内存映像

    大方向分为

    应用空间 + 内核空间

    他俩内存空间布局差不多。这里重点回顾分析应用空间布局,应用空间分为:代码段 + 数据段(静态数据段+动态数据段)

    代码段

    为啥是只读的

    代码段在编译时就定好了,在程序的运行过程中,不能在代码段去开辟空间,以及释放空间。

    包含哪几部分

    ELF头、段头部表、init节

    参考:剖析可执行文件ELF组成

    .text

    指令节,也叫代码节,所有函数中的指令都放在了.text节中。能够与指令直接弄在一起的常量,也随指令一起放在了.text中。

    .rodata

    常量节,无法直接和指令放在一起的常量,就放在.rodata中。

    数据段

    静态数据段

    为啥是只读的(为啥是静态的)

    代码段在编译时就定好了,在程序的运行过程中,不能在代码段去开辟空间,以及释放空间。也不能随意释放已有变量的空间,像这种在编译阶段完成了变量空间安排的情况,就是静态的。

    包含哪几部分

    .bss

    未初始化的静态变量的空间,都开辟于.bss中。

    .data

    初始化了的静态变量的空间,都开辟于.data。

    动态数据段

    为什么称为动态的?

    变量空间的开辟和释放不是在编译阶段决定的,而是在程序的运行过程中完成的,这就是动态的含义。

    包含哪几部分

    堆(手动区)

    程序在运行的过程中,通过调用malloc函数来开辟空间,以及调用free来释放空间。之所以叫手动的,是因为我们在编写程序时,必须亲自动写调用malloc和free函数的代码。

    栈(自动区)

    函数运行时自动从栈中开辟空间,函数运行结束时又会自动释放开辟的空间,开辟和释放的过程,完全是自动完成的。从栈里面开辟空间就是压栈,释放空间其实就是弹栈。

    存储类

    什么是存储类

    存储类的全称叫“存储类型”,存储类就是用来说明“常量、变量和函数”的存储位置的,比如自动局部变量在栈中,存储位置为栈,那么它的存储类就是栈。

    细说存储类

    函数指令

    存储类为.text,因为函数指令都是存放在.text中的。函数指令的存储类是固定的,所以函数指令的存储位置是固定不变的。

    常量

    常量的存储类也是固定的,要么是.text,要么是.rodata。

    ①当常量比较小时,小到能够和指令放在一起的话,就会和指令一起直接放在.text中。此时常量的存储类为.text。

    ②常量比较大时,大到无法直接成为指令放在一部分,此时就需要单独的存在.rodata中,此时常量的存储类就为.rodata

    全局变量

    ①初始化了的全局变量,存储类为.data。

    ②未初始化的全局变量,存储类为.bss

    全局变量的存储类也是固定的,要么在.data中要么在.bss。.data、.bss合称为静态数据段,或者静态数据区,所以全局变量的存储类可统称为“静态数据段”。

    局部变量

    局部变量的存储类不是固定,根据修饰的auto、static、register关键字的不同,存储类不同。

    (1)自动局部变量 与 auto关键字

    fun()
    {
        auto int a; //等价于int a,auto可以省略不写
    }
    View Code

    ①auto为局部变量的存储类关键字,auto只对局部变量有效。如果不写auto的话,默认就是auto的,所以我们平常定义的局部变量,默认都是auto修饰的。

    ②auto就是automatic自动的意思,以auto int a为例,auto就表示a的空间是自动开辟、自动释放的,我们知道只有栈才满足自动开辟自动释放的特点,因此auto就表示a的存储类为“栈”。正因为自动开辟、自动释放的特点,auto修饰的局部变量,我们就称为“自动局部变量”。

    疑问:怎么感觉auto这个关键字是多余的?

    :auto这个关键字对于我们程序员来说确实是多余的,既然auto可以省略,那我们就不会傻到去把auto写上,auto在C程序中几乎看不到,因为没有那个程序员是傻子。其实这个auto关键字主要是给编译器用的,因为编译器必须通过这个关键字来识别“栈”这个存储类,就算auto被省略了,但是在编译时会被自动加,用以标记局部变量是“栈”这种存储类。

    (2)静态局部变量 与 staitc关键字

    fun()
    {
        static int a=100; //static不能省,省了就默认是自动局部变量
        static int b;        //未初始化的静态局部变量
    }
    View Code

    static有两种用法

    第一种:修饰局部变量  当static修饰局部变量时,static用于标记局部变量的存储类。

    第二种:修饰函数和全局变量  与链接域有关

    static对应的存储类

    初始化了的静态局部变量,存储类为.data。比如例子中的a就被初始化了,所以a的存储类为.data。

    未初始化的静态局部变量,在.bss中。比如例子中的b就没有被初始化,所以b的存储类为.bss。

    .data和.bss合称为静态数据段,所以静态局部变量的存储类合称为静态数据段。

    静态变量

    由于全局变量与静态局部变量的存储类都是静态数据段,因此我们就将全局变量和静态局部变量统称为静态变量。

    (3)寄存器局部变量 与 register关键字

    int fun()
    {
        register int a=100; //register不能省略,省略了就变为了默认的自动局部变量    
    }
    View Code

    回顾CPU对存储器的访问

    存储容量:寄存器  < 1级cache < 2级cache < ... < 内存 < 外存

    CPU访问时的访问速度:寄存器>1级cache > 2级cache > ... > 内存 > 外存

    以上情况,是由各存储器的材质和制作工艺来决定的。

    register

    register为寄存器的意思。使用register修饰局部变量后,局部变量的存储类就为寄存器,也就是说此时局部变量的空间开辟于寄存器中。register修饰的局部变量,我们就称为寄存器局部变量。

    疑问:将局部变量的存储类设为register有什么好处?

    :cpu访问寄存器的速度 远远> 访问内存的速度,所以如果你希望cpu能够更快速的访问局部变量的话,我们就可以使用register修饰,让局部变量的空间在寄存器中。  

    测试代码

    #include <stdio.h>
    #include <time.h>             //time函数所需的头文件
    int main(void)
    {
        register int a = 0;     //寄存器局部变量
        int b = 0;                         //自动局部变量(栈)
        int old_time = 0;                                     
        old_time = time(NULL);                //记录循环开始时的时刻
        for(a=0; a<1000000000; a++);     //循环累加a的值,实现延时
        printf("%ld
    ", time(NULL)-old_time); //延时时间 = 结束时刻-其实时刻
            old_time = time(NULL);                 //起始时刻
        for(b=0; b<1000000000; b++);     //延时
        printf("%ld
    ", time(NULL)-old_time); //延时时间
        return 0;
    }    
    View Code

    疑问:什么时候可以使用“寄存器局部变量”?

    答:

    (a)如果某个局部变量的访问速度要求很高的,我们就可以使用regster来修饰。

    (b)如果程序中某个局部变量的使用频次非常高,此时为了提高访问效率,我们也可以使用register修饰。

    register令人糊涂的地方

    ①第一个令人糊涂的地方:虽然写了register,但不一定有效。如果cpu的寄存器数量很少,比如intel cpu的寄存器数量相对ARM CPU来说就偏少,所以很有可能出现寄存器不够用的情况,如果编译器编译时发现寄存器不够用了,编译器就会将register自动改为auto。说白了就是虽然是register修饰的,但是最终能不能起作用不一定,看编译器。

    ②就算你不写register,以优化方式编译时,编译器也可能会帮你自动改为register,这里说的是可能会。gcc编译指定Onum优化等级时,其实是我们给了编译器优化权限。这样当编译器觉得代码不够好时,就会进行优化。不过编译器也不是一定会优化为register,因为如果编译器发现寄存器数量不足的话,此时b的存储类就还是auto。

    我们应该如何对待register

    ①明白它的用途,如果你在别人的代码中看到了这个关键字,你要明白这是什么意思。

    ②在我们自己的程序中,不建议使用。因为这个关键字并不能一定管用,到底管不管用取决于编译器的处理,带有不确定性,因此不建议使用,而且现在确实也用的少了。

    形参

    一般情况下形参的默认存储类为栈,所以形参空间默认就是开辟于栈中。

    疑问:auto、static能不能修饰形参?

    不能,这两个关键字不能用于修饰形参,对于形参来说默认的存储类就是栈,不需要auto来说明。

    疑问:register能不能修饰形参?

    可以,此时存储类为寄存器,所以形参的存储类就两种:

    默认:栈

    register修饰:寄存器。

    ARM下的一个特殊情况

    ARM cpu的寄存器特别丰富,为了能够提高效率,编译器在编译针对ARM的c程序时,如果函数的形参小于4个的话,形参的存储类默认会定为register。只有当形参数量超过5个时,第5个以后的形参的存储类才默认为栈。如果编译器编译的是针对Intel CPU的程序的话,由于Intel cpu的寄存器数量相对比较少,所以函数形参的存储类默认都是栈,如果在程序中人为指定为register的话,存储类有可能会是寄存器。

    能否使用auto、static、register修饰全局变量

    auto和register

    全局变量的存储类是固定的,为静态数据区,如果使用auto和register修饰全局变量的话,其实是在尝试使用auto和register将全局变量的存储类改为栈和寄存器,显然这是不行的,这会导致编译出错。

    staitc

    可以,使用static修饰全局变量时,static与存储类半毛钱关系都没有,static修饰全局变量时只与与链接域有关。

    指令、常量、变量的生命周期

    什么是生命周期

    生命周期,指的就是空间从诞生到消亡。诞生即从内存中开辟出空间。消亡即释放空间。只有在生命周期这段时间内,空间才是有效的,在生命周期外的时间,空间是无效的,不能访问。

    指令和常量的生命周期

    指令在.text中,常量要么在.text中,要么在.rodata中。指令和常量的生命周期为整个程序运行期间。

    .data、.bss变量的生命周期

    全局变量和静态局部变量的存储类为.data或者.bss,所以.data、.bss变量指的就是全局变量和静态局部变量,.data、.bss变量的生命周期也为整个程序运行期间。也就是说程序一开始运行时变量空间就存在,直到到整个程序运行结束.data和.bss被释放时,.data和.bss中的全局变量和静态局部变量才会被释放。

    int i = 0; //i一直有效,直到程序运行结束
    int fun()
    {
        static int fnum = 0; //fnum一直有效,直到程序结束,每次调用fun函数时,累加的都是同一个fnum
        printf("%d
    ", fnum++);
    }
                
    int main(void)
    {
        for(i=0; i<5; i++)
        {
            fun();
        }
    }
    View Code

    栈变量的生命周期   

    形参和自动局部变量的存储类为栈,所以栈变量指的就是形参和自动局部变量。定义形参和自动局部变量的代码,编译后会变成代码块的压栈、弹栈指令。

    栈变量的生命周期 = 从push指令开辟空间 到 pop指令释放空间 期间。代码块开始运行时执行push,代码块运行结束时执行pop,因此栈变量的生命周期 约等于 代码块的生命周期。

    疑问:register变量的生命周期?

    认为与栈变量相同。

    堆变量的生命周期

    malloc成功后,堆变量的生命周期开始,调用free将空间释放后,生命周期结束。所以堆变量的生命周期 == malloc 到 free之间的时间。

    疑问:如果忘了free怎么办呢?

    :程序运行结束时整个堆会释放,堆中忘了free的堆变量空间自然也会被释放,但是一定要在程序运行时就free,不要等到程序运行结束再释放。

    变量和函数的作用域

    什么作用域

    就是变量和函数起作用的范围,只要在这个范围内,你就可以访问该变量和函数。分为3种

    (1)局部变量的代码块作用域

    (2)函数和全局变量的本文件作用域

    (3)跨文件作用域 —— 链接域,与链接有关

    局部变量的代码模块作用域

    简单理解就是{}括起来的就是代码块,不要把代码块等价为函数,因为if、for、while等同样有{}这个东西。代码块作用域 的 范围,从定义处到代码块结束。

    int main(void)
    {
        int a; //a的代码块作用域:从定义位置开始到main函数的}。
        {
            int b; //b的代码块作用域:定义位置开始到内部}。
            ...
        }
                            
        ...
    }
    View Code

    int b所在的内部{}实际上才是真正的代码块,只不过在广义上我们将所有带{}的都理解为代码块,当然结构体类型定义除外,虽然结构体类型的定义有{},但不是代码块。

    struct student
    {               //这个不是代码块
        int num;
        ...
    };
    View Code

    形参的作用域

    形参的作用域也是代码块作用域,不过有些特殊的地方需要说明下。

    int fun(int n, int buf[][n]) 
    {
        ...
    }
                                
    int main(void)
    {
        int buf[3][4] = {{1,2,3,4}, {5,6,7,8}, {9,10,11,12}};
                            
        fun(4, buf);
                        
    }    
    View Code

    n的作用域:定义位置开始,到参数列表末尾,再到fun的}

    buf[][n]的作用域:定义位置开始,到参数列表末尾,再到fun的}

    由于n的作用域覆盖了buf,所以才能在buf中使用n,如果反过来的话fun(int buf[][n],int n),编译时会提示buf中的n无法识别,因为buf不在n的作用域内。

    函数和全局变量的本文件作用域

    本文件作用域的有效范围

    从定义位置开始到文件结束。

    int main(void)               //main的本文件作用域:从定义位置到文件末尾
    {
        fun(g_var1, g_var2);
        return 0;
    }
                    
    int g_var1 = 100;                 //g_var的本文件作用域:从定义位置到文件末尾
    int g_var2;
                    
    int fun(int a)                //fun的本文件作用域:从定义位置到文件末尾
    {
        int var;                    
        return var +a;
    }
    View Code

    通过声明改变本文件作用域

    在上面的例子中,如果我们想在main函数中使用g_var1、g_var2、fun的话怎么办?

    ①方法1  挪到main的前面去。

    ②方法2  在main前面进行声明,通过声明将作用域提前。

    int g_var1;  
    int g_var1; 
    int g_var1; 
    int g_var1;                     
    int g_var2;
    int g_var2;
    int g_var2;
                    
    int fun(int a);
                        
    int main(void)           
    {
        fun(g_var1, g_var2);
    }
                        
    int g_var1 = 100;                    
    int g_var2;
                        
    int fun(int a)                
    {
        int var;                
        return var +a;
    }
    View Code

    定义与声明的关系

    参考:强弱符号关系

    声明全局变量的特殊例子

    int g_var1;
                        
    int main(void)           
    {
        extern int g_var1; //声明  
        g_var1 = 100;
    }
                        
    int fun(int a)                
    {
        int var;                    
        return var +a;
    }
                        
    int g_var1 = 100;  
    View Code

    main函数中extern int g_var1这种的生命方式表示,g_var1只在main函数内有效,对后面的fun无效。

    extern可以省略吗?

    不能省,省了g_var1就变成main的自动局部变量了,extern表示这个全局变量来自于函数外部。假如你只想在main中使用g_var1,不想让g_var1的作用域覆盖到fun函数,就可以使用这种方法,不过这种声明方式用的确实不多,但是在有些源码中可能会看见,这里需要了解下。同样的,函数也可以进行类似的声明。

    int main(void)           
    {
        extern int fun(int a); //fun的声明。
        fun(g_var1, g_var2);
    }
                        
    int fun(int a)                
    {
        int var;                    
        return var +a;
    } 
    View Code

    局部变量有声明吗?

    局部变量没有声明一说,以下做法时错误的。

    int main(void)
    {
        int a; //声明:错误用法
        a = a + 1;
        int a = 100;
    }
    View Code

    说白了就是,对于局部变量来说,变量符号只能有一个,不允许同名符号重复出现。

    同一个.c中,变量同名的问题

    全局变量

    (a)如果同名变量都是强符号,这会导致变量重复定义,编译时会报错。

    (b)如果同名的是弱符号,它只会改变符号的作用域,除此外没有影响

    局部变量

    不允许存在同名符号。

    链接域 与 extern、static关键字

    链接时主要做两件事

    符号解析

    1)对全局符号进行符号统一
    2)将符号的引用 与 符号的定义关联起来

    地址重定位

    链接域 ———— 跨文件作用域

    回顾 代码块作用域

    形参和局部变量的作用域就是代码块作用域,对于形参和局部变量来说,不允许出现同名符号,所以不存在需要统一同名符号的情况。而且代码块作用域只局限在代码块内,与其它文件没有任何关系,所以与链接无关。

    回顾 本文件作用域

    在单个.c中,全局变量和函数的作用域就是本文件作用域,由于允许对全局变量和函数进行声明,所以在单个.c中存在同名符号的问题,编译时需要进行同名符号的统一,统一规则就是强弱符号的统一规则。由于本文件作用域只与当前文件有关,与其它文件无关,因此也与链接无关。

     跨文件作用域 与 extern关键字

    为什么需要跨文件作用域

    对于全局变量和函数来说,有时不仅仅只希望在本文件可以被使用,还希望在其它的文件中也能被使用,此时作用域就必须跨越到其它文件中,这就所谓的涉及跨文件作用域。跨文件作用域说白了就是将作用域延伸到到其它文件中。跨文件作用域涉及到多个文件,由于多文件最后要被链接到一起,与链接有关,所以我们也将跨文件作用域称为链接域

    如何实现跨文件的作用域

    只要满足两个条件即可。

    ①将定义标记为extern。extern表示定义的符号是一个全局符号,由于是全局符号,因此对于其它文件来说这个符号是可见的。

    ②在其它文件中进行声明,声明也需要标记为extern。extern表示声明的符号也是一个全局符号,对于其它文件也是可见的。

    正是因为extern将符号标记为了全局可见,在链接阶段才能对全局符号进行“符号统一”。

    例子

    a.c

    extern int a;    
    extern int fun();
    int main(void)   
    {                
                    
                     
    }                
    View Code

    b.c

    int a = 100; //全局符号,extern可以省略
                    
    int fun()
    {
        printf("helloworld
    ");
    }
    View Code

    extern可以省略,省略后默认就是extern的,与auto有点像。对于几乎所有的编译器来说,都认可在定义时将extern省略,但是对于声明来说,有些编译其允许省略extern,但是有些就不允许,我们目前使用的gcc就允许声明时省略extern。不过为了保证不出错,经常的做法是,定义时省略extern,但是声明时必须保留extern。

    由于全局符号的定义和声明是同名的,所以在链接阶段需要按照强弱符号的统一规则,对全局符号进行统一,声明作为弱符号最后会消失,虽然消失了,但是它却将“作用域跨”拓展到了其它文件中。从这里可以看出,想要实现跨文件作用域的话,必须使用声明这个弱符号来拓展作用域。

    不过有一点需要注意,我们说全局变量和全局符号时,这两个全局的意思不相同。

    全局变量的“全局”:指的是文件

    全局符号的“全局”:指的是整个C工程项目

    全局符号的重名问题 与 static关键字

    全局符号的重名问题

    extern所修饰的符号是所有文件都可见的全局符号。如果在不同文件中存在同名强符号的话,全局符号符号统一时就会报错,但是大家要知道一旦C工程变得复杂之后,在不同的文件中,误定义同名的函数和全局变量的情况是无法避免的。为了避免同名全局强符号的错误,我们应该尽量使用static关键字来避免这个问题。

    如何理解这句话?

    如果只在.c定义,在其他.c extern引用,全局符号统一时就没问题。 但现实情况是 ,有很多定义是在.h中,.h会被很多文件包含,所以如果不加static,全局符号统一时就会出现全局符号同名问题

    static修饰函数和全局变量时的作用

    将符号标记为本地符号。

    什么是本地符号?

    所谓本地符号,就是符号只在本文件内可见,其它文件不可见,链接阶段进行全局符号统一时,所有static修饰的本地符号在全局是不可见的,所以不参与链接阶段的符号统一,因此就算同名了也不会报错。

    本地符号的作用域

    static将符号变为本地符号,说白了就是关闭符号的链接域,或者说关闭符号的跨文件作用域,符号此时只剩下“本文件作用域”。为了最大化的防止重名问题,建议凡事只在本文件起作用,而其它文件根本用不到的函数和全局变量,统统使用static修饰,让符号在全局不可见,防止全局强符号的同名冲突。C中使用static来解决全局强符号的命名冲突,其实是非黑即白的解决方式,为了能够更加精细化的解决命名冲突问题,从c扩展得到c++时,C++引入了命名空间这一概念,当然这个就是属于C++的内容。

    总结一下extern 和 static关键字

    static

    1)修饰局部变量  与存储类有关,表示局部变量的存储类为静态数据段。

    2)修饰全局变量  与存储类无关,因为全局变量的存储类本来就是固定的静态数据段。static修饰全局变量,表示符号为本地符号,关闭链接域(跨文件作用域),让其在全局不可见。

    3)修饰函数    与修饰全局变量是一样的,将符号变为本地符号,关闭链接域,让其全局不可见。

    extern

    1)修饰函数、全局变量的定义和声明时  表示符号是全局符号,将链接域(跨文件作用域)被打开,让其全局可见。

    2)将函数体外的全局变量和函数,声明到函数内部

    int main(void)
    {
        extern int a;
        extern int fun();
                                
        a = a+1;
        fun();
    }
                            
    int a;
    int fun()
    {
                                
    }
    View Code

    此时fun函数也可以在其它的.c中,此时涉及到的就跨文件作用域。

    声明的作用

    变量的声明

    拓宽变量的作用域。如果没有通过声明来拓宽变量作用域的话,在第二阶段编译时,编译器就会提示你所使用的某个符号找不到,有了声明后,其实就是告诉编译器,你所使用的这符号是由定义,不要报错。

    函数的声明

    ①拓宽函数的作用域

    ②进行形参、返回值的类型检查

    1)如果函数的定义位置在调用位置之前时

    此时函数定义本身就是一个函数声明,无需额外的声明。编译阶段进行函数的类型检查时,直接通过函数定义来进行类型检查。

    2)如果函数定义的位置不在调用位置之前

    果不进行声明的话,编译时是不会进行类型检查的,所以我们必须进行声明,声明后再进行编译时,就会通过声明来进行函数的类型检查。

    3)类型检查有什么用

    类型检查其实很有用,进行类型检查时如果发现类型有问题的话,编译时打印提示信息,这样可以帮助我们更好的排查函数错误。

    不进行函数声明,编译可以通过吗?

    如果函数定义本来就在函数调用位置的前面,定义本身就是声明,编译肯定能过。但是如果函数定义不在调用位置的前面,而且还没有给额外的声明,编译还能过吗?

    这要看编译器的严格程度,有可能编译通过,但是有些严格编译器编译时不能通过。不过不管人家编译器严不严格,按照正规操作,我们必须要进行声明,如果不进行声明的话,编译时不会进行函数的类型检查,由于没有做类型检查,就算程序能够编译通过,而且还能运行,但是很有可能会出现因传参和返回值类型不对而导致的错误。

    调用库函数为什么需要进行声明?

    比如在程序中调用printf时,必须在.c中包含stdio.h头文件,因为这里面有printf函数的声明。对库函数进行声明,函数声明的目的是一样的。一是为了拓展作用域,二是为了进行参数检查,与我们前面所举例子的唯一不同是,库函数时别人帮我们写好。

    如果调用的函数不在本文件中,而在其它的文件中,比如

    在我自己写的其它.c中

    在库文件中

    除了要对函数进行声明外,还必须链接函数所在的文件,函数声明的只用于“作用域的拓展”和类型检查,第四阶段链接时,必须要链接函数所在的文件。只有在链接了函数所在文件后,在链阶段进行声明和定义的全局符号统一时,你才能在链接文件中找到全局函数的定义,不然链接时会报该函数没有定义的错误。

     

  • 相关阅读:
    percona-toolkit 之 【pt-heartbeat】说明
    sql-索引的作用(超详细)
    SqlServer2005 海量数据 数据表分区解决难题
    怎样玩转千万级别的数据(表分区)
    SQL数据缓存依赖总结
    关于缓存中Cookie,Session,Cache的使用
    Anti-Forgery Request Recipes For ASP.NET MVC And AJAX
    jquery 父、子页面之间页面元素的获取,方法的调用
    win7系统部分软件显示乱码怎么办
    转:mvc 当中 [ValidateAntiForgeryToken] 的作用
  • 原文地址:https://www.cnblogs.com/kelamoyujuzhen/p/9484492.html
Copyright © 2011-2022 走看看