C语言变量存储类及相关关键字

1.一些相关的概念

1.1 C语言存储类

变量的存储类就是存储类型(storage class),也就是描述存储C语言变量值的内存类型。在前面内存管理中可以了解到:内存有多种管理方法,比如栈、堆、数据段、bss段、.text段等,一个变量的存储类属性就是描述这个变量存储在何种内存段中。

举几个例子:比如局部变量分配在栈上,所以它的存储类型就是栈;显式初始化为非0的全局变量分配在数据段;显式初始化为0和没有显示初始化(默认为0)的全局变量分配在bss段。

1.2 作用域

作用域是描述一个变量起作用的代码范围。当变量在程序的某个部分被声明时,它只有在程序的一定区域内才能被访问,这个区域就是它的作用域。

基本来说,C语言变量的作用域规则是代码块作用域,意思就是这个变量起作用的范围是当前的代码块。代码块就是一对大括号{}括起来的范围,所以一个变量的作用域是:这个变量定义所在的{}范围内从这个变量定义开始往后的部分,这就解释了为什么变量定义一般都在一个函数的最前面。

1.3 生命周期

生命周期声明周期是描述这个变量什么时候诞生,即运行时分配内存空间给这个变量和什么时候死亡,即运行时收回这个内存空间,此后再不能访问这个内存地址,或者访问这个内存地址已经和这个变量无关了的。

变量和内存的关系,就和人(变量)去图书馆借书(内存)一样,变量的生命周期就好象人借书的这段周期一样。研究变量的生命周期可以我们理解程序运行的一些现象、理解C语言的一些规则。

1.4 链接属性

程序从源代码到最终可执行程序经历的过程主要有:编译、链接。编译阶段就是把源代码搞成.o目标文件,目标文件里面有很多符号和代码段、数据段、bss段等分段,符号就是编程中的变量名、函数名等。运行时变量名、函数名能够和相应的内存对应起来,靠符号来做链接的。.o的目标文件链接生成最终可执行程序的时候,其实就是把符号和相对应的段给链接起来。

C语言中的符号有三种链接属性:外链接属性、内链接属性、无链接属性。

1.5 总结

以上4个概念,其实就是从4个不同角度来分析C语言的一些运行规则。综合这4种分析角度能够让程序员完全掌握C语言程序的运行规则和方法。

2.Linux下C程序的内存映像

2.1 Linux下C程序的内存映像

  • 代码段、只读数据段:代码段对应着程序中的代码,代码段在Linux中又叫文本段(.text)。只读数据段就是在程序运行期间只能读不能写的数据,const修饰的常量有可能是存在只读数据段的,但是不一定,const常量的实现方法在不同平台是不一样的。

  • 数据段、bss段:数据段存放显式初始化为非0的全局变量和显式初始化为非0的static局部变量;bss段存放显式初始化为0或者未显式初始化的全局变量和显式初始化为0或未显式初始化的static局部变量。

  • :栈内存区,局部变量分配在栈上;函数调用传参过程也会用到栈。

  • C语言不会自动往堆中存放东西,堆的操作是程序员根据需求自己判断要不要使用堆内存,用的时候自己申请,自己使用,完了自己释放的。

  • 文件映射区:文件映射区就是进程打开了文件后,将这个文件的内容从硬盘读到进程的文件映射区,以后就直接在内存中操作这个文件,读写完了后在保存时再将内存中的文件写到硬盘中去。

  • 内核映射区:内核映射区就是将操作系统内核程序映射到的这个区域。对于Linux中的每一个进程来说,它都以为整个系统中只有它自己和内核,它认为内存地址0xC0000000以下都是它自己的活动空间,0xC0000000以上是OS内核的活动空间。一个进程都活在自己独立的进程空间中,0-3G的空间每一个进程是不同的,因为用了虚拟内存技术,但是内核是唯一的。

2.2 操作系统下和裸机上C程序加载执行的差异

C语言程序运行时环境有一定要求,直接写出来的C语言程序是没法直接在内存中运行的,需要外部一定的协助,这段协助的代码叫加载运行代码,或者叫构建C运行时环境的代码,这一段代码在操作系统下是别人写好的,会自动添加到我们写的程序上,这段代码的主要作用是给全局变量赋值和清bss段。

ARM裸机中写shell时定义一个全局变量初始化为0但是实际不为0,后来在start.S中加了清bss段代码就变0了。这就说明在裸机程序中不会自动做这一段加载运行时代码,要程序员自己做,start.S中的重定位和清bss段就是在做这个事。

在操作系统中运行程序时则不用操心这个事,操作系统中会自动完成重定位和清bss,所以我们可以看到现象:C语言中未初始化的全局变量默认为0。数据段的全局变量或静态局部变量都是有非0的初值的,这些初值在main()函数运行之前就已经被初始化了,是重定位期间完成的初始化。

3.存储类相关的关键字

3.1 auto关键字

auto关键字在C语言中只有一个作用,那就是修饰局部变量,表示这个局部变量是自动局部变量,自动局部变量分配在栈上,既然在栈上,说明它如果不初始化那么值就是随机的。

平时定义局部变量时就是定义的auto的,只是省略了auto关键字而已。可见,auto的局部变量其实就是默认定义的普通的局部变量。

3.2 static关键字

static关键字在C语言中有2种用法,而且这两种用法彼此没有任何关联、完全是独立的。

  • static的第一种用法是:用来修饰局部变量,形成静态局部变量。要搞清楚静态局部变量和非静态局部变量的区别。本质区别是存储类不同,存储类不同就衍生出很多不同:非静态局部变量分配在栈上,而静态局部变量分配在数据段/bss段上。
  • static的第二种用法是:用来修饰全局变量,形成静态全局变量。要搞清楚静态全局变量和非静态全局变量的区别。区别是在链接属性上不同,讲到链接属性时详细讲。

静态局部变量和全局变量的相同点和不同点:

  • 静态局部变量在存储类和生命周期方面跟全局变量一样。
  • 静态局部变量和全局变量的区别是在作用域、链接属性。静态局部变量作用域是代码块作用域,和普通局部变量是一样的,链接属性是无链接;全局变量作用域是文件作用域,和函数是一样的,链接属性方面是外链接。

3.3 register

register关键字只有一个作用,那就是:register修饰的变量,编译器会尽量将它分配在寄存器中。平时分配的一般的变量都是在内存中的,分配在寄存器中用法是一样的,但是读写效率会高很多。所以register修饰的变量用在那种变量被反复高频率的使用,通过改善这个变量的访问效率可以极大的提升程序运行效率时,register是一种极致提升程序运行效率的手段。

编译器只能承诺尽量将register修饰的变量放在寄存器中,但是不保证一定放在寄存器中,主要原因是因为寄存器数量有限,不一定有空用。平时写代码要被定义成register这种情况很少,一般慎用。

3.4 extern

extern主要用来声明全局变量,声明的目的主要是在a.c中定义全局变量,在b.c中也可以使用该变量。

C语言中程序的编译时以单个.c源文件为单位的,因此编译b.c时只考虑b.c中的内容,不会考虑a.c的内容,这就导致b.c中使用了a.c中定义的变量时在编译时报错。解决方案是:假设在a.c中定义了全局变量int tmp;,则应该在b.c中使用tmp之前先声明extern int tmp;,声明就是告诉b.c我在别的文件中定义了tmp,并且它的原型和声明的一样,将来在链接的时候链接器会在别的.o文件中找到这个同名变量。

3.5 volatile

volatile的字面意思:可变的、易变的。C语言中volatile用来修饰一个变量表示这个变量可以被编译器之外的东西改变。

编译器之内的意思是变量的值的改变是代码的作用,编译器之外的改变就是这个改变不是代码造成的,或者不是当前代码造成的,编译器在编译当前代码时无法预知。比如在中断处理程序中更改了这个变量的值,比如多线程中在别的线程更改了这个变量的值,比如硬件自动更改了这个变量的值(一般这个变量是一个寄存器的值)。

以上说的三种情况(中断ISR中引用的变量,多线程中共用的变量,硬件会更改的变量)都是编译器在编译时无法预知的更改,此时应使用volatile告诉编译器这个变量属于这种可变的、易变的情况。编译器在遇到volatile修饰的变量时就不会对改变量的访问进行优化,就不会出现错误。

编译器的优化在一般情况下非常好,可以帮助提升程序效率。但是在特殊情况(volatile)下,变量会被编译器想象之外的力量所改变,此时如果编译器没有意识到而去优化则就会造成优化错误,优化错误就会带来执行时错误。而且这种错误很难被发现。volatile是程序员意识到需要volatile然后在定义变量时加上volatile,如果遇到了应该加的情况而没有加程序可能会被错误的优化。如果在不应该加的地方加了程序不会出错只是会降低效率。

所以对于volatile的态度应该是:正确区分,该加的时候加不该加的时候不加,如果不能确定该不该加为了保险起见就加上。

3.6 restrict

restrict关键字是c99中才支持的,所以很多延续c89的编译器是不支持restrict关键字,gcc支持的。

restrict也是和编译器行为特征有关的,它只能用来修饰指针,不能修饰普通变量。该关键字用于告知编译器,所有修改该指针所指向内容的操作全部都是基于该指针的,不可以通过其它途径修改它指向的变量。

该关键字一个有用的场景是下面的内存复制函数:

void *memcpy( void * restrict dest ,const void * restrict src,sizi_t n) 

这是一个内存复制函数,由于两个参数都加了restrict限定,所以两块区域不能重叠,即dest指针所指的区域,src的指针不能修改。

另一个内存复制函数:

memmove(void *dest,const void * src,size_t)

则可以重叠。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值