开篇介绍:
hello 大家,那么在上一篇博客中,我们学习了智能指针,那么在本篇博客中,我们就来模拟实现一下auto_ptr、unique_ptr、shared_ptr,相信我,真的很简单!!!
在 C++ 的学习之路上,"智能指针" 绝对是绕不开的核心知识点 ------ 它既是 RAII 思想的经典落地,也是解决手动内存管理噩梦的 "终极武器"。但很多开发者对智能指针的理解,只停留在 "会用" 的层面:知道 unique_ptr 独占、shared_ptr 共享、auto_ptr 过时,却不清楚它们底层是如何实现的,为什么要这么设计,以及不同设计背后的权衡取舍。
开篇:为什么要自己写智能指针?
在正式动手之前,我们先想一个问题:C++ 标准库已经提供了现成的智能指针,为什么还要花时间自己模拟实现?
答案很简单:"会用" 和 "理解" 之间,差了一个 "亲手实现" 的距离。就像我们知道汽车能跑,但只有拆开引擎,才明白动力如何传递;知道智能指针能自动释放内存,却只有亲手写一遍,才懂 RAII 思想如何落地,引用计数如何设计,移动语义如何应用。
模拟实现的核心目标,不是为了在实际项目中替代标准库(标准库的实现更严谨、更高效),而是为了:
- 吃透 RAII 的核心逻辑(资源获取即初始化,对象析构即释放);
- 理解不同智能指针的设计取舍(独占 vs 共享,安全 vs 灵活);
- 掌握 C++11 关键特性的实战应用(移动语义、function 包装器、模板编程);
- 遇到智能指针相关的 bug 时,能快速定位问题根源。
一、auto_ptr:C++98 的 "先驱者"------ 虽不完美,但意义重大
auto_ptr 是 C++98 标准中第一个智能指针,它的设计思路很简单:用对象生命周期管理动态内存,拷贝时转移资源管理权。虽然它因为致命缺陷被 C++11 淘汰,但作为智能指针的 "先驱者",其核心逻辑(RAII + 运算符重载)是后续所有智能指针的基础。
1. auto_ptr 的设计思路
auto_ptr 的核心目标:解决 "手动 delete 容易遗漏" 的问题,让内存自动释放。
设计核心:
- 遵循 RAII:构造函数接收动态内存指针(new 返回值),析构函数自动 delete 该指针;
- 模拟普通指针:重载
*和->运算符,让用户使用方式和普通指针一致; - 拷贝语义:拷贝构造 / 赋值时,将资源管理权从原对象转移到新对象(原对象悬空)。
类比理解:auto_ptr 就像一个 "霸道总裁"------ 他手里有一份资源(动态内存),当你想 "拷贝" 他的资源时,他会直接把资源抢过来给你,自己手里什么都不留,让你成为新的 "所有者"。
2. auto_ptr 的完整模拟实现(带详细注释)
cpp
#include <iostream>
// 为了避免和标准库冲突,我们自己实现的智能指针放在自定义命名空间win中
namespace win
{
template <typename T>
class auto_ptr
{
public:
// 1. 构造函数:接收动态内存指针,绑定资源
explicit auto_ptr(T* ptr) // explicit禁止隐式转换(关键!避免裸指针意外转为智能指针)
: _ptr(ptr) // 把外界new的资源指针,存储到类的私有成员变量中
{}
// 2. 析构函数:自动释放资源(RAII核心)
~auto_ptr()
{
if(_ptr != nullptr) // 避免重复释放空指针
{
std::cout << "auto_ptr释放资源:" << _ptr << std::endl;
delete _ptr; // 自动delete资源
}
_ptr = nullptr; // 置空,避免野指针
}
// 3. 拷贝构造函数:转移资源管理权(auto_ptr的核心特性)
auto_ptr(auto_ptr<T>& autoptr)
{
// 核心逻辑:把autoptr的资源抢过来,自己接管
this->_ptr = autoptr._ptr;
autoptr._ptr = nullptr; // 原对象置空,失去资源管理权(悬空)
std::cout << "auto_ptr拷贝构造:资源管理权转移" << std::endl;
}
// 4. 赋值运算符重载:先释放自己的资源,再转移别人的资源
auto_ptr<T>& operator=(auto_ptr<T>& autoptr)
{
// 防止自己赋值给自己(比如ap1 = ap1)
if(&autoptr != this)
{
// 第一步:先释放当前对象管理的资源(如果有的话)
if(this->_ptr != nullptr)
{
std::cout << "auto_ptr赋值:释放原有资源" << std::endl;
delete this->_ptr;
}
// 第二步:转移autoptr的资源管理权
this->_ptr = autoptr._ptr;
autoptr._ptr = nullptr; // 原对象悬空
std::cout << "auto_ptr赋值:资源管理权转移" << std::endl;
}
else
{
std::cout << "auto_ptr赋值:自我赋值,无需操作" << std::endl;
}
return *this; // 支持链式赋值(如ap1 = ap2 = ap3)
}
// 5. 重载*运算符:解引用,访问资源(模拟普通指针)
T& operator*() const
{
return *_ptr; // 直接对成员指针解引用,返回对象引用
}
// 6. 重载->运算符:访问对象成员(模拟普通指针)
T* operator->() const
{
return _ptr; // 返回成员指针,编译器会自动补一个->(如ap->func()等价于(ap.operator->())->func())
}
private:
T* _ptr = nullptr; // 存储指向动态资源的裸指针
};
}
3. 核心代码逐行解析
(1)构造函数:explicit auto_ptr(T* ptr)
explicit关键字 :禁止裸指针隐式转换为 auto_ptr。比如auto_ptr<int> ap = new int(10)会编译报错,必须显式写auto_ptr<int> ap(new int(10))。- 为什么要禁止隐式转换?避免 "无意识的资源管理权转移"------ 比如函数参数是 auto_ptr,你不小心传入裸指针,后续再手动 delete,会导致重复释放。
- 资源绑定 :将外界 new 的裸指针存储到私有成员
_ptr,完成 "资源获取即初始化"。
(2)析构函数:~auto_ptr()
- RAII 核心 :对象超出作用域(函数结束、异常栈展开)时,编译器自动调用析构函数,释放
_ptr指向的资源。 - 安全检查 :先判断
_ptr != nullptr,避免对空指针执行 delete(虽然 C++ 标准允许 delete nullptr,但显式判断更严谨)。 - 置空指针 :释放后将
_ptr置为 nullptr,避免野指针(后续误操作*_ptr会直接崩溃,而非访问非法内存)。
(3)拷贝构造:auto_ptr(auto_ptr<T>& autoptr)
- 资源转移逻辑 :把被拷贝对象
autoptr的_ptr赋值给当前对象,然后将autoptr._ptr置空。- 示例:
auto_ptr<int> ap1(new int(10)); auto_ptr<int> ap2(ap1);- 执行后:
ap2._ptr = 原ap1._ptr,ap1._ptr = nullptr(ap1 悬空)。
- 执行后:
- 示例:
- 设计初衷:早期 C++ 没有移动语义,为了避免 "多个 auto_ptr 管理同一份资源"(导致重复释放),只能让拷贝时转移管理权。
(4)赋值运算符重载:operator=(auto_ptr<T>& autoptr)
- 自我赋值检查 :
if(&autoptr != this),避免ap1 = ap1时,先释放ap1的资源,再转移自己的(已释放)资源。 - 释放原有资源:赋值前,当前对象可能已经管理了一份资源,必须先 delete,否则会内存泄漏。
- 资源转移:和拷贝构造逻辑一致,转移后被赋值对象悬空。
(5)operator*和operator->:模拟普通指针
operator*:返回*_ptr,让用户可以像普通指针一样解引用,比如*ap = 20。operator->:返回_ptr,编译器会自动补一个->,比如ap->func()等价于(ap.operator->())->func(),和普通指针用法完全一致。
4. auto_ptr 的致命缺陷与淘汰原因
虽然 auto_ptr 实现了 "自动释放内存",但它的 "拷贝转移管理权" 设计存在致命问题:
(1)拷贝后原对象悬空,容易引发崩溃
cpp
#include <iostream>
using namespace std;
using namespace win;
int main() {
auto_ptr<int> ap1(new int(100));
auto_ptr<int> ap2 = ap1; // 拷贝构造,ap1悬空
cout << *ap2 << endl; // 正常:输出100
cout << *ap1 << endl; // 崩溃:ap1._ptr是nullptr,解引用空指针
return 0;
}
- 问题本质:拷贝操作改变了原对象的状态,这和普通指针的拷贝逻辑完全不同(普通指针拷贝后,两个指针都指向同一资源),开发者很容易忘记 "原对象已悬空",从而误操作。
(2)不能用于容器存储
容器(如 vector)在扩容、排序、拷贝时,会频繁调用元素的拷贝构造函数。如果容器存储 auto_ptr,会导致大量对象悬空:
vector<auto_ptr<int>> vec;
vec.push_back(auto_ptr<int>(new int(10)));
vec.push_back(auto_ptr<int>(new int(20)));
// 扩容时,vec会拷贝原有auto_ptr到新内存,原对象悬空,后续访问vec[0]会崩溃
(3)不支持数组资源管理
auto_ptr 的析构函数用的是delete _ptr,而非delete[] _ptr,如果用它管理 new \[\] 分配的数组,会导致内存泄漏(只释放数组首元素,其余元素未析构):
auto_ptr<int> ap(new int[5]); // 错误:析构时执行delete,而非delete[]
正因为这些缺陷,C++11 标准彻底淘汰了 auto_ptr,推荐用 unique_ptr 替代它的 "独占式管理" 场景。
5. auto_ptr 的总结
| 优点 | 缺点 | 适用场景 |
|---|---|---|
| 实现简单,符合 RAII | 拷贝后原对象悬空,易崩溃 | 无(已淘汰) |
| 自动释放内存,避免泄漏 | 不支持容器存储 | - |
| 模拟普通指针用法,学习成本低 | 不支持数组管理 | - |
核心启示 :auto_ptr 的失败,告诉我们一个重要的设计原则 ------拷贝操作不应该改变原对象的状态。这也是后续 unique_ptr 和 shared_ptr 的设计核心。
二、unique_ptr:C++11 的 "独占王者"------ 安全优先,拒绝拷贝
unique_ptr 是 auto_ptr 的 "升级版",它的设计思路更简单粗暴:既然拷贝会导致问题,那就直接禁止拷贝。同时,它支持移动语义,允许 "显式转移资源管理权",既解决了 auto_ptr 的安全问题,又保留了灵活性。
1. unique_ptr 的设计思路
核心目标:在 auto_ptr 的基础上,解决 "拷贝后原对象悬空" 的安全问题,实现 "独占式资源管理"。
设计核心:
- 继承 RAII:构造函数绑定资源,析构函数自动释放;
- 禁止拷贝:删除拷贝构造函数和拷贝赋值运算符,从语法上杜绝拷贝;
- 支持移动:通过移动构造和移动赋值,允许显式转移资源管理权(用
std::move触发); - 优化细节:支持数组管理、空指针判断等,实用性更强。
类比理解:unique_ptr 就像一个 "专属管家"------ 它只服务于一份资源,绝不允许别人 "拷贝" 它的管理权;如果一定要转移,必须你明确 "下令"(std::move),它才会把资源交给别人,自己则彻底 "下岗"(悬空)。
2. unique_ptr 的完整模拟实现(带详细注释)
cpp
#include <iostream>
#include <utility> // 包含std::move
#include <cassert> // 包含assert断言
using namespace std;
template <typename T>
class unique_ptr
{
public:
// 1. 构造函数:支持普通指针和空构造
explicit unique_ptr(T* ptr = nullptr) // explicit禁止隐式转换
: _ptr(ptr)
{}
// 2. 析构函数:自动释放资源(支持普通指针和数组)
~unique_ptr()
{
if (_ptr != nullptr)
{
// 注意:这里模拟实现简化为delete,标准库中对数组类型有特化(delete[])
delete _ptr;
cout << "unique_ptr释放资源:" << _ptr << endl;
}
_ptr = nullptr;
}
// 3. 禁止拷贝:删除拷贝构造和拷贝赋值(核心设计)
unique_ptr(const unique_ptr&) = delete; // 禁止拷贝构造
unique_ptr& operator=(const unique_ptr&) = delete; // 禁止拷贝赋值
// 4. 移动构造:显式转移资源管理权(noexcept避免异常)
unique_ptr(unique_ptr<T>&& uniqueptr) noexcept
: _ptr(uniqueptr._ptr) // 直接接管原对象的资源
{
uniqueptr._ptr = nullptr; // 原对象置空,失去管理权
cout << "unique_ptr移动构造:资源管理权转移" << endl;
}
// 5. 移动赋值:先释放自己的资源,再转移别人的
unique_ptr<T>& operator=(unique_ptr<T>&& uniqueptr) noexcept
{
// 防止自我赋值
if (this == &uniqueptr)
{
cout << "unique_ptr移动赋值:自我赋值,无需操作" << endl;
return *this;
}
// 第一步:释放当前对象的资源
if (_ptr != nullptr)
{
delete _ptr;
cout << "unique_ptr移动赋值:释放原有资源" << endl;
}
// 第二步:接管原对象的资源
_ptr = uniqueptr._ptr;
uniqueptr._ptr = nullptr; // 原对象置空
cout << "unique_ptr移动赋值:资源管理权转移" << endl;
return *this;
}
// 6. 重载*和->:模拟普通指针访问
T& operator*() const
{
assert(_ptr != nullptr && "unique_ptr: 解引用空指针!"); // 断言,调试时触发
return *_ptr;
}
T* operator->() const
{
assert(_ptr != nullptr && "unique_ptr: 访问空指针成员!");
return _ptr;
}
// 7. 辅助接口:获取裸指针(谨慎使用)
T* get() const
{
return _ptr;
}
// 8. 重载operator bool:支持直接判断是否为空
explicit operator bool() const
{
return _ptr != nullptr;
}
private:
T* _ptr = nullptr; // 存储指向资源的裸指针
};
// 数组特化版本(解决普通unique_ptr不能管理数组的问题)
template <typename T>
class unique_ptr<T[]>
{
public:
explicit unique_ptr(T* ptr = nullptr)
: _ptr(ptr)
{}
~unique_ptr()
{
if (_ptr != nullptr)
{
delete[] _ptr; // 数组专用:delete[]
cout << "unique_ptr<T[]>释放数组资源:" << _ptr << endl;
}
_ptr = nullptr;
}
// 禁止拷贝
unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;
// 移动构造和移动赋值
unique_ptr(unique_ptr<T[]>&& uniqueptr) noexcept
: _ptr(uniqueptr._ptr)
{
uniqueptr._ptr = nullptr;
cout << "unique_ptr<T[]>移动构造" << endl;
}
unique_ptr<T[]>& operator=(unique_ptr<T[]>&& uniqueptr) noexcept
{
if (this == &uniqueptr)
return *this;
if (_ptr != nullptr)
delete[] _ptr;
_ptr = uniqueptr._ptr;
uniqueptr._ptr = nullptr;
cout << "unique_ptr<T[]>移动赋值" << endl;
return *this;
}
// 重载[]:支持数组下标访问
T& operator[](size_t idx) const
{
assert(_ptr != nullptr && "unique_ptr<T[]>: 访问空数组!");
return _ptr[idx];
}
T* get() const
{
return _ptr;
}
explicit operator bool() const
{
return _ptr != nullptr;
}
private:
T* _ptr = nullptr;
};
3. 核心代码逐行解析
(1)禁止拷贝:= delete的妙用
这是 unique_ptr 最核心的设计!通过unique_ptr(const unique_ptr&) = delete和unique_ptr& operator=(const unique_ptr&) = delete,直接删除了拷贝构造和拷贝赋值运算符。
-
效果:任何尝试拷贝 unique_ptr 的行为都会编译报错:
unique_ptr<int> up1(new int(10)); unique_ptr<int> up2(up1); // 编译报错:拷贝构造已删除 unique_ptr<int> up3; up3 = up1; // 编译报错:拷贝赋值已删除 -
设计意义:从语法上杜绝 "多个 unique_ptr 管理同一份资源" 的可能,彻底解决了 auto_ptr 的拷贝安全问题。
(2)移动语义:&&和std::move
既然禁止了拷贝,那如何转移资源管理权?答案是 "移动语义"------ 这是 C++11 引入的核心特性,用于 "将资源从一个对象转移到另一个对象,原对象不再有效"。
-
移动构造函数 :
unique_ptr(unique_ptr<T>&& uniqueptr) noexcept- 参数是 "右值引用"(
&&),只能绑定到右值(如临时对象、std::move转换后的左值); - 逻辑:直接接管
uniqueptr的_ptr,然后将uniqueptr._ptr置空,原对象不再管理任何资源; noexcept:告诉编译器这个函数不会抛异常,让编译器可以进行优化(如容器扩容时避免额外的异常安全检查)。
- 参数是 "右值引用"(
-
使用示例:
unique_ptr<int> up1(new int(100)); unique_ptr<int> up2 = std::move(up1); // 触发移动构造,up1悬空 if (!up1) { cout << "up1已无资源管理权" << endl; // 输出 } cout << *up2 << endl; // 正常:100
(3)数组特化版本:unique_ptr<T[]>
普通 unique_ptr 的析构函数用delete,不能管理 new \[\] 分配的数组。为了解决这个问题,我们提供了数组特化版本:
-
析构函数用
delete[],正确释放数组资源; -
重载
operator[],支持像普通数组一样用下标访问(up[0]); -
使用示例:
unique_ptr<int[]> up(new int[5]{1,2,3,4,5}); cout << up[2] << endl; // 输出3 // 析构时自动执行delete[],释放整个数组
(4)辅助接口:get()和operator bool
-
get():返回内部的裸指针,方便在需要裸指针的场景中使用(如调用 C 风格函数),但要谨慎使用 ------ 避免用get()返回的指针手动 delete,或赋值给其他智能指针; -
operator bool:支持直接用if (up)判断智能指针是否管理资源,和普通指针的用法一致:unique_ptr<int> up; if (!up) { cout << "up未管理任何资源" << endl; } up = make_unique<int>(20); // C++14的make_unique,模拟实现见下文 if (up) { cout << "up管理的资源:" << *up << endl; }
(5)断言assert:增强调试体验
在operator*和operator->中加入assert,当用户尝试解引用空指针时,调试模式下会直接触发断言,显示错误信息,帮助快速定位问题( Release 模式下 assert 会被优化掉,不影响性能)。
4. unique_ptr 的关键特性与使用场景
(1)核心特性总结
- 独占性:一份资源只能被一个 unique_ptr 管理,禁止拷贝,避免重复释放;
- 移动语义 :支持显式转移管理权(
std::move),转移后原对象悬空; - 高效性:大小和普通指针一致(只存储一个裸指针),无额外性能开销;
- 安全性:析构自动释放,支持数组管理,有断言和空指针判断;
- 灵活性:支持自定义删除器(标准库实现中,我们的模拟实现简化了这部分,后面 shared_ptr 会详细讲)。
(2)常用场景
-
局部动态内存管理:函数内 new 的资源,用 unique_ptr 管理,函数结束自动释放:
void func() { unique_ptr<Date> up(new Date(2025, 12, 23)); up->show(); // 安全使用,无需手动delete } -
函数返回动态对象:返回 unique_ptr,调用方自动接管资源,避免裸指针返回导致的泄漏:
unique_ptr<Date> createDate(int y, int m, int d) { return unique_ptr<Date>(new Date(y, m, d)); // 自动转为右值,触发移动构造 } int main() { auto up = createDate(2025, 12, 23); // 接收返回值,无需手动释放 up->show(); return 0; } -
容器存储动态对象:vector 等容器存储 unique_ptr,避免拷贝,支持高效转移:
vector<unique_ptr<int>> vec; vec.push_back(unique_ptr<int>(new int(10))); // 临时对象,自动移动 vec.push_back(std::move(unique_ptr<int>(new int(20)))); // 显式移动 // 遍历访问 for (const auto& up : vec) { cout << *up << " "; // 输出10 20 } // 容器析构时,自动释放所有unique_ptr管理的资源 -
管理数组资源 :用
unique_ptr<T[]>管理 new \[\] 分配的数组,自动用delete[]释放:unique_ptr<int[]> up(new int[5]{1,2,3,4,5}); up[0] = 100; cout << up[0] << endl; // 输出100
5. unique_ptr 的优缺点总结
| 优点 | 缺点 | 适用场景 |
|---|---|---|
| 完全独占,无拷贝安全问题 | 不支持资源共享 | 局部动态内存、函数返回对象 |
| 轻量级,性能和裸指针一致 | 移动后原对象悬空(显式,风险低) | 容器存储动态对象 |
| 支持数组管理、自定义删除器 | 不能用于多线程共享资源 | 数组资源管理 |
| 自动释放,无内存泄漏 | - | 不需要共享资源的所有场景 |
核心启示:unique_ptr 的设计哲学是 "安全优先,兼顾灵活"------ 禁止拷贝避免了大部分错误,移动语义又满足了资源转移的需求,是日常开发中 "独占式资源管理" 的首选。
三、shared_ptr:C++11 的 "共享王者"------ 引用计数,灵活共享
unique_ptr 解决了 "独占式管理" 的问题,但实际开发中,我们经常需要 "多个指针共享同一份资源"(比如多线程访问同一个对象、多个模块共享同一个配置)。这时候,shared_ptr 应运而生 ------ 它通过 "引用计数" 机制,实现了资源的安全共享。
1. shared_ptr 的设计思路
核心目标:支持多指针共享同一份资源,同时保证资源自动释放(当最后一个共享指针析构时)。
设计核心:
- 继承 RAII:构造绑定资源,析构自动释放;
- 引用计数:用动态开辟的
int*存储引用计数(所有共享指针共享同一个计数); - 拷贝语义:拷贝构造 / 赋值时,引用计数 + 1;
- 析构逻辑:析构时引用计数 - 1,当计数为 0 时,释放资源和引用计数;
- 支持移动:移动构造 / 赋值时,直接接管资源,不改变引用计数(原对象置空);
- 灵活删除:用
std::function包装删除器,支持普通函数、lambda、仿函数等。
类比理解:shared_ptr 就像 "合租房子"------ 一套房子(资源)可以有多个租客(shared_ptr),门口放一个计数器(引用计数),租客入住时计数器 + 1,退租时 - 1;当最后一个租客退租时,计数器为 0,房子被回收(资源释放)。
2. shared_ptr 的核心难题:引用计数如何设计?
shared_ptr 最关键的设计是 "引用计数",而设计引用计数的核心难题是:如何让所有共享同一份资源的 shared_ptr,都能访问到同一个计数器?
我们先排除两个错误方案,再看正确方案:
(1)错误方案 1:普通 int 成员变量
如果把引用计数设计为普通 int 成员(int _ref_count),每个 shared_ptr 对象都会有一个独立的计数器副本:
- 示例:
shared_ptr<int> sp1(new int(10)); shared_ptr<int> sp2(sp1);- sp1 的
_ref_count=1,sp2 的_ref_count=1(拷贝时复制值); - 析构 sp2 时,
_ref_count--变为 0,释放资源; - 析构 sp1 时,
_ref_count--变为 0,再次释放资源 → 双重释放,程序崩溃。
- sp1 的
结论:普通 int 成员无法实现 "共享计数",直接淘汰。
(2)错误方案 2:static int 静态成员
如果把引用计数设计为 static int(static int _ref_count),所有 shared_ptr 对象共享同一个计数器:
- 示例:
shared_ptr<int> sp1(new int(10)); shared_ptr<string> sp2(new string("test"));- sp1 构造时,
_ref_count=1; - sp2 构造时,
_ref_count=2(不同资源共享同一个计数); - 析构 sp1 时,
_ref_count=1,不释放资源; - 析构 sp2 时,
_ref_count=0,释放资源 → 不知道释放哪个资源,导致内存泄漏或崩溃。
- sp1 构造时,
结论:static int 会混淆不同资源的计数,且外部可篡改,不安全,淘汰。
(3)正确方案:动态开辟的 int * 指针(int* _ref_count)
这是 shared_ptr 引用计数的最优解,核心逻辑:
- 每个资源对应一个独立的计数器(动态开辟在堆上,
new int(1)); - 所有共享该资源的 shared_ptr,都存储指向这个计数器的指针(
_ref_count); - 拷贝构造 / 赋值时,共享同一个
_ref_count,计数器 + 1; - 析构时,计数器 - 1,当计数为 0 时,释放资源和计数器。
类比理解:每个房子(资源)门口挂一个独立的计数器(堆上的 int),所有租客(shared_ptr)都拿着这个计数器的 "地址",入住时通过地址找到计数器 + 1,退租时 - 1;最后一个租客退租时,计数器为 0,房子和计数器一起被回收。
优势:
- 同资源共享计数:所有共享指针的
_ref_count指向同一个堆上 int; - 不同资源独立计数:不同资源的计数器是不同的堆上 int,互不干扰;
- 安全性高:
_ref_count是私有成员,外部无法篡改; - 生命周期匹配:资源释放时,计数器也被释放,无内存泄漏。
3. shared_ptr 的完整模拟实现(带详细注释)
#include <iostream>
#include <utility>
#include <functional> // 包含std::function(用于包装删除器)
#include <cassert>
#include <cstdlib> // 包含malloc/free(用于测试自定义删除器)
using namespace std;
namespace win
{
template <typename T>
class shared_ptr
{
public:
// 1. 空构造函数:不管理任何资源
shared_ptr()
: _ptr(nullptr)
, _ref_count(nullptr)
, _deleter([](T* ptr) { delete ptr; }) // 默认删除器:delete
{}
// 2. 构造函数:绑定资源和删除器
explicit shared_ptr(T* ptr, function<void(T*)> deleter = [](T* ptr) { delete ptr; })
: _ptr(ptr)
, _deleter(deleter)
{
if (_ptr != nullptr)
{
_ref_count = new int(1); // 动态开辟计数器,初始值1(第一个指针指向资源)
cout << "shared_ptr构造:资源" << _ptr << ",引用计数=1" << endl;
}
else
{
_ref_count = nullptr;
}
}
// 3. 拷贝构造函数:共享资源,计数+1
shared_ptr(const shared_ptr& sp)
: _ptr(sp._ptr)
, _ref_count(sp._ref_count)
, _deleter(sp._deleter)
{
// 如果sp管理了资源,计数+1
if (_ref_count != nullptr)
{
++(*_ref_count);
cout << "shared_ptr拷贝构造:资源" << _ptr << ",引用计数=" << *_ref_count << endl;
}
}
// 4. 拷贝赋值运算符:先释放自己的资源,再共享别人的
shared_ptr& operator=(const shared_ptr& sp)
{
// 防止自我赋值(sp1 = sp1)
if (this->_ptr == sp._ptr)
{
cout << "shared_ptr拷贝赋值:自我赋值,无需操作" << endl;
return *this;
}
// 第一步:释放当前对象管理的资源(如果有的话)
release();
// 第二步:共享sp的资源和计数器
_ptr = sp._ptr;
_ref_count = sp._ref_count;
_deleter = sp._deleter;
// 第三步:计数+1
if (_ref_count != nullptr)
{
++(*_ref_count);
cout << "shared_ptr拷贝赋值:资源" << _ptr << ",引用计数=" << *_ref_count << endl;
}
return *this;
}
// 5. 移动构造函数:接管资源,不改变计数
shared_ptr(shared_ptr&& sp) noexcept
: _ptr(sp._ptr)
, _ref_count(sp._ref_count)
, _deleter(move(sp._deleter)) // 移动删除器(避免拷贝)
{
// 原对象置空,不再管理任何资源
sp._ptr = nullptr;
sp._ref_count = nullptr;
cout << "shared_ptr移动构造:接管资源" << _ptr << endl;
}
// 6. 移动赋值运算符:先释放自己的,再接管别人的
shared_ptr& operator=(shared_ptr&& sp) noexcept
{
if (this->_ptr == sp._ptr)
{
cout << "shared_ptr移动赋值:自我赋值,无需操作" << endl;
return *this;
}
// 第一步:释放当前对象的资源
release();
// 第二步:接管sp的资源、计数器、删除器
_ptr = sp._ptr;
_ref_count = sp._ref_count;
_deleter = move(sp._deleter);
// 第三步:原对象置空
sp._ptr = nullptr;
sp._ref_count = nullptr;
cout << "shared_ptr移动赋值:接管资源" << _ptr << endl;
return *this;
}
// 7. 析构函数:计数-1,为0则释放资源和计数器
~shared_ptr()
{
release();
}
// 8. 重载*和->:模拟普通指针访问
T& operator*() const
{
assert(_ptr != nullptr && "shared_ptr: 解引用空指针!");
return *_ptr;
}
T* operator->() const
{
assert(_ptr != nullptr && "shared_ptr: 访问空指针成员!");
return _ptr;
}
// 9. 辅助接口:获取裸指针
T* get() const
{
return _ptr;
}
// 10. 辅助接口:获取引用计数
int use_count() const
{
return _ref_count != nullptr ? *_ref_count : 0;
}
// 11. 重载operator bool:判断是否管理资源
explicit operator bool() const
{
return _ptr != nullptr;
}
private:
// 核心辅助函数:释放当前对象的资源(计数-1,为0则删除)
void release()
{
if (_ref_count != nullptr)
{
--(*_ref_count); // 计数-1
cout << "shared_ptr释放:资源" << _ptr << ",引用计数=" << *_ref_count << endl;
// 计数为0:释放资源和计数器
if (*_ref_count == 0)
{
_deleter(_ptr); // 调用自定义删除器释放资源
delete _ref_count; // 释放计数器
cout << "shared_ptr释放:资源" << _ptr << "和计数器已删除" << endl;
}
// 置空,避免野指针
_ptr = nullptr;
_ref_count = nullptr;
}
}
private:
T* _ptr = nullptr; // 指向资源的裸指针
int* _ref_count = nullptr; // 指向引用计数的指针(堆上动态开辟)
function<void(T*)> _deleter; // 删除器(支持多种可调用对象)
};
// 数组特化版本(支持new[]分配的数组)
template <typename T>
class shared_ptr<T[]>
{
public:
shared_ptr()
: _ptr(nullptr)
, _ref_count(nullptr)
, _deleter([](T* ptr) { delete[] ptr; }) // 数组默认删除器:delete[]
{}
explicit shared_ptr(T* ptr, function<void(T*)> deleter = [](T* ptr) { delete[] ptr; })
: _ptr(ptr)
, _deleter(deleter)
{
if (_ptr != nullptr)
{
_ref_count = new int(1);
cout << "shared_ptr<T[]>构造:数组资源" << _ptr << ",引用计数=1" << endl;
}
else
{
_ref_count = nullptr;
}
}
// 拷贝构造、拷贝赋值、移动构造、移动赋值(逻辑和普通版本一致,略)
shared_ptr(const shared_ptr& sp)
: _ptr(sp._ptr)
, _ref_count(sp._ref_count)
, _deleter(sp._deleter)
{
if (_ref_count != nullptr)
{
++(*_ref_count);
cout << "shared_ptr<T[]>拷贝构造:引用计数=" << *_ref_count << endl;
}
}
shared_ptr& operator=(const shared_ptr& sp)
{
if (this->_ptr == sp._ptr)
return *this;
release();
_ptr = sp._ptr;
_ref_count = sp._ref_count;
_deleter = sp._deleter;
if (_ref_count != nullptr)
{
++(*_ref_count);
cout << "shared_ptr<T[]>拷贝赋值:引用计数=" << *_ref_count << endl;
}
return *this;
}
shared_ptr(shared_ptr&& sp) noexcept
: _ptr(sp._ptr)
, _ref_count(sp._ref_count)
, _deleter(move(sp._deleter))
{
sp._ptr = nullptr;
sp._ref_count = nullptr;
cout << "shared_ptr<T[]>移动构造" << endl;
}
shared_ptr& operator=(shared_ptr&& sp) noexcept
{
if (this->_ptr == sp._ptr)
return *this;
release();
_ptr = sp._ptr;
_ref_count = sp._ref_count;
_deleter = move(sp._deleter);
sp._ptr = nullptr;
sp._ref_count = nullptr;
cout << "shared_ptr<T[]>移动赋值" << endl;
return *this;
}
~shared_ptr()
{
release();
}
// 重载[]:支持数组下标访问
T& operator[](size_t idx) const
{
assert(_ptr != nullptr && "shared_ptr<T[]>: 访问空数组!");
return _ptr[idx];
}
T* get() const
{
return _ptr;
}
int use_count() const
{
return _ref_count != nullptr ? *_ref_count : 0;
}
explicit operator bool() const
{
return _ptr != nullptr;
}
private:
void release()
{
if (_ref_count != nullptr)
{
--(*_ref_count);
cout << "shared_ptr<T[]>释放:引用计数=" << *_ref_count << endl;
if (*_ref_count == 0)
{
_deleter(_ptr);
delete _ref_count;
cout << "shared_ptr<T[]>释放:数组资源和计数器已删除" << endl;
}
_ptr = nullptr;
_ref_count = nullptr;
}
}
private:
T* _ptr = nullptr;
int* _ref_count = nullptr;
function<void(T*)> _deleter;
};
// 辅助函数:make_shared(更高效的构造shared_ptr,避免二次内存分配)
template <typename T, typename... Args>
shared_ptr<T> make_shared(Args&&... args)
{
// 动态开辟T对象,传入构造参数(完美转发)
return shared_ptr<T>(new T(forward<Args>(args)...));
}
// 数组版本make_shared(C++20支持,这里模拟实现)
template <typename T>
shared_ptr<T[]> make_shared(size_t size)
{
return shared_ptr<T[]>(new T[size]);
}
}
4. 核心代码逐行解析
(1)成员变量:_ptr、_ref_count、_deleter
T* _ptr:存储指向资源的裸指针,和其他智能指针一致;int* _ref_count:指向堆上引用计数的指针,所有共享资源的 shared_ptr 都指向同一个_ref_count;function<void(T*)> _deleter:删除器,用std::function包装,支持普通函数、lambda、仿函数等,默认是delete ptr。
(2)构造函数:shared_ptr(T* ptr, function<void(T*)> deleter)
- 资源绑定 :
_ptr = ptr,绑定外界传入的资源; - 计数器初始化 :如果
ptr != nullptr,动态开辟_ref_count = new int(1),初始计数为 1(第一个指针指向资源); - 删除器初始化 :默认删除器是
delete ptr,用户也可以传入自定义删除器(如delete[]、fclose等)。
(3)拷贝构造:shared_ptr(const shared_ptr& sp)
- 共享资源 :
_ptr = sp._ptr、_ref_count = sp._ref_count,当前对象和sp共享同一个资源和计数器; - 计数 + 1 :如果
_ref_count != nullptr,++(*_ref_count),表示又多了一个指针指向资源。
示例运行过程:
win::shared_ptr<int> sp1(new int(100)); // 构造:_ref_count=1
win::shared_ptr<int> sp2 = sp1; // 拷贝构造:_ref_count=2
cout << sp1.use_count() << endl; // 输出2
cout << sp2.use_count() << endl; // 输出2
(4)拷贝赋值:operator=(const shared_ptr& sp)
拷贝赋值比拷贝构造复杂,因为当前对象可能已经管理了一份资源,需要先释放:
- 自我赋值检查 :
if (this->_ptr == sp._ptr),避免自我赋值; - 释放当前资源 :调用
release()函数,计数器 - 1,若为 0 则释放资源和计数器; - 共享新资源 :
_ptr = sp._ptr、_ref_count = sp._ref_count; - 计数 + 1:新资源的计数器 + 1。
示例运行过程:
win::shared_ptr<int> sp1(new int(100)); // 计数=1
win::shared_ptr<int> sp2(new int(200)); // 计数=1
sp2 = sp1; // 拷贝赋值
// 步骤:
// 1. sp2释放原有资源:计数1→0,释放200的资源和计数器;
// 2. sp2共享sp1的资源:_ptr=100的地址,_ref_count=sp1的计数器;
// 3. 计数+1:sp1的计数器1→2;
// 最终:sp1和sp2都指向100,计数=2
(5)移动构造:shared_ptr(shared_ptr&& sp) noexcept
- 接管资源 :
_ptr = sp._ptr、_ref_count = sp._ref_count,当前对象直接接管sp的资源和计数器; - 原对象置空 :
sp._ptr = nullptr、sp._ref_count = nullptr,sp不再管理任何资源; - 不改变计数:移动构造只是 "接管",不是 "共享",所以计数器的值不变。
示例运行过程:
win::shared_ptr<int> sp1(new int(100)); // 计数=1
win::shared_ptr<int> sp2 = std::move(sp1); // 移动构造
cout << sp2.use_count() << endl; // 输出1(计数不变)
cout << (bool)sp1 << endl; // 输出false(sp1已置空)
(6)析构函数与release()函数
析构函数直接调用release()函数,release()是 shared_ptr 的核心辅助函数:
- 计数 - 1 :
--(*_ref_count),当前指针不再指向资源; - 判断计数是否为 0 :
- 若为 0:调用
_deleter(_ptr)释放资源,delete _ref_count释放计数器; - 若不为 0:不释放资源,计数器继续有效;
- 若为 0:调用
- 置空指针 :
_ptr = nullptr、_ref_count = nullptr,避免野指针。
示例运行过程:
{
win::shared_ptr<int> sp1(new int(100)); // 计数=1
win::shared_ptr<int> sp2 = sp1; // 计数=2
}
// 析构sp2:计数2→1,不释放;
// 析构sp1:计数1→0,释放资源和计数器;
(7)删除器:function<void(T*)> _deleter
shared_ptr 的删除器支持多种可调用对象,极大提升了灵活性:
- 管理数组资源 :传入
delete[]的删除器; - 管理文件句柄 :传入
fclose的删除器; - 管理 malloc 内存 :传入
free的删除器。
示例:自定义删除器的使用:
// 1. 管理数组资源(用lambda作为删除器)
win::shared_ptr<int> sp1(new int[5], [](int* ptr) {
delete[] ptr;
cout << "删除数组资源" << endl;
});
// 2. 管理文件句柄(用lambda作为删除器)
FILE* fp = fopen("test.txt", "w");
win::shared_ptr<FILE> sp2(fp, [](FILE* ptr) {
fclose(ptr);
cout << "关闭文件" << endl;
});
// 3. 管理malloc内存(用普通函数作为删除器)
void freeInt(int* ptr) {
free(ptr);
cout << "释放malloc内存" << endl;
}
int* p = (int*)malloc(sizeof(int));
win::shared_ptr<int> sp3(p, freeInt);
(8)辅助函数make_shared
make_shared是构造 shared_ptr 的推荐方式,比直接new T更高效:
- 普通版本 :
make_shared<T>(args...),直接构造 T 对象,避免二次内存分配(标准库中make_shared会将对象和计数器分配在同一块内存,我们的模拟实现简化为new T(...)); - 数组版本 :
make_shared<T[]>(size),构造数组对象,删除器默认是delete[]。
使用示例:
// 普通对象
auto sp1 = win::make_shared<int>(100);
auto sp2 = win::make_shared<Date>(2025, 12, 23);
// 数组对象
auto sp3 = win::make_shared<int[]>(5); // 构造5个int的数组
sp3[0] = 10;
cout << sp3[0] << endl; // 输出10
5. shared_ptr 的关键特性与使用场景
(1)核心特性总结
- 共享性:多指针共享同一份资源,拷贝构造 / 赋值时计数 + 1;
- 自动释放:最后一个指针析构时,计数为 0,释放资源和计数器;
- 灵活性:支持自定义删除器,可管理数组、文件句柄、malloc 内存等;
- 高效性:虽然比 unique_ptr 多一个计数器指针,但开销很小,日常使用可忽略;
- 线程安全:引用计数的增减是原子操作(标准库实现,我们的模拟实现简化为普通操作),多线程拷贝 / 析构 shared_ptr 是安全的,但访问资源本身需要手动加锁。
(2)常用场景
-
多线程共享资源:多个线程访问同一个对象,用 shared_ptr 管理,避免线程退出后资源泄漏:
void threadFunc(win::shared_ptr<Date> sp) { // 线程安全:拷贝sp时,计数+1 sp->show(); } int main() { auto sp = win::make_shared<Date>(2025, 12, 23); thread t1(threadFunc, sp); thread t2(threadFunc, sp); t1.join(); t2.join(); cout << sp.use_count() << endl; // 输出1(线程结束后,sp1和sp2析构,计数1) return 0; } -
跨模块共享资源:多个模块共享同一个配置对象,用 shared_ptr 管理,无需关心谁先释放:
// 模块A创建配置 win::shared_ptr<Config> createConfig() { return win::make_shared<Config>("config.json"); } // 模块B使用配置 void moduleB(win::shared_ptr<Config> sp) { sp->load(); } // 模块C使用配置 void moduleC(win::shared_ptr<Config> sp) { sp->save(); } int main() { auto config = createConfig(); moduleB(config); moduleC(config); return 0; // 所有模块使用完毕,config析构,释放资源 } -
管理非 new 资源:用自定义删除器管理 malloc、文件句柄、数组等:
// 管理malloc内存 auto sp1 = win::shared_ptr<int>((int*)malloc(sizeof(int)), [](int* p) { free(p); cout << "释放malloc内存" << endl; }); // 管理文件句柄 FILE* fp = fopen("test.txt", "r"); auto sp2 = win::shared_ptr<FILE>(fp, [](FILE* p) { fclose(p); cout << "关闭文件" << endl; }); // 管理数组 auto sp3 = win::make_shared<int[]>(5);
6. shared_ptr 的缺点与注意事项
shared_ptr 虽然强大,但也有一些缺点,需要特别注意:
(1)循环引用问题(最致命)
当两个对象互相用 shared_ptr 指向对方时,会形成循环引用,导致引用计数永远不为 0,资源无法释放:
class Node {
public:
int value;
win::shared_ptr<Node> next; // 互相引用
win::shared_ptr<Node> prev;
Node(int val) : value(val) {
cout << "Node构造:" << val << endl;
}
~Node() {
cout << "Node析构:" << value << endl; // 循环引用时不会执行
}
};
int main() {
auto n1 = win::make_shared<Node>(1);
auto n2 = win::make_shared<Node>(2);
n1->next = n2; // n2的计数=2
n2->prev = n1; // n1的计数=2
// 析构n1和n2:计数各减1,变为1,无法释放资源
return 0;
}
解决方案:用 weak_ptr(shared_ptr 的辅助指针)打破循环引用,weak_ptr 不增加引用计数,本文暂不展开,后续会专门讲解。
(2)不能管理栈指针
和其他智能指针一样,shared_ptr 不能管理栈上的指针,否则会导致 double free:
int a = 10;
win::shared_ptr<int> sp(&a); // 错误:析构时会delete栈指针,程序崩溃
(3)同一裸指针构造多个 shared_ptr
用同一个裸指针构造多个 shared_ptr,会导致多个独立的计数器,析构时重复释放:
int* p = new int(100);
win::shared_ptr<int> sp1(p); // 计数器1
win::shared_ptr<int> sp2(p); // 计数器1(独立)
// 析构时sp1释放p,sp2再次释放p → 双重释放,程序崩溃
解决方案 :始终用make_shared构造 shared_ptr,或确保一个裸指针只构造一个 shared_ptr。
(4)性能开销
shared_ptr 比 unique_ptr 多一个计数器指针,且计数的增减需要原子操作(标准库实现),存在轻微的性能开销。如果不需要共享资源,优先用 unique_ptr。
7. shared_ptr 的优缺点总结
| 优点 | 缺点 | 适用场景 |
|---|---|---|
| 支持多指针共享资源 | 存在循环引用风险 | 多线程共享资源 |
| 支持自定义删除器,灵活性高 | 性能开销比 unique_ptr 略大 | 跨模块共享资源 |
| 自动释放,无内存泄漏 | 不能管理栈指针 | 管理非 new 资源(malloc、文件等) |
| 支持数组管理 | 同一裸指针构造多个会崩溃 | 需要共享资源的所有场景 |
核心启示:shared_ptr 是 "共享式资源管理" 的首选,但使用时必须注意循环引用问题。它的设计哲学是 "灵活优先,兼顾安全"------ 用引用计数实现共享,用删除器实现灵活的资源释放,是 C++ 中最强大的智能指针。
四、三大智能指针核心对比与总结
1. 核心差异对比表
| 特性 | auto_ptr(已淘汰) | unique_ptr | shared_ptr |
|---|---|---|---|
| 资源管理方式 | 独占,拷贝转移管理权 | 独占,禁止拷贝,支持移动 | 共享,引用计数 |
| 拷贝语义 | 支持(转移管理权,不安全) | 禁止(= delete) | 支持(计数 + 1,安全) |
| 移动语义 | 不支持(C++98 无移动语义) | 支持(显式 std::move) | 支持(接管资源,计数不变) |
| 大小 | 1 个指针大小 | 1 个指针大小 | 2 个指针大小(资源 + 计数器) |
| 性能 | 无额外开销 | 无额外开销 | 轻微开销(计数原子操作) |
| 数组管理 | 不支持 | 支持(unique_ptr<T \[\]>) | 支持(shared_ptr<T \[\]>) |
| 自定义删除器 | 不支持 | 支持 | 支持(function 包装) |
| 线程安全 | 不支持 | 不支持共享,无线程安全问题 | 计数增减线程安全,资源访问需手动加锁 |
| 主要缺陷 | 拷贝后原对象悬空,不支持容器 | 不支持共享 | 循环引用,性能开销 |
| 适用场景 | 无(已淘汰) | 局部内存、函数返回、容器存储 | 多线程共享、跨模块共享、非 new 资源 |
2.模拟实现的核心收获
通过手动实现三大智能指针,我们不仅掌握了它们的底层原理,更理解了 C++ 的核心设计思想:
- RAII 思想:资源的生命周期与对象的生命周期绑定,是智能指针的灵魂;
- 设计取舍:没有完美的工具,只有适合的工具 ------unique_ptr 牺牲共享换安全,shared_ptr 牺牲性能换灵活;
- C++11 特性的实战:移动语义(&&、std::move)、function 包装器、模板特化、完美转发等,这些特性不是孤立的,而是为了解决实际问题;
- 工程化思维:智能指针的设计不仅要考虑功能,还要考虑安全性(explicit、assert)、灵活性(删除器)、性能(计数器设计)。
3. 实际开发中的选择原则
- 优先用 unique_ptr:如果不需要共享资源,unique_ptr 是首选 ------ 安全、高效、无额外开销;
- 需要共享用 shared_ptr:多线程、跨模块共享资源时,用 shared_ptr,但要注意避免循环引用;
- 永远不用 auto_ptr:用 unique_ptr 替代,避免拷贝后悬空的安全问题;
- 管理数组用特化版本:unique_ptr<T \[\]> 或 shared_ptr<T \[\]>,避免用普通版本导致的内存泄漏;
- 管理非 new 资源用 shared_ptr:自定义删除器更灵活,shared_ptr 的删除器使用更方便(无需指定删除器类型)。
完整代码:
auto_ptr.hpp:
cpp
#include <iostream>
#include <utility>
//ok,那么在本文件中,简单的实现一下auto_ptr智能指针
//其实他的本质就是指针转移,将拷贝对象的资源占为己有
namespace win
{
template <typename T>
class auto_ptr
{
public:
//构造函数
explicit auto_ptr(T* ptr)//防止隐式转换
:_ptr(ptr)//把外界new到的资源,也就是指针变量丢给成员变量
{}
//析构函数,负责把申请到的资源delete掉
~auto_ptr()
{
if(_ptr!=nullptr)
{
std::cout<<"delete auto_ptr"<<std::endl;
delete _ptr;
}
_ptr=nullptr;
}
//拷贝构造
//将传过来的auto_ptr的资源占为己有
auto_ptr(auto_ptr<T>& autoptr)
{
//很简单,直接把指针转移就行,资源不变
//霸道总裁
this->_ptr=autoptr._ptr;
autoptr._ptr=nullptr;//将被拷贝的智能指针的指针成员变量赋值为空,避免出现野指针
std::cout<<"拷贝构造"std::endl;
}
//重载!=运算符,用于比较两个智能指针变量是不是一样的
// bool operator!=(auto_ptr<T>& autoptr)
// {
// return this->_ptr!=autoptr._ptr;
// }
//赋值运算符重载函数
auto_ptr<T>& operator=(auto_ptr<T>& autoptr)
{
//和拷贝构造一样的道理
if(&autoptr!=this)//直接比较两个变量的地址
{
//先释放自己的资源
if(this->_ptr!=nullptr)
{
delete this->_ptr;
}
this->_ptr=autoptr._ptr;
autoptr._ptr=nullptr;
std::cout<<"赋值运算符重载函数"<<std::endl;
}
else
{
std::cout<<"=for myself"<<std::endl;
}
return *this;
}
//移动构造
auto_ptr(auto_ptr<T>&& autoptr) noexcept
{
//对于auto_ptr来说,移动构造和拷贝构造一样的道理
//所以我这里也就不实现了,没有意义
}
//移动赋值
auto_ptr<T>& operator=(auto_ptr<T>&& autoptr) noexcept
{
//和拷贝赋值一样的道理
//依旧不实现
return *this;
}
//智能指针肯定也要有和普通指针差不多的效果
//所以我们这里需要重载*和->运算符
//获取到指针所指向的资源
T& operator*() const
{
return *_ptr;//就是这么简单,对我们的成员变量解引用即可
}
T* operator->() const
{
return _ptr;
//返回成员变量指针本身即可,因为编译器会自动省略一个->,所以其实是auto_ptr->->指针内部变量
}
private:
T* _ptr=nullptr;//指针成员变量
};
}
//auto_ptr由于会将被拷贝的变量资源给悬空,所以一直被人诟病
//所以我们这里实现也只是为了大致了解智能指针的一些原理
//智能指针的出现主要就是用于RAII风格,而这一块内容在我们之前的mutex的RAII风格封装的时候,就已经讲过了
//智能指针真正比较实用常用的,还是unique_ptr和shared_ptr,我们后面也都会去一一实现。
unique_ptr.hpp:
cpp
#include <iostream>
//本文件就来实现一下unique_ptr指针
//它的特性就是,不支持拷贝不支持拷贝赋值
//什么,你就是要赋值???
//那不好意思,请你使用移动构造和移动赋值
//你自己显式使用move去把要拷贝的左值智能指针给抢劫了
//然后你再传给我,然后我才去将那个被拷贝的资源给抢走!!!
//那么unique_ptr搞这一套就很鸡贼了,是你自己要把被拷贝的智能指针给悬空的
//可不是我自己偷摸干的,由此一来,别人想骂他也有心无力
//那么unique_ptr怎么禁止拷贝构造和拷贝赋值呢?
//其实就是使用delete给禁止罢了,还是很简单的
//我们下面就来简单实现一下
//本质上是和auto_ptr的实现思路差不多
template <typename T>
class unique_ptr
{
public:
//构造函数
explicit unique_ptr(T* ptr=nullptr)//禁止隐式转换
:_ptr(ptr)
{}
//析构函数
~unique_ptr()
{
//释放资源罢了
if(_ptr!=nullptr)
{
delete _ptr;
std::cout<<"释放资源"<<std::endl;
}
_ptr=nullptr;//置空指针,避免野指针
}
//那么接下来就是要禁用unique_ptr的拷贝构造和拷贝赋值了
unique_ptr(const unique_ptr&)=delete;
unique_ptr& operator=(const unique_ptr&)=delete;
//然后就是实现像普通指针一样去用智能指针
//也就是重载*和->运算符,这个在auto_ptr那里是有讲到过了
T& operator*() const
{
return *_ptr;//so easy
}
T* operator->() const
{
return _ptr;//直接返回指针本身即可
}
//重载一个==,用于比较两个智能指针变量是不是同一个
//那么其实本质上就是比较他们两个指针成员变量是不是一样的
//用一个万能模板,其实用const 巴拉巴拉也行,这里纯粹是为了用上C++11
// template<typename X>
// bool operator==(X&& uniqueptr)
// {
// return this->_ptr==uniqueptr._ptr;//本质上是访问对方成员变量,所以不用完美转发
// }
// bool operator==(const unique_ptr<T>& uniqueptr)
// {
// return this->_ptr==uniqueptr._ptr;
// }
//实现移动构造函数
//本质就是掠夺资源
unique_ptr(unique_ptr<T>&& uniqueptr) noexcept
:_ptr(uniqueptr._ptr)//这么实现比较高效一点
{
//this->_ptr=uniqueptr._ptr;
//将被移动的指针变量置空
uniqueptr._ptr=nullptr;
std::cout<<"移动构造"<<std::endl;
}
//实现移动赋值,和移动构造的思路差不多
//多了一步将本身资源置空的操作
unique_ptr<T>& operator=(unique_ptr<T>&& uniqueptr) noexcept
{
if(this==&uniqueptr)//可以直接比较两个变量的地址即可,最高效实用
{
std::cout<<"=for myself"<<std::endl;
}
else
{
//先将自身资源清空
if(this->_ptr!=nullptr)
{
delete _ptr;
}
//掠夺资源
this->_ptr=uniqueptr._ptr;
uniqueptr._ptr=nullptr;//避免野指针
std::cout<<"移动赋值"<<std::endl;
}
return *this;
}
private:
T* _ptr=nullptr;//依旧是内置指针变量拿来使用
};
//unique_ptr的使用算是还行,但是也有一定的问题,就是不能对左值进行拷贝构造以及拷贝赋值
//所以后面也就出现了shared_ptr,而它才是智能指针的主力军,我们后续也主要是用它来进行RAII
//后续我们也会去模拟实现shared_ptr
shared_ptr.hpp:
cpp
#include <iostream>
#include <functional>
#include <cassert>
// OK,那么在本文件中,我们就来实现智能指针中最常用的一个指针:shared_ptr!!!
// 那么它其实和unique_ptr差不多,只是它支持拷贝构造和=运算符重载
// 那么它的最牛逼的一点就是引用计数了,即每次有一个新的shared_ptr对象指向同一份指针时
// 那么引用计数就要+1,那么其实我们也就知道了
// 当触发shared_ptr的拷贝构造和=运算符重载的时候,引用计数就得+1
// 而最开始初始化构造shared_ptr的时候,引用计数要为最开始的0++为1,代表申请一份资源
// 且有一个shared_ptr指向这份资源!!!
// 但是要怎么实现引用计数呢???
// 毋庸置疑的是引用计数肯定是整型变量
// 但是这个整型变量最重要的一点就是,它要能被所有指向同一份资源的shared_ptr对象看到!!!
// 所以它就不能是单纯的int整型变量了!!!
// 那么它应该是什么呢???有人可能会说是static int
// 可是这么一来,所有其他的类啊什么的,也都能访问到,一下子就不安全了
// 所以我们应该怎么办呢???
// 答案是:动态开辟出整型指针变量!!!
// 每次初始化新的shared_ptr指向新的资源时,我们就new一个新的整型指针变量出来
// 然后每次拷贝构造初始化的时候,就把对方的整型指针变量也赋值给this本身
// 使得二者的整型指针变量是指向同一个引用计数!!!
// (要注意赋值=的时候得先把this自身的引用计数减一(若减至0则释放资源和计数),
// 再共享赋值对象的计数指针,计数+1;因为赋值是要把指向其他资源的shared_ptr变量给this自己
// 即this指向的资源要改变为别的资源,所以得如上那么操作!!!)
// 这样子就能实现这个整型指针变量只服务于这个资源
// 当我们使用整型指针变量作为shared_ptr的成员变量时
// 其他地方访问不到该引用计数,安全性大大增高
// 而其他相同类型的shared_ptr却都能访问到它
// (因为它们的整型指针变量是同一个,不同类型的shared_ptr的整型指针变量则不是同一个),
// 对他进行解引用然后++or--
// 所以,这个就是最好的解决引用计数的方式!!!
// 大家要仔细去理解上面所说的,这是关键中的关键!!!
// 要彻底理解"shared_ptr为何必须用动态开辟的整型指针实现引用计数",
// 需先拆解「普通int成员」「static int」的致命缺陷,
// 再分析动态指针的核心优势------核心逻辑是:
// 引用计数需要满足「同资源共享、不同资源独立、外部不可篡改」三大要求,
// 只有动态开辟的整型指针能同时满足这三点!
// ===================== 一、普通int成员变量实现引用计数的致命问题 =====================
// 若将引用计数设计为shared_ptr的普通int成员变量(如int _ref_count;),
// 每个shared_ptr对象都会拥有独立的_ref_count副本,
// 完全失去"引用计数"的核心意义:
// 示例场景(普通int成员的错误表现):
// 1. 构造第一个shared_ptr对象sp1:sp1._ref_count = 1(表示有1个对象指向资源);
// 2. 用sp1拷贝构造sp2:sp2._ref_count = sp1._ref_count(即sp2._ref_count=1),
// 但sp1和sp2的_ref_count是两个独立的int变量,
// 各自存在于不同对象的内存中;
// 3. 此时对sp2进行析构:sp2._ref_count--(变为0),触发资源释放;
// 4. 但sp1的_ref_count仍为1,sp1析构时会再次执行"_ref_count--(变为0)+ 释放资源",
// 导致「双重释放」程序崩溃;
// 通俗类比:
// 就像两个同学各拿一个笔记本,都记录"同一支笔的使用人数"------A同学记"1",B同学抄A的"1";
// B用完笔后把自己本子上的数改成"0"并扔了笔,A看到自己本子上还是"1",用完后又去扔一次笔,显然是错误的。
// 结论:
// 普通int成员是"每个对象一份副本",
// 无法让指向同一份资源的多个shared_ptr共享同一个计数,完全不符合引用计数的设计初衷。
// ===================== 二、static int静态成员变量实现引用计数的致命问题 =====================
// 有人会想:既然普通int不能共享,那用static int(类级别的静态成员)总可以吧?
// 但static int的问题更严重------
// 它是「所有shared_ptr对象共用同一个计数」,不管这些对象指向的是哪份资源:
// 示例场景(static int的错误表现):
// 1. 构造sp1管理资源p1:static int _ref_count = 1(此时所有shared_ptr对象的计数都是1);
// 2. 构造sp3管理另一份资源p3:static int _ref_count++(变为2),
// 但sp3指向的是p3,却把sp1的p1的计数也加了1;
// 3. 析构sp1:_ref_count--(变为1),因计数未到0,不释放p1;
// 4. 析构sp3:_ref_count--(变为0),触发"释放资源"操作------但此时程序无法区分该释放p1还是p3,
// 最终要么错误释放p1(p3泄漏),要么错误释放p3(p1泄漏);
// 更致命的问题:
// static int是类级别的全局变量,外部代码或其他类若想篡改,
// 能通过"SharedPtr::_ref_count"直接访问,安全性完全无法保障。
// 通俗类比:
// 全校同学共用一个笔记本记录"所有笔的使用人数"------不管是红色笔、蓝色笔,都记在同一个数字里;
// 红色笔被用一次,数字+1;蓝色笔被用一次,数字也+1;红色笔用完减1,蓝色笔用完也减1,
// 根本分不清哪支笔该回收、哪支笔还在使用。
// 结论:
// static int虽实现了"共享",但丢失了"不同资源计数独立"的核心要求,且安全性极低,完全不可行。
// ===================== 三、核心答案:动态开辟的整型指针是最优方案(为什么可以?) =====================
// 我们将引用计数设计为「int* _ref_count_ptr」(指向堆内存的整型指针),
// 作为shared_ptr的私有成员变量------
// 动态开辟的核心优势体现在4个维度:
// 优势1:完美实现「同资源共享计数」
// 指向同一份资源的所有shared_ptr,
// 持有同一个_ref_count_ptr(即所有对象的指针都指向堆上同一块int内存)。
// 示例场景(动态指针的正确表现):
// 1. 构造sp1管理资源p1:_ref_count_ptr = new int(1)
// (在堆上开辟一块int内存,值为1,_ref_count_ptr指向它);
// 2. 用sp1拷贝构造sp2:sp2._ref_count_ptr = sp1._ref_count_ptr
// (sp2的指针直接指向sp1的堆内存,而非复制值);
// 3. 此时执行sp2._ref_count_ptr++:是对堆上同一块int内存的值++(变为2),
// sp1的_ref_count_ptr解引用后也能看到2,
// 真正实现"同资源计数共享";
// 4. 析构sp2:*_ref_count_ptr--(变为1),计数未到0,不释放资源;
// 5. 析构sp1:*_ref_count_ptr--(变为0),
// 触发"释放p1 + delete _ref_count_ptr",资源和计数内存都被正确清理;
// 通俗类比:
// 所有用同一支笔的同学,都共用同一个挂在笔上的计数器------A同学用笔,计数器+1;B同学用笔,计数器也+1;
// A用完笔,计数器-1;B用完笔,计数器-1;计数器到0时,就把笔回收,完全符合逻辑。
// 优势2:完美实现「不同资源计数独立」
// 不同资源对应不同的动态计数指针,互不干扰。
// 示例场景:
// 1. sp1管理p1:_ref_count_ptr1 = new int(1)(堆内存地址0x100);
// 2. sp3管理p3:_ref_count_ptr3 = new int(1)(堆内存地址0x200);
// 3. sp1的计数操作仅修改0x100的值,sp3的计数操作仅修改0x200的值,两者完全独立,不会混淆不同资源的计数;
// 通俗类比:
// "红色笔挂红色计数器,蓝色笔挂蓝色计数器",各自的计数增减互不影响,彻底解决了static int的"全局混淆"问题。
// 优势3:极高的「安全性」
// 动态计数指针是shared_ptr的私有成员,外部无法直接访问:
// 1. _ref_count_ptr作为shared_ptr的private成员,外部代码无法直接获取或修改这个指针,更无法篡改堆上的计数值;
// 2. 所有对计数的++/--操作,都只能通过shared_ptr的拷贝构造、赋值运算符重载、析构函数等成员函数完成,
// 且这些操作都是类内部封装好的安全逻辑;
// 3. 对比static int(类级别的全局变量,易被外部篡改),动态指针的安全性提升了一个维度------
// 只有指向同一份资源的shared_ptr能修改同一个计数,其他无关代码完全接触不到。
// 优势4:「生命周期匹配」
// 计数内存与资源内存的生命周期完全绑定:
// - 当引用计数>0时:计数内存持续存在,供所有指向该资源的shared_ptr共享;
// - 当引用计数减至0时:在释放资源(delete _ptr)的同时,
// 也会释放计数内存(delete _ref_count_ptr);
// 结论:
// 做到了"资源在,计数在;资源释放,计数也释放",不会出现计数内存泄漏的问题------
// 而static int的生命周期是整个程序运行期,即使所有资源都释放了,static int仍占用内存,造成浪费。
// ===================== 四、补充:动态计数指针的核心操作时机 =====================
// 1. 初始化构造shared_ptr时:
// _ref_count_ptr = new int(1)(动态开辟计数内存,初始值为1,表示第一个对象指向资源);
// 2. 拷贝构造shared_ptr时:
// _ref_count_ptr = 被拷贝对象的_ref_count_ptr; *(_ref_count_ptr)++(共享同一个计数指针,计数+1);
// 3. 赋值运算符重载时:
// 先将当前对象的计数--(若减至0则释放资源和计数),再共享赋值对象的计数指针,计数+1;
// 因为赋值是要把指向其他资源的shared_ptr变量给this自己
// 即this指向的资源要改变为别的资源,所以得如上那么操作!!!
// 4. 析构shared_ptr时:
// *(_ref_count_ptr)--;若计数减至0,
// 则delete _ptr(释放资源) + delete _ref_count_ptr(释放计数内存);
// ===================== 五、最终总结 =====================
// 动态开辟的整型指针,是唯一能同时满足shared_ptr引用计数「同资源共享、不同资源独立、外部不可篡改、生命周期匹配」
// 四大核心要求的实现方式------既解决了普通int"无法共享"的问题,又解决了static int"全局混淆+不安全"的问题,
// 因此是shared_ptr引用计数的最优解!
// 然后解决了这个问题之后,我们还剩下一个问题,那么就是删除器要怎么搞呢???
// 我们shared_ptr可不想像unique_ptr那样子还得显式实例化删除函数类型,这太鸡肋了
// 但是我们又要怎么把删除函数作为shared_ptr类的成员变量呢???
// 答案是:function包装器!!!
// 是吧,使用它我们就可以把相同返回值且相同参数类型、个数的函数都包装为同一个类型
// 所以我们只要把删除函数这个成员变量的类型设置为function<void(T*)>即可
// 由此不就能顺利解决这个问题了
// 那么接下来,我们就来实现一下
namespace win
{
template <typename T>
class shared_ptr
{
public:
// 默认构造函数:创建空的shared_ptr对象
shared_ptr() //不用禁止隐式转换,无参构造压根不会隐式转换
: _ptr(nullptr)
, _pcount(nullptr)
, _deletefunc([](T* ptr){ delete ptr; })
{}
// 构造函数
explicit shared_ptr(T *ptr, std::function<void(T *)> deletefunc= [](T *ptr){ delete ptr;}) //禁止隐式转换
: _ptr(ptr)// 将传进来的指针变量丢给成员变量
,_deletefunc(deletefunc) // 将外界传来的删除函数丢给成员变量
{
// 在构造函数中
// 我们就得new出引用计数来
// 然后解引用加1
if(_ptr!=nullptr)//传入指针为空的话,就没必要引用计数
{
_pcount = new int(1);//直接在new的时候就给值1
//++(*_pcount);
}
// 还是比较简单的
}
// 析构函数
// 先对引用计数--,要是--后为0了
// 就得把指向的资源以及引用计数该资源给释放掉
// 那么释放指向的资源就要使用删除函数
~shared_ptr()
{
// 先判断引用计数是否为nullptr,不是了
// 再对引用计数--
if(_pcount==nullptr)
{
return ;//直接返回
}
--*(_pcount);
// 要是--后为0了
// 就得把指向的资源以及引用计数该资源给释放掉
// 那么释放指向的资源就要使用删除函数
if (*_pcount == 0)
{
_deletefunc(_ptr);
delete _pcount;
// 要给指针置空哦
_ptr = nullptr;
_pcount = nullptr;
}
}
// 然后就是实现像普通指针一样去用智能指针
// 也就是重载*和->运算符,这个在auto_ptr那里是有讲到过了
T &operator*() const
{
assert(_ptr != nullptr && "shared_ptr: dereference null pointer");
return *_ptr; // so easy
}
T *operator->() const
{
assert(_ptr != nullptr && "shared_ptr: access member of null pointer");
return _ptr; // 直接返回指针本身即可
}
//接着我们要实现拷贝构造函数
//拷贝构造的实现思路也是非常的简单
//就是将被拷贝对象的指针和引用计数还有删除函数都拿给this本类
//然后对引用计数++
shared_ptr(const shared_ptr& sp)
{
_ptr=sp._ptr;//使得this和被拷贝对象的_ptr指向同一块资源
_deletefunc=sp._deletefunc;//使得this和被拷贝对象的删除函数相同
_pcount=sp._pcount;//使得this和被拷贝对象的_pcount指向同一个引用计数
if(_pcount!=nullptr)//在本对象引用计数不为空的时候,才能进行下面的操作
{
++(*_pcount);//对引用计数++
}
}
//实现拷贝赋值运算符重载函数
//也是比较简单的
//需要额外注意的就是赋值肯定不能支持自己赋值给自己(即两个变量指向同一块资源),
//所以我们要先进行判断
//接着把自己的引用计数--(要是减到0了,那就释放资源)
//然后就是和拷贝构造一样的处理了
//只不过引用计数++变成了对新的资源的引用计数的++
shared_ptr& operator=(const shared_ptr& sp)
{
if(_ptr==sp._ptr)//要是两个变量指向同一块资源,那就没必要赋值
{
return *this;
}
if(_pcount!=nullptr)//在本对象引用计数不为空的时候,才能进行--
{
--(*_pcount);
if (*_pcount == 0)
{
_deletefunc(_ptr);
delete _pcount;
// 要给指针置空哦
_ptr = nullptr;
_pcount = nullptr;
}
}
//不管本对象引用计数为不为nullptr,都得执行赋值操作
_ptr=sp._ptr;//使得this和被拷贝对象的_ptr指向同一块资源
_deletefunc=sp._deletefunc;//使得this和被拷贝对象的删除函数相同
_pcount=sp._pcount;//使得this和被拷贝对象的_pcount指向同一个引用计数
if(_pcount!=nullptr)//在本对象引用计数不为空的时候,才能进行下面的操作
{
++(*_pcount);//对引用计数++
}
return *this;
}
//OK,接下来是美丽的移动构造
//实现思路和拷贝构造差不多,简简单单
//只是要注意因为移动本质上是抢劫,所以不用去++引用计数
//因为是移动,所以得把被移动的对象的资源全部置为nullptr
shared_ptr(shared_ptr&& sp) noexcept
{
_ptr=sp._ptr;//使得this和被拷贝对象的_ptr指向同一块资源
_deletefunc=sp._deletefunc;//使得this和被拷贝对象的删除函数相同
_pcount=sp._pcount;//使得this和被拷贝对象的_pcount指向同一个引用计数
//因为是移动,所以得把被移动的对象的资源全部置为nullptr
sp._ptr=sp._pcount=nullptr;
}
//移动赋值,和拷贝赋值的思路差不多
//只是要注意因为移动本质上是抢劫,所以不用去++引用计数
//因为是移动,所以得把被移动的对象的资源全部置为nullptr
shared_ptr& operator=(shared_ptr&& sp) noexcept
{
if(_ptr==sp._ptr)//要是两个变量指向同一块资源,那就没必要赋值
{
return *this;
}
if(_pcount!=nullptr)//在本对象引用计数不为空的时候,才能进行--
{
--(*_pcount);
if (*_pcount == 0)
{
_deletefunc(_ptr);
delete _pcount;
// 要给指针置空哦
_ptr = nullptr;
_pcount = nullptr;
}
}
//不管本对象引用计数为不为nullptr,都得执行赋值操作
_ptr=sp._ptr;//使得this和被拷贝对象的_ptr指向同一块资源
_deletefunc=sp._deletefunc;//使得this和被拷贝对象的删除函数相同
_pcount=sp._pcount;//使得this和被拷贝对象的_pcount指向同一个引用计数
//因为是移动,所以得把被移动的对象的资源全部置为nullptr
sp._ptr=sp._pcount=nullptr;
return *this;
}
private:
T *_ptr=nullptr; // 指针变量本身
int *_pcount=nullptr; // 引用计数,使用整型指针变量
// 删除器,即删除函数,用function包装
std::function<void(T *)> _deletefunc = [](T *ptr){ delete ptr;};
// 得给个缺省值,万一用户没传入删除函数的话
// 而我们的析构函数可就是使用删除函数去释放资源的
// 所以得给个缺省值以防万一
};
}
结语:以代码为舟,渡原理之海 ------ 智能指针模拟实现的终极收获
亲爱的朋友们,当你逐行敲完 shared_ptr 的最后一个分号,看着控制台输出 "shared_ptr 释放:资源和计数器已删除" 的那一刻,这场跨越三大智能指针的模拟实现之旅,也终于画上了圆满的句号。从 auto_ptr 的 "先驱者之殇",到 unique_ptr 的 "独占式之智",再到 shared_ptr 的 "共享式之慧",我们不仅亲手搭建了智能指针的底层框架,更在一行行代码中,触摸到了 C++ 设计思想的精髓。
回望这段旅程,你或许曾为 auto_ptr 的 "拷贝转移" 而困惑,为 unique_ptr 的 "禁止拷贝" 而赞叹,为 shared_ptr 的 "引用计数" 而折服。但我相信,最终沉淀在你心中的,绝不仅仅是几行代码的实现逻辑,更是一套解决问题的思维方式 ------ 如何用 RAII 思想绑定资源生命周期,如何在 "安全" 与 "灵活" 之间做设计取舍,如何用 C++11 特性解决实际工程问题。这些收获,远比 "会用智能指针" 更有价值,也会成为你未来 C++ 学习之路上的 "核心竞争力"。
我们为什么要花这么多精力模拟实现标准库已经存在的智能指针?答案在旅程的终点愈发清晰:标准库是 "黑盒",而模拟实现是 "透明的引擎"。标准库告诉我们 "智能指针能自动释放内存",但模拟实现让我们知道 "它如何自动释放";标准库告诉我们 "unique_ptr 不能拷贝",但模拟实现让我们知道 "它如何通过 = delete 禁止拷贝";标准库告诉我们 "shared_ptr 支持共享",但模拟实现让我们知道 "它如何通过动态计数指针实现共享"。这种从 "知其然" 到 "知其所以然" 的跨越,正是编程能力进阶的关键。
在模拟实现的过程中,我们其实一直在和 C++ 的核心思想对话。auto_ptr 的设计,让我们明白 "拷贝操作不应改变原对象状态" 的设计原则 ------ 它的失败,不是因为思路错误,而是因为受限于 C++98 没有移动语义的时代背景,最终成为了后续智能指针的 "铺路石"。unique_ptr 的设计,让我们看到 "极致安全" 的实现路径 ------ 通过禁止拷贝从语法上杜绝错误,通过移动语义保留资源转移的灵活性,这种 "一刀切" 的设计,恰恰体现了 "取舍" 的智慧。而 shared_ptr 的设计,则让我们领略了 "灵活共享" 的精妙 ------ 用动态计数指针解决共享计数问题,用 function 包装器支持自定义删除器,每一个细节都在平衡 "功能" 与 "安全",让我们明白优秀的工具从来不是 "面面俱到",而是 "精准解决核心问题"。
同时,这场模拟实现也让我们对 C++11 及后续标准的特性有了更深刻的理解。当我们用&&实现移动构造,用std::move触发资源转移,我们不再是死记 "右值引用用于移动语义" 的定义,而是真正理解了它的实战价值 ------ 解决 "如何在不拷贝的情况下转移资源" 的核心问题。当我们用std::function包装删除器,我们不再是单纯记住 "function 能包装可调用对象",而是体会到它如何让 shared_ptr 突破 "只能管理 new 分配资源" 的限制,变得灵活多样。当我们用模板特化实现unique_ptr<T[]>和shared_ptr<T[]>,我们不再是机械套用模板语法,而是理解了 "如何让同一个类适配不同场景" 的工程思维。这些特性不再是孤立的知识点,而是串联起智能指针设计的 "珍珠",让我们看到 C++ 语言的强大与优雅。
当然,我们也要清醒地认识到,模拟实现的智能指针与标准库的实现还有差距 ------ 标准库的智能指针更注重性能优化(如 shared_ptr 的原子计数、make_shared 的内存优化)、异常安全性(如构造过程中抛出异常的资源回收)、以及更多细节处理(如弱指针支持、类型转换等)。但这并不影响我们的收获,因为我们的目标从来不是 "替代标准库",而是 "通过实现理解原理"。就像我们不需要知道汽车引擎的每一个零件如何制造,但理解动力传递的逻辑,能让我们更安全地驾驶;不需要重写标准库,但理解智能指针的底层原理,能让我们在实际开发中更合理地选择、更高效地排查问题。
在实际开发中,当你面对 "如何管理局部动态内存" 的问题时,你会毫不犹豫地选择 unique_ptr,因为你知道它的 "独占性" 能避免大部分错误;当你需要 "多线程共享一个对象" 时,你会谨慎地使用 shared_ptr,同时警惕循环引用的陷阱,因为你知道它的引用计数如何工作;当你需要管理 "malloc 分配的内存" 或 "文件句柄" 时,你会熟练地传入自定义删除器,因为你亲手实现过 function 包装器的逻辑。这些基于 "理解" 的选择,远比 "跟着感觉用" 更可靠,也能让你的代码更健壮、更易维护。
亲爱的朋友,C++ 的学习之路从来都不是一帆风顺的。它不像有些语言那样 "开箱即用",而是需要你深入底层、理解原理,才能真正驾驭。但正是这份 "门槛",让 C++ 在系统编程、高性能计算、游戏开发等领域始终占据不可替代的地位。而智能指针的模拟实现,正是你跨越这个 "门槛" 的重要一步 ------ 它让你明白,C++ 的复杂不是 "无意义的晦涩",而是 "为了更强大、更灵活、更高效" 的必然选择。
当你回顾这段学习经历,你会发现,真正让你成长的,不仅是掌握了智能指针的实现原理,更是在这个过程中培养的 "拆解问题" 的能力 ------ 把复杂的智能指针拆解为构造、析构、运算符重载、计数管理等小模块,逐一攻克;培养的 "换位思考" 的能力 ------ 站在语言设计者的角度,思考为什么要这样设计,背后的权衡是什么;培养的 "精益求精" 的态度 ------ 从 auto_ptr 的缺陷,到 unique_ptr 的优化,再到 shared_ptr 的完善,每一次迭代都是对 "更好" 的追求。
最后,我想对你说:编程的世界里,"亲手实践" 永远是最好的老师。不要害怕啃硬骨头,不要满足于 "会用",多问几个 "为什么",多尝试 "自己写一遍"。当你亲手实现了智能指针,再去看 STL 源码、看框架源码时,你会发现曾经晦涩的代码变得清晰;当你遇到智能指针相关的 bug 时,你能快速定位到问题根源,而不是束手无策。
这场智能指针的模拟实现之旅虽然结束了,但你的 C++ 进阶之路才刚刚开始。接下来,你可以继续深入学习 weak_ptr 如何解决循环引用,探索 atomic 智能指针的线程安全设计,甚至研究更复杂的资源管理模式。但无论你走到哪里,都请记住这段亲手实现的经历 ------ 它告诉我们,再复杂的工具,其底层原理都是由简单的逻辑构成;再难的知识点,只要一步一个脚印地拆解、实践,都能被攻克。
愿你以这段经历为起点,在 C++ 的世界里继续深耕细作,既要仰望星空(追求更强大的功能),也要脚踏实地(夯实底层原理)。相信终有一天,你会感谢现在这个愿意花时间、下功夫、亲手实现智能指针的自己。祝你在编程的道路上,越走越远,收获满满!