问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

C语言进程内存映像详解:堆、栈、数据段、代码段

创作时间:
作者:
@小白创作中心

C语言进程内存映像详解:堆、栈、数据段、代码段

引用
CSDN
1.
https://blog.csdn.net/m0_63939847/article/details/142966813

C语言进程的内存布局是理解程序运行机制的关键。本文将详细介绍C语言进程的内存映像,包括堆、栈、数据段和代码段等内存区域的特性。通过本文的学习,读者将能够更好地理解C语言程序的内存管理机制。

1、C进程内存布局

任何一个程序,正常运行都需要内存资源,用来存放诸如变量、常量、函数代码等等。这些不同的内容,所存储的内存区域是不同的,且不同的区域有不同的特性。因此我们需要研究C语言进程的内存布局,逐个了解不同内存区域的特性。

每个C语言进程都拥有一片结构相同的虚拟内存,所谓的虚拟内存,就是从实际物理内存映射出来的地址规范范围,最重要的特征是所有的虚拟内存布局都是相同的,极大地方便内核管理不同的进程。例如三个完全不相干的进程p1、p2、p3,它们很显然会占据不同区段的物理内存,但经过系统的变换和映射,它们的虚拟内存的布局是完全一样的。

  • PM:Physical Memory,物理内存。
  • VM:Virtual Memory,虚拟内存。

将其中一个C语言含如进程的虚拟内存放大来看,会发现其内部包下区域:

  • 栈(stack)
  • 堆(heap)
  • 数据段
  • 代码段

虚拟内存中,内核区段对于应用程序而言是禁闭的,它们用于存放操作系统的关键性代码,另外由于 Linux 系统的历史性原因,在虚拟内存的最底端 0x0 ~ 0x08048000 之间也有一段禁闭的区段,该区段也是不可访问的。

虚拟内存中各个区段的详细内容:

2、栈内存

什么东西存储在栈内存中?

  • 环境变量(包括当前文件路径之类的很多环境变量)
  • 命令行参数(要启动c程序时在命令行中输入的参数)
  • 局部变量(包括形参)

细说命令行参数:

void main(int argc,char *argv[]){
    for (size_t i = 0; i < argc; i++) {
        printf("%s\t", argv[i]);
    }
}

执行结果:

栈内存有什么特点?

  • 空间有限,尤其在嵌入式环境下。因此不可以用来存储尺寸太大的变量。
  • 每当一个函数被调用,栈就会向下增长一段,用以存储该函数的局部变量。
  • 每当一个函数退出,栈就会向上缩减一段,将该函数的局部变量所占内存归还给系统。

总结:

空间有限,随着函数的调用和退出动态分配(主要是分配给函数局部变量的空间和收回局部变量的空间)。

  • 注意:

  • 示例代码:

void func(int a, int *p) // 在函数 func 的栈内存中分配
{
    double f1, f2; // 在函数 func 的栈内存中分配
    ...
    // 退出函数 func 时,系统的栈向上缩减,释放内存
}

int main(void)
{
    int m = 100; // 在函数 main 的栈内存中分配
    func(m, &m); // 调用func时,系统的栈内存向下增长
}

3、静态数据

C语言中,静态数据有两种:

  • 全局变量:定义在函数外部的变量。

  • 静态局部变量:定义在函数内部,且被static修饰的变量。

  • 示例:

int a; // 全局变量,退出整个程序之前不会释放

void f(void)
{
    static int b; // 静态局部变量,退出整个程序之前不会释放
    printf("%d\n", b);
    b++;
}

int main(void)
{
    f();
    f(); // 重复调用函数 f(),会使静态局部变量 b 的值不断增大
}

为什么需要静态数据?

  1. 全局变量在默认的情况下,对所有文件可见,为某些需要在各个不同文件和函数间访问的数据提供操作上的方便。
  2. 当我们希望一个函数退出后依然能保留局部变量的值,以便于下次调用时还能用时,静态局部变量可帮助实现这样的功能。

注意1:

  • 若定义时未初始化,则系统会将所有的静态数据自动初始化为0

  • 静态数据初始化语句,只会执行一遍。

  • 静态数据从程序开始运行时 便已存在,直到程序退出时才释放。

注意2:

  • static修饰局部变量:使之由栈内存临时数据,变成了静态数据(存储于数据段)。
  • static修饰全局变量:使之由各文件可见的静态数据,变成了本文件可见的静态数据(缩小可见范围,为了减少命名冲突)。
  • static修饰函数:使之由各文件可见的函数,变成了本文件可见的静态函数。

4、数据段与代码段

数据段细分成如下几个区域:

  • .bss 段:存放未初始化的静态数据,它们将被系统自动初始化为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 中
}
  • 注意:数据段和代码段内存的分配和释放,都是由系统规定的,我们无法干预。

5、堆内存

(1)堆内存概述:

堆内存(heap)又被称为动态内存、自由内存,简称堆。堆是唯一可被开发者自定义的区段,开发者可以根据需要申请内存的大小、决定使用的时间长短等。但又由于这是一块系统“飞地”,所有的细节均由开发者自己把握,系统不对此做任何干预,给予开发者绝对的“自由”,但也正因如此,对开发者的内存管理提出了很高的要求。对堆内存的合理使用,几乎是软件开发中的一个永恒的话题。

(2)堆内存基本特征:

(3)什么时候需要用堆内存

当程序需要存储大量数据时,就需要申请堆内存来存放数据。堆内存是指程序运行时自己分配的内存,被称为动态内存。与静态内存(即内存块)不同,堆内存的大小不固定,可以随着程序运行需要而增加或减少。

当程序需要存储大量数据时,例如处理图形图像、视频音频、文件操作等需要大量内存的操作时,就需要申请堆内存来存放数据。此外,在程序中使用动态内存分配时,也需要使用堆内存。

eg:

在学生管理系统中,我们需要申请堆内存来存放学生数据,因为学生的数量是不固定的,而且学生的数据结构体包含了很多成员变量,这意味着这些变量需要存放的数据量很大,所以需要在程序运行时动态地分配内存。如果不申请堆内存,程序运行时会无法为学生数据分配足够的内存,导致程序崩溃或者数据丢失。

申请堆内存可以通过以下方法实现:

  1. 使用 calloc 函数为数组分配堆空间。
  2. 使用 malloc 函数为每个学生实例分配内存。
  3. 使用 free 函数归还堆内存。

申请堆内存和不申请堆内存的区别主要体现在内存分配方面。

不申请堆内存时,程序会使用栈内存来存放数据,当数据量很大时,栈内存可能会被填满,导致程序崩溃。

而申请堆内存时,程序会使用堆内存来存放数据,这样可以动态地分配内存,从而避免程序崩溃。

(4)相关API:

  • 示例:
int *p = malloc(sizeof(int)); // 申请1块大小为 sizeof(int) 的堆内存
bzero(p, sizeof(int));        // 将刚申请的堆内存清零
*p = 100; // 将整型数据 100 放入堆内存中
free(p);  // 释放堆内存
p=NULL;
//将释放内存的指针指向空,防止以后不小心用到的时候,对不属于自己的内存进行操作,改动到其他地方的东西
// 申请3块连续的大小为 sizeof(double) 的堆内存,并且会自动清零
double *k = calloc(3, sizeof(double));
k[0] = 0.618;
k[1] = 2.718;
k[2] = 3.142;
free(k);  // 释放堆内存
k=NULL;
double *k = calloc(3, sizeof(double));
完全等价于
double *k = malloc(3 * sizeof(double));
bzero(k ,3 * sizeof(double));  

(5)realloc函数:

void *realloc (void *ptr,size_t size)

参数分析:

  • ptr-->原本的内存的入口地址
  • size -->期望的新空间的大小

返回值:

  • 成功 返回新的内存入口地址

注意:

  1. 新内存大于旧内存:该函数会重新设置内存区的大小
  2. 新内存与旧内存入口地址一致:原地拓展,新的内存区不会被清空
  3. 新内存与旧内存入口地址不一致:realloc 函数会把旧地址中的数据拷贝到新的区域中,旧的内存会被释放掉
  4. 新内存小于旧内存:只是把内存的大小进行调整,数据依然保持不变

释放内存的含义:

  • 释放内存意味着将内存的使用权归还给系统。

  • 释放内存并不会改变指针的指向。

  • 释放内存并不会对内存做任何修改,更不会将内存清零。

如何检查内存是否泄漏:

  • 在Ubuntu中,使用sudo gpt install valgrind下载内存分析器,然后在qt中可以方便地看到堆内存未释放的报错
  • 通过valgrind运行程序:valgrind ./可执行文件
© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号