指针综合实战:四个必练项目
学指针学到这个份上,我觉得是时候来点真家伙了。前面那些概念、语法、陷阱,说白了都是热身。真正让你把指针刻进骨子里的,是动手做项目。
这一章,我挑了四个经典实战项目。它们覆盖了指针最常见的应用场景:内存管理、数据解析、动态容器、调试辅助。每个项目都不大,但足够让你把指针玩得转。
项目一:简易内存池
先说说内存池。我在做嵌入式项目时,最头疼的就是频繁 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__)。这样每次分配都会自动记录位置。
这个工具的核心就是链表 + 指针。每个分配记录是一个节点,用指针串起来。释放时从链表中删除节点。最后检查链表是否为空,就知道有没有泄漏了。
四个项目的知识图谱
下面这张图,把这四个项目用到的指针知识串起来了。你看看,是不是每个项目都离不开指针?
这四个项目,我建议你一个一个亲手敲一遍。别光看,代码要跑起来。调试工具那个项目尤其值得做——做完之后,你写代码时心里会更有底。
嗯,指针这东西,说白了就是多练。练多了,你自然就懂了。我在带新人时经常说:指针不是学出来的,是用出来的。
公众号:蓝海资料掘金营,微信deep3321