一、变量生存周期与内存分区
(一)变量的分类
1.全局变量
定义在函数外部,程序运行开始分配空间,整个程序结束才销毁释放。
默认初始化:系统自动初始化为 0,使用 00 进行字节填充。
2.局部变量
定义在函数内部,进入函数开辟空间,函数执行结束自动销毁。
默认初始化:无默认值,使用 cc 进行字节填充(debug 模式下的填充标记,代表未初始化)。
3.块域变量
定义在**{}代码块内部**,进入代码块开辟空间,花括号执行结束立即销毁。
默认初始化:无默认值,使用 cc 进行字节填充(debug 模式下的填充标记,代表未初始化)。
(二)内存两大区域核心区别
| 内存区域 | 管理方式 | 特点 | 优缺点 |
|---|---|---|---|
| 栈区 | 系统自动申请、自动释放 | 存放全局、局部、块变量,大小固定 | 安全、无需手动管理,灵活性极低 |
| 堆区 | 程序员手动申请、手动释放 | 动态开辟空间,支持自由扩容 | 灵活可控,用完必须手动释放,易出现内存泄漏 |
| 堆区空间一般往往大于栈区;也就是说,程序员自己可以操作的空间远远大于计算机所能操作的空间 |
为什么要发明动态内存?
问题一:计算机对所有变量的生存周期特别严格,我们很容易访问到没有权限的内存,进而造成非法访问内存,导致程序崩溃。
问题二:计算机栈区空间时固定的,无法扩容,但现实场景中,我们经常需要动态扩容空间。
动态内存则可以解决以上问题。它可以实现 1.自由掌控生存周期 ;2.空间可以自由扩容。
二、动态内存四大核心函数
(一)malloc函数
cpp
void* malloc(size_t size);
功能 :向内存申请一块连续可用的空间,并返回指向这块空间的指针。
返回值 :如果开辟成功 ,则返回一个指向开辟好空间的指针 ;如果开辟失败 ,则返回一个NULL指针。
注意 :返回值一定要做判空检查,避免空指针解引用导致程序崩溃。
返回值类型 :void*
注意:malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
如果参数 size 为0,malloc 的行为是标准是未定义的,取决于编译器。
(二)free函数
cpp
void free(void* ptr);
功能:用来释放动态开辟的内存。
如果参数 ptr 指向的空间不是动态开辟的,那 free 函数的行为是未定义的;如果参数 ptr 是 NULL指针,则函数什么事都不做。
注意 :free只能释放由程序员通过 malloc/calloc/realloc 手动申请的堆区内存 ,且传入参数必须是该段空间最起始的首字节地址,传入栈地址、偏移地址、非法地址都会触发程序报错。
cpp
int main() {
int a;
// 计算机自动申请4个字节
// 地址范围:0X11111111 ~ 0X11111114
// 内容:cc cc cc cc
// 生存周期:创建 ~ 主函数结束
void* p = malloc(4);
// 程序员手动申请4个字节
// 地址范围:0X12345678 ~ 0X1234567B
// 内容:cd cd cd cd
// 生存周期:malloc创建 ~ free释放
// 返回第一个字节地址 → 相当于这片空间的"钥匙"
free(p);
// 程序员手动释放4字节
// 结束这片内存的生存周期
// 归还权限给操作系统
return 0;
}
(三)realloc函数
cpp
void* realloc(void* ptr, size_t size);
功能:修改已开辟堆内存大小,实现扩容、缩容
两种扩容机制:
(1)原空间后方剩余内存充足:直接原地扩容,返回源地址
(2)原空间无连续空余:开辟新空间、拷贝旧数据、释放旧内存,返回新地址
返回值 :申请成功 返回指向堆空间首字节的指针,申请失败则返回 NULL
注意:
(1)必须用临时指针接收返回值,避免扩容失败丢失原地址
(2)返回值一定要做判空检查
(四)calloc函数
cpp
void* calloc(size_t num, size_t size);
功能:申请 num 个大小为 size 的连续空间
特点:自动全部初始化为0
返回值 :申请成功 返回指向堆空间首字节的指针,申请失败则返回 NULL
注意 :返回值一定要做判空检查
三、堆区内存泄漏问题
(一)本质
堆内存释放必须依托空间首个字节地址,一旦该地址丢失,对应内存就无法正常释放,形成内存泄漏。
(二)泄漏现象
失效内存既无法被程序调用,操作系统也不能回收,其他程序同样无法占用,内存资源被无效占用。
(三)地址管理规则
堆空间的使用于释放权限,由首字节地址决定,需依靠栈区指针变量留存地址,才能正常管控堆内存。
(四)两类泄漏成因
1.指针地址被修改
保存堆首地址的指针被重新赋值指向其他空间,原始地址永久丢失,内存无法释放。
2.指针生命周期结束
存储地址的栈区指针,因作用域结束被销毁,未执行释放操作,堆内存地址随之丢失。
四、动态内存常见错误
(一)动态内存申请后不判断返回值是否为空
cpp
void test() {
int* p = (int*)malloc(INT_MAX / 4);
// 必须判空
if (p == NULL) {
printf("申请内存失败\n");
return;
}
*p = 20;
free(p);
p = NULL;
}
(二)堆区内存非法越界访问
cpp
void test() {
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL) exit(EXIT_FAILURE);
// 错误:i <= 10 越界
// 正确:i < 10
for (int i = 0; i < 10; i++) {
*(p + i) = i;
}
free(p);
p = NULL;
}
(三)对栈区内存使用free
cpp
void test() {
int a = 10;
int* p = &a; // a 在栈区
free(p); // 错误!不能 free 栈内存
}
(四)不使用首地址释放堆内存
cpp
void test() {
int* p = (int*)malloc(100);
p++; // 指针偏移,丢失首地址
free(p); // 错误!不是首地址
}
(五)重复释放同一块堆内存
cpp
void test() {
int* p = (int*)malloc(100);
free(p);
free(p); // 错误!重复释放
}
(六)只申请内存不释放,造成内存泄漏
cpp
void test() {
int* p = (int*)malloc(100);
if (p != NULL) {
*p = 20;
}
// 缺少 free(p)
}
int main() {
while (1) { // 长期运行
test(); // 只申请不释放 → 内存泄漏
}
}