const static extern 存储与进程空间布局

原创博客,转载请说明来源:http://blog.csdn.net/lizhihaoweiwei/article/details/14548585

本文打算从C程序文件,进程空间着手,重点描述不同的声明,定义方式对应的内存布局方式。

首先说明一下UNIX系统下一个C程序的执行流程。内核启动C程序时(可以使用 exec系列函数),在调用main之前,先启动一个特殊的启动例程。这一行为是在源程序的编译链接阶段由C编译程序调用连接编辑程序,将这个可执行程序文件的起始地址指定为了这个特殊的启动例程。这个启动例程的作用是从内核取得命令行参数和环境变量值,为下一步调用main函数做准备的。

磁盘中的C程序文件存储组织包括三个部分:正文段,只读数据段和初始化数据段。《UNIX 环境高级编程》这本书里只给出两个部分:正文和初始化数据。这和本文讨论的并不矛盾,只读数据段也可以假定被放在正文段里面,因为正文段本身也是只读的,某些编译器的实现当然也可以将正文和只读数据放在一起,只不过这样没有严格将指令和数据分开,显得不那么科学。
正文即是由源程序编译后形成的机器指令集合,只读数据段存放程序运行过程中使用到的不变的变量或者严格意义上的常量(这两者的区别在于,前者可以被修改,后者是真的不能修改!),初始化数据段我们常简称为数据段,它包含了程序中需要赋初值的变量,这里的“需要”是针对操作系统来说的,即,操作系统需要将这些变量赋初值,这些变量包括全局变量全局和静态变量或局部静态变量。但操作系统如何知道该给这些变量赋什么样的值呢?答案就是,程序文件中某个存储段必定已经指出了!
以示例的形式给出不同形式的语法带来的不同段的存储:
1).C源程序任何函数之外的说明,也即全局的变量定义:int max_size = 19;//初化始化数据段
2).全局的或者非全局的静态变量:static int max_size = 19;//初始化数据段
2).全局 const 变量一定会存放到只读数据段里面:
const char *pstr = "i love you";//只读数据段
const char [100] = {"i love you"};//在只读数据段分配了100个字节,只使用sizeof(“i love you”)  = 11个字节去初始化这个区域(注意sizeof 自动计算了最后一个 \0 字符)。
const char[] = {"i love you"};//在只读数据段分配11个字节并使用“i love you” 初始化。
3).局部const 变量不一定会存放到只读数据段里面,这个是与编译器的实现有关系的,如果一个编译器实现严格意义上的const语义,应该要将局部const变量也放入数据段里面而不是只放入运行时的栈里面,因为如果采用后者实现,放入栈里面,那么,也就意味着它是可以使用某些手段被改的,例如指针,const_cast等等技术。另外,编译器的优化也使这个问题更加扑朔迷离了,假定位于某函数中的一个const 的局部变量,编译器得知在这个函数块中,没有对这个变量取地址的操作,则编译器可能会将这个变量直接放入只读数据段,进行变量折叠,提高访问速度。据我所知,目前大多数编译器把局部const变量放在了栈里面,也见诸于一些网上的问答:const 变量竟然被修改了!这个问题稍后会在后面详细分析。


C程序进程分为六个段,依低地址到高地址来排序为:正文段,只读数据段,初始化数据段(简称:数据段),非初始化数据段,堆,栈。
那么,内核调用 exec 将一个C程序文件读入到内存中时,会将文件映射到进程的地址空间中。正文段被映射为正文段,只读数据段映射为只读数据段,数据段被映射为数据段(也称为初始化数据段)。相应的变量也被存储到了相应的内存区。
前面已经说了正文段,只读数据段,初始化数据段,那么进程空间里还包括末初始化数据段,堆,栈。下面逐一分析。
末初始化数据段: 也即BBS段,BBS源于早期汇编程序的一个操作符,block started by symbol。BBS段是在进程初始化时产生的,内核将此段初始化为0。BBS段与初始化数据段都属于静态数据区。

堆:  也即 动态内存分配发生的场所,如使用 malloc,calloc,realloc系列函数将在堆上面分配空间,该空间使用链表管理,支持随机访问,堆向着高地址方向升长。
栈: 平时所说的堆栈就指栈,自动变量的存放位置,函数调用发生的场所。栈向低地址升长。关于函数栈可以参考我的另一篇文章:函数栈

根据上面的描述,堆和栈确实可能在某一时刻会增长重合,所以操作系统需要对这种错误捕捉并处理,最好是能预防。

以示例的形式给出不同形式的语法带来的不同段的存储:

1).函数之外的说明: int array[20];//BBS段。

2).函数内的说明   :  int a = 2;//栈

3).函数内的说明   :  int *a = (int *)malloc(sizeof(int) * 3)//堆 C++中的new操作符也分配在堆中

4).函数内的说明   :  int *a = (int *)alloca(sizeof(int) * 3);//alloca直接在函数的栈帧上分配内存。《UNIX环境高级编程》中指出:alloca缺点是,某些系统在函数已被调用后不能增加栈帧长度,于是也就不能支持alloca。


经过上面的描述至少已经大概明确了某一些说明会使得变量存储在程序磁盘文件或进程内存空间的哪一个段。如果更进一步的分析,那就必须深入分析各个说明关键字的意义,下面即展开分析。

先来说一下全局变量,全局变量分为两种,static 和非static的全局变量,static全局变量表示只能在它所在的源文件中使用,而不能在别的文件中使用,也即内联,非static全局变量是外联的。但这样的描述其实是存在问题的,因为某些关键字隐含了 static 的语义。比如C++里面的const就默认包含了static的语义,看来想把全局变量描述清楚,还必须分析 const 这个关键字。const 在C里面和C++里面是不一样的,C里面的const是指:“不变的变量”,而C++里面的const是指常量。

 下面说说const 在C 和 C++的特性和不同点:

1).特性:全局和局部const不同。

若非全局的const,则直接作为自动变量存储在栈中(据我所知,这是大部分编译器的实现),而全局的const变量对C而言则存储在只读数据段里面,C++放在符号表中,如果有内存分配情况另当别论!

局部const,用C语言编写以下测试代码

void func()

{

const int a = 4;

int  *p = &a;

*p = 1;// 更改成功

}

Ok,没有问题!

全局const,编写以下测试代码

const int a = 4;

void func()

{

int  *p = &a;

*p = 1;// 更改失败,会发生segmentation fault。

}


Ok,编译没有错误,但是运行时,会产生一个运行时期错,segmentation fault。由之前的分析,C语言程序的进程里面,全局的const一定会存放在只读数据段顺,而一旦尝试修改这个段里面的数据,操作系统便抛出一个只读段写入错误!

2) .const变量在C中不能做数组下标,在C++中可以。

正如前面已说,C中的const表示不变的变量,C++中的是常量!

C代码:

const int size = 3;

int array[size] = {0};//错误,在编译时期 size 的值是不能确定的,这里你可能会争论,size 的值明明确定了啊,的确,从汇编的角度来讲,size 的值确实是可以在编译时期确定的,但有一个问题,因为C语言对于const的实现机制,const修饰的并不是永远一成不变的常量,而修饰的是一个不可变化的变量!你也知道,这种实现是不靠谱的,因为这个“不变的变量”是可以被修改的,一旦程序在其它地方修改了这个 const 变量,这将是一个不安全的行为!例如:

const int size = 3;

int array[size] = {0};//假设C编译器允许

int  *p = &size;

*size = 100;// 更改成功

long long sum = 0;

for(int i = 0;i < size;++ i)

{

sum += array[i];

}

这一个很低级的错误就轻易地使进程的运行结果处于某种随机状态!而该行为永远不会是你想要的!

所以C语言就直接规定了,不允许用变量作数组的下标!所以,干脆C标准就直接跟程序员说,在编译时期size的值是不确定的!至于c ++这种做法是允许的,它是建立在const的实现与c不同,C++的  const 实现了语义上的常量,如果你尝试去修改一个局部的const,你必须要使用 const_cast<int *>,意味着,你若坚持这种更改,那么你已经保证了你会负责任!

C++代码:
const int size = 9;
int array[size] = {};
此时这种写法是正确的,因为C++真的把const当成一个常量,其在编译时期会把 size 放入符号表,其值确定为9,且C++对于const的语义实现是常量,即这个值不应该被改变,除非你承诺你将负责任!所以上述做法是正确的。同时,务必要注意,在编译时期,编译器会存在“常量折叠”,这是一个优化策略,即编译器会把所有使用 size 的标志替换为9,因为这免去了从地址取值的操作。

但这里面有可以深究的细节,C++编译器在常量折叠的时候,到底有没有为size分配内存呢?答案是,不确定!是的,编译器就是这么讨厌,它总是在你后面做了太多的事情而你完全不知道!这是由C++语言本身的复杂造成的,编译器之所以以这种乖张的手法工作,是因为你的代码只有在它定义的规则集合里面才能正确工作,按你的意愿来工作!那么到底什么时候分配内存或不分配内存呢?有三种情况:

a).extern const int size = 9;//因为size要支持外联,也即,别的源文件需要用到这个常量,所以它必须要有一个地址才行,也就是会导致内存分配。这个定义是全局的,所以内存分被分配在只读数据段,以作全局用。

b).const int size = 9;

    const int *p = &size;//导致内存分配,如果 size是全局的,则会在只读数据段分配,如果是局部的,会在栈上分配。

c).const int size = 3;

      const int array[size] = {0};

这个时候, size  用于一个集合,这个集合当然应该是确定大小,确定值的。那么,这个确定的东西是否应该放入符号表而不分配内存呢?答案是,若放入符号表,那么管理它就显得太复杂了,毕竟对于数组的操作还是在连续内存上操作的效率要高一些,所以编译器会分配内存,将它们一并放入该语句所处的环境,如果语句在函数之外,那就会在只读数据段分配,如果在函数内,那么会在栈上分配。

如果我们结合2)改写代码,使这成为这样,

const int size = 3;

const int *p = &size;

const int array[size] = {0};

你就会怀疑,size 已经被分配内存了,则本质上它属于变量了,那么,你还有十足的把握说,size这个变量能够用于集合,也就是用作一个数组的下标吗?是不是怀疑了?这里,解释一下,前面关于常量用于数组下标的论述依然是有效的,你写出这样的C++代码,那么就意味着你保证它的长度不变,这是其一,其二是C++语义也进行了保证。所以C++可以以这样的乖张方式实现:常量折叠发生在内存分配之前,即它先将常量替换为一个数值,然后当它检测到了存在内存分配的情况,则它再分配内存!

上面的写法是完全没有问题的!但,如果我们再改一下:

const int size = 3;

const int *p = &size;

const int array[*p] = {0}; 

这里,你应该有十足的把握说,这是非法的!的确,它不被编译器所接受,因为  *p 不是一个常量,而是一个变量,接下来的解释就与C里面的const 变量不能用作数组下标的原因一样了!

我们再改一下:

const int size = 3;

const int array[*(&size)] = {0}; 

你也应该有十足的把握,这是合法的!不要忘记了编译器不会放过在任何角落以非常乖张的方法工作!它检测到你的语句:*(&size) 自然会将它优化为 size,这是不是感觉像“脱了啥放了啥” | 粗口 :)

d).class 里面的const成员变量会有内存分配,你可能会反驳,C++类的成员变量不应该是非外联(确切来讲,链接为 none)的吗?那为什么还需要分配内存?《effective C++》里面谈到,封装的一个重要原则,C++类的所有成员都应该是private的,也就是说,它的所有成员变量都应该是不向外开放的,也即,是非外联的!的确,它是非外联的,但这并不说明,它不需要分配内存!这里只作简单的解释,当需要对一个类的对象作引用时,是否需要分配内存,而对于每一个类对象的那个const成员,可能不都一样!原因是,类对象在构建时,可能会接受一个值去给这个const成员赋值,可能会赋不一样的值。那么,它就必须要分配内存,而不可能为这个成员只包含一个符号表常量!简单来讲,把类的const成员当成是一个函数内部的const常量就一清二楚了!对于函数内部定义的const常量,它是分配在栈上的,并没有纳入符号表!原因与此类似!

3).在C和C++修改const变量有些不同。

前面已经说过,C里面改变一个const 变量,对于全局的const,会导致动行时崩溃,触发段错误。对于局部的const,则可以正确地更改!

但在C++里面呢?我们来详细分析一下:

C++代码:

const int size = 9;
int *q = const_cast<int*>(&size);
*q = 1;

printf("size is %d\r\n",size);//输出:size is 9

printf("*q is %d\r\n",*q);//输入:*q = 1

上述的代码能正确的执行,但没有任何实际意义,因为常量的值根本没有被改变(这点C++已经保证过了),这个是由编译器保证的,任何用到size的地方已经被9所替换,而不会再改变为1,只有为size分配内存的那个地方的值改为1了!这并不矛盾,你可以这样理解,常量折叠(类似宏替换)在前,分配内存在后!

但如果这样写:

volatile const int size = 9;
int *q = const_cast<int*>(&size);
*q = 1;

printf("size is %d\r\n",size);//输出:size is 1

printf("*q is %d\r\n",*q);//输入:*q = 1

volatile 表示 size 是一个可能随时变化的一个量,每次只要涉及取它的值,都要去内存里取,拒绝从寄存器或者符号表里面拿备份值!所以会输出:size is 1


3).C/C++ 全局const

C代码:

const int size;//正确

C++代码:

const int size;//错误

C中全局的const变量默认为外联,即使声明时不给它赋值,这个变量依然有机会在其它源文件中被赋值。而C++中,全局的const变量默认为内联,若声明它的时候不给它赋值,那么以后就没有任何机会给它赋值了,那么这个变量就不会被用到,当然它也就没有任何存在的必要了,谁会需要声明一个一定不会用到的变量?
如果在C++中这样写:extern const int size; 则这样写是正确的,因为显示地指定了size是一个外联的变量,它会在其它文件中被定义!同样,如果在C中这样写:const int size = 4;也是正确的,这是定义了一个全局的const变量,它可以被其它源文件引用。

最后再单独说一说static关键字:

static修饰文件作用域内的变量或者函数时,表示它们的链接属性为内联,不修改存储性质和作用域。如果修饰函数内部变量时,不改变链接属性和作用域,只改变其存储性质,它也像全局变量那样,放入了静态区(准确来讲,是初始化数据段)。这里有一个微妙的地方:

全局变量:static int a = 3;//a不是因为static的修饰而放入初始化数据段,而是因为它是一个全局变量。链接属性为内联

局部变量:static int a = 3;//a因为有static修饰,成为了一个静态变量,放入初始化数据段,链接属性为 none。



至此,关于const牵扯的种种已经分析完毕,可能不全,欢迎补充!

最后给出一些编译排错的例程,就当是大量理论之后的一小些容易消化的甜点!

C++中:
const int a;//error 
const int a = 1;//right
c ++ const 是内联的,如果在声明时不立即对于一个常量赋值,以后将没有任何机会赋值了,那么,这个常量就没有任何存在的必要性了!所以,上面的第一个声明是错误的。
external const int a;//正确,因为它的属性被改变为外联了

static int a ;//a 是一个静态变量,只在本文件中被使用。即使别的文件中使用 extern int a;也是使用这个文件中的 a 的。
external int a;//a 是一个来自于外部的变量,它会去其它目标文件中链接,如果找到了多个目标文件中都有实体可链接,则链接程序报错,否则,链接成功。


考虑C程序中:

//common.h

const int size = 9;//全局变量默认为外联,这里无论是否有const 修饰都不影响后面的结论

//a.c

#include "common.h"

void main(){...}

//b.c

#include "common.h"

void main(){...}

编译时会报错说size被重复定义了,因为C里面全局变量默认是外联的,也即每个具有相同标志的全局变量应该只被定义一次,但可以声明多次,声明和定义的差别在这里不再缀述了!上述的例子中,在a.c和b.c 中分别被给size定义了一次,这显然是错误的!

考虑C++程序中:

//common.h

const int size = 9;//C++全局变量默认为外联,这里无论是否有const会导致后面结论的变化

//a.c

#include "common.h"

int main(){...}

//b.c

#include "common.h"

int main(){...}

编译的时候无错误!这是因为在C++里面对于const全局变量默认为内联,即相当于在const前面加了一个static,各个源文件维持一个size。

若将const int size = 9;这一句的const去掉,则编译出错,size 被重复定义了!这是因为去掉const之后默认为外联了,C++编译器这样实现是有道理的,因为没有了const修饰则说明 size 这个全局变量可以被多个文件共享,自然它的定义只能有一个!所以去掉const后会编译报错认为size被重复定义了!

若将const int size = 9;改为 static int size = 9;编译也不会报错,因为static 表示为静态变量,只在当前文件中有效。自然不会发生编译重定义。


最后再给一组稍难的C++例子供参考:

1).从外链接的const常量不能用作数组下标

//文件 lib.cpp

extern const int size = 4;//外联

文件 main.cpp

void main()

{

extern const int size;

int array[size] = {0};

}

编译报错:变量不可用作数组的下标!C++之所以能够实现const常量作数组下标是因为常量折叠发生的替换。而此程序中size在main.cpp中不是符号表中的size,符号表中的size是lib.cpp的!也即,常量折叠并没有发生在main.cpp 中,只发生于lib.cpp中,毕竟main.cpp中的size是外联的!main.cpp中关于size发生的是内存分配,它导致编译器为外联size在只读数组段中分配空间。


2).为什么会触发只读段写入错误?

//文件 lib.cpp

extern const int size = 4;//外联

文件 main.cpp

void main()

{

extern int size;

        size = 2;

}

上面使用extern int size是可以的,可以不使用 extern const int size,导致运行崩溃的是size  = 2这一句。外联时只考虑标志名和类型而不考虑存储方式的。故在main.cpp中可以使用extern int size,使size = 4,当然size的地址在只读存储数据段的,这个是由被引入的size定义处决定的,所以一旦尝试去修改,则会触发只读段写错误!





评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值