【数据结构与算法】第3篇:C语言核心机制回顾(二):动态内存管理与typedef

一、写在前面

上一篇讲了指针和结构体,这一篇我们聊动态内存管理。

为什么数据结构离不开动态内存?因为大部分数据结构的大小不是固定的。一个链表,你无法提前知道用户要存多少个数据,只能在程序运行时根据需要动态申请内存。

C语言里,动态内存的申请和释放全靠四个函数:malloc、calloc、realloc、free。用不好,轻则内存泄漏,重则程序崩溃。


二、四个核心函数

这四个函数的头文件都是 <stdlib.h>

2.1 malloc:申请内存

c

复制代码
void *malloc(size_t size);
  • 参数:要申请的字节数

  • 返回值:申请到的内存地址(void*类型),失败返回NULL

c

复制代码
int *p = (int*)malloc(5 * sizeof(int));
if (p == NULL) {
    printf("内存申请失败\n");
    return -1;
}
// 现在可以用p了,就像一个长度为5的int数组
p[0] = 10;
p[1] = 20;

注意

  • malloc申请的内存不会初始化,里面存的是垃圾值

  • 要自己计算字节数,5 * sizeof(int) 才是5个int需要的空间

  • 一定要判断返回值是否为NULL

2.2 calloc:申请并清零

c

复制代码
void *calloc(size_t nmemb, size_t size);
  • 参数1:元素个数

  • 参数2:每个元素的大小

  • 返回值:申请到的内存地址,失败返回NULL

c

复制代码
int *p = (int*)calloc(5, sizeof(int));
// 等价于 malloc(5 * sizeof(int)),但内存会被初始化为0
// p[0]到p[4]都是0

calloc 比 malloc 多了一步清零操作。如果你需要初始化为0,用 calloc 更方便。如果不需要清零,malloc 更快。

2.3 realloc:调整内存大小

c

复制代码
void *realloc(void *ptr, size_t new_size);
  • 参数1:之前申请的内存地址

  • 参数2:新的字节数

  • 返回值:新内存的地址(可能和原来不同),失败返回NULL

这个函数很常用,比如动态数组扩容的时候。

c

复制代码
int *p = (int*)malloc(5 * sizeof(int));
// 想扩容到10个int
int *temp = (int*)realloc(p, 10 * sizeof(int));
if (temp == NULL) {
    // 扩容失败,原来的内存还在,要小心处理
    free(p);
    return -1;
}
p = temp;
// 现在p有10个int的空间

关键点

  • realloc 可能移动内存(如果原位置后面空间不够),所以要用新指针接收返回值

  • 扩容失败时返回NULL,原来的内存不会被释放,所以要先用临时变量接,判断成功再赋值

  • 如果 ptr 是NULL,realloc 等价于 malloc

2.4 free:释放内存

c

复制代码
void free(void *ptr);

申请的动态内存,不用了必须释放。

c

复制代码
int *p = (int*)malloc(10 * sizeof(int));
// 使用p...
free(p);      // 释放
p = NULL;     // 建议置NULL,防止野指针

为什么一定要 free?

  • 程序不会自动释放动态内存(除非程序结束)

  • 不释放会导致内存泄漏,长时间运行的程序(比如服务器)会耗尽内存

释放后的注意事项

  • free 之后,指针变量还保存着原来的地址,但这个地址已经不能用了(野指针)

  • 再次使用这个指针会导致未定义行为,可能崩溃

  • 习惯:free 之后立即置 NULL


三、常见错误与内存泄漏

3.1 内存泄漏示例

c

复制代码
void func() {
    int *p = (int*)malloc(100 * sizeof(int));
    // 忘记写 free(p);
    return;  // p被销毁了,但内存没释放,泄漏了
}

每次调用这个函数,就漏掉400字节(假设int4字节)。调用一万次,漏4MB。

3.2 重复释放

c

复制代码
free(p);
free(p);  // 错误!重复释放,程序可能崩溃

3.3 使用已释放的内存

c

复制代码
free(p);
*p = 10;  // 错误!p指向的内存已经还给系统了

3.4 内存泄漏检测

写数据结构代码时,可以用一个简单的方法:写一个函数打印当前已分配但未释放的内存数量。或者用 Valgrind 工具(Linux下):

bash

复制代码
valgrind --leak-check=full ./你的程序

它会告诉你哪里申请的内存没有释放。


四、typedef 深入:简化结构体定义

上一篇简单提了 typedef,这一篇详细说说它在数据结构里的用法。

4.1 基本用法

c

复制代码
// 定义结构体
typedef struct Node {
    int data;
    struct Node *next;
} Node;

// 现在可以这样用
Node n1;           // 不用写 struct Node
Node *p = &n1;

4.2 同时定义指针类型

c

复制代码
typedef struct Node {
    int data;
    struct Node *next;
} Node, *PNode;

// Node 是结构体类型
// PNode 是 Node* 类型

Node n1;
PNode p = &n1;     // p 是 Node* 类型

这样写的好处是代码更简洁,尤其是函数参数:

c

复制代码
// 不用typedef
void insert(struct Node *head, int data);

// 用typedef
void insert(PNode head, int data);

4.3 在数据结构代码中的标准写法

后面写链表、栈、队列时,我通常这样写:

c

复制代码
// 节点类型
typedef struct Node {
    int data;               // 数据域
    struct Node *next;      // 指针域
} Node, *PNode;

// 链表结构(如果需要)
typedef struct LinkedList {
    PNode head;             // 头指针
    int size;               // 节点个数
} LinkedList, *PList;

这样做的好处:

  • 代码清晰,一眼看出类型含义

  • 减少了 struct 关键字的重复

  • 后续修改结构体内部时,接口不变


五、综合例子:动态数组的简单实现

把 malloc 和 realloc 结合起来,实现一个可以自动扩容的数组:

c

复制代码
#include <stdio.h>
#include <stdlib.h>

typedef struct {
    int *data;      // 数据指针
    int size;       // 当前元素个数
    int capacity;   // 总容量
} DynamicArray;

// 初始化
void initArray(DynamicArray *arr, int initCapacity) {
    arr->data = (int*)malloc(initCapacity * sizeof(int));
    arr->size = 0;
    arr->capacity = initCapacity;
}

// 添加元素
void push(DynamicArray *arr, int value) {
    // 如果满了,扩容
    if (arr->size >= arr->capacity) {
        int newCapacity = arr->capacity * 2;
        int *temp = (int*)realloc(arr->data, newCapacity * sizeof(int));
        if (temp == NULL) {
            printf("扩容失败\n");
            return;
        }
        arr->data = temp;
        arr->capacity = newCapacity;
        printf("扩容到 %d\n", arr->capacity);
    }
    arr->data[arr->size] = value;
    arr->size++;
}

// 打印
void printArray(DynamicArray *arr) {
    printf("size=%d, capacity=%d, data=", arr->size, arr->capacity);
    for (int i = 0; i < arr->size; i++) {
        printf("%d ", arr->data[i]);
    }
    printf("\n");
}

// 释放内存
void destroyArray(DynamicArray *arr) {
    free(arr->data);
    arr->data = NULL;
    arr->size = 0;
    arr->capacity = 0;
}

int main() {
    DynamicArray arr;
    initArray(&arr, 3);
    
    // 添加6个元素,会触发扩容
    for (int i = 1; i <= 6; i++) {
        push(&arr, i * 10);
        printArray(&arr);
    }
    
    destroyArray(&arr);
    return 0;
}

输出:

text

复制代码
size=1, capacity=3, data=10 
size=2, capacity=3, data=10 20 
size=3, capacity=3, data=10 20 30 
扩容到 6
size=4, capacity=6, data=10 20 30 40 
size=5, capacity=6, data=10 20 30 40 50 
size=6, capacity=6, data=10 20 30 40 50 60 

这个例子演示了:

  • malloc 初始分配

  • realloc 动态扩容

  • free 释放内存

  • typedef 简化结构体定义


六、小结

函数 作用 注意事项
malloc 申请内存 不初始化,要判断NULL
calloc 申请并清零 比malloc多一步清零
realloc 调整内存大小 用临时变量接返回值,失败原内存还在
free 释放内存 释放后置NULL,避免重复释放

动态内存管理的核心原则

  1. 谁申请,谁释放

  2. 释放后指针置NULL

  3. 每次malloc/realloc都要判断返回值

  4. 用工具检测内存泄漏


七、思考题

  1. 下面的代码有什么问题?

c

复制代码
int *p = (int*)malloc(10 * sizeof(int));
p = (int*)realloc(p, 20 * sizeof(int));
free(p);
  1. 为什么链表节点要用 malloc 动态创建,不能直接定义局部变量?

  2. 写一个函数,接收一个整数指针和长度,给数组动态扩容到原来的2倍。

欢迎在评论区讨论你的答案。

相关推荐
23.2 小时前
【Java】char字符类型的UTF-16编码解析
java·开发语言·面试
无小道2 小时前
关于mmap的理解和使用
开发语言·mmap
froginwe112 小时前
jQuery 隐藏/显示详解
开发语言
码云数智-大飞2 小时前
分布式数据库:2026年数据架构的基石与挑战
开发语言
不想写代码的星星2 小时前
C++模板特化:别把“特例”写成“特坑”——从全特化到变量模板
c++
查古穆2 小时前
python进阶-推导式
开发语言·python
njidf2 小时前
C++中的访问者模式
开发语言·c++·算法
英俊潇洒美少年2 小时前
js 同步异步,宏任务微任务的关系
开发语言·javascript·ecmascript
C_Si沉思2 小时前
C++中的工厂模式变体
开发语言·c++·算法