内存之谜:C语言动态内存管理

简介:

为什么要进行动态内存分配

动态内存分配允许程序根据实际需要来分配内存。这意味着程序可以根据不同的输入和条件来处理不同大小的数据结构,如数组.

下面列举一般的开辟空间的方式:

int a=10;
int arr[10]={0};

int a =10;在栈空间连续开辟了四个字节大小的空间;

int arr[10]={0};在栈空间连续开辟了四十个字节大小的空间;

上述开辟空间的特点:


  • 开辟的大小是固定的
  • 数组一旦确定了大小则无法进行改变
  • 如果尝试分配过多的内存,可能会造成栈溢出。
  • 接下来我们则引入对c语言中动态内存分配的讲解

  • 动态内存分配函数

    malloc函数

    malloc 是在 C 语言中用于动态内存分配的函数。它的作用是在堆上分配指定字节数的未初始化内存,并返回指向这块内存的指针。如果分配成功,将返回一个指针,该指针可以被转换为适当类型的指针以访问该内存区域。如果分配失败,将返回一个 NULL 指针。

    malloc 函数的原型在 stdlib.h 头文件中定义,其函数原型如下:


    void* malloc(size_t size);


    size 参数是你想要分配的字节数;

    返回指的类型是void*,所以malloc函数并不知道开辟空间的类型,具体使用的时候再进行决定;


    例如,如果建立一个有十个整形元素的数组,可以这样定义:


    int *p = NULL;
        int n = 10; // 假设我们要创建大小为10的整型数组
        // 动态分配内存
        p = (int*)malloc(n * sizeof(int));


    由于malloc函数开辟可能会失败,因此malloc的返回值需要做检查


    if(p=NULL)
    {
    perror("malloc");
    return 1; // 分配失败,结束程序
    }


    完整代码:


    int main()
    {   int *p = NULL;
        int n = 10; 
        p = (int*)malloc(n * sizeof(int));
        if(p=NULL)
        {
        perror("malloc");
        return 1; 
        }
        // 使用分配的内存
        for (int i = 0; i < n; i++) {
            *(p+i) = i; // 初始化内存
        }
        p仍指向起始地址
        // 打印分配的内存
        for (int i = 0; i < n; i++) {
            printf("%d ", p[i]);
        }
    }


    这里的代码并没有完善,接下来我们再介绍一个必不可少的函数


    free函数

    free 是 C 语言中的一个标准库函数,用于 释放 之前通过 malloc、calloc 或 realloc 等函数动态分配的内存。一旦使用 free 释放了内存,该内存区域就不再属于你的程序,你的程序应该停止访问它。如果尝试访问已释放的内存,会导致未定义的行为,通常称为悬挂指针。


    void free(void* ptr);


    ptr 参数是一个指向之前通过 malloc、calloc 或 realloc 分配的内存的指针。

    如果 ptr 是一个空指针(即 NULL),free 函数不会执行任何操作。


    下面是 free 函数的一个使用示例,结合之前的 malloc 例子:


    #include 
    #include 
    int main()
    {   int *p = NULL;
        int n = 10; 
        p = (int*)malloc(n * sizeof(int));
        if(p=NULL)
        {
        perror("malloc");
        return 1; 
        }
        // 使用分配的内存
        for (int i = 0; i < n; i++) {
            *(p+i) = i; // 初始化内存
        }
        p仍指向起始地址
        // 打印分配的内存
        for (int i = 0; i < n; i++) {
            printf("%d ", p[i]);
        }
        free(p);
        // 为避免悬挂指针,将 p 设置为 NULL
        p = NULL;
    }


    在上面代码中,p指针所指向的内存地址已经“悬空”,也就是说指针并没有被清除或者重置,但它指向的内存已经不再属于你的程序,因此如果你尝试通过悬挂指针访问或者修改数据,会导致未定义行为,如程序崩溃、数据损坏或安全漏洞。

    在释放指针指向的内存后立即将指针置为 NULL;


    calloc函数

    calloc函数用来动态地分配内存,并初始化所有字节为零。这与 malloc 函数不同,malloc分配的内存含有未定义的值。calloc 函数特别适用于分配固定数量和类型的对象时,因为它将所有位初始化为零,这通常代表了数字 0 和空指针等类型的空值。


    void* calloc(size_t num, size_t size);


    num 参数是要分配的元素数量。

    size 参数是每个元素的大小(以字节为单位)

    calloc 函数返回一个指向新分配的内存的指针,该内存的大小为 num * size。如果分配成功,返回的内存块中的所有位都被初始化为零。如果分配失败,则返回 NULL 指针

    举例如下:


    #include 
    #include 
    int main()
    {
     int *p = (int*)calloc(10, sizeof(int));
     if(NULL != p)
     {
     int i = 0;
     for(i=0; i<10; i++)
     {
     printf("%d ", *(p+i));
     }
     }
     free(p);
     p = NULL;
     return 0;
    }



    输出结果如下:

    0 0 0 0 0 0 0 0 0 0


    realloc函数

    realloc函数用于调整之前分配的内存块的大小。这个函数特别有用,当你不确定最初需要多少内存或者后来发现需要更多(或更少)内存时,realloc 可以帮助你增加或减少已分配内存的大小,而不需要你手动分配一个新的内存块和复制数据。


    void* realloc(void* ptr, size_t size);


    ptr 参数是指向之前已分配的内存块的指针(通常是通过 malloc 或 calloc 分配的)。如果 ptr 是 NULL,则 realloc 的行为就像 malloc,分配一个全新的内存块。返回值为调整之后的内存起始位置

    size 参数是新的内存块的大小

    继续用之前的例子:在原来基础上扩大二十个整形


    #include 
    #include 
    int main()
    {   int *p = NULL;
        int n = 10; 
        //p = (int*)malloc(n * sizeof(int));
        p = (int*)calloc(n ,sizeof(int));
        if(p=NULL)
        {
        perror("malloc");
        return 1; 
        }
        // 使用分配的内存
        for (int i = 0; i < n; i++) {
            *(p+i) = i; // 初始化内存
        }
        realloc(p,20*sizeof(int));
        for (int i = 0; i < n; i++) {
            printf("%d ", p[i]);
        }
        free(p);
        p = NULL;
    }


    这串代码并不完整,我们没有接收realloc的返回值,接下来我们讨论realloc调用时会产生的结果

    调用 realloc 时,会发生以下几种情况:

    原有空间之后没有足够大的空间

    原有空间之后有足够大的空间

    调整空间失败,返回NULL

    情况1

    我们想要在已经开辟好的40个空间后面扩展40个空间,发现后面没有足够的空间

    在这种情况下,realloc函数会在内存的堆区重新找一个空间(满足新的空间的大小需求的),同时会把旧的数据拷贝到新的新空间,然后释放旧的空间,同时返回新的空间的起始地址


    情况2

    在已经开辟好的空间后边,有足够的空间,直接进行扩大,扩大后,返回旧的空间的起始地址;


    所以,对于刚刚的代码


    int*ptr=(int *) realloc(p,20*sizeof(int));
    if(ptr!=NULL)
    {
    p=ptr;
    }


    realloc函数返回值放在一个临时指针ptr中,判断其不为空指针再交给p;


    动态内存的常见错误

    例题

    1:对NULL指针的解引用操作


    void test()
     {
     int *p = (int *)malloc(INT_MAX/4);
     *p = 20;
     free(p);
     }


    2.对动态开辟空间的越界访问

     

        int n = 5; // 分配内存用于存储5个整数
        int *arr = (int*)malloc(n * sizeof(int));
    
        if (arr == NULL) {
            return 1;
        }
    
        // 正常使用内存
        for (int i = 0; i < n; i++) {
            arr[i] = i; // 初始化数组
        }
    
        // 越界访问
        arr[n] = 10; // 这里访问的是数组的第6个元素,越过了边界
        free(arr);
    
    

    这里越界会导致未定义行为

    3.对非动态开辟内存使用free释放


    void test()
    {
     int a = 10;
     int *p = &a;
     free(p);
    }


    free 函数只能用于释放动态分配的内存(即通过 malloc、calloc 或 realloc 分配的内存)。尝试释放栈上的内存或者全局/静态变量的内存会导致未定义的行为,通常会导致程序崩溃或其他严重错误。

    a 是一个局部变量,它存储在栈上,而不是在堆上,我们在这里补充一个知识:


    栈区堆区静态区存储的数据类型

    通过 malloc、calloc、realloc 和 free 等函数手动管理的内存分配在堆区。这部分内存的生命周期由程序员控制,它不会自动被回收。


    4.使用free释放一块动态开辟内存的一部分


    void test()
     {
     int *p = (int *)malloc(100);
     p++;
     free(p);//p不再指向动态内存的起始位置
     }


    当 p 被增加之后,它不再指向原先由 malloc 分配的内存的起始地址,而是指向该块内存中的下一个 int 位置。当你尝试用 free§ 释放这个不正确的地址时,你将会传递一个非法指针给 free 函数,因为 free 只能接受之前由 malloc(及其他分配函数如 calloc 或 realloc)返回的指针。

    5. 对同⼀块动态内存多次释放


    1. void test()
    2.  {
    3.  int *p = (int *)malloc(100);
    4.  free(p);
    5.  free(p);//重复释放
    6.  }

    在第一次调用 free 后,p 指向的内存已经被释放,操作系统可能已经将其重新分配给其他用途。第二次调用 free 将试图操作一个不再有效的内存地址。


    为了避免此类错误,通常的做法是在释放内存后将指针设为 NULL,这样就能防止后续对同一个已释放内存的误用:


    void test() {
        int *p = (int *)malloc(100); // 分配 100 字节的内存
        if (p == NULL) {
            // 分配内存失败,应处理错误
            return;
        }
        free(p); // 正确地释放内存
        p = NULL; // 将 p 设为 NULL,避免悬垂指针
        // ... 其他代码 ...
        // 再次检查 p 是否为空,可以避免重复释放
        if (p != NULL) {
            free(p);
        }
    }

    动态开辟内存忘记释放(内存泄漏)

    void test()
    {
      int* p = (int*)malloc(100);
      if (NULL != p)
      {
      *p = 20;
      }
    }

    切记:动态开辟的空间⼀定要释放,并且正确释放!


    经典例题分析

    (一)


    1. void GetMemory(char *p)
    2.  {
    3.  p = (char *)malloc(100);
    4.  }
    5. void Test(void)
    6.  {
    7.  char *str = NULL;
    8.  GetMemory(str);
    9.  strcpy(str, "hello world");
    10.  printf(str);
    11.  }

    p = (char *)malloc(100); 试图分配 100 个字节的内存给指针 p。

    但这里的 p 是一个局部变量,它是主调函数 Test 中 str 的一个拷贝。因此,当 GetMemory 返回时,分配的内存地址并没有传递回 str。str 仍然是 NULL。

    对空指针再进行解引用,则程序崩溃。


    我们可以进行一个类比:


    void zhen(int x)
    {
       x=10;
    }
    void test(void)
    {
    int n=0;
    zhen(n);
    }


    进行这个类比就好理解多了,这里x与p都是临时拷贝,并没有对实参进行任何改变,在之前的学习中,我们知道,想要对实参进行改变,可以使用指针,而对于指针p,则需要使用二级指针:


    void GetMemory(char **p) {
        *p = (char *)malloc(100);
    }
    void Test(void) {
        char *str = NULL;
        GetMemory(&str);  // 注意,这里传递的是 str 的地址
        if(str != NULL) {
            strcpy(str, "hello world");
            printf("%s\n", str);
            free(str);  // 记得释放内存
        }
    }


    (二)


    1. char *GetMemory(void)
    2.  {
    3.  char p[] = "hello world";
    4.  return p;
    5.  }
    6. void Test(void)
    7.  {
    8.  char *str = NULL;
    9.  str = GetMemory();
    10.  printf(str);
    11.  }

    在 GetMemory 函数中,p 是一个局部字符数组,它在函数的栈帧上分配。

    当 GetMemory 函数返回时,它返回的是数组 p 的地址。

    但是,一旦 GetMemory 返回,其栈帧(包括 p)将被销毁。因此,返回的地址指向一个已经不再有效的内存区域。

    str = GetMemory(); 这一行导致 str 指向一个不再有效的内存区域。

    使用 printf(str); 试图访问这个内存区域将导致未定义行为,通常是程序崩溃

    这里有两种解决办法:

    1.动态分配内存:在堆上分配内存并返回指针


    1. char *GetMemory(void) {
    2.     char *p = malloc(12); // "hello world" 加上 null 终止符
    3.     if (p != NULL) {
    4.         strcpy(p, "hello world");
    5.     }
    6.     return p;
    7. }
    8. 
    9. void Test(void) {
    10.     char *str = GetMemory();
    11.     if (str != NULL) {
    12.         printf("%s\n", str);
    13.         free(str); // 释放内存
    14.     }
    15. }

    2.使用静态分配:将局部数组改为静态数组


    1. char *GetMemory(void) {
    2.    static char p[] = "hello world";
    3.    return p;
    4. }


    3.传递缓冲区:让调用者提供内存。


    1. void GetMemory(char *p) {
    2.    strcpy(p, "hello world");
    3. }

    4. void Test(void) {
    5.    char str[12];
    6.    GetMemory(str);
    7.    printf("%s\n", str);
    8. }


    所以,在处理指针和内存分配时,特别注意内存的有效性和生命周期是非常重要的!


    1. void Test(void)
    2. {
    3. char *str = (char *) malloc(100);
    4. strcpy(str, "hello");
    5. free(str);
    6. if(str != NULL)
    7. {
    8. strcpy(str, "world");
    9. printf(str);
    10. }
    11. }


    这里的问题是,free(str) 调用后,str 依然保持着之前分配的内存的地址,但是这块内存已经被释放,不再属于程序。因此,它现在是一个悬挂指针。接着,代码检查 str != NULL,但这个检查没有任何实际意义,因为 free 函数并不会设置指针为 NULL,它只是释放指针指向的内存。此时,str 仍然是一个非 NULL 的悬垂指针。


    尝试访问或操作悬垂指针指向的内存将导致未定义行为,这可能包括数据损坏、程序崩溃、或者安全漏洞。(非法访问)

    进行如下修改:


    1. void Test(void) {
    2.    // 为 str 分配 100 个字节的内存
    3.    char *str = (char *) malloc(100);
    4.    if (str != NULL) {
    5.        // 将字符串 "hello" 复制到 str 指向的内存
    6.        strcpy(str, "hello");

    7.        // 释放 str 指向的内存
    8.        free(str);
    9.        // 将 str 设置为 NULL,防止悬垂指针
    10.        str = NULL;
    11.    }

    12.    // 由于我们将 str 设置为 NULL,这个条件永远不会为真,
    13.    // 因此下面的代码不会被执行,从而避免了未定义行为
    14.    if (str != NULL) {
    15.        strcpy(str, "world");
    16.        printf(str);
    17.    }
    18. }


    柔性数组

    柔性数组是 C 语言中的一个特性,允许在结构体的最后声明一个没有指定大小的数组。用于创建含有可变大小数组的结构体。柔性数组通常用于处理动态大小的数据。

    声明方式:在结构体中,柔性数组是通过在最后一个成员声明一个数组而不指定其大小来定义的。

    int size = 10; // 假设我们需要10个字符的空间
    struct my_struct *p = malloc(sizeof(struct my_struct) + size * sizeof(char));
    p->length = size;

    内存分配:为使用柔性数组的结构体分配内存时,需要根据实际需要的数组大小动态计算所需内存


    int size = 10; // 假设我们需要10个字符的空间
    struct my_struct *p = malloc(sizeof(struct my_struct) + size * sizeof(char));
    p->length = size;
    


    使用:柔性数组成员像普通数组一样使用,但是你需要确保不要越界访问

    strcpy(p->data, "Hello");

    柔性数组的特点:

    结构中的柔性数组成员前面必须至少有一个其他成员,且前面的成员遵循对齐原则(前面结构体文章中有讲解)。

    sizeof返回的结构大小不包括柔性数组的内存

    包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    struct my_struct {
        int length;
        char data[];  // 柔性数组
    };
    
    int main() {
        int size = 20;
        struct my_struct *p = malloc(sizeof(struct my_struct) + size * sizeof(char));
        if(p=NULL)
        {
        return 1;
        }
        p->length = size;
        strcpy(p->data, "Hello, World!");
     
        printf("%s\n", p->data);
    
        free(p);
        return 0;
    }
    

    后面的size*sizeof(char)的大小是给数组的.


    如果不使用柔性数组而是使用指向可变数据的指针,需要分别为结构体和数据动态分配内存。下面是不使用柔性数组,而改用指针实现可变大小数组的方法:


    定义结构体

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    struct my_struct {
        int length;
        char *data;  // 指针指向动态分配的数据
    };
    

    分配和初始化结构体

    int main() {
        int size = 20;
        // 首先为结构体本身分配内存
        struct my_struct * p= malloc(sizeof(struct my_struct));
       
        // 检查分配是否成功
        if (p == NULL) {
            // 处理内存分配失败的情况
            exit(EXIT_FAILURE);
        }
        // 然后分配结构体中指针所指向的数据空间
        p->data = malloc(size * sizeof(char));
       
        // 检查分配是否成功
        if (p->data == NULL) {
            // 如果数据分配失败,清理已分配的结构体内存
            free(p);
            // 处理内存分配失败情况
            exit(EXIT_FAILURE);
        }
        p->length = size;
        // 使用分配的内存
        strcpy(p->data, "Hello, World!");
        printf("%s\n", p->data);
        // 释放分配的内存
        free(p->data);  // 首先释放数据指针
        free(p);        // 然后释放结构体本身
        return 0;
    }


    在这个例子中,我们通过 data 指针来间接引用一块动态分配的内存,用于存储字符串。与柔性数组相比,这种方法需要额外的一个 malloc 调用来分配指向的数据,并且在释放时,需要分别释放数据和结构体本身。


    需要注意的是,使用指针时,可以再次为 p->data 分配不同大小的内存或者使用 realloc 来更改内存大小。这提供了更大的灵活性,但同时也需要更多的内存管理工作。


    柔性数组相比于这种方法有一些好处:


    内存分配的连续性:使用柔性数组时,结构体和数组数据是在一个连续的内存块中分配的。提高缓存的效率,因为数据更有可能位于相邻的内存位置。

    内存分配的简化:当使用柔性数组时,只需要进行一次内存分配(malloc)和一次内存释放(free)。相比之下,使用指针访问动态分配的数组通常需要为结构体和数据分别进行内存分配和释放,这增加了编程的复杂性和出错的可能性。

    代码简洁性:柔性数组提供了一种更简洁的方式来表示具有动态大小数组的结构体。这使得代码更易于理解和维护

    关于c语言动态内存管理内容就到此结束,希望对大家有收获!感谢观看!


    相关文章
    |
    1月前
    |
    缓存 算法 Java
    Java面试题:深入探究Java内存模型与垃圾回收机制,Java中的引用类型在内存管理和垃圾回收中的作用,Java中的finalize方法及其在垃圾回收中的作用,哪种策略能够提高垃圾回收的效率
    Java面试题:深入探究Java内存模型与垃圾回收机制,Java中的引用类型在内存管理和垃圾回收中的作用,Java中的finalize方法及其在垃圾回收中的作用,哪种策略能够提高垃圾回收的效率
    25 1
    |
    1月前
    |
    存储 设计模式 监控
    运用Unity Profiler定位内存泄漏并实施对象池管理优化内存使用
    【7月更文第10天】在Unity游戏开发中,内存管理是至关重要的一个环节。内存泄漏不仅会导致游戏运行缓慢、卡顿,严重时甚至会引发崩溃。Unity Profiler作为一个强大的性能分析工具,能够帮助开发者深入理解应用程序的内存使用情况,从而定位并解决内存泄漏问题。同时,通过实施对象池管理策略,可以显著优化内存使用,提高游戏性能。本文将结合代码示例,详细介绍如何利用Unity Profiler定位内存泄漏,并实施对象池来优化内存使用。
    56 0
    |
    7天前
    |
    存储 Java 程序员
    JVM自动内存管理之运行时内存区
    这篇文章详细解释了JVM运行时数据区的各个组成部分及其作用,有助于理解Java程序运行时的内存布局和管理机制。
    JVM自动内存管理之运行时内存区
    |
    6天前
    |
    存储 程序员 C语言
    【C语言】动态内存管理
    【C语言】动态内存管理
    |
    11天前
    |
    存储 编译器 C语言
    C++内存管理(区别C语言)深度对比
    C++内存管理(区别C语言)深度对比
    39 5
    |
    11天前
    |
    C语言
    C语言动态内存管理
    C语言动态内存管理
    20 4
    |
    1月前
    |
    存储 缓存 C语言
    【C语言】字符函数,字符串函数,内存函数
    C语言中的字符串函数和内存函数
    24 0
    【C语言】字符函数,字符串函数,内存函数
    |
    1月前
    |
    存储 监控 算法
    Java中如何管理内存?
    【7月更文挑战第10天】Java中如何管理内存?
    30 2
    |
    1月前
    |
    设计模式 SQL 安全
    Java面试题:设计一个线程安全的内存管理器,使用观察者模式来通知所有线程内存使用情况的变化。如何确保在添加和移除内存块时的线程安全?如何确保任务的顺序执行和调度器的线程安全?
    Java面试题:设计一个线程安全的内存管理器,使用观察者模式来通知所有线程内存使用情况的变化。如何确保在添加和移除内存块时的线程安全?如何确保任务的顺序执行和调度器的线程安全?
    20 0
    |
    1月前
    |
    并行计算 安全 算法
    Java面试题:Java内存管理与多线程并发处理,设计一个Java应用,该应用需要处理大量并发用户请求,同时要求对内存使用进行优化,如何通过垃圾回收机制优化内存使用?
    Java面试题:Java内存管理与多线程并发处理,设计一个Java应用,该应用需要处理大量并发用户请求,同时要求对内存使用进行优化,如何通过垃圾回收机制优化内存使用?
    25 0