目录
一. 为什么需要智能指针
- 手动管理动态内存容易忘记 delete,导致内存泄漏。
- 不使用智能指针,在异常发生时可能导致 delete 未执行,从而引发内存泄漏
可以参考下面这个例子
#include <iostream>
#include <stdexcept>
void unsafeFunction() {
int* raw_ptr = new int(100); // 动态分配内存
// 模拟一个可能抛出异常的操作
if (true) { // 假设这里条件触发异常
throw std::runtime_error("Something went wrong!");
}
delete raw_ptr; // 如果抛出异常,这句不会执行!
std::cout << "Resource released (this won't print)\n";
}
int main() {
try {
unsafeFunction();
} catch (const std::exception& e) {
std::cerr << "Caught exception: " << e.what() << "\n";
}
return 0;
}
//运行结果:Caught exception: Something went wrong!
二.RAII和智能指针的设计思路
- RALL是一种资源管理的类的设计思想,通过类的生命周期对获取到的资源进行管理,在类的生命周期内,资源保持有效,类的声明周期结束时,通过析构函数完成对资源的析构,以此防止内存泄漏。
- 智能指针是RAII设计思想的实现,智能指针还会通过重载operator->,operator*等操作符,来模仿指针的行为
三.C++标准库智能指针的使用
智能指针的构造
- unique_ptr,不支持拷贝,只支持移动,一份资源只能由一个对象管理
unique_ptr<Date> up1(new Date);
// 不⽀持拷⻉
//unique_ptr<Date> up2(up1); //错误做法
// ⽀持移动,但是移动后up1也悬空,所以使⽤移动要谨慎
unique_ptr<Date> up3(move(up1));
- shared_ptr
支持拷贝,也支持移动,底层用引用计数的方法实现,
shared_ptr<Date> sp1(new Date);
// ⽀持拷⻉
shared_ptr<Date> sp2(sp1);
//
⽀持移动,但是移动后sp1也悬空,所以使⽤移动要谨慎
shared_ptr<Date> sp4(move(sp1));
- weak_ptr
不支持直接指向对象
用来解决shared_ptr循环引用的问题,可以用shared_ptr初始化,具体在后面的原理讲

- 上述这些智能指针也都可以使用std::make_…接口来进行创建对象,
unique_ptr<Date> up=std::make_unique<Date>;
shared_ptr<Date> sp=std::make_shared<Date>;
删除器的使用
智能指针在析构时默认使用delete析构,这也意味着如果不是new出来的资源,在析构时可能就会出错,标准库提供了传入删除器的接口,unique_ptr和shared_ptr的删除器实现不同,
- 因为new[]经常使⽤,所以unique_ptr和shared_ptr 实现了⼀个特化版本,这个特化版本析构时用的delete[]
unique_ptr<Date[]> up1(new Date[5]);
shared_ptr<Date[]> sp1(new Date[5]);
- unique_ptr在实例化模板阶段传入删除器,建议使用仿函数,函数指针,或者lambda表达式用decltype推到类型,仿函数最好实现

template<class T>
class DeleteArray
{
public:
void operator()(T* ptr)
{
delete[] ptr;
}
};
//delete[]特化
unique_ptr<Date[]> up1(new Date[5]);
//仿函数
unique_ptr<Date, DeleteArray<Date>> up2(new Date[5]);
//函数指针
unique_ptr<Date, void(*)(Date*)> up3(new Date[5],DeleteArrayFunc<Date>);
//lambad表达式
auto delArrOBJ = [](Date* ptr) {delete[] ptr; };
unique_ptr<Date, decltype(delArrOBJ)> up4(new Date[5], delArrOBJ);
- shared_ptr在构造智能指针对象传入删除器,同样可以用上述三个方法,lambda最好实现


//delete[]特化
shared_ptr<Date[]> sp1(new Date[5]);
//仿函数
shared_ptr<Date> sp2(new Date[5], DeleteArray<Date>());
//函数指针
shared_ptr<Date> sp3(new Date[5], DeleteArrayFunc<Date>);
//lambad表达式
auto delArrOBJ = [](Date* ptr) {delete[] ptr; };
shared_ptr<Date> sp4(new Date[5], delArrOBJ);
四.智能指针的原理
unique_ptr
基于一份资源只由一个智能指针对象管理的特性,
在底层实现时,要禁掉左值引用的复制拷贝和赋值操作
注意1:在实现右值引用的的赋值操作时,要先析构原来指向的对象
注意2:右值引用的赋值和拷贝操作都要让被操作指针的指向变成空
template<class T>
class unique_ptr
{
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{ }
explicit unique_ptr(unique_ptr&& up)
:_ptr(up._ptr)
{
up._ptr = nullptr;
}
unique_ptr(const unique_ptr& up) = delete;
unique_ptr<T>& operator=(unique_ptr&& up)
{
//先销毁原来的
delete _ptr;
_ptr = up._ptr;
up._ptr = nullptr;
return *this;
}
unique_ptr<T>& operator=(const unique_ptr& up)=delete;
~unique_ptr()
{
delete _ptr;
_ptr = nullptr;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
if(_ptr!=nullptr)
return _ptr;
}
private:
T* _ptr = nullptr;
};
shared_ptr
支持拷贝,也支持移动,底层用引用计数的方法实现,
在一个对象直接构造时,在堆上申请一个空间,用来存储引用计数,拷贝和赋值操作时,引用计数就++,这样指向同一资源的智能指针用的就是同一个引用计数了
注意:赋值操作实现时,要先判断是否是自己给自己赋值,
还要判断原来指向的对象是否只有这一个智能指针,如果是,还要析构原来指向的资源
template <class T>
class shared_ptr
{
private:
void release()
{
if (--(*_pcount)==0)
{
_del(_ptr);
delete _pcount;
_ptr = nullptr;
_pcount = nullptr;
}
}
public:
explicit shared_ptr(T* ptr=nullptr)
:_ptr(ptr)
,_pcount(new int (1))
{}
template <class D>
shared_ptr(T* ptr,D del)
:_ptr(ptr)
, _pcount(new int (1))
,_del=del
{}
shared_ptr(shared_ptr& copy)
:_ptr = copy._pcount
,_pcount = copy._pcount
,_del = copy._del
{
(*_pcount)++;
}
shared_ptr<T>& operator=(const shared_ptr& copy)
{
if(_ptr != copy._ptr)
{
//重要!!!
//要先判断自己原来管理的资源需不需要释放
release();
_ptr = copy._pcount;
_pcount = copy._pcount;
_del = copy._del;
*_pcount++;
}
return *this;
}
~shared_ptr()
{
release();
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T* get()
{
return _ptr;
}
int use_count()
{
return *_pcount;
}
private:
T* _ptr;
int* _pcount;
function<void(T*)> _del = [](T* ptr) {delete ptr; }
};
weak_ptr
循环引用的场景

n1析构了,引用记数变成1,n2析构了,引用记数变成1,
要想n1节点析构,引用计数要变成0,那就要n2析构
要想n2节点析构,引用计数要变成0,那就要n1析构
这样下来,哪个节点也析构不了,造成内存泄漏
解决方法
weak_ptr实现中,只是指向资源,但不增加引用计数,不承担析构资源的责任,这样一来,只要shared_ptr的引用计数变成0,就可以析构资源啦
注意:weak_ptr不支持直接绑定到资源,只支持直接绑定到shared_ptr
struct ListNode
{
int _data;
std::weak_ptr<ListNode> _next;
std::weak_ptr<ListNode> _prev;
};
底层简单实现
template <class T>
class weak_ptr
{
public:
//weak_ptr就是为了shared_ptr服务的,不提供直接构造
weak_ptr()
{}
weak_ptr(const shared_ptr<T>& sp)
_ptr(sp.get())
{}
weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
_ptr = sp.get();
return *this;
}
private:
T* _ptr=nullptr;
};
五. 思考
为什么shared_ptr和unique_ptr的构造需要explicit
// 假设 shared_ptr 允许隐式转换(实际不允许!)
void useSharedPtr(std::shared_ptr<int> ptr) {
// 使用 ptr
}
int main() {
int* raw_ptr = new int(42);
useSharedPtr(raw_ptr); // ❌ 如果允许隐式转换,会出问题!
// 问题:raw_ptr 会被隐式转为 shared_ptr,但所有权不明确!
// 可能导致 double-free(如果其他地方也接管了这个 raw_ptr)
return 0;
}
-
对于上面这种情况,就可能重复delete导致出错
-
还有哪种情况也会出现所有权不明确的问题呢?
int* b = new int(42);
std::shared_ptr<int> sp1(b);
std::shared_ptr<int> sp2(b); // 两个 shared_ptr 独立管理同一个 raw pointer
对于这种错误,可以通过std::make_shared规避,
如果只能用上述方法构造,要确保一个资源只初始化一个对象,这就需要程序员来控制了
3万+

被折叠的 条评论
为什么被折叠?



