c语言中进程内存空间分段(堆区、栈区、全局静态区、代码区、文字常量区)

c语言中进程内存空间分段(堆区、栈区、全局静态区、代码区、文字常量区)

进程的内存分配包含:

  • 堆 heap
  • 栈 stark
  • 全局静态区(.bss、.data)
  • 文字常量区(.rodata)
  • 代码段
    如图所示:
    在这里插入图片描述
    这里的4G空间是操作系统(32位Linux操作系统)给每个进程开辟的虚拟内存空间,实际的物理内存大小可能总共就4G,这里可以理解为操作系统是一个渣男,为每个进程画了一个4G的大饼,每个进程都以为自己独占实实在在的4G空间,其实并不是,操作系统怎么看都是一个时间管理大师(时间片切换快的亿马匹),每个进程都以为自己独占物理内存空间,实际上进程的内核空间一定是所有进程共享的,毕竟内核空间就是操作系统内核所在的区域,代码段、文字常量区也是共享的,只是底层物理内存被虚拟内存封装屏蔽,进程无法直接伸手触碰访问到物理内存。

其中堆栈区在编译时是不会给他们开辟并占用文件空间的,堆栈段是在进程执行过程中动态开辟的内存空间;

1、堆区
是程序员自己按自己意愿开辟的内存空间,由malloc函数开辟,可以灵活运用;

对于堆区中的变量,肯定有初学者会问,堆中创建的变量是局部变量么?还是全局变量?会跟局部变量一样函数结束就自动销毁么?
因为堆区比较特殊,需要理解一点就是,堆中没有你定义的“变量”只有一大片空间,这你就会跟我急眼,我明明定义了呀。如:

int *func(void){
	int *a =int*malloc(int);
	*a = 50;
	return a;
}

这一段示例中,你在func函数栈空间中创建了一个局部变量a,a是一个int *指针,并且将50赋予到a指向空间中,并且你也知道a指向的空间在堆空间上,那么我问你,这里你创建了堆变量么?没有吧,a明明是栈上的变量,get到没有?堆上虽然有数据50,但是这个变量它没有绑定的变量名,堆上的“变量”只能通过指针来访问,所以堆就是一片内存空间,它地址就是变量名,只能通过指针来访问堆中的数据。再来接解释其他疑问,这里理解了堆的本质就是一个可以被程序员主观操控的一大片地址空间,那么程序员只要手里捏着堆中数据的指针,该指针指向的空间是用malloc获得的,也就是说有正规户口的,不是黑户(野指针),那么只要你拿着这个指针,在进程中任何地方都能使用它,它不会像局部变量a一样伴随函数的结束而被系统销毁,所以func()函数返回a的值即50这个堆中整型变量的地址,外部函数就得到了堆上这块空间的地址,就能够随时用指针间接访问它。需要仔细理解堆的含义和栈的区别,堆本质上就是一块空间,有登记在册的指针就能访问,当然如果堆指针被free()掉了,那这个地址就是被注销掉,就是黑户了,用黑户地址系统肯定是不允许的。

我在学习c的时候,开始很长一段时间都在想这个问题,很多书上也没有讲的很清楚,对这块的理解一直都很模糊,希望这篇博客能够让你对c语言的变量、存储类型、内存空间结构分布的理解有帮助。如果你是学了一遍c语言,但还是对一些概念比较模糊,推荐一本书《c和指针》,多看几遍,每看一遍你都会觉得前面是白学了,看了几遍后不能说精通吧,起码会很熟悉c语言,哈哈哈,c语言说简单也简单,说难真的很难,重要是c跟编译、计算机组成原理等底层的概念交集很多,并且c是系统级语言,不是应用级的,花样少但是概念很深。

2、栈区
是用来储存函数局部自动变量和函数形参的动态内存空间,其储存形式是随着函数调用压栈和弹栈;调用函数时将形参、局部变量压到该函数的栈帧上,函数运行结束时,局部变量随着函数栈帧的弹出而被销毁。

3、全局静态区
包含.bss段和.data段,即数据段,全局静态区储存的是由static修饰的变量(不管是全局还是局部都存放在全局静态区)和全局变量(其实在概念上,任何声明在所有代码块外的变量都是静态变量,因为他们存储在静态内存中而不是堆栈中,另外使用static修饰的局部变量也是静态变量存储在静态区中而不是函数栈帧上。因此全局变量本质上就是静态变量,static修饰全局变量不是多此一举显示变量的静态存储属性,而是更改变量的链接属性即让该全局变量变为内部链接属性只对本源文件可见,不用static修饰的全局变量默认为外部连接属性)。静态区的分配是在编译阶段就已经确定,意思就是编译完后静态区内的数据就已经占据目标文件、可执行文件实际文件空间了,在程序一加载到内存中,进程运行之前就会在内存中创建完毕,并且其生命周期是进程整个运行周期。需要注意的是,static修饰的不管是全局变量还是局部变量都是在编译阶段放入静态区的,static修饰的局部变量和全局变量一样都占文件空间(代码编译后的可执行文件大小)。

.bss段,储存的是未显式初始化或者是说显式初始化为0的静态变量(全局、局部),为了提高程序的性能(早期计算机内存、外存空间资源有限价格昂贵,为了节约资源就设计了bss段,让那些未初始、初始化为0的静态变量存放在bss段,.bss几乎不占程序空间,而是使用符号标记这些变量大小,在程序被加载到内存中时根据符号到内存中创建对应大小的变量空间),.bss段中的数据只会创建一个占位符,不会实际占文件空间,当程序运行时就会在静态区创建实际的变量空间。

.data段,储存的是显式初始化(非零、非NULL),并且初始化非零的全局变量和static修饰的静态变量,编译阶段就已经生成,占据文件空间大小。

注意:
1、静态变量,不管是全局、全局静态、局部静态变量,在声明时若没有显式初始化,那么在进程加载到内存中时就会被自动初始化为零(该零根据类型为0、NULL),这一点可以在编程时利用。而局部自动变量(代码块中声明而没有被static修饰的变量)若没有显式初始化,在函数栈中创建他们时,他们内部填入的是垃圾值。

2、 当static在代码块外修饰全局变量或者函数时,其作用是改变变量和函数的链接属性,不改变存储类型和作用域,即static的全局变量、函数只在所在源文件可见,不具有外部链接属性,其他源文件不可通过extern声明进行访问,static修饰的全局变量依然存放在全局静态区。

3、当static在代码块中修饰局部变量时,不改变变量的链接属性和作用域,但改变局部变量的存储类型,即将原本的局部变量从栈区存储改变到全局静态区中存储。但是其作用域还是原本的代码块作用域,即static修饰的局部变量,其变量名还是跟其他局部变量一样只能在其所在的代码块内访问,作用域没变,只是当函数执行完后,其他普通的局部变量会随着函数栈帧销毁,而static修饰的局部变量存储在全局静态区不会随着弹栈销毁,保留其值,即使再次调用所在函数,依然是上次运行完保留的值。

4、另外,很多初学者可能会想到一点,是否可以在static修饰的局部变量作用域外使用该静态局部变量?答案是可以,上面说了static修饰的局部变量其作用域没有改变,即只能在其所在代码块作用域中引用,是的没错,但是作用域只是该变量名的作用域,换句话说在该变量作用域内可以直接用变量名来引用该变量,而在其作用域外就不能用它的变量名直接使用它了,但是c语言有一个非常BUG的东西叫指针,我们可以通过某些手段获得该局部静态变量的指针,因为静态变量的生命周期是整个进程运行周期,所以在外部使用指针来引用它跟全局变量是一样的,但是这样干需要注意的是对代码逻辑、对原本所在函数的影响。

4、代码段
.text段,.text段是储存进程的指令代码的区域(即代码编译、汇编、连接后的二进制机器指令),该段只读,被进程严格保护。

5、文字常量区
.rodata段是文字常量区,储存字符串常量以及const修饰全局变量,字符串常量都是放在.rodata段,如其名字read only一样这就是一个数据只读数据段(内存属性被设定为只读权限,与全局区中的变量不同,全局区的变量可读可写)。字符串存放在文字常量区,其他字面值常量只有使用const修饰全局变量时才会放入,其他的比如表达式的值其实也是字面值常量,它可能是作为立即数放在代码指令中亦或者开辟临时内存存放。

注意:文字常量区内存放的是常量,包括字面值常量、字符串常量、符号常量。字符串常量很好理解,即源文件中出现的字符串本身都存放在文字常量区,那如何理解字面值常量如何存放在文字常量区呢?

  • 当const修饰全局变量时,该变量就会存放在文字常量区,即const int a = 200;在全局区域定义时,a就会被存放在文字常量区,由于文字常量区只读特性,不可以通过指针隐性修改a的值。

  • 还有一点,字符串常量本身在编译阶段就会存放到文字常量区,并且系统保证该字符串的唯一性,即程序中相同重复的字符串在文字常量区是同一个,唯一的,不会为重复字符串开辟另外的空间,所以对于“hello”字符串,在文字常量区只有一份;只有字符串本身是一个char *地址,而字面值常量本身是不具备地址属性的,因此不存在像字符串那样的唯一性,重复创建的多个相同字面值常量的const全局变量地址是不同的。这里表达的比较模糊,就像下面的a和e变量,存的都是100,但是a和e不是同一个对象。

案例,通过内存地址判断变量所在区,进程的内存分区都是连续分区的(在虚拟内存来说,物理内存不是,这涉及到操作系统内存方面的内容,不加以讨论,观察变量内存地址的距离判断它们是否处于同一个区)

#include <stdio.h>
const int a = 100;//const修饰的是变量a本身,a存放在文字常量区
int b = 200;
char* c = "sdf";
char d = 'd';
const int e = 100;//const修饰的是变量e本身,e存放在文字常量区
char* f = "sdf";
const char* g = "sdf";//const修饰的是指针指向的内容*g,因此指针变量本身还是全局变量,存放在全局区
char* const h = "sdf";//cosnt修饰的是指针变量本身h,因此变量h本身存放在文字常量区

int main() {
	char* p_c = c;
	b = b++;
	d = d++;
	char** p_g = &g;
	*p_g = *p_g + 1;//g本身可以更改说明g本身是一个全局变量,const修饰的是*g
    
    char** p_h = &h;
	*p_h = *p_h + 1;//这里会出现异常,h本身不可以修改,说明h只读,存放在文字常量区
    
    char* const i = "sdf";//const修饰的局部变量都存放在栈中,因此可以隐式修改
	char** p_i = &i;//这里的i地址&i与全局区文字常量区距离很远,说明在栈上
	*p_i = *p_i + 1;
	return 0;
}

在这里插入图片描述
可以看出,全局变量的地址距离很近,几乎是紧挨着的;文字常量区的只读变量(以及常量)也是紧挨着的;而栈区中的i地址距离全局变量、文字常量非常远;还可以发现,全局静态区和文字常量区也是紧挨着的,有些老师说文字常量区其实是全局静态区中的一个特殊部分,只是文字常量区的内存属性为只读被进程保护起来了,这种说法我不太清楚,但是我很赞同这个说法,这涉及到很底层的编译原理,这里不探究,但是从内存地址上看全局变量地址和文字常量地址确实很近。

  • 17
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值