引言

在C语言中,内存分为几个不同的区域:栈区、堆区、静态区。栈区的内存由计算机自动管理,大小固定(通常1-2MB),而堆区的内存则允许程序员手动申请和释放。理解动态内存管理,是写出健壮C程序的关键。

内存分区:

  • 栈区:由计算机自动管理,存储局部变量,大小固定,由CC填充

  • 堆区:由程序员手动管理,动态分配,由CD填充

  • 静态区:存储全局变量和静态变量

不同操作系统的内存限制:

系统 栈区大小 堆区大小
32位 约1MB 2GB~3GB
64位 约2MB 10GB~15GB

今天,我将从底层视角,全面讲解C语言中的动态内存管理函数:malloccallocreallocfree,以及常见的内存错误和内存泄漏问题。


第一部分:动态内存管理函数

一、malloc——内存分配

#include <stdlib.h>
#include <stdio.h>

int main() {
    // malloc:分配指定字节数的内存,不初始化(内容是随机值)
    void* p = malloc(40);  // 分配40字节
    
    if (p == NULL) {
        printf("内存分配失败\n");
        return 1;
    }
    
    // 将void*转换为int*,作为int数组使用
    int* arr = (int*)p;
    for (int i = 0; i < 10; i++) {
        arr[i] = i + 1;
    }
    
    for (int i = 0; i < 10; i++) {
        printf("%d ", arr[i]);
    }
    
    // 释放内存
    free(p);
    p = NULL;  // 释放后置空,防止野指针
    
    return 0;
}

二、calloc——分配并初始化

#include <stdlib.h>
#include <stdio.h>

int main() {
    // calloc:分配内存并初始化为0
    // 参数1:元素个数,参数2:每个元素的大小
    int* p1 = (int*)calloc(10, sizeof(int));  // 分配10个int,全部初始化为0
    
    // 等价写法
    int* p2 = (int*)malloc(sizeof(int) * 10);
    for (int i = 0; i < 10; i++) {
        p2[i] = 0;  // 手动初始化
    }
    
    // 验证calloc已将内存初始化为0
    for (int i = 0; i < 10; i++) {
        printf("%d ", p1[i]);  // 输出:0 0 0 0 0 0 0 0 0 0
    }
    
    free(p1);
    free(p2);
    p1 = NULL;
    p2 = NULL;
    
    return 0;
}

三、realloc——重新调整内存大小

#include <stdlib.h>
#include <stdio.h>

int main() {
    // 分配40字节(10个int)
    int* p = (int*)malloc(40);
    if (p == NULL) {
        printf("分配失败\n");
        return 1;
    }
    
    printf("原地址: %p\n", p);
    
    // 重新分配为80字节(20个int)
    // realloc会尝试在原地址后扩展,如果空间不足,会重新找一块更大的内存并复制数据
    int* new_p = (int*)realloc(p, 80);
    
    if (new_p == NULL) {
        printf("重新分配失败\n");
        free(p);
        return 1;
    }
    
    printf("新地址: %p\n", new_p);
    
    // realloc成功时,原指针p失效,应使用新指针
    free(new_p);
    new_p = NULL;
    
    return 0;
}

四、结构体的动态内存分配

#include <stdlib.h>
#include <string.h>
#include <stdio.h>

#define MAX 100

typedef struct student {
    char name[20];
    int age;
    long ID;
    long long grade;
} stu;

int main() {
    // 分配100个结构体的空间
    stu* p = (stu*)malloc(sizeof(stu) * MAX);
    
    if (p == NULL) {
        printf("内存分配失败\n");
        return 1;
    }
    
    // 初始化结构体数组
    for (int i = 0; i < MAX; i++) {
        strcpy(p[i].name, "张三");
        p[i].age = 18;
        p[i].ID = 100 + i;
        p[i].grade = 100;
    }
    
    // 输出前5个
    for (int i = 0; i < 5; i++) {
        printf("%s %d %ld %lld\n", 
               p[i].name, p[i].age, p[i].ID, p[i].grade);
    }
    
    // 释放内存
    free(p);
    p = NULL;  // 重要:释放后必须置空
    
    return 0;
}

第二部分:内存泄漏

一、什么是内存泄漏?

内存泄漏的本质是丢失了部分地址,导致无法释放已分配的内存。

// 错误示例:丢失地址,造成内存泄漏
int main() {
    int* p = (int*)malloc(40);
    p = (int*)malloc(40);  // 原来的地址丢失,40字节内存泄漏
    
    // 正确做法:先释放再重新分配
    int* q = (int*)malloc(40);
    free(q);
    q = (int*)malloc(40);  // 先释放旧内存,再分配新内存
    
    free(q);
    return 0;
}

二、内存泄漏的危害

// 无限申请内存,最终导致程序崩溃
int main() {
    int i = 1;
    while (1) {
        printf("申请了 %d 字节\n", 100 * i++);
        malloc(100);  // 从未释放,内存不断增长
    }
    // 最终电脑资源耗尽,程序崩溃
    return 0;
}

三、正确释放内存的原则

// 原则1:谁申请,谁释放
void test() {
    int* p = (int*)malloc(40);
    // ... 使用p
    free(p);  // 必须在函数内释放
    p = NULL;
}

// 原则2:释放后必须置空,防止野指针
int main() {
    int* p = (int*)malloc(40);
    free(p);
    p = NULL;  // 必须置空
    
    // 如果忘记置空,p成为野指针
    // free(p);  // 重复释放会导致未定义行为
    return 0;
}

野指针:指向已释放内存或无效地址的指针。访问野指针会导致未定义行为。

第三部分:常见内存错误

一、对空指针解引用

#include <stdlib.h>
#include <string.h>

// 错误示例:未检查分配是否成功
int main() {
    int* p = (int*)malloc(1000000000);  // 可能分配失败
    *p = 10;  // 危险!如果p为NULL,程序崩溃
    
    // 正确做法:检查返回值
    int* q = (int*)malloc(1000000000);
    if (q == NULL) {
        printf("内存分配失败\n");
        return 1;
    }
    *q = 10;
    free(q);
    
    return 0;
}

二、对动态开辟的空间越界访问

int main() {
    int* p = (int*)malloc(5 * sizeof(int));  // 分配5个int的空间
    if (p == NULL) return 1;
    
    for (int i = 0; i < 10; i++) {  // 错误!越界访问
        p[i] = i;  // 访问了未分配的内存
    }
    
    free(p);
    return 0;
}

三、对栈内存使用free

int main() {
    int a = 10;  // 栈上的变量
    // free(&a);  // 错误!不能释放栈内存
    
    int arr[10];  // 栈上的数组
    // free(arr);  // 错误!不能释放栈内存
    
    return 0;
}

四、重复释放(double free)

int main() {
    int* p = (int*)malloc(40);
    free(p);
    // free(p);  // 错误!重复释放,未定义行为
    
    // 正确做法:释放后置空,重复释放也不会出错
    int* q = (int*)malloc(40);
    free(q);
    q = NULL;
    free(q);  // 对NULL执行free是安全的
    
    return 0;
}

五、指针移动后释放

// 错误示例:移动指针后释放
void test() {
    int* p = (int*)malloc(40);
    int* a = p;
    p++;  // 指针移动
    free(a);  // 正确:释放原始指针
    // free(p);  // 错误:p已经不是原始地址
}

int main() {
    int* p = (int*)malloc(40);
    free(p);
    // p++;  // 错误!p已被释放,不能再操作
    
    test();
    return 0;
}

第四部分:内存操作函数的模拟实现

一、memcpy——内存拷贝

#include <assert.h>
#include <stdio.h>

// memcpy:从source拷贝num字节到destination
// 注意:memcpy不能处理内存重叠的情况
void mymemcpy(void *dest, const void *src, size_t n) {
    assert(dest != NULL && src != NULL);
    
    char *d = (char*)dest;
    const char *s = (const char*)src;
    
    // 直接复制,不做任何重叠判断
    for (size_t i = 0; i < n; i++) {
        d[i] = s[i];
    }
    
    return dest;
}

int main() {
    int a[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int b[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    
    // 内存重叠:将a[0-2]拷贝到a[2-4]
    mymemcpy(a + 2, a, 12);
    for (int i = 0; i < 10; i++) printf("%d ", a[i]);
    printf("\n");
    
    // 正常拷贝
    mymemcpy(b, b + 2, 12);
    for (int i = 0; i < 10; i++) printf("%d ", b[i]);
    
    return 0;
}

二、memmove——安全内存拷贝(支持重叠)

// memmove:可以安全处理内存重叠的情况
void mymemmove(void* destination, const void* source, size_t num) {
    assert(destination != NULL && source != NULL);
    
    char* dest = (char*)destination;
    const char* src = (char*)source;
    
    // 如果目标地址在源地址之后,从后往前拷贝
    if (dest > src && dest < src + num) {
        for (size_t i = num; i > 0; i--) {
            dest[i - 1] = src[i - 1];
        }
    } else {
        // 否则从前往后拷贝
        for (size_t i = 0; i < num; i++) {
            dest[i] = src[i];
        }
    }
}

第五部分:常见指针错误分析

错误1:函数内分配内存,但传参错误

// 错误:传值,无法修改外部指针
void getmemory(char* p, int num) {
    p = (char*)malloc(num);  // 修改的是形参,实参不变
}

void test1() {
    char* str = NULL;
    getmemory(str, 100);
    strcpy(str, "hello world");  // str仍然是NULL,崩溃!
    printf(str);
}

// 正确:传二级指针
void getmemory2(char** p, int num) {
    *p = (char*)malloc(num);  // 修改实参指针
}

void test2() {
    char* str = NULL;
    getmemory2(&str, 100);
    strcpy(str, "hello world");
    printf(str);
    free(str);
}

错误2:返回栈区地址

// 错误:返回局部变量的地址(栈内存)
char* getmemory3() {
    char p[] = "hello world";  // 栈上的数组
    return p;  // 危险!函数结束后p被销毁
}

void test3() {
    char* str = getmemory3();
    printf(str);  // 未定义行为
}

// 正确:返回堆内存或静态内存
char* getmemory4() {
    char* p = (char*)malloc(100);
    strcpy(p, "hello world");
    return p;
}

错误3:传递NULL给函数

void getmemory5(char* p) {
    p = (char*)malloc(100);  // 修改形参,实参仍是NULL
}

int main() {
    char* str = NULL;
    getmemory5(str);
    strcpy(str, "Hello World");  // 空指针解引用,崩溃!
    return 0;
}

第六部分:不同指针类型的访问差异

一、不同类型指针解引用的区别

int main() {
    char a[4] = {0};  // 4字节,初始为0
    // 内存:CC CC CC CC(栈区初始值)
    
    int* p = (int*)a;  // 将char数组当作int指针使用
    *p = 10;
    // 10的十六进制:0x0000000A
    // 小端存储:0A 00 00 00
    
    // a[0] = 0x0A, a[1] = 0, a[2] = 0, a[3] = 0
    
    return 0;
}

二、通过不同类型指针修改内存

int main() {
    int a = 10;
    // a的内存:0A 00 00 00(小端)
    
    char* p = (char*)&a;
    for (int i = 0; i < 4; i++) {
        p[i] = 'a' + i;  // 逐个字节修改
    }
    // 修改后:61 62 63 64
    // 作为int解释:0x64636261 = 1684234849
    
    printf("%d\n", a);  // 输出:1684234849
    
    return 0;
}

第七部分:总结

一、动态内存分配函数对比

函数 参数 初始化 返回值 用途
malloc(size) 字节数 不初始化 void* 分配内存
calloc(n, size) 个数、单个大小 初始化为0 void* 分配并清零
realloc(ptr, size) 原指针、新大小 保留原数据 void* 调整内存大小
free(ptr) 指针 - void 释放内存

二、内存管理核心规则

规则 说明
检查返回值 malloc/calloc/realloc都可能返回NULL
释放后置空 free后指针置NULL,防止野指针
谁申请谁释放 避免内存泄漏
不能释放栈内存 free只能释放堆内存
不能重复释放 重复释放是未定义行为
不能越界访问 只能访问已分配的内存范围

三、内存泄漏的常见场景

场景 说明
丢失指针 重新赋值前未释放原内存
忘记释放 申请后从未调用free
异常退出 程序异常终止前未释放
指针移动 移动指针后无法释放原地址

动态内存管理是C语言中最容易出错的地方之一。理解malloccallocreallocfree的用法,以及常见的内存错误,是写出健壮C程序的关键。

学习建议:

  1. 每次malloc后都要检查返回值是否为NULL

  2. free后立即将指针置为NULL

  3. 避免移动指针,如必须移动,保留原始地址

  4. 使用valgrind等工具检测内存泄漏

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐