目录

内存分区

运行之前

代码区

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

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

总结

运行之后

代码区 (text segment)

未初始化数据区(bss)

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

栈区(stack)

堆区(heap)


内存分区

运行之前

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

1

预处理

宏定义展开,头文件展开,条件编译,这里不会检查语法

2

编译

检查语法,将预处理后的文件编译成汇编文件

3

汇编

将汇编文件生成目标文件(二进制) .o文件已生成

4

链接

 将目标文件链接为可执行程序  二进制文件转换可执行文件 类似.ext

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

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

通过上图可以得知,在没有运行程序前,也就是说,程序没有加载到内存前,可执行程序内部已分好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段,默认值为0
   static int i; // 局部静态变量,默认值也为0,存储在BSS段

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

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

案例

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

栈区(stack)

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

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

5.C语言内存分区-堆-栈_初始化_02

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);
}

运行结果:

5.C语言内存分区-堆-栈_开发语言_03

从上面结果来看,不是我们预期的结果,我们预期结果是 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);
}

运行结果:

5.C语言内存分区-堆-栈_代码区_04

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

栈的释放过程

5.C语言内存分区-堆-栈_数据区_05

从上面图中可以看出,当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;
}

运行结果:

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

从上面代码来看我们使用了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);
}

运行结果

5.C语言内存分区-堆-栈_代码区_07

上面的原因是因为同级指针通过函数参数是无法修饰到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);
}

运行结果:

5.C语言内存分区-堆-栈_初始化_08

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

流程图

5.C语言内存分区-堆-栈_初始化_09

总结

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

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

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