C进程内存布局
任何一个程序,正常运行都需要内存资源,用来存放诸如变量、常量、函数代码等等。这些不同的内容,所存储的内存区域是不同的,且不同的区域有不同的特性。因此我们需要研究才能清楚内存布局,逐个了解不同内存区域的特性。
每个C语言进程都拥有一片结构相同的虚拟内存,所谓的虚拟内存,就是从实际物理内存映射出来的地址规范范围,最重要的特征是所有的虚拟内存布局都是相同的,极大地方便内核管理不同的进程。例如三个完全不相干的进程p1、p2、p3,它们很显然会占据不同区段的物理内存,但经过系统的变换和映射,它们的虚拟内存的布局是完全一样的。
- PM:Physical Memory,物理内存。
- VM:Virtual Memory,虚拟内存。
将其中一个C语言含如进程的虚拟内存放大来看,会发现其内部包下区域:
- 栈(stack)
- 堆(heap)
- 数据段
- 代码段
虚拟内存中,内核区段对于应用程序而言是禁闭的,它们用于存放操作系统的关键性代码,另外由于 Linux 系统的历史性原因,在虚拟内存的最底端 0x0 ~ 0x08048000 之间也有一段禁闭的区段,该区段也是不可访问的。
虚拟内存中各个区段的详细内容:
栈内存
- 什么东西存储在栈内存中?
- 环境变量
- 命令行参数 (./a.out 123 Hello Even ... )
- 局部变量(包括形参)
- 定义在函数体内{ }的变量成为局部变量,反之则成为全局变量
- 形参则是在函数头中的参数列表内定义的变量 成为形参 int main(int argc, char const *argv[])
- 栈内存有什么特点?
- 空间有限,尤其在嵌入式环境下(STM32 / 51...)。因此不可以用来存储尺寸太大的变量。(如果超出范围则会出现栈溢出程序崩溃)
- 每当一个函数被调用,栈就会向下增长一段(被分配),用以存储该函数的局部变量。
- 每当一个函数退出,栈就会向上缩减一段(被回收),将该函数的局部变量所占内存归还给系统。
栈内存的分配和释放,都是由系统规定的,我们无法干预。
局部作用域
- 概念:在代码块中定义的变量(局部变量),其可见范围从其定义处开始,到代码块结束为止。
- 示例:
int main() { // 主函数的作用域 // a = 123 ; // [错误] 需要在定义语句后,才能正确访问变量 int a=1; // a的作用域从该语句运行结束后开 int b=2; // 变量 c 的作用域是第4行到第9行 printf("LINE:%d\ta:%d\n" , __LINE__ ,a ); { // 👇👇👇👇👇👇新的作用域的开始👇👇👇👇👇👇 int c=4; int d=5; // 变量 d 的作用域是第7行到第8行 printf("LINE:%d\ta:%d\n" , __LINE__ ,a ); printf("LINE:%d\tc:%d\n" , __LINE__ ,c ); // 这里又定义了一个a 但是当前的a 属于一个小范围的作用域, // 在该作用域的a 会临时掩盖大作用域中同名的变量 int a = 100; printf("LINE:%d\ta:%d\n" , __LINE__ ,a ); }// 👆👆👆👆👆新的作用域的结束👆👆👆👆👆👆👆👆 // 当我们离开小作用域后,该作用域中所有的变量都会被释放 (交给系统重新分配) // printf("LINE:%d\tc:%d\n" , __LINE__ ,c ); // [编译保存] 未定义 printf("LINE:%d\ta:%d\n" , __LINE__ ,a ); } // 直到该 大括号 } 表示结束
- 要点:
- 代码块指的是一对花括号 { } 括起来的区域。
- 代码块可以嵌套包含,外层的标识符(变量名...)会被内嵌的同名标识符临时掩盖变得不可见。
- 代码块作用域的变量,由于其可见范围是局部的,因此被称为局部变量。
- 自动存储期
在栈内存中分配的变量,统统拥有自动存储期,因此也都被称为自动变量。这里自动的含义,指的是这些变量的内存管理不需要开发者操心,都是全自动的:在变量定义处自动分配,出了变量的作用域后自动释放。
- 以下三个概念是等价的:
- 自动变量:从存储期的角度,描述变量的时间特性。
- 临时变量:同上。
- 局部变量:从作用域(作用访问是局部 { 只能在代码块内部使用 })的角度,描述变量的空间特性。
- 注意:
数据段与代码段
- 数据段细分成如下几个区域:
- .bss(Block Started by Symbol) 段:存放未初始化的静态数据,它们将被系统自动初始化为0
- .data段:存放已初始化的静态数据
- .rodata段:存放常量数据 (该区域的所有数据都是不允许被修改的)
- 代码段细分成如下几个区域:
- .text段:存放用户代码
- .init段:存放系统初始化代码(编译器根据实际的目标系统自动添加)
int a; // 未初始化的全局变量,放置在.bss 中
int b = 100; // 已初始化的全局变量,放置在.data 中
int main(void)
{
static int c; // 未初始化的静态局部变量,放置在.bss 中
static int d = 200; // 已初始化的静态局部变量,放置在.data 中
// 以上代码中的常量100、200防止在.rodata 中
}
- 注意:数据段和代码段内存的分配和释放,都是由系统规定的,我们无法干预。
静态数据
概念: 数据的生存周期是固定, 不会因为程序执行某些操作而申请或释放它所占用的内存。
他们的内存是在程序运行之处就被申请出来, 直到程序运行结束才会释放回收。
C语言中,静态数据有两种:
- 全局变量:定义在函数外部的变量。
- 静态局部变量:定义在函数内部,且被static修饰的变量。
- 示例:
int a; // 全局变量,退出整个程序之前不会释放 void f(void) { static int b; // 静态局部变量,退出整个程序之前不会释放 printf("%d\n", b); b++; } int main(void) { f(); f(); // 重复调用函数 f(),会使静态局部变量 b 的值不断增大 }
- 为什么需要静态数据?
-
- 全局变量在默认的情况下,对所有文件可见,为某些需要在各个不同文件和函数间访问的数据提供操作上的方便。
- 当我们希望一个函数退出后依然能保留局部变量的值,以便于下次调用时还能用时,静态局部变量可帮助实现这样的功能。
- 注意1:
- 若定义静态数据 ( 全局变量 、 被static修饰的局部变量 ) 时未初始化,则系统会将所有的静态数据自动初始化为0
- 静态数据初始化语句,只会执行一遍。
- 静态数据从程序开始运行时便已存在,直到程序退出时才释放。
- 注意2 static关键字的作用:
- static修饰局部变量:使之由栈内存临时数据,变成了静态数据。
- static修饰全局变量:使之由各文件可见的静态数据,变成了本文件可见的静态数据。
- static修饰函数:使之由各文件可见的函数,变成了本文件可见的静态函数。
#include <stdio.h>
// 全局变量定义
int Num1 = 456 ; // 该变量Num 的可见范围(作用域)是整个程序(可能由多个.c文件组成)可见
// 如果在其他文件也定义一样全局变量,那么将会发成重定义的冲突问题
// 不利于模块化编程
static int Num2 = 567 ; // 使用static 修饰的全局变量,他的作用域(可见范围)尽本文件可见
// 如果在其他文件也定义一样全局变量,那么将bu会发成重定义的冲突问题
// 有利于模块化编程
// 如果其他文件确实需要使用到该变量,
// 那么一般就会把该变量定义的语句写入到头文件中,需要用到的文件直接包含头文件即可
int main(int argc, char const *argv[])
{
int a = 123 ; // 该变量a 属于函数main的局部变量
// 当该函数被执行的时候 自动分配出来 它所在区域为 栈空间
// 当函数运行结束时会被系统自动回收
static int b = 456 ; // 该变量是在程序被执行的时候直接分配
// 该变量的存储区域 是 数据段 的.data段
// 他的生命周期与程序保持一致 , 不会因为函数运行结束而释放
// (只要程序没有退出,他的值可以一直延用)
// 该数据为静态数据,因此无法手动对他进行释放
char * p = "Hello Even\n" ; // 指针 p 是一个局部变量.他的存储区在栈空间
// 它指向了 数据段中 .rodata 中"Hello Even\n" 的入口地址
static char * p1 = "Hello Even\n" ; // 指针 p1 是一个静态的局部变量, 它的存储区在 数据段的 .data 段
// 它指向了 数据段中 .rodata 中"Hello Even\n" 的入口地址
// p 会因为函数的运行结束而释放, p1 则不会, 他的生命周期与程序保持一致
return 0;
}
尝试使用画图工具把以上示例代码中各个变量在内存中进行标记。
堆内存
堆内存(heap)又被称为动态内存、自由内存,简称堆。堆是唯一可被开发者自定义的区段,开发者可以根据需要申请内存的大小、决定使用的时间长短等。但又由于这是一块系统“飞地”,所有的细节均由开发者自己把握,系统不对此做任何干预,给予开发者绝对的“自由”,但也正因如此,对开发者的内存管理提出了很高的要求。对堆内存的合理使用,几乎是软件开发中的一个永恒的话题。
- 堆内存基本特征:
- 相比栈内存,堆的总大小仅受限于物理内存,在物理内存允许的范围内,系统对堆内存的申请不做限制。
- 相比栈内存,堆内存从下往上增长。
- 堆内存是匿名的,只能由指针来访问。
- 自定义分配的堆内存,除非开发者主动释放,否则永不释放,直到程序退出。
- 相关API:
- 申请堆内存:malloc ( ) / calloc ( )
- 清零堆内存:bzero ( )
- 释放堆内存:free ( )
- 重新申请 realloc ( )
分析堆内存的API:
申请堆内存空间,并返回该内存的入口地址, 但是不会对该内存进行初始化操作。
头文件:
#include <stdlib.h>
函数原型:
void *malloc(size_t size);
参数分析:
size --> 需要申请的内存的大小(字节)
返回值:
成功 返回一个已经分配好的内存
失败 返回 NULL
释放内存空间:
函数原型:
void free(void *ptr);
参数分析:
ptr --> 需要释放的堆内存的入口地址 (必须是堆内存+入口地址)
返回值:
成功 无
失败 无
函数原型(更适用于数组的申请)并对申请得到的内存进行初始化为 0 :
void *calloc(size_t nmemb, size_t size);
参数分析:
nmemb --> 数组元素的个数
size --> 每一个元素的大小
返回值:
成功 返回申请到的内存入口地址
失败 返回 NULL
函数原型:
void *realloc(void *ptr, size_t size);
参数分析:
ptr --> 旧的堆空间入口地址
size --> 新空间的内存大小
返回值:
成功 返回新的入口地址 (如果没有换到新的内存下则返回值与参数ptr相同)
失败 返回null
void *reallocarray(void *ptr, size_t nmemb, size_t size);
- 注意:
- malloc()申请的堆内存,默认情况下是随机值,一般需要用 bzero() 来清零(拓展)。
- calloc()申请的堆内存,默认情况下是已经清零了的,不需要再清零。
- free()只能释放堆内存,并且只能释放整块堆内存(必须是某一块堆空间的入口地址),不能释放别的区段的内存或者释放一部分堆内存。
- 释放内存的含义:
- 释放内存意味着将内存的使用权归还给系统。
- 释放内存并不会改变指针的指向(会出现野指针)。
- 释放内存并不会对内存做任何修改,更不会将内存清零(所以内存中会有随机值)。
- 尝试自己画一下内存布局图,过程中回顾一下各个区域的特点
- 编写代码验证一下内存布局(地址大小关系)
#include <stdio.h> #include <stdlib.h> // 全局变量 (静态数据) int a ; // a 是静态数据, 没有初始化因此它的存储区域在 .bss 默认初始值为 0 int b = 333 ; // b 是静态数据,有进行初始化因此它存储的区域在 .data int main(int argc, char const *argv[]) { printf("argv[0]:%p\n" , argv[0]); // 命令行参数 printf("&argc的地址:%p\n" , &argc); // 在参数列表中定义的变量属于该函数的局部变量 int Num = 123 ; printf("&Num的地址:%p\n" , &Num); // 在函数体内部定义,因此它属于该函数的局部变量 // 在堆内存中申请了10个字节的内存空间并把该内存的入口地址 存入到 msg 指针变量中 char * msg = calloc(10 , sizeof(char) ) ; printf("msg:%p\n" , msg); // 数据段的内存 printf("a的地址:%p\n" , &a); printf("b的地址:%p\n" , &b); // 使用指针p 指向了常量区的 "Hello Even" 的入口地址 char * p = "Hello Even" ; printf("p:%p\n" , p); // 代码段 printf("main 主函数的入口地址:%p\n" , main); printf("&main 主函数的入口地址:%p\n" , &main); return 0; }