Files
BlogPosts/Collection/YoudaoyunNotes/02C语言/11-内存管理.md

11 KiB
Raw Blame History

C语言程序内存布局

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

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

  • PMPhysical Memory物理内存。

  • VMVirtual Memory虚拟内存。

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

  • stack

  • heap

  • 数据段

  • 代码段

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

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

栈内存

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

  • 环境变量

  • 命令行参数

  • 局部变量(包括形参)

  • 栈内存有什么特点?

  • 空间有限,尤其在嵌入式环境下。因此不可以用来存储尺寸太大的变量。

  • 每当一个函数被调用,栈就会向下增长一段,用以存储该函数的局部变量。(随用随申请,用完系统自动释放)

  • 每当一个函数退出,栈就会向上缩减一段,将该函数的局部变量所占内存归还给系统。(由系统自动进行管理)

  • 注意:

Linux中栈空间的大小可以使用ulimit -a进行查看若使用时超出这个范围则称为"栈溢出"

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

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

数据段

C语言中数据段中存放静态数据静态数据有两种

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

  • 静态变量:静态局部变量(定义在函数内部且被static修饰的变量)静态全局变量定义在函数外部且被static修饰的变量

  • 示例:

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

int main(void)
{
    f();
    f(); // 重复调用函数 f(),会使静态局部变量 b 的值不断增大
}
  • 为什么需要静态数据?
  1. 全局变量在默认的情况下,对所有文件可见,为某些需要在各个不同文件和函数间访问的数据提供操作上的方便(extern<声明外部变量>)。

  2. 当我们希望一个函数退出后依然能保留局部变量的值,以便于下次调用时还能用时,静态局部变量可帮助实现这样的功能。

  • 注意1

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

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

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

  • 注意2

  • static修饰局部变量使之由栈内存临时数据变成了静态数据。

  • static修饰全局变量使之由各文件可见的静态数据变成了本文件可见的静态数据。

  • static修饰函数使之由各文件可见的函数变成了本文件可见的静态函数。

数据段与代码段

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

  • .bss 段:存放未初始化(初始赋值)的静态数据它们将被系统自动初始化为0

  • .data段存放已初始化的静态数据

  • .rodata段存放常量数据程序内出现的所有常量不包含const修饰的变量

  • 代码段细分成如下几个区域:

  • .text段存放用户代码用户自己编写的所有程序源码

  • .init段存放系统初始化代码编译系统会自动为每一个程序文夹添加系统初始化代码。

int a;       // 未初始化的全局变量,放置在.bss 中
int b = 100; // 已初始化的全局变量,放置在.data 中

int main(void)
{
    static int c;       // 未初始化的静态局部变量,放置在.bss 中
    static int d = 200; // 已初始化的静态局部变量,放置在.data 中
    
    // 以上代码中的常量100、200防止在.rodata 中
}
  • 注意:数据段和代码段内存的分配和释放,都是由系统规定的,我们无法干预。

堆内存

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

  • 堆内存基本特征:

  • 相比栈内存,堆的总大小仅受限于物理内存,在物理内存允许的范围内,系统对堆内存的申请不做限制。

  • 相比栈内存,堆内存从下往上增长。

  • 堆内存是匿名的,只能由指针来访问。

  • 自定义分配的堆内存,除非开发者主动释放,否则永不释放,直到程序退出。

  • 相关API

  • 申请堆内存malloc() / calloc()

  • 清零堆内存bzero()

  • 释放堆内存free()

  • 示例:
int *p = malloc(sizeof(int)); // 申请1块大小为 sizeof(int) 的堆内存
bzero(p, sizeof(int));        // 将刚申请的堆内存清零

*p = 100; // 将整型数据 100 放入堆内存中
free(p);  // 释放堆内存

// 申请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;
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>

int main(int argc, char *argv[])
{
    char *p = NULL;
    // char str[10] = {0};  // 栈数组,有名字

    p = malloc(7766279631452241920); // malloc可以动态的申请内存空间10字节成功返回这个连续10字节空间的地址堆数组无名字
    if(p == NULL)
    {
        // printf("申请内存失败\n");  // 特殊情况
        perror("申请内存失败");  // 输出错误内容
        return -1;
    }

    // p = calloc(2, 10);  // calloc动态的申请内存空间2片连续的内存每片10字节
    // char (*q)[10] = calloc(4, 10);// malloc与calloc的用法相似的返回值都是void *因此可以为你申请所需要的所有类型内存。
 

    // for (int i = 0; i < 20; i++)
    // {
    //     str[i] = 'a'+i;
    //     p[i] =  'a'+i;
    // }
    // str[9] = '\0';
    // p[19] = '\0';

    // printf("%s\n", str);
    // printf("%s\n", p);

    // free(p); // 释放空间,使用完毕后将内存归还系统。

    p = malloc(20);   // 未进行初始化的内存
    bzero(p,20); // 初始化内存(清零)
    for (int i = 0; i < 20; i++)
    {
        printf("%x\t",p[i]);  // *(p+i)  *(y+x) == y[x]
    }
    printf("\n");

    free(p); // 已经释放, p虽然保存的还是刚刚的空间但这空间能否使用未知
    p = NULL; // 防止释放后在进行访问

    p = calloc(1,20);  // 会进行初始化(清零)的内存
    for (int i = 0; i < 20; i++)
    {
        printf("%x\t",p[i]);  // *(p+i)  *(y+x) == y[x]
    }
    printf("\n");

    free(p); // 已经释放, p虽然保存的还是刚刚的空间但这空间能否使用未知
    p = NULL; // 防止释放后在进行访问

//     for (int i = 0; i < 20; i++)
//     {
//         p[i] =  'a'+i;
//     }
//    printf("%s\n", p);

    return 0;
}
  • 注意:

  • malloc()申请的堆内存,默认情况下是随机值,一般需要用 bzero()或者memset() 来清零。

  • calloc()申请的堆内存默认情况下是已经清零了的不需要再清零且calloc可以申请多片连续内存。

  • free()只能释放堆内存,并且只能释放整块堆内存,不能释放别的区段的内存或者释放一部分堆内存。

  • realloc()重设内存的大小,若原地址后有足够的空间则新开拓的地址在原地址的基础上进行增加并返回原内存地址,若原地址后内存不足则重新开辟一片内存空间并将原地址内存中的数据拷贝到新的内存地址中,然后释放原地址内存并返回新内存地址。

  /* 验证realloc的使用 若原来的空间后有空闲的可以追加的空间则在原来的空间后进行追加,返回原地址;若不够则将原来空间中的数据拷贝到新的空间中并释放原来的空间,再返回新空间的地址 */
    p = malloc(20);  // 申请一片空间
    printf("p: %p\n", p);

    p = realloc(p, 20+2); // 重新设置已申请空间的大小  追加(再开辟)
    printf("p: %p\n", p); 

    p = realloc(p, 20-15); // 重新设置已申请空间的大小 缩减(释放一部分)
    printf("p: %p\n", p);
  • 释放内存的含义:

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

  • 释放内存并不会改变指针的指向手动立即令指针指向NULL。

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

  • 什么时候用栈什么时候用堆?

  • 基本数据类型int char float double就用栈复合数据类型结构体、联合体就用堆。

  • 练习

  • 使用堆空间计算两个大数的乘积123456789123456789123456789*987456321987456123698745