栈上对象生命周期由作用域决定;堆上内存手动申请、手动释放——核心四兄弟:malloc/calloc/realloc/free。
int *p = malloc(100 * sizeof(int)); // 申请 100 个 int,不初始化(垃圾值)
int *q = calloc(100, sizeof(int)); // 申请并清零,等价 malloc + memset 0
if (!p) { /* 分配失败:返回 NULL */ }
malloc(n):分配 n 字节,不初始化,内容是垃圾。calloc(cnt, size):分配 cnt × size 字节,全部清零(还能防乘法溢出)。NULL——必须检查,否则解引用空指针就是 UB。malloc(n * sizeof(int)) 而不是 malloc(n),避免手算字节出错。free(p);
p = NULL; // 好习惯:防止再用成悬空指针
free 之后指针仍存着旧地址,但指向的内存已经还给堆——这叫悬空指针(dangling pointer)。free 一次是 double free,也是 UB。free(NULL) 是安全的(什么都不做),所以「置 NULL」能让后续误用变得相对可控。申请了却从不释放,内存就永远占着——程序跑久了越吃越多:
void leak() {
int *p = malloc(1024); // 没 free,函数返回后 p 丢失,1024 字节泄漏
}
int *p = malloc(4 * sizeof(int));
int *tmp = realloc(p, 8 * sizeof(int)); // 扩到 8 个
if (!tmp) { /* 失败:p 仍有效,别覆盖 */ }
p = tmp;
realloc 有两个经典陷阱:
// 错误写法:失败时 p 变成 NULL,原来的内存地址丢失 → 泄漏
p = realloc(p, new_size);
| 错误 | 后果 |
|---|---|
忘记 free | 内存泄漏 |
free 后继续用(悬空) | UB,可能读到脏数据/崩 |
重复 free(double free) | UB,堆损坏 |
| 越界写(缓冲区溢出) | UB,破坏相邻内存/堆元数据 |
不检查 malloc 返回值 | 解引用 NULL,段错误 |
封装一个不会忘记检查、不会泄漏的分配函数:
#include <stdlib.h>
// 分配 cnt 个 size 字节,失败直接退出(或按你的策略处理)
void *xmalloc(size_t cnt, size_t size) {
void *p = calloc(cnt, size);
if (!p) {
fprintf(stderr, "out of memory\n");
exit(1);
}
return p;
}
malloc 是库函数,内部向 OS 要内存(brk/mmap),维护空闲链表/分配器结构;free 把块放回空闲列表,两者都只是普通函数调用。new/delete 与 malloc/free 对应,但 RAII + 智能指针(unique_ptr/shared_ptr)让手动释放几乎消失。谁 malloc 谁 free;free 后置 NULL;realloc 先存临时变量;每次分配都检查 NULL。