zl程序教程

您现在的位置是:首页 >  后端

当前栏目

【维生素C语言】第十三章 - 动态内存管理

C语言 管理 动态内存 第十三章
2023-09-14 09:15:59 时间

分手后还打电话骚扰?ptr=NULL解决!动态内存管理【C语言】

前言:

本章将讲解C语言动态内存管理,由浅到深的讲解动态内存管理。学习完本章后可以做一下动态内存分配的练习加深巩固,降低踩动态内存分配坑的概率:

🚪 传送门:动态内存分配笔试题题目+答案+详解)


一、动态内存分配

0x00 引入

📚 目前我们已经掌握了以下两种开辟内存的方式:

// 在栈上开辟4个字节
int val = 20;

// 在栈空间上开辟10个字节的连续空间
char arr[10] = {0};

📚 上述开辟空间的方式有两个特点:

     ① 空间开辟的大小是固定的。

     ② 数组在声明时必须指定数组的长度,在编译时会开辟并分配其所需要的内存空间。

0x01 定义

🔍 [百度百科] 动态分配内存

所谓动态内存分配(Dynamic Memory Allocation) 就是指在程序执行的过程中动态地分配或者回收存储空间的分配内存的方法。动态内存分配不象数组等静态内存分配方法那样需要预先分配存储空间,而是由系统根据程序的需要即时分配,且分配的大小就是程序要求的大小。

0x02 存在的原因

❓ 为什么会存在动态内存开辟?

💡 有时我们需要的空间大小在程序运行的时候才能知道,这时在数组编译时开辟空间的方式就不能满足了,这时我们就需要动态内存开辟来解决问题。

二、动态内存函数介绍

0x00 malloc 函数

📜 头文件:stdlib.h

 📚 介绍:malloc 是C语言提供的一个动态内存开辟的函数,该函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。具体情况如下:

      ① 如果开辟成功,则返回一个指向开辟好空间的指针。

      ② 如果开辟失败,则返回一个 NULL 指针。

      ③ 返回值的类型为 void*malloc 函数并不知道开辟空间的类型,由使用者自己决定。

      ④ 如果 size 为 0(开辟0个字节),malloc 的行为是标准未定义的,结果将取决于编译器。

🔍 官方介绍:malloc - C++ Reference

0x01 free 函数

📜 头文件:stdlib.h

 📚 介绍:free 函数用来释放动态开辟的内存空间。具体情况如下:

      ① 如果参数 ptr 指向的空间不是动态开辟的,那么 free 函数的行为是未定义的。

      ② 如果参数 ptrNULL 指针,那么 free 将不会执行任何动作。

📌 注意事项:

      ① 使用完之后一定要记得使用 free 函数释放所开辟的内存空间。

      ② 使用指针指向动态开辟的内存,使用完并 free 之后一定要记得将其置为空指针。

🔍 官方介绍:http://www.cplusplus.com/reference/cstdlib/malloc/?kw=free

💬 代码演示:动态内存开辟10个整型空间(完整步骤)

#include <stdio.h>
#include <stdlib.h>

int main(void) 
{
    // 假设开辟10个整型空间
    int arr[10]; // 在栈区上开辟

    // 动态内存开辟
    int* p = (int*)malloc(10*sizeof(int)); // 开辟10个大小为int的空间

    // 使用这些空间的时候
    if (p == NULL) {
        perror("main"); // main: 错误信息
        return 0;
    }
    
    // 使用
    int i = 0;
    for (i = 0; i < 10; i++) {
        *(p + i) = i;
    }
    for (i = 0; i < 10; i++) {
        printf("%d ", p[i]);
    }

    // 回收空间
    free(p);
    p = NULL; // 需要手动置为空指针

    return 0;
}

🚩  0 1 2 3 4 5 6 7 8 9

 ❗  动态内存开辟失败的情况:(perror 函数)

#include <stdio.h>
#include <stdlib.h>

int main(void) 
{
    ...
    int* p = (int*)malloc(9999999999*sizeof(int)); // 狮子大开口。拿来吧你
    ...

}

🚩   main: Not enough space

❓ 为什么 free 之后,一定要把 p 置为空指针?

🔑 解析:因为 free 之后那块开辟的内存空间已经不在了,它的功能只是把开辟的空间回收掉,但是 p 仍然还指向那块内存空间的起始位置,这合理吗?这不合理。所以我们需要使用 p = NULL 把他置成空指针。为了加深印象,举一个形象的例子:

❓ 为什么 malloc 前面要进行强制类型转换呢?

int* p = (int*)malloc(10*sizeof(int));

🔑 解析:为了和 int* p 类型相呼应,所以要进行强制类型转换。你可以试着把强转删掉,其实也不会有什么问题。但是因为有些编译器要求强转,所以最好进行一下强转,避免不必要的麻烦。

 0x02 calloc 函数

📜 头文件:stdlib.h

📚 介绍:calloc 函数的功能实为 num 个大小为 size 的元素开辟一块空间,并把空间的每个字节初始化为 0 ,返回一个指向它的指针。

⭕ 对比:

      ① malloc 只有一个参数,而 calloc 有两个参数,分别为元素的个数和元素的大小。

      ② 与函数 malloc 的区别在于 calloc 会在返回地址前把申请的空间的每个字节初始化为 0 。

🔍 官方介绍:http://www.cplusplus.com/reference/cstdlib/malloc/?kw=calloc

💬 验证: calloc 会对内存进行初始化

#include <stdio.h>
#include <stdlib.h>

int main()
{
    // malloc
    int* p = (int*)malloc(40); // 开辟40个空间
    if (p == NULL)
        return 1;
    int i = 0;
    for (i = 0; i < 10; i++)
        printf("%d ", *(p + i));
    free(p);
    p = NULL;

    return 0;
}

 🚩  (运行结果是10个随机值)

#include <stdio.h>
#include <stdlib.h>

int main()
{
    // calloc
    int* p = (int*)calloc(10, sizeof(int)); // 开辟10个大小为int的空间,40
    if (p == NULL)
        return 1;
    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

🔺 总结:说明 calloc 会对内存进行初始化,把空间的每个字节初始化为 0 。如果我们对于申请的内存空间的内容,要求其初始化,我们就可以使用 calloc 函数来轻松实现。

0x03 realloc 函数

📜 头文件:stdlib.h

📚 介绍:realloc 函数,让动态内存管理更加灵活。用于重新调整之前调用 malloccalloc 所分配的 ptr 所指向的内存块的大小,可以对动态开辟的内存进行大小的调整。具体介绍如下:

      ① ptr 为指针要调整的内存地址。

      ② size 为调整之后的新大小。

      ③ 返回值为调整之后的内存起始位置,请求失败则返回空指针。

      ④ realloc 函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。

📌 realloc 函数在调整内存空间时存在的三种情况:

      情况1:原有空间之后有足够大的空间。

      情况2:原有空间之后没有足够大的空间。

      情况3realloc 有可能找不到合适的空间来调整大小。

情况1:当原有空间之后没有足够大的空间时,直接在原有内存之后直接追加空间,原来空间的数组不发生变化。

情况2:当原有空间之后没有足够大的空间时,会在堆空间上另找一个合适大小的连续的空间来使用。函数的返回值将是一个新的内存地址。

情况3:如果找不到合适的空间,就会返回一个空指针。

🔍 官方介绍:http://www.cplusplus.com/reference/cstdlib/malloc/?kw=realloc

💬 代码演示:realloc 调整内存大小

#include <stdio.h>
#include <stdlib.h>

int main()
{
    int* p = (int*)calloc(10, sizeof(int));
    if (p == NULL) {
        perror("main");
        return 1;
    }
    // 使用
    int i = 0;
    for (i = 0; i < 10; i++) {
        *(p + i)  = 5;
    }
    // 此时,这里需要p指向的空间更大,需要20个int的空间
    // realloc 调整空间
    p = (int*)realloc(p, 20*sizeof(int)); // 调整为20个int的大小的空间

    // 释放
    free(p);
    p = NULL;
}

 ❗  刚才提到的第三种情况,如果 realloc 找不到合适的空间,就会返回空指针。我们想让它增容,他却存在返回空指针的危险,这怎么行?

💡 解决方案:不要拿指针直接接收 realloc,可以使用临时指针判断一下。

#include <stdio.h>
#include <stdlib.h>

int main() 
{
    int* p = (int*)calloc(10, sizeof(int));
    if (p == NULL) {
        perror("main");
        return 1;
    }
    // 使用
    int i = 0;
    for (i = 0; i < 10; i++) {
        *(p + i)  = 5;
    }
    // 此时,这里需要 p 指向的空间更大,需要 20 个int的空间
    // realloc 调整空间
    int* ptmp = (int*)realloc(p, 20*sizeof(int));
    // 如果ptmp不等于空指针,再把p交付给它
    if (ptmp != NULL) {
        p = ptmp;
    }

    // 释放
    free(p);
    p = NULL;
}

 📚 有趣的是,其实你可以把 realloc 当 malloc 用:

// 在要调整的内存地址部分,传入NULL:
int* p = (int*)realloc(NULL, 40); // 这里功能类似于malloc,就是直接在堆区开辟40个字节

三、常见的动态内存错误

0x00 对空指针的解引用操作

❌ 代码演示:

#include <stdlib.h>
#include <stdio.h>

int main()
{
    int* p = (int*)malloc(9999999999);
    int i = 0;
    for (i = 0; i < 10; i++) {
        *(p + i) = i; // 对空指针进行解引用操作,非法访问内存
    }

    return 0;
}

💡 解决方案:对 malloc 函数的返回值做判空处理

#include <stdlib.h>
#include <stdio.h>

int main()
{
    int* p = (int*)malloc(9999999999);
    // 对malloc函数的返回值做判空处理
    if (p == NULL) {
        perror("main")
        return 1;
    }
    int i = 0;
    for (i = 0; i < 10; i++) {
        *(p + i) = i; // 对空指针进行解引用操作,非法访问内存
    }

    return 0;
}

0x01 对动态开辟空间的越界访问

❌ 代码演示:

#include <stdio.h>
#include <stdlib.h>

int main()
{
    int* p = (int*)malloc(10*sizeof(int)); // 申请10个整型的空间
    if (p == NULL) {
        perror("main");
        return 1;
    }
    int i = 0;
    // 越界访问 - 指针p只管理10个整型的空间,根本无法访问40个
    for (i = 0; i < 40; i++) {
        *(p + i) = i;
    }

    free(p);
    p = NULL;

    return 0;
}

💡 提醒:为了防止越界访问,使用空间时一定要注意开辟的空间大小。

0x02 对非动态开辟的内存使用free释放

❌ 代码演示:

#include <stdio.h>
#include <stdlib.h>

int main()
{
    int arr[10] = {0}; // 在栈区上开辟
    int* p = arr;
    // 使用  略

    free(p); // 使用free释放非动态开辟的空间
    p = NULL;

    return 0;   
}

 

 💡 提醒:不要对非动态开辟的内存使用 free,否则会出现难以意料的错误。

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

❌ 代码演示:

#include <stdio.h>
#include <stdlib.h>

int main()
{
    int* p = malloc(10*sizeof(int));
    if (p == NULL) {
        return 1;
    }
    int i = 0;
    for (i = 0; i < 5; i++) {
        *p++ = i; // p指向的空间被改变了
    }

    free(p);
    p = NULL;
  
    return 0;
}

📌 注意事项:这么写代码会导致 p 只释放了后面的空间。没人记得这块空间的起始位置,再也没有人找得到它了,这是很件很可怕的事情,会存在内存泄露的风险。

 💡 提醒:释放内存空间的时候一定要从头开始释放。

0x04 对同一块动态内存多次释放

❌ 代码演示:

#include <stdio.h>
#include <stdlib.h>

int main()
{
    int* p = malloc(10*sizeof(int));
    if (p == NULL) {
        return 1;
    }
    int i = 0;
    for (i = 0; i < 10; i++) {
        p[i] = i;
    }

    // 释放
    free(p);
    // 一时脑热,再一次释放
    free(p);
  
    return 0;
}

💡 解决方案:在第一次释放后紧接着将 p 置为空指针

// 释放
free(p);
p = NULL;

free(p); // 此时p为空,free什么也不做

0x05 动态开辟内存忘记释放导致内存泄漏

❌ 代码演示:

#include <stdio.h>
#include <stdlib.h>

void test()
{
    int* p = (int*)malloc(100);
    if (p == NULL) {
        return;
    }
    // 使用 略
    
    // 此时忘记释放了
}

int main()
{
    test();
    
    free(p); // 此时释放不了了,没人知道这块空间的起始位置在哪了
    p = NULL;
}

动态开辟的内存空间有两种回收方式:  1. 主动释放(free)      2. 程序结束

如果这块程序在服务器上 7x24 小时运行,如果你不主动释放或者你找不到这块空间了,最后就会导致内存泄漏问题。内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

💡 提醒:malloc 这一系列函数 和 free 一定要成对使用,记得及时释放。你自己申请的空间,用完之后不打算给别人用,就自己释放掉即可。如果你申请的空间,想传给别人使用,传给别人时一定要提醒别人用完之后记得释放。


参考资料:

Microsoft. MSDN(Microsoft Developer Network)[EB/OL]. []. .

比特科技. C语言进阶[EB/OL]. 2021[2021.8.31]. .

📌 本文作者: 王亦优

📃 更新记录: 2021.8.3

勘误记录:

💬 参考资料: 百度百科、比特科技、www.cplusplus.com、MSDN

📜 本文声明: 由于作者水平有限,本文有错误和不准确之处在所难免,本人也很想知道这些错误,恳望读者批评指正!

本章完。