
前言C 语言没有new、没有容器、没有析构函数动态内存全靠四个函数malloc、calloc、realloc、free。它们只负责分配和释放原始字节不调用构造也不记录长度。因此「分配了多少、谁负责释放、释放后还有没有指针指向它」这三个问题必须由程序员自己保证。最常见的错误是释放后继续使用use-after-free和重复释放double free。这两种错误在很多情况下不会立刻崩溃而是悄悄破坏分配器的内部结构等到程序运行到别处才出问题。另一个高频错误是realloc的返回值直接赋回原指针扩容失败时返回NULL原内存仍然有效但指针已经丢失造成泄漏。本文按「分配—使用—扩容—释放」的顺序讲清四个函数的语义给出一个完整的动态数组示例并列出空指针、长度计算、所有权这几个容易忽略的细节。示例以 C11 为标准。一、四个函数的语义函数作用初始化失败返回malloc(size)分配size字节不初始化NULLcalloc(count, size)分配count * size字节全部置 0NULLrealloc(ptr, size)调整已分配块的大小新增部分不初始化NULL原块不变free(ptr)释放malloc/calloc/realloc返回的块不适用无返回值malloc返回的指针对齐到适合任何对象类型的边界因此可以安全地转换成int*、double*或结构体指针。calloc会把所有字节置零对整数这是「值为 0」对指针这是「空指针」但对浮点数是否等同于0.0、对某些平台是否等同于空指针标准只保证「所有位为零」不要过度依赖。realloc的行为要分三种情况成功且能原地扩展返回原指针内容不变。成功但需要搬家分配新块、复制旧内容、释放旧块返回新指针。失败返回NULL原块保持不变仍然需要由调用方释放。因此不能写p realloc(p, n)否则失败时p被覆盖成NULL旧块泄漏。free(NULL)是合法的什么也不做但free一个不是由上述函数返回的指针例如栈数组地址、已经释放过的指针、malloc返回指针加偏移后的地址是未定义行为undefined behavior, UB。二、完整的动态数组示例下面的程序分配一个int数组填值后扩容再释放。每一步都检查了失败路径。/* 适用于 C11 */#include stdio.h#include stdlib.hint main(void) {size_t n 10;int* values malloc(n * sizeof *values);if (values NULL) {return 1;}for (size_t i 0; i n; i) {values[i] (int)(i * i);}size_t newSize n * 2;int* grown realloc(values, newSize * sizeof *values);if (grown NULL) {free(values); /* 扩容失败原数组仍然有效必须释放 */return 1;}values grown;for (size_t i n; i newSize; i) {values[i] 0; /* realloc 新增的部分不保证初始化 */}for (size_t i 0; i newSize; i) {printf(%d , values[i]);}printf(\n);free(values);values NULL; /* 降低悬垂指针被误用的概率 */return 0;}sizeof *values是一个值得坚持的写法它不依赖元素类型名即使以后把int改成long long分配大小也会自动跟着变。写成sizeof(int)在重构时容易漏改。指针置NULL只能降低误用概率不是万能药。如果还有其他指针指向同一块内存置空一个并不能让它们失效根本办法是明确所有权让每块内存在任何时刻只有一个「负责人」。三、长度计算与溢出malloc(n * sizeof *p)中的乘法可能溢出。若n是外部输入攻击者可以传入一个让n * size回绕成小值的数分配出远小于预期的缓冲区后续写入就越界。防御性写法是先检查/* 适用于 C11 */#include stdint.h#include stdlib.hvoid* allocateArray(size_t count, size_t elementSize) {if (elementSize ! 0 count SIZE_MAX / elementSize) {return NULL; /* 乘法会溢出 */}return malloc(count * elementSize);}SIZE_MAX定义在stdint.h中。calloc(count, size)的语义是分配能容纳count个元素的空间如果乘积超出size_t能表示的范围实现无法满足这个请求通常会返回NULL。在需要零初始化时它比malloc加memset更省心但仍不要把它当成唯一的溢出防线外部输入的长度还是要先检查。malloc(0)的返回值由实现定义可能返回空指针也可能返回一个可以传给free的非空指针。无论哪种都不能解引用。若逻辑上需要零长度最好在接口层直接拒绝或返回空指针避免依赖实现细节。四、所有权与生命周期动态内存的问题几乎都能归结为所有权不清。建议在项目里约定场景所有权约定函数返回指针调用方负责free在文档中写明函数接收指针默认只借用不释放若要接管函数名或注释必须说明结构体成员指针由结构体的销毁函数统一释放扩容用临时指针接realloc成功后再覆盖原指针释放后不再访问可置NULL但不要依赖它解决别名问题对于结构体通常写一对create/destroy函数把分配和释放配对封装起来。这样调用方不需要知道内部有几个malloc也不容易漏掉某个成员。常见坑点❌realloc的返回值直接赋回原指针。/* 适用于 C11 */p realloc(p, newSize); /* 失败时 p 变成 NULL旧块泄漏 */if (p NULL) { return -1; }✅ 用临时指针接收成功后再赋值。/* 适用于 C11 */void* tmp realloc(p, newSize);if (tmp NULL) { free(p); return -1; }p tmp;❌ 释放后继续访问。/* 适用于 C11 */free(p);printf(%d\n, p[0]); /* use-after-freeUB */✅ 释放后不再使用必要时把指针置空。/* 适用于 C11 */free(p);p NULL;❌ 重复释放。/* 适用于 C11 */free(p);free(p); /* double freeUB */✅ 每块内存只释放一次置空后再free(NULL)虽然安全但不能替代所有权设计。/* 适用于 C11 */free(p);p NULL;❌ 用sizeof(指针)计算元素大小。/* 适用于 C11 */int* p malloc(n * sizeof(p)); /* 只分配了 n 个指针大小通常偏小 */✅ 用sizeof *p。/* 适用于 C11 */int* p malloc(n * sizeof *p);❌ 在 C 中强制转换malloc的返回值。/* 适用于 C11 */int* p (int*)malloc(n * sizeof *p); /* 可能掩盖忘记包含 stdlib.h 的错误 */✅ C 中直接赋值让void*隐式转换。/* 适用于 C11 */int* p malloc(n * sizeof *p);❌ 对栈上数组或偏移后的指针调用free。/* 适用于 C11 */int arr[10];free(arr); /* 不是 malloc 返回的块UB */free(p 1); /* 不是块起始地址UB */✅ 只释放分配函数返回的原始指针。/* 适用于 C11 */free(p);❌ 认为realloc新增的内存已经清零。/* 适用于 C11 */int* p realloc(old, 2 * n * sizeof *p);/* 直接读取 p[n..2n-1]值是未指定的 */✅ 手动初始化新增部分。/* 适用于 C11 */for (size_t i n; i 2 * n; i) { p[i] 0; }❌ 不检查malloc是否返回NULL。/* 适用于 C11 */int* p malloc(n * sizeof *p);p[0] 1; /* 分配失败时解引用空指针UB */✅ 每次分配后检查并设计好失败路径。/* 适用于 C11 */if (p NULL) { /* 处理错误 */ }总结操作关键点malloc不初始化检查NULL用sizeof *pcalloc零初始化乘法溢出时通常失败返回NULLrealloc用临时指针失败时原块仍有效free只释放原始指针每块一次free(NULL)安全长度防乘法溢出malloc(0)不解引用所有权每个块有唯一负责人接口写清约定动态内存管理没有捷径核心是两条纪律每次分配都想好谁释放每次释放后都不再碰。把这两条落实到接口设计和代码审查里比事后用工具排查更有效。