31、C++项目实战:智能指针实现
智能指针,说白了就是C++里帮你自动管理内存的“小管家”。
我刚开始写C++那会儿,最头疼的就是手动new和delete。项目一复杂,漏掉delete是常事,内存泄漏查起来简直要命。后来C++11引入了智能指针,我才算真正解脱。今天我们就自己动手,从零实现一个简单的智能指针。
为什么需要智能指针?
你想想看,传统指针有什么问题?
- 忘记释放:new了不delete,内存泄漏
- 重复释放:多个指针指向同一块内存,一个delete了,另一个再delete就崩了
- 异常安全:函数中途抛出异常,delete语句没执行到
智能指针通过RAII(资源获取即初始化)机制,把内存管理绑定到对象的生命周期上。对象析构时自动释放内存,完美解决上述问题。
核心思想:智能指针是一个栈上的对象,它包装了一个原始指针。当智能指针对象离开作用域时,它的析构函数会自动delete所管理的指针。
智能指针的种类
标准库提供了三种智能指针,我们这次重点实现最常用的两种:
| 类型 | 所有权 | 特点 |
|---|---|---|
std::unique_ptr |
独占所有权 | 不能拷贝,只能移动 |
std::shared_ptr |
共享所有权 | 引用计数,最后一个释放时删除 |
std::weak_ptr |
弱引用 | 配合shared_ptr,解决循环引用 |
我个人习惯是:能用unique_ptr就不用shared_ptr。为什么?因为引用计数有性能开销,而且容易搞出循环引用。我在项目中遇到过好几次,就是因为滥用shared_ptr导致内存泄漏,排查了两天才找到原因。
实现一个简易的unique_ptr
我们先从最简单的开始。unique_ptr的核心就是:禁止拷贝,允许移动。
template <typename T>
class MyUniquePtr {
private:
T* ptr_;
public:
// 构造函数
explicit MyUniquePtr(T* p = nullptr) : ptr_(p) {}
// 析构函数
~MyUniquePtr() {
delete ptr_;
}
// 禁止拷贝
MyUniquePtr(const MyUniquePtr&) = delete;
MyUniquePtr& operator=(const MyUniquePtr&) = delete;
// 允许移动
MyUniquePtr(MyUniquePtr&& other) noexcept : ptr_(other.ptr_) {
other.ptr_ = nullptr;
}
MyUniquePtr& operator=(MyUniquePtr&& other) noexcept {
if (this != &other) {
delete ptr_;
ptr_ = other.ptr_;
other.ptr_ = nullptr;
}
return *this;
}
// 运算符重载
T& operator*() const { return *ptr_; }
T* operator->() const { return ptr_; }
T* get() const { return ptr_; }
// 释放所有权
T* release() {
T* temp = ptr_;
ptr_ = nullptr;
return temp;
}
// 重置
void reset(T* p = nullptr) {
delete ptr_;
ptr_ = p;
}
};
小提示:移动语义是C++11引入的。移动构造函数把原指针置空,这样原对象析构时就不会重复释放了。这个技巧在实现智能指针时非常关键。
实现shared_ptr:引用计数的艺术
shared_ptr要复杂一些,因为它需要多个智能指针共享同一块内存。核心机制是引用计数。
嗯,这里要注意:引用计数本身必须是线程安全的。我们先用一个简单的非线程安全版本,让你理解原理。
template <typename T>
class MySharedPtr {
private:
T* ptr_;
int* ref_count_; // 引用计数
public:
// 构造函数
explicit MySharedPtr(T* p = nullptr) : ptr_(p), ref_count_(new int(1)) {}
// 析构函数
~MySharedPtr() {
release();
}
// 拷贝构造
MySharedPtr(const MySharedPtr& other)
: ptr_(other.ptr_), ref_count_(other.ref_count_) {
if (ref_count_) {
(*ref_count_)++;
}
}
// 拷贝赋值
MySharedPtr& operator=(const MySharedPtr& other) {
if (this != &other) {
release(); // 释放当前资源
ptr_ = other.ptr_;
ref_count_ = other.ref_count_;
if (ref_count_) {
(*ref_count_)++;
}
}
return *this;
}
// 运算符重载
T& operator*() const { return *ptr_; }
T* operator->() const { return ptr_; }
T* get() const { return ptr_; }
int use_count() const { return ref_count_ ? *ref_count_ : 0; }
private:
void release() {
if (ref_count_) {
(*ref_count_)--;
if (*ref_count_ == 0) {
delete ptr_;
delete ref_count_;
}
}
}
};
避坑指南:我曾经在项目里犯过一个低级错误——把引用计数定义成静态成员变量。结果所有shared_ptr实例共享同一个计数,完全乱套了。记住:每个被管理的对象对应一个独立的引用计数。
核心逻辑流程图
下面这张图展示了shared_ptr的引用计数工作原理:
从图中可以看到:三个shared_ptr共享同一个堆对象,引用计数为3。当某个shared_ptr析构时,引用计数减1。只有当引用计数变为0时,才会真正delete堆对象和引用计数块。
实战中的注意事项
实现智能指针时,有几个坑我踩过,分享给你:
- 不要用同一个原始指针构造多个shared_ptr:这样会导致多个独立的引用计数,析构时重复释放。应该用make_shared。
- 循环引用问题:两个对象互相持有shared_ptr,会导致内存泄漏。解决方案是用weak_ptr打破循环。
- 数组支持:标准库的unique_ptr支持数组特化(
unique_ptr<T[]>),析构时调用delete[]。我们上面的实现没有处理,你可以自己加上。 - 自定义删除器:有时候你需要自定义释放逻辑(比如文件句柄的关闭),智能指针应该支持传入删除器。
个人经验:我建议你在实际项目中使用标准库的智能指针,而不是自己造的轮子。自己实现主要是为了理解原理。标准库的智能指针经过了大量测试和优化,性能更好,也更安全。
总结
智能指针是C++现代编程的基石。通过自己实现,你就能深刻理解RAII、引用计数、移动语义这些核心概念。
说白了,智能指针就是把内存管理这件麻烦事,交给编译器去操心。你只需要关注业务逻辑,剩下的交给智能指针就好。
嗯,代码量不大,但背后的设计思想很值得玩味。如果你能把这个实现吃透,C++的内存管理就算入门了。