【C语言】动态内存管理(malloc,free,calloc,realloc详解 )

🦄个人主页:小米里的大麦-CSDN博客

🎏所属专栏:https://blog.csdn.net/huangcancan666/category_12718530.html

🎁代码托管:C语言: C语言方向(基础知识和应用) (gitee.com)

⚙️操作环境:Visual Studio 2022

目录

一、引言

二、malloc

[1. 简介](#1. 简介)

[2. 语法](#2. 语法)

[3. 使用方法](#3. 使用方法)

[4. 关于内存的使用(重点)](#4. 关于内存的使用(重点))

工作原理:

[示例一: malloc(sizeof(int))](#示例一: malloc(sizeof(int)))

[示例二: malloc(20)](#示例二: malloc(20))

重要点:

[5. 结论](#5. 结论)

三、free

[1. 简介](#1. 简介)

[2. 语法](#2. 语法)

[3. 使用方法](#3. 使用方法)

[4. 注意事项](#4. 注意事项)

[1. 只释放一次](#1. 只释放一次)

[2. 检查指针是否为 NULL](#2. 检查指针是否为 NULL)

[3. 避免悬挂指针](#3. 避免悬挂指针)

[4. 动态数组的处理](#4. 动态数组的处理)

[5. 释放结构体中的指针](#5. 释放结构体中的指针)

四、calloc

[1. 简介](#1. 简介)

[2. 语法](#2. 语法)

[3. 使用方法](#3. 使用方法)

[4. 注意事项](#4. 注意事项)

五、realloc

[1. 简介](#1. 简介)

[2. 语法](#2. 语法)

[3. 使用方法](#3. 使用方法)

[4. 注意事项](#4. 注意事项)

[5. 缩容的语法和基本用法](#5. 缩容的语法和基本用法)

缩容注意事项

realloc缩容的工作原理

缩容的实际应用场景

总结

六、总结

共勉


一、引言

动态内存管理是C语言中一个重要的部分,它允许程序在运行时动态地分配、使用和释放内存资源。这篇博客将详细讲解C语言中的四个核心函数:malloc、free、calloc和realloc,并讨论它们的使用方法和注意事项。

二、malloc

1. 简介

malloc(memory allocation)用于动态分配指定大小的内存块。它从堆中分配内存,并返回指向该内存块的指针。分配的内存内容未初始化。(malloc - C++ Reference

2. 语法

cpp 复制代码
#include <stdlib.h>//头文件
void* malloc(size_t size);//函数原型
  • size_t 是一个无符号整数类型,用来表示对象的大小。
  • size 参数指定要分配的内存字节数。
  • 如果成功分配内存,则 malloc 返回指向这块内存的指针;如果失败(例如没有足够的可用内存),则返回 NULL

3. 使用方法

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>

int main() {
    int *p = (int*)malloc(sizeof(int) * 10); // 分配能存储10个int类型元素的内存

    if (p == NULL) {
        printf("内存分配失败\n");
        return 1;
    }

    // 使用分配的内存
    for (int i = 0; i < 10; i++) {
        p[i] = i;
        printf("%d ", p[i]);
    }

    // 释放内存
    free(p);

    return 0;
}
cpp 复制代码
分配内存:
#include <stdio.h>
#include <stdlib.h>

int main() {
    int *p;
    p = (int *)malloc(sizeof(int));  // 分配一个 int 类型大小的内存
    if (p == NULL) {
        printf("内存分配失败!\n");
        return 1;
    }
    *p = 10;  // 给分配的内存赋值
    printf("值: %d\n", *p);  // 输出值
    free(p);  // 释放内存
    return 0;
}
cpp 复制代码
分配多个元素:
#include <stdio.h>
#include <stdlib.h>

int main() {
    int *p;
    size_t num_elements = 5;
    p = (int *)malloc(num_elements * sizeof(int));
    if (p == NULL) {
        printf("内存分配失败!\n");
        return 1;
    }
    for (int i = 0; i < num_elements; i++) {
        p[i] = i + 1;
    }
    for (int i = 0; i < num_elements; i++) {
        printf("%d ", p[i]);
    }
    printf("\n");
    free(p);
    return 0;
}
cpp 复制代码
#include <stdio.h>
#include <stdlib.h>

int main() {
    int *p = (int*)malloc(sizeof(int)); // 分配4字节内存
    char *q = (char*)malloc(20);        // 分配20字节内存

    if (p == NULL || q == NULL) {
        printf("内存分配失败\n");
        return 1;
    }

    *p = 100; // 存储一个int类型的值
    for (int i = 0; i < 20; i++) {
        q[i] = 'A'; // 使用分配的20字节空间
    }

    // 当你尝试访问 q[20](第21字节)时,程序行为未定义,可能会崩溃
    // q[20] = 'B'; // **不要这样做**,因为它会导致未定义行为

    printf("p: %d\n", *p);
    printf("q: %.20s\n", q);

    free(p); // 释放分配的内存
    free(q);

    return 0;
}

4. 关于内存的使用(重点)

  • 内存对齐:

实际上,malloc 不仅分配请求的字节数,还会考虑内存对齐的要求。这意味着它可能分配比你请求的更多的内存,以便确保内存地址能够适当地对齐。例如,如果系统要求int对齐到4字节边界,那么malloc在分配内存时,会确保返回的地址是4的倍数。如果当前的内存块不满足对齐要求,malloc可能会跳过一些字节(称为填充字节),以找到一个符合对齐要求的位置。

  • 内存碎片:

内存管理库会试图高效地管理内存,避免碎片化。意味着它可能会保留一些额外的内存,以备将来分配使用,但这不是用户直接控制的。

工作原理:

示例一: malloc(sizeof(int))
  • sizeof(int)通常为4(这取决于具体的系统和编译器),意味着malloc(sizeof(int))分配了4字节的内存。
  • malloc(sizeof(int))的返回值是一个指向这4字节内存块的指针。程序可以通过这个指针来存储和访问一个int类型的变量。
  • 总结malloc(sizeof(int))一次性分配了4字节的内存供程序使用。
示例二: malloc(20)
  • 当你调用malloc(20)时,内存管理器(通常由操作系统或运行时库管理)会在堆中找到一块足够大的、至少有20字节的空闲内存块,一次性分配给程序使用(将其分配给你)
  • 分配的20字节内存块起始地址由malloc返回,返回的指针指向这段20字节的内存块,你可以在这块内存上存储数据,但只能使用这20字节的空间。
  • 一旦这块内存分配给你,你就完全拥有了这20字节的空间,程序可以自由地在这个范围内操作。
  • 内存管理 :如果你需要更多的空间,你需要手动调用realloc来调整这块内存的大小,或者分配一块新的、更大的内存块。

重要点

  • 一次性分配malloc(20)一次性 从堆中分配20字节的内存。这20字节的内存是连续的,并且可以立即使用。
  • 使用限制 :这20字节的内存并不会像"用一点给一点"那样逐渐分配,而是一次性分配。如果在使用过程中超过了这20字节的边界(例如试图访问第21字节),则可能会引发未定义行为,通常会导致程序崩溃或内存损坏。
  • 不会自动扩展malloc(20)分配的内存是固定的20字节。如果这20字节的空间用完了,程序必须手动使用realloc来扩展内存块,否则无法存储更多的数据。malloc本身不会自动再给20字节空间。

5. 结论

  • malloc是一次性分配 :不管你请求多少字节,malloc都会一次性分配指定数量的内存给你,供你自由使用。
  • 内存不会自动扩展 :一旦分配完内存,除非你使用realloc,否则内存块大小固定,不会自动增长。
  • 边界检查:在使用分配的内存时,必须确保不超出已分配的范围,否则会导致程序崩溃或数据损坏。
  • 内存对齐:malloc不仅分配你请求的内存量,还会考虑对齐要求,因此可能会分配比请求的更多的内存。其返回的内存地址会满足特定的对齐要求,确保高效且安全的内存访问。尽管你可能请求了例如malloc(20),实际分配的内存可能会大于20字节,以确保内存对齐,但对你而言,能安全使用的仍然是请求的20字节空间。

三、free

1. 简介

free用于释放由malloccallocrealloc分配的内存,使这部分内存重新可用。(手动释放内存)(free - C++ Reference

2. 语法

cpp 复制代码
#include <stdlib.h>//头文件
void free(void *ptr);//函数原型
ptr: 是一个指向要释放的内存块的指针。如果 ptr 是 NULL,free 不做任何操作。

3. 使用方法

free的使用非常简单,以下是一个例子:

cpp 复制代码
int *p = (int*)malloc(sizeof(int) * 10);
if (p != NULL) {
    // 使用内存
    free(p); // 释放内存
}

4. 注意事项

1. 只释放一次

  • 不要对同一块内存多次调用 free。这会导致未定义行为,比如程序崩溃。
  • 不要对未分配的内存调用 free 。确保你只释放通过 malloc, calloc, 或 realloc 分配的内存。

2. 检查指针是否为 NULL

cpp 复制代码
在调用 free 之前检查指针是否为 NULL:
if (p != NULL) {
    free(p);
    p = NULL; // 避免悬挂指针
}

3. 避免悬挂指针

cpp 复制代码
当释放内存后,通常将指针设置为 NULL 来避免悬挂指针问题。
这样可以防止意外地再次尝试释放相同的内存。
free(p);
p = NULL;

4. 动态数组的处理

cpp 复制代码
如果使用 malloc 或 calloc 分配了数组,释放内存时也需要正确处理:
int *arr = (int *)malloc(10 * sizeof(int));
...
free(arr);
arr = NULL;

5. 释放结构体中的指针

cpp 复制代码
如果结构体中包含指向动态分配内存的指针,在销毁结构体前需要释放这些内存:
struct Node {
    int data;
    struct Node *next;
};

void freeNode(struct Node *node) {
    if (node != NULL) {
        freeNode(node->next); // 递归释放链表中的每个节点
        free(node);
    }
}

正确使用 free 函数是良好编程实践的一部分,它可以避免内存泄漏,并确保程序的稳定性和性能。

四、calloc

1. 简介

calloc(contiguous allocation)用于分配内存并初始化所有位为零。与malloc不同,它接受两个参数:分配的元素个数每个元素的大小。(calloc - C++ Reference

2. 语法

cpp 复制代码
#include <stdlib.h>//头文件
void* calloc(size_t num, size_t size);//函数原型

参数说明

  • num:需要分配的元素数量。
  • size:每个元素的大小(以字节为单位)。

返回值

  • 如果内存分配成功,则返回指向新分配内存的指针。
  • 如果内存分配失败,则返回NULL

3. 使用方法

calloc()函数为所请求的元素数量分配一个连续的内存块,并且初始化所有元素为零值。意味着对于基本数据类型如int, float, 或者char等,所有元素都会被设置为0。

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>

int main() {
    int *p = (int*)calloc(10, sizeof(int)); // 分配能存储10个int类型元素的内存,并初始化为0

    if (p == NULL) {
        printf("内存分配失败\n");
        return 1;
    }

    // 使用分配的内存
    for (int i = 0; i < 10; i++) {
        printf("%d ", p[i]);
    }

    // 释放内存
    free(p);

    return 0;
}

4. 注意事项

  • malloc相似,calloc返回的指针也应检查是否为NULL
  • 使用完动态分配的内存后,必须调用free释放内存。

与 malloc 的区别

  • malloc()只分配内存但不初始化。
  • calloc()不仅分配内存,还会初始化所有元素为零值。
  • calloc()接受两个参数:元素个数和单个元素的大小;而malloc()仅接受一个参数:总大小。
cpp 复制代码
如果你需要分配一段内存并且希望该内存被自动初始化为零,那么calloc()是一个更好的选择。
如果你只需要分配内存而不关心初始化,或者需要更灵活地控制内存大小,那么malloc()可能更适合。

这里有一个简单的例子来展示calloc()和malloc()的区别:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {
    int *array_calloc, *array_malloc;//array:数组
    int n = 5;

    // 使用 calloc
    array_calloc = (int *) calloc(n, sizeof(int));
    if (array_calloc == NULL) {
        printf("calloc failed\n");
        return 1;
    }
    for (int i = 0; i < n; i++) {
        printf("calloc[%d]: %d\n", i, array_calloc[i]);
    }
    free(array_calloc);

    // 使用 malloc
    array_malloc = (int *) malloc(n * sizeof(int));
    if (array_malloc == NULL) {
        printf("malloc failed\n");
        return 1;
    }
    memset(array_malloc, 0, n * sizeof(int));  // 手动初始化为零
    for (int i = 0; i < n; i++) {
        printf("malloc[%d]: %d\n", i, array_malloc[i]);
    }
    free(array_malloc);

    return 0;
}

五、realloc

1. 简介

realloc(reallocation)在 C 语言中用于改变已分配内存块的大小。这个函数允许您动态地增加或减少内存空间的大小,这对于需要根据运行时条件调整数据结构大小的应用程序非常有用。它可以扩展或缩小内存块,如果新大小大于旧大小,未初始化的新内存内容是不确定的。(realloc - C++ Reference

2. 语法

cpp 复制代码
#include <stdlib.h>//头文件
void* realloc(void *ptr, size_t size);//函数原型
  • ptr: 指向要重新分配的内存块的指针**(先前通过malloccallocrealloc分配的内存块的指针)** 。如果 ptrNULL,那么 realloc() 将执行与 malloc(size) 相同的操作。
  • size: 新的内存大小**(以字节为单位)** 。如果 size0 并且 ptr 不是 NULL,那么 realloc() 将释放 ptr 指向的内存块,并返回一个 NULL 值。
  • 返回值: realloc() 返回一个指向新内存块的指针。如果内存重新分配成功,则返回的新指针可能与原来的指针不同。如果失败,它将返回 NULL。如果size为0,则相当于调用free(ptr),并返回NULL

3. 使用方法

以下是一个简单的例子:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>

int main() {
    int *p = (int*)malloc(sizeof(int) * 5); // 初始分配5个int类型元素的内存

    if (p == NULL) {
        printf("内存分配失败\n");
        return 1;
    }

    // 扩展内存到10个int类型元素
    p = (int*)realloc(p, sizeof(int) * 10);

    if (p == NULL) {
        printf("内存重新分配失败\n");
        return 1;
    }

    // 使用扩展的内存
    for (int i = 0; i < 10; i++) {
        p[i] = i;
        printf("%d ", p[i]);
    }

    // 释放内存
    free(p);

    return 0;
}

4. 注意事项

  • realloc的返回值应检查是否为NULL,因为重新分配可能失败。
  • 如果realloc失败,原来的内存块仍然有效,应该避免内存泄漏
  • 如果新大小为0,realloc等同于调用free

5. 缩容的语法和基本用法

语法不变,同 realloc 的函数原型。

缩容注意事项

1. 数据保留:

  • 如果新的大小大于或等于原始大小,那么原始数据会被保留。
  • 如果新的大小小于原始大小,那么原始数据中超出新大小范围的部分将被丢弃。
  • 因此,在缩小内存之前,最好先备份重要数据,以防丢失。

2. 检查返回值:

  • 总是要检查 realloc() 的返回值是否为 NULL,这表示内存分配失败。
  • 如果返回值非 NULL,则需要将指针更新为新的地址。

3. 类型转换:

  • 类型转换通常需要应用到 realloc() 的返回值上,以保持类型安全。

4. 性能考虑:

  • 频繁缩容可能导致内存碎片化和性能降低。
  • 如果知道内存大小变化频繁,考虑使用其他数据结构或者技术,如自定义内存池。
cpp 复制代码
以下是一个示例代码,展示了如何使用 realloc() 缩小内存大小:
#include <stdio.h>
#include <stdlib.h>

int main() {
    int *array;
    int n = 10;
    int new_n;

    // 分配初始内存
    array = (int *) malloc(n * sizeof(int));

    if (array == NULL) {
        printf("Memory allocation failed!\n");
        return 1;
    }

    // 初始化数组
    for (int i = 0; i < n; i++) {
        array[i] = i + 1;
    }

    // 打印初始数组
    printf("Initial array:\n");
    for (int i = 0; i < n; i++) {
        printf("%d ", array[i]);
    }
    printf("\n");

    // 缩小内存
    new_n = 5;
    array = (int *) realloc(array, new_n * sizeof(int));
    
    if (array == NULL) {
        printf("Memory reallocation failed!\n");
        free(array);
        return 1;
    }

    // 打印缩容后的数组
    printf("Reduced array:\n");
    for (int i = 0; i < new_n; i++) {
        printf("%d ", array[i]);
    }
    printf("\n");

    // 释放内存
    free(array);

    return 0;
}
在这个例子中,原始数组大小为 10,之后通过 realloc() 将其大小减小到了 5。
注意,缩容后只打印了前五个元素,这是因为原始数据中的后五个元素已经不再存在于新的内存块中。
cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {
    char *p = (char*)malloc(20 * sizeof(char)); // 分配20字节内存
    if (p == NULL) {
        printf("内存分配失败\n");
        return 1;
    }

    strcpy(p, "Hello, World!"); // 向内存块中写入字符串

    // 缩小内存块到10字节
    p = (char*)realloc(p, 10 * sizeof(char));

    if (p == NULL) {
        printf("内存重新分配失败\n");
        return 1;
    }

    printf("缩容后的内容: %s\n", p); // 输出可能出现意外的内容

    free(p); // 释放内存

    return 0;
}
代码解析:

初始时,我们分配了20字节的内存并存储了字符串"Hello, World!"。
然后,我们使用realloc将内存块缩小到10字节。此时,只能保证前10字节的数据被保留。
由于字符串"Hello, World!"超过了10字节,后面的部分可能被裁剪或者造成字符串结尾缺失。

打印时,由于没有足够的空间保存完整的字符串以及字符串的终止符\0,因此可能会出现意外的输出(未定义行为)。

realloc缩容的工作原理

当你使用realloc缩减内存块的大小时(即size比原来分配的大小小),其行为如下:

  1. 数据保留realloc会保留内存块起始位置的一部分内容,保留的部分大小为size(new_size)。换句话说,缩容后的内存块中,前size字节的数据会保留并保持不变。

  2. 释放多余内存 :对于超出size范围的多余内存,realloc会将其释放。具体的释放方式取决于内存管理器的实现,但对程序员来说,这部分内存块将不再可用,不能访问和使用。

  3. 指针返回 :如果缩容后的内存块可以在原始地址上处理,那么realloc会直接返回原指针。如果内存管理器为了效率或其他原因在缩容过程中移动了内存块,realloc将返回一个新的内存地址,并且会将原来ptr指向的内容复制到新地址。

缩容的实际应用场景

缩容操作常用于以下场景:

  • 内存优化 :在动态数据结构(如可变大小数组、缓冲区)中,随着数据量减少,使用realloc来释放多余的内存空间。
  • 节省资源:在程序运行中,及时缩减不再需要的内存块大小,可以节省系统资源,特别是在嵌入式系统或内存受限的环境中。

总结

通过合理使用realloc缩容,可以有效管理程序的内存资源,确保程序在执行过程中占用最小的内存空间,同时保持灵活性和高效性。

  • 在缩小内存时,一定要确保新大小能够容纳必要的数据。
  • 使用 realloc() 缩容时,必须检查返回值是否为 NULL
  • 对于经常需要改变大小的情况,考虑使用更高效的数据结构或内存管理策略。

六、总结

动态内存管理是C语言编程中不可或缺的一部分,正确使用mallocfreecallocrealloc可以提高程序的灵活性和效率。以下是一些关键点:

  • 始终检查内存分配函数的返回值是否为NULL
  • 使用完动态内存后,一定要释放内存,防止内存泄漏。
  • 对于复杂的数据结构,确保正确释放所有分配的内存,以避免内存泄漏和悬空指针。

掌握这些基本函数和相关注意事项,可以帮助你编写出更健壮和高效的C程序。希望这篇博客能帮助你更好地理解和使用C语言中的动态内存管理!

共勉

相关推荐
RS&5 分钟前
python学习笔记
笔记·python·学习
hong1616881 小时前
VSCode中配置C/C++环境
c语言·c++·vscode
liuwill2 小时前
从技术打磨到产品验证:读《程序员修炼之道》的务实之道
笔记·程序人生
Crossoads2 小时前
【数据结构】排序算法---快速排序
c语言·开发语言·数据结构·算法·排序算法
6230_2 小时前
git使用“保姆级”教程2——初始化及工作机制解释
开发语言·前端·笔记·git·html·学习方法·改行学it
Pandaconda2 小时前
【计算机网络 - 基础问题】每日 3 题(十)
开发语言·经验分享·笔记·后端·计算机网络·面试·职场和发展
Mercury Random3 小时前
Qwen 个人笔记
android·笔记
小立爱学习3 小时前
Linux 给 vmlinux 添加符号
linux·c语言
Crossoads3 小时前
【数据结构】排序算法---基数排序
c语言·开发语言·数据结构·算法·排序算法
老薛爱吃大西瓜3 小时前
DAY15:链表实现学生信息管理系统
c语言·数据结构·学习·链表