18、智能指针(C风格):引用计数、手动实现、RAII思想
说实话,C语言的内存管理一直是个让人又爱又恨的话题。爱的是它给了你完全的控制权,恨的是稍不留神就给你来个段错误或者内存泄漏。我在做嵌入式项目的时候,就曾经因为一个野指针问题排查了整整两天——最后发现是某个模块释放了内存后,另一个模块还在用。
今天咱们聊聊智能指针。别急着说“C语言哪来的智能指针”,咱们可以自己造一个。说白了,就是用结构体+函数封装,把裸指针包起来,让它自己管理自己的生命周期。
18.1 什么是智能指针?
智能指针,本质上是一个包装了原始指针的结构体。它负责在合适的时机自动释放内存。你想想看,如果每次malloc之后都得记着free,那多累啊。智能指针就是帮你干这活的。
我个人的习惯是:能用智能指针的地方,绝不用裸指针。这不是矫情,是血的教训换来的经验。
18.2 引用计数原理
引用计数是智能指针最常用的实现方式。它的逻辑很简单:
- 每有一个指针指向某块内存,计数就加1
- 每有一个指针不再指向它,计数就减1
- 当计数降到0时,自动释放内存
嗯,这里要注意:引用计数必须放在堆上,不能放在栈上。为什么?因为多个智能指针实例需要共享同一个计数器,栈上的变量没法共享。
我曾经在项目里见过有人把计数器定义成全局变量,结果多个不相关的内存块共用了一个计数器,那场面...简直灾难。
18.3 手动实现一个C风格的智能指针
咱们直接上代码。先定义一个结构体:
// 智能指针结构体
typedef struct SmartPtr {
void* ptr; // 裸指针
int* ref_count; // 引用计数(堆上分配)
void (*destructor)(void*); // 析构函数
} SmartPtr;
接下来是创建函数:
SmartPtr* smart_ptr_create(void* ptr, void (*destructor)(void*)) {
SmartPtr* sp = (SmartPtr*)malloc(sizeof(SmartPtr));
if (!sp) return NULL;
sp->ptr = ptr;
sp->ref_count = (int*)malloc(sizeof(int));
if (!sp->ref_count) {
free(sp);
return NULL;
}
*(sp->ref_count) = 1;
sp->destructor = destructor;
return sp;
}
拷贝函数——这是引用计数的关键:
SmartPtr* smart_ptr_copy(SmartPtr* sp) {
if (!sp) return NULL;
(*(sp->ref_count))++; // 计数加1
return sp; // 返回同一个指针
}
释放函数:
void smart_ptr_destroy(SmartPtr* sp) {
if (!sp) return;
(*(sp->ref_count))--; // 计数减1
if (*(sp->ref_count) == 0) {
// 最后一个引用,释放内存
if (sp->destructor && sp->ptr) {
sp->destructor(sp->ptr);
}
free(sp->ref_count);
free(sp);
}
}
18.4 RAII思想
RAII是C++里的概念,全称是“资源获取即初始化”。但咱们C语言也能借鉴这个思想。说白了就是:资源的生命周期和对象的生命周期绑定。
在C语言里怎么实现?用结构体+函数对:
- 构造函数里分配资源(malloc、fopen等)
- 析构函数里释放资源(free、fclose等)
- 用户只管用,不用管释放
举个例子,文件操作的RAII封装:
typedef struct {
FILE* fp;
char* filename;
} FileHandle;
FileHandle* file_open(const char* path, const char* mode) {
FileHandle* fh = (FileHandle*)malloc(sizeof(FileHandle));
fh->fp = fopen(path, mode);
fh->filename = strdup(path);
return fh;
}
void file_close(FileHandle* fh) {
if (fh) {
if (fh->fp) fclose(fh->fp);
free(fh->filename);
free(fh);
}
}
你看,用户只需要调用file_open和file_close,中间不用操心fclose的事。这就是RAII的精髓。
18.5 知识体系总览
下面这张图帮你理清本章的核心逻辑:
18.6 避坑指南
我总结几个常见的坑,都是我曾经踩过的:
- 循环引用:两个智能指针互相指向对方,引用计数永远不为0。解决办法是用弱引用(weak_ptr),但C语言里得自己实现。
- 多线程竞争:引用计数的加减操作不是原子的。我建议用原子操作或者加锁。
- 析构函数为空:如果忘记设置destructor,内存永远不会被释放。嗯,这个坑我掉进去过两次。
18.7 总结
智能指针不是什么高深的技术,说白了就是用结构体把裸指针包起来,加上引用计数,再配上RAII的思想。虽然C语言没有原生的支持,但咱们完全可以自己动手实现。
我个人建议:在团队项目中,把智能指针封装成公共库,统一使用。这样既能减少内存错误,又能提高代码的可维护性。你想想看,如果每个模块都自己管理内存,那排查问题的难度得翻多少倍?
好了,这一章就到这里。记住:好的代码不是写出来的,是设计出来的。智能指针就是设计思维的一个体现。