C语言——动态内存

一、变量生存周期与内存分区

(一)变量的分类

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();  // 只申请不释放 → 内存泄漏
    }
}
相关推荐
xyq20241 小时前
Java 数组
开发语言
雨辰AI1 小时前
人大金仓 V9 生产级专用监控大盘(含 120 + 指标 + 告警规则 + 一键导入)
java·开发语言·数据库·mysql·政务
时寒的笔记1 小时前
day13~14核心案例某采招网
开发语言·javascript·ecmascript
彦为君2 小时前
Java文件处理效率库Commons-IO(速览)
java·开发语言·mfc
sycmancia3 小时前
Qt——文本打印与光标定位
开发语言·qt
故事和你913 小时前
洛谷-【动态规划1】动态规划的引入2
开发语言·数据结构·c++·算法·动态规划·图论
AI人工智能+电脑小能手3 小时前
【大白话说Java面试题 第71题】【Mysql篇】第1题:索引是什么?
java·开发语言·b树·mysql·面试
九皇叔叔3 小时前
VMware 安装 麒麟操作系统
java·开发语言·虚拟机·麒麟操作系统·vmware安装
weixin199701080164 小时前
[特殊字符] 人工抓取数据革命:从“人肉爬虫”到“智能数据工厂”全面转型指南
开发语言·爬虫·python