存储类生命周期作用域链接域

Posted kelamoyujuzhen

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了存储类生命周期作用域链接域相关的知识,希望对你有一定的参考价值。

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的作用域内。

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

 

 

 

 

以上是关于存储类生命周期作用域链接域的主要内容,如果未能解决你的问题,请参考以下文章

存储类&作用域&生命周期&链接属性

存储类作用域生命周期链接属性

4.7 C语言的存储类,作用域,生命周期,链接属性

7C_存储类 &amp; 作用域 &amp; 生命周期 &amp; 链接属性

C语言中,哪种存储类的作用域与生命周期是不一致的?

储存类生命周期链接属性作用域总结