【数据结构与算法】第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倍。

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

相关推荐
MY_TEUCK2 小时前
【Java 后端】SpringBoot 登录认证与会话跟踪实战(JWT + Filter/Interceptor)
java·开发语言·spring boot
飞Link2 小时前
大模型长文本的“救命稻草”:深度解析 TurboQuant 与 KV Cache 压缩技术
算法
QQ2422199792 小时前
基于python+微信小程序的家教管理系统_mh3j9
开发语言·python·微信小程序
沐知全栈开发2 小时前
JavaScript 条件语句
开发语言
RSTJ_16252 小时前
PYTHON+AI LLM DAY THREETY-SEVEN
开发语言·人工智能·python
郝学胜-神的一滴2 小时前
深度学习优化核心:梯度下降与网络训练全解析
数据结构·人工智能·python·深度学习·算法·机器学习
清水白石0083 小时前
《Python性能深潜:从对象分配开销到“小对象风暴”的破解之道(含实战与最佳实践)》
开发语言·python
Je1lyfish3 小时前
CMU15-445 (2025 Fall/2026 Spring) Project#3 - QueryExecution
linux·c语言·开发语言·数据结构·数据库·c++·算法
许彰午3 小时前
03-二叉树——从递归遍历到非递归实现
java·算法
Brilliantwxx3 小时前
【C++】 vector(代码实现+坑点讲解)
开发语言·c++·笔记·算法