指针综合实战:四个必练项目

学指针学到这个份上,我觉得是时候来点真家伙了。前面那些概念、语法、陷阱,说白了都是热身。真正让你把指针刻进骨子里的,是动手做项目。

这一章,我挑了四个经典实战项目。它们覆盖了指针最常见的应用场景:内存管理、数据解析、动态容器、调试辅助。每个项目都不大,但足够让你把指针玩得转。

项目一:简易内存池

先说说内存池。我在做嵌入式项目时,最头疼的就是频繁 malloc/free。系统跑着跑着,内存碎片就出来了,最后连一块连续内存都申请不到。后来我学乖了——自己管内存。

简易内存池的核心思想很简单:提前申请一大块内存,然后自己分配出去。用完还回来,不交给系统管。

typedef struct {
    void *pool;          // 内存池起始地址
    size_t block_size;   // 每个块的大小
    int total_blocks;    // 总块数
    int *free_list;      // 空闲块索引链表
    int free_head;       // 空闲链表头
} MemPool;

// 初始化内存池
void mempool_init(MemPool *mp, void *buf, 
                  size_t blk_sz, int cnt) {
    mp->pool = buf;
    mp->block_size = blk_sz;
    mp->total_blocks = cnt;
    mp->free_list = (int*)malloc(cnt * sizeof(int));
    
    // 构建空闲链表
    for (int i = 0; i < cnt - 1; i++) {
        mp->free_list[i] = i + 1;
    }
    mp->free_list[cnt - 1] = -1;  // 尾节点
    mp->free_head = 0;
}

// 分配一个块
void* mempool_alloc(MemPool *mp) {
    if (mp->free_head == -1) return NULL;
    
    int idx = mp->free_head;
    mp->free_head = mp->free_list[idx];
    
    return (char*)mp->pool + idx * mp->block_size;
}

// 释放一个块
void mempool_free(MemPool *mp, void *ptr) {
    int idx = ((char*)ptr - (char*)mp->pool) 
              / mp->block_size;
    
    mp->free_list[idx] = mp->free_head;
    mp->free_head = idx;
}

我的经验:内存池的块大小要选好。我一般取项目中最大结构体的大小,再对齐到 4 或 8 字节。太小了浪费链表开销,太大了内部碎片多。

你看,这里用到了指针算术。计算索引时,用指针相减再除以块大小。这个技巧在嵌入式开发中很常见。

项目二:JSON 解析器

JSON 解析,说白了就是字符串遍历 + 指针跳转。别被「解析器」三个字吓到,核心逻辑其实不复杂。

我写过一个微型 JSON 解析器,只支持整数和字符串。但麻雀虽小五脏俱全,指针的用法全在里面了。

typedef enum {
    JSON_NULL, JSON_INT, JSON_STR, JSON_OBJ
} JsonType;

typedef struct JsonNode {
    JsonType type;
    char *key;           // 键名指针
    union {
        int int_val;
        char *str_val;   // 字符串指针
    } value;
    struct JsonNode *next;  // 链表指针
} JsonNode;

// 跳过空白字符
static char* skip_whitespace(char *p) {
    while (*p == ' ' || *p == '\t' || 
           *p == '\n' || *p == '\r') p++;
    return p;
}

// 解析字符串值
static char* parse_string(char *p, char **out) {
    p++;  // 跳过开头引号
    char *start = p;
    while (*p != '"' && *p != '\0') p++;
    *out = start;
    *(p++) = '\0';  // 用 '\0' 替换结尾引号
    return p;
}

注意:上面这段代码直接修改了原始字符串。如果你需要保留原始数据,记得先拷贝一份。我曾经在这个坑里栽过跟头——解析完发现原始数据被改得面目全非。

JSON 解析器的精髓在于:用指针遍历字符串,遇到结构字符({、[、"、:、,)就做相应处理。整个过程就是指针的移动和判断。

项目三:动态数组(Vector)

C 语言没有现成的动态数组,但我们可以用指针自己造一个。说白了,就是封装一个可扩容的数组。

typedef struct {
    void *data;      // 数据指针
    size_t elem_size; // 每个元素大小
    int capacity;    // 容量
    int size;        // 当前元素个数
} Vector;

// 初始化
void vector_init(Vector *v, size_t elem_sz) {
    v->elem_size = elem_sz;
    v->capacity = 4;
    v->size = 0;
    v->data = malloc(v->capacity * elem_sz);
}

// 添加元素
void vector_push(Vector *v, void *elem) {
    if (v->size >= v->capacity) {
        v->capacity *= 2;
        v->data = realloc(v->data, 
                         v->capacity * v->elem_size);
    }
    // 用 memcpy 拷贝元素
    memcpy((char*)v->data + v->size * v->elem_size, 
           elem, v->elem_size);
    v->size++;
}

// 获取元素
void* vector_get(Vector *v, int index) {
    return (char*)v->data + index * v->elem_size;
}

这里有个关键点:为什么用 void*?因为我们要支持任意类型。用 char* 做指针算术,每次移动 elem_size 字节。这就是泛型编程的雏形。

核心思想:void* 指针 + 元素大小 + 指针算术 = C 语言的泛型容器。这个模式在很多开源库中都能看到。

项目四:内存调试工具

最后一个项目,是我个人最推荐的。写一个内存调试工具,能帮你追踪 malloc/free 的调用情况。

为什么要做这个?因为内存泄漏是 C 程序的头号杀手。我曾经在一个项目中,内存泄漏查了整整三天,最后发现是某个回调函数里忘了 free。

typedef struct MemRecord {
    void *ptr;           // 分配的内存地址
    size_t size;         // 分配的大小
    const char *file;    // 文件名
    int line;            // 行号
    struct MemRecord *next;
} MemRecord;

static MemRecord *head = NULL;

// 带追踪的 malloc
void* debug_malloc(size_t size, 
                   const char *file, int line) {
    void *ptr = malloc(size);
    if (ptr == NULL) return NULL;
    
    // 记录分配信息
    MemRecord *rec = malloc(sizeof(MemRecord));
    rec->ptr = ptr;
    rec->size = size;
    rec->file = file;
    rec->line = line;
    rec->next = head;
    head = rec;
    
    return ptr;
}

// 带追踪的 free
void debug_free(void *ptr) {
    MemRecord *prev = NULL, *curr = head;
    while (curr != NULL) {
        if (curr->ptr == ptr) {
            // 找到记录,从链表中移除
            if (prev == NULL) head = curr->next;
            else prev->next = curr->next;
            
            free(curr);
            free(ptr);
            return;
        }
        prev = curr;
        curr = curr->next;
    }
    // 没找到记录,说明是非法释放
    fprintf(stderr, "警告:释放未记录的内存 %p\n", ptr);
}

// 打印所有未释放的内存
void debug_print_leaks(void) {
    MemRecord *curr = head;
    int count = 0;
    while (curr != NULL) {
        printf("泄漏: %p, %zu 字节, %s:%d\n",
               curr->ptr, curr->size,
               curr->file, curr->line);
        count++;
        curr = curr->next;
    }
    printf("总计 %d 处内存泄漏\n", count);
}

使用技巧:用宏定义替换 malloc/free,这样不用改原有代码。比如:#define malloc(s) debug_malloc(s, __FILE__, __LINE__)。这样每次分配都会自动记录位置。

这个工具的核心就是链表 + 指针。每个分配记录是一个节点,用指针串起来。释放时从链表中删除节点。最后检查链表是否为空,就知道有没有泄漏了。

四个项目的知识图谱

下面这张图,把这四个项目用到的指针知识串起来了。你看看,是不是每个项目都离不开指针?

指针综合实战:四个项目的知识体系 指针 核心技能 内存池 指针算术 void* 转换 链表指针 JSON解析器 字符串指针 指针遍历 结构体指针 动态数组 void* + 指针算术 内存调试工具 链表 + 函数指针 每个项目都围绕指针展开,覆盖了指针的核心应用场景

这四个项目,我建议你一个一个亲手敲一遍。别光看,代码要跑起来。调试工具那个项目尤其值得做——做完之后,你写代码时心里会更有底。

嗯,指针这东西,说白了就是多练。练多了,你自然就懂了。我在带新人时经常说:指针不是学出来的,是用出来的。


公众号:蓝海资料掘金营,微信deep3321