【C】内存布局

动态内存管理

动态内存管理的几个函数

  • malloc -- 申请动态内存空间
  • free -- 释放动态内存空间
  • calloc -- 申请并初始化一系列内存空间
  • realloc -- 重新分配内存空间

malloc

  • 函数原型:

    • void *malloc(size_t size);
  • malloc函数向系统申请分配size个字节的内存空间,并返回一个指向这块空间的指针void类型)。

  • 如果函数调用成功,返回一个指向申请的内存空间的指针,由于返回类型是 void 指针(void *,所以它可以被转换成任何类型的数据;如果函数调用失败,返回值是NULL。另外,如果 size 参数设置为0, 返回值也可能是 NULL ,但这并不意味着函数调用失败。

    #include<stdio.h>
    #include<stdlib.h>
    int main()
    {
        int *ptr;
        ptr = (int *)malloc(sizeof(int)); 
        //因为不是所有的操作系统的int都是4个字节,所以参数使用sizeof()
        //  (int *)将返回的void指针转化为int指针
        if (ptr == NULL)
        {
            printf("分配内存失败了! \n");
            exit(1); //退出程序,需导入头文件<stdlib.h>
        }
        printf("请输入一个整数 : ");
        scanf("%d", ptr);
        printf("你输入的整数是 : %d\n", *ptr);
        return 0;
    }
    

    运行结果

    请输入一个整数 : 100
    你输入的整数是 : 100
    
  • 注意:由于malloc申请的空间位于内存的上,如果你不主动释放堆上的内存资源,那么它将永远存在,直到程序被关闭。所以当我们不再使用这块内存时,要自己动手释放,避免造成内存泄漏。

  • malloc不仅可以申请存储基本数据类型的空间,事实上它还可以申请一块任意尺寸的内存空间。对于后者,由于申请得到的空间是连续的,所以我们经常用数组来进行索引即可:

    #include <stdio.h>
    #include <stdlib.h>
    int main(void)
    {
        int *ptr = NULL;
        int num, i;
        printf("请输入待录入整数的个数:");
        scanf("%d", &num);
        ptr = (int *)malloc(num * sizeof(int));
        for (i = 0; i < num; i++)
        {
            printf("请录入第%d个整数:", i+1);
            scanf("%d", &ptr[i]);
        }
        printf("你录入的整数是:");
        for (i = 0; i < num; i++)
        {
            printf("%d ", ptr[i]);
        }
    
        putchar('\n');
        free(ptr);
        return 0;
    }
    

free

  • 函数原型:

    • void free(void *ptr);
  • free 函数释放 ptr 参数指向的内存空间。该内存空间必须是由 malloccallocrealloc函数申请的。否则,该函数将导致未定义行为。如果 ptr
    参数是 NULL不执行任何操作

    注意:该函数并不会修改 ptr 参数的值,所以调用后它仍然指向原来的地方(变为非法空间)。

    #include<stdio.h>
    #include<stdlib.h>
    int main()
    {
        int *ptr;
        ptr = (int *)malloc(sizeof(int)); 
        //因为不是所有的操作系统的int都是4个字节,所以参数使用sizeof()
        //  (int *)将返回的void指针转化为int指针
        if (ptr == NULL)
        {
            printf("分配内存失败了! \n");
            exit(1); //退出程序,需导入头文件<stdlib.h>
        }
        printf("请输入一个整数 : ");
        scanf("%d", ptr);
        printf("你输入的整数是 : %d\n", *ptr);
        free(ptr); //释放内存空间
        printf("你输入的整数是 : %d\n", *ptr); //再次打印会出错
        return 0;
    }
    

内存泄漏

导致内存泄漏主要有两种情况:

  • 隐式内存泄漏(即用完内存块没有及时使用free函数释放)

    注: 下面这段程序可能会造成系统假死,运行需谨慎

    #include<stdio.h>
    #include<stdlib.h>
    int main()
    {
        while (1) // 死循环
        {
            malloc(1024); //不断地申请内存空间
        }
        return 0;
    }
    
  • 丢失内存块地址

    #include<stdio.h>
    #include<stdlib.h>
    int main()
    {
        int *ptr;
        int  num = 123;
        ptr = (int *)malloc(sizeof(int)); 
        //因为不是所有的操作系统的int都是4个字节,所以参数使用sizeof()
        //  (int *)将返回的void指针转化为int指针
        if (ptr == NULL)
        {
            printf("分配内存失败了! \n");
            exit(1); //退出程序,需导入头文件<stdlib.h>
        }
        printf("请输入一个整数 : ");
        scanf("%d", ptr);
        printf("你输入的整数是 : %d\n", *ptr);
        ptr = &num; //ptr指向局部变量
        printf("你输入的整数是 : %d\n", *ptr);
        free(ptr); //此时ptr指向的不是malloc申请的内存空间,使用free函数释放会出错
        return 0;
    }
    

初始化内存空间

由于malloc并不会帮你初始化申请的内存空间,所以需要自己进行初始化。

memset

  • 以men开头的函数被编入字符串标准库,函数的声明包含在string.h这个头文件中:

    • memset —— 使用一个常量字节填充内存空间
    • memcpy —— 拷贝内存空间
    • memmove —— 拷贝内存空间
    • memcmp —— 比较内存空间
    • memchr —— 在内存空间中搜索一个字符
  • memset函数可以用来初始化内存空间

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #define N 10
    int main(void)
    {
        int *ptr = NULL;
        int i;
        ptr  = (int *)malloc(N*sizeof(int));
        if (ptr == NULL)
        {
            exit(1);
        }
        memset(ptr, 0, N*sizeof(int));
        for (i = 0;i < N; i++)
        {
            printf("%d ",ptr[i]);
        }
        putchar('\n');
        free(ptr);
    }
    

calloc

  • 函数原型:

    • void *calloc(size_t nmemb, size_t size);
  • calloc函数在内存中动态地申请nmemb个长度为size的连续内存空间(即申请的总空间尺寸为nmemb * size),这些内存空间全部被初始化为0.

  • calloc函数与malloc函数的一个重要区别是:

    • calloc 函数在申请完内存后,自动初始化该内存空间为零
    • malloc 函数不进行初始化操作,里边数据是随机的
  • 下面两种写法是等价的

    // calloc() 分配内存空间并初始化
    int *ptr = (int *)calloc(8, sizeof(int)); //两个参数
    
    // malloc() 分配内存空间并用 memset() 初始化
    int *ptr = (int *)malloc(8 * sizeof(int));  //一个参数
    memset(ptr, 0, 8 * sizeof(int));
    

拓展内存空间

memcpy

有时候我们可能会需要对原来的分配的内存空间进行拓展,这时就需要再调用一次malloc 申请一个更大的内存空间,再使用memcpy将原来的数据搬进去

  • 函数原型:

    • void *memcpy(void *dest,const void *src, size_t n);
  • memcpy 函数从 src 指向的内存空间拷贝 n 个字节到dest指向的内存空间。srcdest指向的内存区域不能出现重叠,否则应该使用 memmove 函数。

  • memcpy函数并不关心被复制的数据类型,只是逐字节地进行复制,这给函数的使用带来了很大的灵活性,可以面向任何数据类型进行复制。

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main(void)
{
    int *ptr1 = NULL;
    int *ptr2 = NULL;
    //第一次申请的内存空间
    ptr1 = (int *)malloc(10 * sizeof(int));
    //进行若干操作之后发现ptr1申请的内存空间不够用
    //第二次申请的内存空间
    ptr2 = (int *)malloc(20 * sizeof(int));
    //将ptr1的数据拷贝到ptr2中
    memcpy(ptr2, ptr1, 10);
    free(ptr1);
    //对ptr2申请的内存空间进行若干操作...
    free(ptr2); //释放对ptr2申请的内存空间
    return 0;
}

realloc

  • 函数原型:

    • void *realloc(void *ptr, size_t size);
  • 以下几点是需要注意的:

    • realloc函数修改 ptr指向的内存空间大小为size字节
    • 如果新分配的内存空间比原来的大,则旧内存块的数据不会发生改变;如果新的内存空间的大小小于旧的内存空间,可能会导致数据丢失,慎用!
    • 该函数将移动内存空间的数据并返回新的指针
    • 如果ptr参数为NULL,那么调用该函数就相当于调用malloc(size)
    • 如果size参数为 0,并且ptr参数不为NULL,那么调用该函数就相当于调用free(ptr)
    • 除非ptr参数为NULL,否则ptr的值必须由先前调用malloccallocrealloc 函数返回
  • 设计一个程序,要求:

    • 不使用malloc函数,使用realloc函数申请内存空间
    • 让用户一直输入一个整数,用户每输入一个整数就新增加一个整形空间存放。直到用户输入-1表示输入结束,再把输入的所有整数打印出来
    #include<stdio.h>
    #include<stdlib.h>
    
    int main(void)
    {
        int i, num;
        int count = 0;
        int *ptr = NULL; //注意,这里必须初始化为NULL
        do
        {
            printf("请输入一个整数(输入-1表示结束) : ");
            scanf("%d",&num);
            count++;
            ptr = (int *)realloc(ptr, count * sizeof(int));
            if (ptr == NULL)
            {
                exit(1);
            }
            ptr[count-1] = num;
        }while (num != -1);
        printf("输入的整数分别是 : ");
        for (i=0;i<count;i++)
        {
            printf("%d ",ptr[i]);
        }
        putchar('\n');
        free(ptr);
        return 0;
    }
    

    运行结果

    请输入一个整数(输入-1表示结束) : 1
    请输入一个整数(输入-1表示结束) : 2
    请输入一个整数(输入-1表示结束) : 3
    请输入一个整数(输入-1表示结束) : -1
    输入的整数分别是 : 1 2 3 -1
    //如果不想打印-1,把for循环条件表达式改为"i<count-1"即可
    

C语言的内存布局

#include<stdio.h>
#include<stdlib.h>
int global_uninit_var;
int global_init_var1 = 520;
int global_init_var2 = 880;
void func(void);
void func(void)
{
    ;
}
int main(void)
{
    int local_var1;
    int local_var2;
    static int static_uninit_var;
    static int static_init_var = 456;
    char *str1 = "I love FishC.com!";
    char *str2 = "You are right!";
    int *malloc_var = (int *)malloc(sizeof(int));
    printf("addr of func -> %p\n",func);
    printf("addr of str1 -> %p\n",str1);
    printf("addr of str2 -> %p\n",str2);
    printf("addr of global_init_var1 -> %p\n",&global_init_var1);
    printf("addr of global_init_var2 -> %p\n",&global_init_var2);
    printf("addr of static_init_var -> %p\n",&static_init_var);
    printf("addr of static_uninit_var -> %p\n",&static_uninit_var);
    printf("addr of global_uninit_var -> %p\n",&global_uninit_var);
    printf("addr of malloc_var -> %p\n",malloc_var);
    printf("addr of local_var1 -> %p\n",&local_var1);
    printf("addr of local_var2 -> %p\n",&local_var2);
    return 0;
}

运行结果:

addr of func -> 00007ff754751591
addr of str1 -> 00007ff754759000
addr of str2 -> 00007ff754759012
addr of global_init_var1 -> 00007ff754758010
addr of global_init_var2 -> 00007ff754758014
addr of static_init_var -> 00007ff754758018
addr of static_uninit_var -> 00007ff75475c044
addr of global_uninit_var -> 00007ff75475c040
addr of malloc_var -> 0000021f4af616a0
addr of local_var1 -> 000000d8845ff674
addr of local_var2 -> 000000d8845ff670
//注意: 每个人的结果不一定相同

规律:

C语言内存空间划分

根据内存地址从低到高分别划分为:

  • 代码段(Text segment)
  • 数据段(Initialized data segment)
  • BSS段(Bss segment/Uninitialized data segment)
  • 栈(Stack)
  • 堆(Heap)

代码段

  • 代码段通常是指用来存放程序执行代码的一块内存区域。
  • 这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读。
  • 在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。

数据段

  • 数据段通常用来存放已经初始化的全局变量和局部静态变量。

BSS段

  • BSS 段通常是指用来存放程序中未初始化的全局变量的一块内存区域。
  • BSS 是英文 Block Started by Symbol 的简称,这个区段中的数据在程序运行前将被自动初始化为数字 0。

  • 前边我们学习了动态内存管理函数,使用它们申请的内存空间就是分配在这个堆里边。
  • 所以,堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩展或缩小。
  • 当进程调用 malloc 等函数分配内存时,新分配的内存就被动态添加到堆上;当利用 free等函数释放内存时,被释放的内存从堆中被剔除。

  • 栈是函数执行的内存区域,通常和堆共享同一片区域。

栈和堆的区别

堆和栈则是 C 语言运行时最重要的元素,下面我们将两者进行对比。

  • 申请方式

    • 堆由程序员手动申请
    • 栈由系统自动分配
  • 释放方式

    • 堆由程序员手动释放
    • 栈由系统自动释放
  • 生存周期

    • 堆的生存周期由动态申请到程序员主动释放为止,不同函数之间均可自由访问

    • 栈的生存周期由函数调用开始到函数返回时结束,函数之间的局部变量不能互相访问

      #include<stdio.h>
      #include<stdlib.h>
      int *func(void)
      {
          int *ptr = NULL; 
          ptr = (int *)malloc(sizeof(int));
          //在堆上申请一个内存空间
          if (ptr == NULL)
          {
              exit(1);
          }
          *ptr = 520;
          return ptr; //返回ptr的地址
      }
      int main(void)
      {
          int *ptr = NULL;
          ptr = func(); 
          printf("%d\n",*ptr);
          //在main函数中访问func函数中申请的内存空间的值
          free(ptr);
      }
      

      注意:这段代码中,在func函数中申请内存空间,在main函数中访问并释放。但实战中不建议这样做,建议在哪个函数中申请了内存空间就在哪个函数中释放。

  • 发展方向

    • 堆和其它区段一样,都是从低地址向高地址发展

    • 栈则相反,是由高地址向低地址发展

      #include<stdio.h>
      #include<stdlib.h>
      int main(void)
      {
          int *ptr1 = NULL; //指针变量存放在栈中
          int *ptr2 = NULL;
          ptr1 = (int *)malloc(sizeof(int)); //内存空间存放在堆中
          ptr2 = (int *)malloc(sizeof(int));
          printf("stack: %p -> %p\n",&ptr1, &ptr2);
          //栈  ->  指针变量的地址
          printf("heap: %p -> %p\n",ptr1,ptr2);
          //堆  ->  指针变量指向的地址
          return 0;
      }
      

      运行结果

      stack: 00000060fadffb28 -> 00000060fadffb20  
      //栈   高    ->     低
      heap: 000001a4c5fe16a0 -> 000001a4c5fe16e0   
      //堆   低    ->     高
      
posted @ 2025-02-14 21:24  芝麻凛  阅读(33)  评论(0)    收藏  举报