当前位置: 首页 > news >正文

5.C语言内存分区-堆-栈

目录

内存分区

运行之前

代码区

全局初始化数据区 、静态数据区 (data)

未初始化数据区(bss(Block Started by Symbol)区)

总结

运行之后

代码区 (text segment)

未初始化数据区(bss)

全局初始化数据区,静态数据区(data segment)

栈区(stack)

堆区(heap)


内存分区

运行之前

如果要执行一个C程序,那么第一步需要对这个程序进行编译。

1预处理宏定义展开,头文件展开,条件编译,这里不会检查语法
2编译检查语法,将预处理后的文件编译成汇编文件
3汇编将汇编文件生成目标文件(二进制) .o文件已生成
4链接 将目标文件链接为可执行程序  二进制文件转换可执行文件 类似.ext

当编译完成生成可执行文件之后,我们可以通过linux下的size买了查看一个可执行二进制文件基本情况:

通过上图可以得知,在没有运行程序前,也就是说,程序没有加载到内存前,可执行程序内部已分好3段信息,分别是 代码区(text)  , 数据区(data)  和未初始化数据区(bss) 3个部分(可以把data和bss合起来叫做静态区,或者全局区)

以下是细分:

bss区域放未初始化的数据如: static int a; //未初始化数据。
static int a = 10 ;//这个时候数据放在数据区 data区。

代码区

存放CPU执行的机器质量。通常代码是可以共享的(即另外的执行程序可以调用它),使其可共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可。代码通常是只读的,使其只读的原因是防止程序意外的修改他的指令。另外,代码区还规划了局部变量的相关信息。

说白了,代码区,就是放代码的

以上重点
共享:比如我们创建了一个a.exe和a1.exe两个代码是一样然后,第一次点击a.exe ,第二次点击a1.ext其实运行的还是a.exe原因是代码一样,共享

只读:比如我们在开发一个游戏币,创建了游戏币和人名币两个变量,如果是可写的,那么吧游戏币写到人名币里面那这样就是大事故,所以设置成只读。

全局初始化数据区 、静态数据区 (data)

该区包含了在程序中明确被初始化的全局变量,已经初始化的静态变量(包括全局静态变量)和常量数据(字符串常量)

未初始化数据区(bss(Block Started by Symbol)区)

存入的是全局未初始化变量和未初始化静态变量。未初始化数据区的数据在程序开始执行之前被内核初始化未0后者空(NULL)

总结

程序源代码被编译之后主要分成两段: 程序指令(代码区)  和  程序数据(数据区) 。代指码段属于程序指令,而数据域段和bss段属于程序数据

为什么要分开?

程序被加载到内存中之后,可以将数据和代码分别映射到两个内存区域。由于数据区域对进程来说是可读可写的,而指令域对程序来说是只读的,所以区分之后,可以将程序指令区域和数据区域分别设置成可读可写或者只读。这样可以防止程序的有意或者无意被修改

当程序中运行着多个同样的程序的时候,这些程序执行的指令都是一样的,所以只需要内中保存一份程序的指令就可以了,只是每一个程序运行中数据不一样而已,这样可以节省大量的内存。

运行之后

程序在加载到内存前,代码区和全局区(data 和 bss)的大小就是固定的,程序运行期间不能改变。然后,运行可执行程序,操作系统吧物理硬盘程序,加载到内存,除了根据可执行程序的信息分出代码区(text) , 数据区(data) 和未初始化数据区(bss)之外,还额外增加了栈区,堆区

代码区 (text segment)

加载的是可执行文件代码段,所有的可执行代码都加载到代码区,这块内存是不可以在运行期间修改的

案例:

int main() {int a = 1; // 这一行对应的机器指令就存储在代码区return 0;
}

未初始化数据区(bss)

加载的是可执行文件bss段,位置可以分开亦可以紧靠数据段,存储于数据段的数据(全局未初始化,静态未初始化数据)的生命周期未整个程序运行过程。

案例

   int a; // 存储在BSS段,默认值为0static int i; // 局部静态变量,默认值也为0,存储在BSS段

全局初始化数据区,静态数据区(data segment)

加载的是可执行文件数据段,存储于数据段(全局初始化,静态初始化数据,字符常量(只读))的数据的生存周期为整个程序运行过程。

案例

   int a= 10; // 存储在数据段static int i= 20; // 局部静态变量,同样存储在数据段

栈区(stack)

栈是一种先进后出的内存结构,由编译器自动分配释放,存放函数的参数值,返回值,局部变量等。在程序运行过程中实时加载和释放,因此,局部变量的生存周期为申请到释放该段栈空间。

栈的空间是有限的,尽量用完就释放掉

1是第一个进入。

如果1想出来,那要吧4先扔掉,3在扔掉,2在扔掉,才是1

可以认为吃米饭一样,先吃上面的,才能见碗底。

堆区(heap)

堆是一个大容器,它的容量要远远大于栈,但没有栈那样的先进后出的顺序。用于动态内存分配。堆在内存中位于bss区和栈区之间。一般由程序员分配释放,若程序不释放,程序结束时由操作系统回收

大容量:大容量,到底有多大,要看机器有多好,看机器

分配:使用malloc函数分配

释放:使用free函数释放 ,如果不释放程序会在系统结束后回收 注意,一定要手动释放

生命周期

类型作用域生命周期存储位置
auto变量一对{}内当前函数栈区
static局部变量一对{}内整个程序运行期初始化在data段,未初始化在BSS段
extern变量整个程序整个程序运行期初始化在data段,未初始化在BSS段
static全局变量当前文件整个程序运行期初始化在data段,未初始化在BSS段
extern函数整个程序整个程序运行期代码区
static函数当前文件整个程序运行期代码区
register变量一对{}内当前函数运行时存储在CPU寄存器
字符串常量当前文件整个程序运行期data段

栈 注意事项

案例1

int* func() {int a = 10;return &a;
}void test01() 
{int* p = func();printf("p = %d\n",p);
}

运行结果:

从上面结果来看,不是我们预期的结果,我们预期结果是 p = 10

为什么是这样?

首先我们来看func函数,函数定义的是int a = 10, 函数最终返回了a的地址,所以a在栈区的值已经释放了,我们没有去操作这一块内存。

案例2

char * getMyName()
{char myName[] = "达帮主";return &myName;
}void test02()
{char* p = getMyName();printf("my name p = %s\n",p);
}

运行结果:

问题与案例1一样,也是释放了,不要在意结果。

栈的释放过程

从上面图中可以看出,当getMyName方法运行完成之后,常量区的内容是会被释放的,放回p收到的只是地址。所以上面案例2是乱码,内容被释放,我们根本不知道是上面东西。

总结

不要返回局部变量地址,局部变量在函数执行之后就释放了,我们没有权限去操作释放后的内存。

堆 注意事项

案例1

int* getSpace() {//手动分配堆空间int *p = malloc(sizeof(int)*5);if (p == NULL) {return 0;}for (int i = 0; i < 5; i++) {p[i] = 1000 + i;}return p;
}void test01() {int* p = getSpace();for (int i = 0; i < 5; i++){printf("p:%d \n",p[i]);}//手动释放堆空间free(p);p = NULL; //防止野指针
}int main() 
{test01();printf("\n\n");system("pause");return EXIT_SUCCESS;
}

运行结果:

从上面代码来看我们使用了malloc来分配空间,分配的内存是存在堆中,所以数据没释放是一直存在的。

案例2

void getMyName(char *pp) 
{//分配内存char * temp = malloc(sizeof(char)*50);if (temp == NULL) {return;}memset(temp,0,50);//赋值strcpy_s(temp,50,"达帮主");pp = temp;
}void test02()
{char* p = NULL;getMyName(p);printf("%s\n",p);
}

运行结果

上面的原因是因为同级指针通过函数参数是无法修饰到p的,所以我们要在函数参数写二级指针。

如果主调函数中没有给指针分配内存,被调函数用同级指针是修饰不到主调函数中的指针的。

看下面案例

void getMyName(char **pp) 
{//分配内存char * temp = malloc(sizeof(char)*50);if (temp == NULL) {return;}memset(temp,0,50);//赋值strcpy_s(temp,50,"达帮主");*pp = temp;
}void test02()
{char* p = NULL;getMyName(&p);printf("%s\n",p);
}

运行结果:

上下的区别是加入二级指针,以及传的是地址,最后吧分配的内存修饰给二级指针

流程图

总结

在理解C内存分区时,常会碰到术语:数据区,堆,栈,静态区,常量区,全局区,字符串常量区,文字常量区,代码区等等。在这里,尝试捋清楚以上分区的关系。

  •  数据区包括:堆,栈,全局/静态存储区。
  • 全局/静态存储区包括:常量区,全局区、静态区。
  • 常量区包括:字符串常量区、常变量区。
  • 代码区:存放程序编译后的二进制代码,不可寻址区。


可以说,C/C++内存分区其实只有两个,即代码区和数据区

http://www.lryc.cn/news/508106.html

相关文章:

  • 传统CV算法——基于opencv的答题卡识别判卷系统
  • 国产 HighGo 数据库企业版安装与配置指南
  • 「Mac畅玩鸿蒙与硬件46」UI互动应用篇23 - 自定义天气预报组件
  • Springboot @Transactional使用时需注意的几个问题
  • 数字经济下的 AR 眼镜
  • 力扣150题
  • 剑指offer搜索二维矩阵
  • 如何设置浏览器不缓存网页
  • Iris简单实现Go web服务器
  • 后端项目java中字符串、集合、日期时间常用方法
  • 【Spring事务】深入浅出Spring事务从原理到源码
  • vue.js滑动到顶便锁定位置
  • EdgeX Core Service 核心服务之 Core Command 命令
  • 掌握常用HTML标签:创建个人简介网页
  • 音视频学习(二十五):ts
  • 10. 虚拟机VMware Workstation Pro下共享Ubuntu和Win11文件夹
  • 单元测试mock框架Mockito
  • Python从0到100(七十八):神经网络--从0开始搭建全连接网络和CNN网络
  • 2024多模态大模型综述最新总结
  • Redis——缓存穿透
  • 1.gitlab 服务器搭建流程
  • McDonald‘s Event-Driven Architecture 麦当劳事件驱动架构
  • GTID详解
  • 图解HTTP-HTTP状态码
  • sh cmake-linux.sh -- --skip-license --prefix = $MY_INSTALL_DIR
  • MySQL 在window免安装启动
  • [JavaScript] 我该怎么去写一个canvas游戏
  • 【潜意识Java】深度解析黑马项目《苍穹外卖》与蓝桥杯算法的结合问题
  • python报错系列(16)--pyinstaller ????????
  • Pytorch | 从零构建ResNet对CIFAR10进行分类