C++ 中的 malloc 和 free 并不是新语法,而是从 C 语言标准库继承过来的动态内存管理函数。它们声明在 <cstdlib> 或 <stdlib.h> 中,作用是向操作系统申请一块指定大小的堆内存,使用完毕后再归还给系统。虽然 C++ 提供了更符合面向对象语义的 new 和 delete,但理解 malloc/free 仍然是掌握内存底层行为的重要一环。

一、malloc 与 free 的基本用法
malloc 的函数原型是 void* malloc(size_t size);,参数 size 表示要分配的字节数,返回值是 void* 类型的指针。因此在实际使用中,通常要把它强制转换为目标类型指针,例如 int* p = (int*)malloc(10 * sizeof(int));。这里强烈建议使用 sizeof 来计算类型大小,而不是直接写死数字,例如 malloc(40) 这种写法在 64 位平台上可能还碰巧正确,但在不同平台上 int 大小会变化,写死字节数会让代码变得脆弱。
调用 malloc 后必须立即检查返回值。如果堆内存不足或者请求的大小不合理,malloc 会返回 NULL。在 C++ 中如果不对 NULL 做判断,后续解引用空指针会直接导致程序崩溃。下面是一个完整的分配、使用、释放示例。
#include <cstdlib>
#include <cstdio>
int main() {
int n = 10;
int* arr = (int*)malloc(n * sizeof(int));
if (arr == NULL) {
printf("分配失败\n");
return 1;
}
for (int i = 0; i < n; ++i) {
arr[i] = i * i;
}
for (int i = 0; i < n; ++i) {
printf("%d ", arr[i]);
}
printf("\n");
free(arr);
arr = NULL;
return 0;
}
free 的函数原型是 void free(void* ptr);,它只接受由 malloc、calloc 或 realloc 返回的指针。调用 free(arr); 之后,最好再把指针置为 NULL,这样可以减少悬空指针带来的风险。需要特别说明的是,free(NULL) 是安全操作,不会做任何事;但重复释放同一个非空指针则属于未定义行为,可能导致堆损坏。
另一个容易忽略的细节是:malloc 分配出来的内存内容是未初始化的,里面可能是随机数据。如果在使用前不初始化,很可能得到不确定的结果。需要清零时可以使用 calloc,或者在 malloc 后手动调用 memset。
二、calloc 与 realloc 的补充用法
calloc 的原型是 void* calloc(size_t num, size_t size);,它会分配 num * size 字节的连续内存,并把每个字节初始化为 0。与 malloc 加 memset 的组合相比,calloc 在分配大块内存时可能更高效,因为操作系统可以提供已经清零的页。它也要求检查返回是否为 NULL。
realloc 用于调整已经分配的内存块大小,原型是 void* realloc(void* ptr, size_t new_size);。当 ptr 为 NULL 时,它的行为类似 malloc;当 new_size 为 0 时,不同实现的行为可能不同,因此不要在跨平台代码中依赖这种用法。realloc 可能直接在原地扩展,也可能重新分配一块更大的内存、复制旧数据并释放旧块。因此必须使用返回值来接收新的地址,不能继续使用原来的指针。
一个典型的错误写法是 p = (int*)realloc(p, new_size);。如果 realloc 失败并返回 NULL,原来的 p 就会被覆盖成空指针,旧内存地址随之丢失,造成内存泄漏。安全写法是先用临时指针接住返回值,确认非空后再赋给原指针。
#include <cstdlib>
#include <cstdio>
#include <cstring>
int main() {
int n = 5;
int* p = (int*)calloc(n, sizeof(int));
if (p == NULL) {
printf("calloc 失败\n");
return 1;
}
int new_n = 10;
int* tmp = (int*)realloc(p, new_n * sizeof(int));
if (tmp == NULL) {
printf("realloc 失败,保持原内存不变\n");
free(p);
return 1;
}
p = tmp;
for (int i = 0; i < new_n; ++i) {
printf("%d ", p[i]);
}
printf("\n");
free(p);
p = NULL;
return 0;
}
三、C++ 环境下使用 malloc/free 的常见问题
第一个问题是内存泄漏。malloc 分配的内存不会自动回收,如果函数在中间提前 return,或者异常抛出导致流程跳出,而没有执行到 free,就会造成泄漏。C++ 的异常机制会放大这个问题,因为很多代码路径可能隐式退出。因此手动管理内存时,尽量把释放逻辑放在同一作用域内,或者使用 RAII 包装类来保证释放。
第二个问题是野指针和重复释放。释放后没有把指针置空,这个指针仍然保存着原来的地址,但该地址已经归还给堆管理器。后续再访问或释放它,可能修改了其他对象的数据,造成难以复现的堆破坏。这个问题在大型程序中非常隐蔽,排查成本很高。
第三个问题更偏向 C++ 特性:malloc/free 只负责原始内存,不会调用构造函数和析构函数。对于 std::string、std::vector 等非平凡类型,如果直接 malloc 一块内存然后当作对象使用,其内部状态未初始化,调用成员函数时行为未定义。只有在分配 POD 类型、或者配合 placement new 和显式析构调用时,才能安全使用 malloc/free 管理对象内存。
调试这类问题时,可以使用 Valgrind、AddressSanitizer 等工具。它们能检测越界访问、使用已释放内存、内存泄漏等问题。在 Visual Studio 中还可以开启 CRT 调试堆,输出分配序号和泄漏位置。把这些工具纳入日常开发流程,可以显著减少内存错误。
四、malloc/free 与 new/delete 的区别
从类型安全角度看,new 会根据类型自动计算需要分配的大小,并返回具体类型指针;malloc 则需要手动计算字节数,并做一次强制类型转换。从失败行为看,malloc 失败返回 NULL,而 new 失败会抛出 std::bad_alloc 异常。这是二者在接口设计上的重要差异。
更关键的区别在于对象生命周期。new 会先调用 operator new 分配内存,再调用构造函数;delete 则先调用析构函数,再释放内存。malloc/free 完全不涉及构造和析构。因此对于带有资源的类,混用 malloc 和 delete 是非常危险的。例如 MyClass* obj = (MyClass*)malloc(sizeof(MyClass)); delete obj; 会在未构造的对象上调用析构函数,可能访问未初始化的成员。反过来用 new 分配再用 free 释放会跳过析构函数,导致资源不释放。
#include <cstdlib>
#include <cstdio>
struct Point {
int x;
int y;
};
int main() {
// C 风格:只分配原始内存
Point* p1 = (Point*)malloc(sizeof(Point));
if (p1 == NULL) {
return 1;
}
p1->x = 3; // 对于 POD 类型可以直接使用
p1->y = 4;
free(p1);
// C++ 风格:类型安全,自动计算大小
Point* p2 = new Point();
p2->x = 5;
p2->y = 6;
delete p2;
return 0;
}
但在某些场景下,C++ 程序员仍然会选择 malloc/free。例如需要与 C 语言接口交互,或者需要 realloc 实现原地扩容,又或者在某些禁用异常的嵌入式环境中不希望 new 抛异常。此时应当清楚地知道这些函数的边界,并确保只用于原始内存或 POD 数据。
五、一个完整的动态数组封装示例
为了把前面的要点串起来,下面用一个简单的动态整数数组封装来演示 malloc、realloc 和 free 的配合。结构体保存数据指针和长度,初始化时分配指定长度,扩容时用临时指针接收 realloc 的返回值,释放时统一调用 free 并把指针置空。
#include <cstdlib>
#include <cstddef>
#include <cstring>
#include <cstdio>
struct IntArray {
int* data;
size_t len;
};
bool initArray(IntArray* arr, size_t initial_len) {
arr->len = initial_len;
arr->data = (int*)malloc(initial_len * sizeof(int));
return arr->data != NULL;
}
bool resizeArray(IntArray* arr, size_t new_len) {
int* tmp = (int*)realloc(arr->data, new_len * sizeof(int));
if (tmp == NULL) {
return false;
}
arr->data = tmp;
arr->len = new_len;
return true;
}
void freeArray(IntArray* arr) {
free(arr->data);
arr->data = NULL;
arr->len = 0;
}
int main() {
IntArray a;
if (!initArray(&a, 3)) {
printf("初始化失败\n");
return 1;
}
a.data[0] = 10;
a.data[1] = 20;
a.data[2] = 30;
if (!resizeArray(&a, 5)) {
printf("扩容失败\n");
freeArray(&a);
return 1;
}
a.data[3] = 40;
a.data[4] = 50;
for (size_t i = 0; i < a.len; ++i) {
printf("%d ", a.data[i]);
}
printf("\n");
freeArray(&a);
return 0;
}
这段代码遵循了几个重要原则:每次分配后检查返回值;realloc 使用临时指针;释放后清空状态。对于小规模工具类或者 C 接口适配层来说,这种写法仍然非常实用。不过在业务代码中,更推荐直接使用 std::vector<int>,它已经封装了这些细节,并支持异常安全和自动扩容。
总的来说,malloc 和 free 是 C 风格内存管理的基础,在 C++ 项目中依然有适用场景。掌握它们的用法、边界和常见错误,能帮助你更好地理解堆内存分配机制,也能在调试底层问题时多一种可靠手段。