目录
智能指针的使用场景分析
下面程序中我们可以看到,new了以后,我们也delete了,但是因为抛异常导,后面的delete没有得到执行,所以就内存泄漏了,所以我们需要new以后捕获异常,捕获到异常后delete内存,再把异常抛出,但是因为new本身也可能抛异常,连续的两个new和下面的Divide都可能会抛异常,让我们处理起来很麻烦。智能指针放到这样的场景里面就让问题简单多了。
double Divide(int a, int b)
{
// 当b == 0时抛出异常
if (b == 0)
{
throw "Divide by zero condition!";
}
else
{
return (double)a / (double)b;
}
}
void Func()
{
int* array1 = new int[10];
int* array2 = new int[10]; // 抛异常呢
try
{
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;
}
catch (...)
{
cout << "delete []" << array1 << endl;
cout << "delete []" << array2 << endl;
delete[] array1;
delete[] array2;
throw; // 异常重新抛出,捕获到什么抛出什么
}
// ...
cout << "delete []" << array1 << endl;
delete[] array1;
cout << "delete []" << array2 << endl;
delete[] array2;
}
int main()
{
try
{
Func();
}
catch (const char* errmsg)
{
cout << errmsg << endl;
}
catch (const exception& e)
{
cout << e.what() << endl;
}
catch (...)
{
cout << "未知异常" << endl;
}
return 0;
}
当Divide函数发生除零错误时,为了保证array1和array2得到正确释放,抛出异常后由Func函数捕获并做异常处理。但是当array2在 new 的时候抛出异常时,会导致array1没有被释放的问题。
在func函数中分配了array1和array2的内存,如果array2的 new 操作抛出异常(比如内存不足),那么程序会立即跳转到最近的异常处理块。但是此时Func()函数内部的 try-catch 块还没有开始,所以异常会直接传播到main()函数中的异常处理。
在这个过程中,array1的内存没有被释放,因为:
异常发生在Func()的 try 块之前,没有对应的 catch 块来处理这个异常
程序控制流直接跳出了Func()函数
为了避免上述问题,这里可以引入智能指针来处理。比如:
template<class T>
class SmartPtr
{
public:
// RAII
SmartPtr(T* ptr)
:_ptr(ptr)
{}
~SmartPtr()
{
cout << "delete[] " << _ptr << endl;
delete[] _ptr;
}
// 重载运算符,模拟指针的行为,方便访问资源
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T& operator[](size_t i)
{
return _ptr[i];
}
private:
T* _ptr;
};
double Divide(int a, int b)
{
// 当b == 0时抛出异常
if (b == 0)
{
throw "Divide by zero condition!";
}
else
{
return (double)a / (double)b;
}
}
void Func()
{
SmartPtr<int> sp1(new int[10]);
SmartPtr<int> sp2(new int[10]);
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;
for (size_t i = 0; i < 10; i++)
{
sp1[i] = sp2[i] = i;
}
}
int main()
{
try
{
Func();
}
catch (const char* errmsg)
{
cout << errmsg << endl;
}
catch (const exception& e)
{
cout << e.what() << endl;
}
catch (...)
{
cout << "未知异常" << endl;
}
return 0;
}
代码中将申请到的内存空间交给了一个SmartPtr对象进行管理。
- 在构造SmartPtr对象时,SmartPtr将传入的需要被管理的内存空间保存起来。
- 在SmartPtr对象析构时,SmartPtr的析构函数中会自动将管理的内存空间进行释放。
- 此外,为了让SmartPtr对象能够像原生指针一样使用,还需要对*和->运算符进行重载。
这样一来,无论程序是正常执行完毕返回了,还是因为某些原因中途返回了,或是因为抛异常返回了,只要SmartPtr对象的生命周期结束就会调用其对应的析构函数,进而完成内存资源的释放。
智能指针的原理
智能指针的原理
实现智能指针时需要考虑以下三个方面的问题:
- 在对象构造时获取资源,在对象析构的时候释放资源,利用对象的生命周期来控制程序资源,即RAII特性。
- 对
*和->运算符进行重载,使得该对象具有像指针一样的行为。 - 智能指针对象的拷贝问题。
概念说明: RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、互斥量等等)的简单技术。
为什么要解决智能指针对象的拷贝问题
对于当前实现的SmartPtr类,如果用一个SmartPtr对象来拷贝构造另一个SmartPtr对象,或是将一个SmartPtr对象赋值给另一个SmartPtr对象,都会导致程序崩溃。比如:
int main()
{
SmartPtr<int> sp1(new int);
SmartPtr<int> sp2(sp1); //拷贝构造
SmartPtr<int> sp3(new int);
SmartPtr<int> sp4(new int);
sp3 = sp4; //拷贝赋值
return 0;
}
原因如下:
- 编译器默认生成的拷贝构造函数对内置类型完成值拷贝(浅拷贝),因此用sp1拷贝构造sp2后,相当于这sp1和sp2管理了同一块内存空间,当sp1和sp2析构时就会导致这块空间被释放两次。
- 编译器默认生成的拷贝赋值函数对内置类型也是完成值拷贝(浅拷贝),因此将sp4赋值给sp3后,相当于sp3和sp4管理的都是原来sp3管理的空间,当sp3和sp4析构时就会导致这块空间被释放两次,并且还会导致sp4原来管理的空间没有得到释放。
需要注意的是,智能指针就是要模拟原生指针的行为,当我们将一个指针赋值给另一个指针时,目的就是让这两个指针指向同一块内存空间,所以这里本就应该进行浅拷贝,但单纯的浅拷贝又会导致空间被多次释放,因此根据解决智能指针拷贝问题方式的不同,从而衍生出了不同版本的智能指针。
C++中的智能指针
C++标准库中的智能指针都在<memory>这个头文件下面,我们包含就可以是使用了, 智能指针有好几种,除了weak_ptr他们都符合RAII和像指针⼀样访问的行为,原理上而言主要是解决智能指针拷贝时的思路不同
std::auto_ptr
管理权转移
auto_ptr是C++98中引入的智能指针,auto_ptr通过管理权转移的方式解决智能指针的拷贝问题,保证一个资源在任何时刻都只有一个对象在对其进行管理,这时同一个资源就不会被多次释放了。比如:
int main()
{
std::auto_ptr<int> ap1(new int(1));
std::auto_ptr<int> ap2(ap1);
*ap2 = 10;
//*ap1 = 20; //error
std::auto_ptr<int> ap3(new int(1));
std::auto_ptr<int> ap4(new int(2));
ap3 = ap4;
return 0;
}
但一个对象的管理权转移后也就意味着,该对象不能再向原来管理的资源进行访问了,否则程序就会崩溃,比如上面的ap1已经悬空了,对其访问就会报错,C++11设计出新的智能指针后,强烈建议不要使用auto_ptr。其他C++11出来之前很多公司也是明令禁止使用这个智能指针的。
auto_ptr的模拟实现
简易版的auto_ptr的实现步骤如下:
- 在构造函数中获取资源,在析构函数中释放资源,利用对象的生命周期来控制资源。
- 对*和->运算符进行重载,使auto_ptr对象具有指针一样的行为。
- 在拷贝构造函数中,用传入对象管理的资源来构造当前对象,并将传入对象管理资源的指针置空。
- 在拷贝赋值函数中,先将当前对象管理的资源释放,然后再接管传入对象管理的资源,最后将传入对象管理资源的指针置空。
namespace lzg
{
template<class T>
class auto_ptr
{
public:
//RAII
auto_ptr(T* ptr = nullptr)
:_ptr(ptr)
{}
~auto_ptr()
{
if (_ptr != nullptr)
{
cout << "delete: " << _ptr << endl;
delete _ptr;
_ptr = nullptr;
}
}
auto_ptr(auto_ptr<T>& ap)
:_ptr(ap._ptr)
{
ap._ptr = nullptr; //管理权转移后ap被置空
}
auto_ptr& operator=(auto_ptr<T>& ap)
{
if (this != &ap)
{
delete _ptr; //释放自己管理的资源
_ptr = ap._ptr; //接管ap对象的资源
ap._ptr = nullptr; //管理权转移后ap被置空
}
return *this;
}
//可以像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr; //管理的资源
};
}
std::unqiue_ptr
唯一指针
unique_ptr是C++11设计出来的智能指针,他的名字翻译出来是唯⼀指针,他的特点的不支持拷 贝,只支持移动。如果不需要拷贝的场景就非常建议使用他。比如:
int main()
{
std::unique_ptr<int> up1(new int(0));
//std::unique_ptr<int> up2(up1); //error
return 0;
}
unique_ptr的模拟实现
简易版的unique_ptr的实现步骤如下:
- 在构造函数中获取资源,在析构函数中释放资源,利用对象的生命周期来控制资源。
- 对*和->运算符进行重载,使unique_ptr对象具有指针一样的行为。
- 用C++98的方式将拷贝构造函数和拷贝赋值函数声明为私有,或者用C++11的方式在这两个函数后面加上=delete,防止外部调用
template<class T>
class unique_ptr
{
public:
explicit unique_ptr(T* ptr)
:_ptr(ptr)
{}
~unique_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
// 像指针⼀样使⽤
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
unique_ptr(const unique_ptr<T>& sp) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;
unique_ptr(unique_ptr<T>&& sp)
:_ptr(sp._ptr)
{
sp._ptr = nullptr;
}
unique_ptr<T>& operator=(unique_ptr<T>&& up)
{
if (this != &up)
{
delete _ptr;
_ptr = up._ptr;
up._ptr = nullptr;
}
}
private:
T* _ptr;
};
std::shared_ptr
std::shared_ptr的基本设计
引用计数
shared_ptr是C++11中引入的智能指针,shared_ptr通过引用计数的方式解决智能指针的拷贝问题。
- 每一个被管理的资源都有一个对应的引用计数,通过这个引用计数记录着当前有多少个对象在管理着这块资源。
- 当新增一个对象管理这块资源时则将该资源对应的引用计数进行++,当一个对象不再管理这块资源或该对象被析构时则将该资源对应的引用计数进行--。
- 当一个资源的引用计数减为0时说明已经没有对象在管理这块资源了,这时就可以将该资源进行释放了。
通过这种引用计数的方式就能支持多个对象一起管理某一个资源,也就是支持了智能指针的拷贝,并且只有当一个资源对应的引用计数减为0时才会释放资源,因此保证了同一个资源不会被释放多次。比如:
int main()
{
lzg::shared_ptr<Date> sp1(new Date);
// ⽀持拷贝
lzg::shared_ptr<Date> sp2(sp1);
lzg::shared_ptr<Date> sp3(sp2);
cout << sp1.use_count() << endl;
sp1->_year++;
cout << sp1->_year << endl;
cout << sp2->_year << endl;
cout << sp3->_year << endl;
return 0;
}
说明一下: use_count成员函数,用于获取当前对象管理的资源对应的引用计数。
shared_ptr的模拟实现
namespace lzg
{
template<class T>
class shared_ptr
{
public:
//RAII
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _pcount(new int(1))
{}
~shared_ptr()
{
if (--(*_pcount) == 0)
{
if (_ptr != nullptr)
{
cout << "delete: " << _ptr << endl;
delete _ptr;
_ptr = nullptr;
}
delete _pcount;
_pcount = nullptr;
}
}
shared_ptr(shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
{
(*_pcount)++;
}
//sp1=sp
shared_ptr& operator=(shared_ptr<T>& sp)
{
if (_ptr != sp._ptr) //管理同一块空间的对象之间无需进行赋值操作
{
if (--(*_pcount) == 0)
{
cout << "delete: " << _ptr << endl;
delete _ptr;
delete _pcount;
}
_ptr = sp._ptr; //与sp对象一同管理它的资源
_pcount = sp._pcount; //获取sp对象管理的资源对应的引用计数
(*_pcount)++; //新增一个对象来管理该资源,引用计数++
}
return *this;
}
//获取引用计数
int use_count()
{
return *_pcount;
}
//可以像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr; //管理的资源
int* _pcount; //管理的资源对应的引用计数
};
}
引用计数_pcount为什么存放在堆区?
我们希望的是多个对象共用一个引用计数,如果_pcount是int类型,那么我们创建的sp1、sp2、sp3等等,每一个对象都各自有一个_pcount而不是它们共用一个引用计数。
其次,shared_ptr中的引用计数count也不能定义成一个静态的成员变量(static修饰),因为静态成员变量虽然是所有类型对象共享的,但这会导致管理相同资源的对象和管理不同资源的对象用到的都是同一个引用计数

std::shared_ptr的线程安全问题
shared_ptr的线程安全问题
当前模拟实现的shared_ptr还存在线程安全的问题,由于管理同一个资源的多个对象的引用计数是共享的,因此多个线程可能会同时对同一个引用计数进行自增或自减操作,而自增和自减操作都不是原子操作,因此需要通过加锁来对引用计数进行保护,否则就会导致线程安全问题。
加锁解决线程安全问题
要解决引用计数的线程安全问题,本质就是要让对引用计数的自增和自减操作变成一个原子操作,因此可以对引用计数的操作进行加锁保护,也可以用原子类atomic对引用计数进行封装,这里以加锁为例。
template<class T>
class shared_ptr
{
public:
// 增加引用计数(线程安全)
void AddRef()
{
std::lock_guard<std::mutex> lock(*_pmutex);
(*_pcount)++;
}
// 减少引用计数(线程安全,计数为0时释放资源)
void ReleaseRef()
{
std::lock_guard<std::mutex> lock(*_pmutex);
bool need_delete_mutex = false;
// 引用计数减1后判断是否为0
if (--(*_pcount) == 0)
{
// 释放资源和引用计数
if (_ptr != nullptr)
{
cout << "delete " << _ptr << endl;
delete _ptr;
_ptr = nullptr;
}
delete _pcount;
_pcount = nullptr;
need_delete_mutex = true; // 标记需要释放互斥锁
}
// 锁释放后再删除互斥锁(避免解锁时访问已释放的锁)
if (need_delete_mutex && _pmutex)
{
_pmutex.reset(); // unique_ptr 自动释放互斥锁
}
//_pmutex:判断 unique_ptr 是否持有有效资源(unique_ptr 重载了 operator bool(),
// 当它持有资源时返回 true,为空时返回 false)。
//加这个条件是为了避免「重复调用 reset()」(比如已经释放过一次,
// _pmutex 为空,再调用 reset() 无意义,但不会出错,只是更严谨)。
}
public:
// 构造函数(RAII 初始化资源、引用计数、互斥锁)
shared_ptr(T* ptr = nullptr)
:_ptr(ptr),
_pcount(new int(1)),
_pmutex(std::make_unique<std::mutex>()) // 用 unique_ptr 管理锁,避免泄漏
{}
// 拷贝构造(共享资源,增加引用计数)
shared_ptr(shared_ptr<T>& sp)
:_ptr(sp._ptr),
_pcount(sp._pcount),
_pmutex(sp._pmutex) // unique_ptr 拷贝需用 shared_ptr,或此处改为 shared_ptr<mutex>
{
AddRef();
}
// 赋值运算符(异常安全、自我赋值检查)
shared_ptr<T>& operator=(shared_ptr<T>& sp)
{
if (this == &sp)
return *this;
// 先增加新资源的引用计数(异常安全:避免释放当前资源后新资源获取失败)
sp.AddRef();
// 释放当前资源
ReleaseRef();
// 接管新资源
_ptr = sp._ptr;
_pcount = sp._pcount;
_pmutex = sp._pmutex; // 若 _pmutex 是 unique_ptr,需改为 shared_ptr<mutex> 或用 move(根据设计)
return *this;
}
// 析构函数(减少引用计数)
~shared_ptr()
{
ReleaseRef();
}
// 重载解引用运算符
T& operator*()
{
return *_ptr;
}
// 重载成员访问运算符
T* operator->()
{
return _ptr;
}
// 获取引用计数(线程安全)
int use_count()
{
std::lock_guard<std::mutex> lock(*_pmutex);
return *_pcount;
}
private:
T* _ptr; // 管理的资源指针
int* _pcount; // 引用计数(动态分配,支持共享)
std::unique_ptr<std::mutex> _pmutex; // 互斥锁(RAII 管理,避免泄漏)
};
说明一下:
shared_ptr只需要保证引用计数的线程安全问题,而不需要保证管理的资源的线程安全问题,就像原生指针管理一块内存空间一样,原生指针只需要指向这块空间,而这块空间的线程安全问题应该由这块空间的操作者来保证。
shared_ptr循环引用问题
shared_ptr⼤多数情况下管理资源非常合适,支持RAII,也⽀持拷贝。但是在循环引用的场景下会 导致资源没得到释放内存泄漏,所以我们要认识循环引用的场景和资源没释放的原因,并且学会使 用weak_ptr解决这种问题。
如下图所述场景,n1和n2析构后,管理两个节点的引用计数减到1

- 右边的节点什么时候释放呢,左边节点中的_next管着呢,_next析构后,右边的节点就释放了。
- _next什么时候析构呢,_next是左边节点的的成员,左边节点释放,_next就析构了。
- 左边节点什么时候释放呢,左边节点由右边节点中的_prev管着呢,_prev析构后,左边的节点就释 放了。
- _prev什么时候析构呢,_prev是右边节点的成员,右边节点释放,_prev就析构了。
至此逻辑上成功形成回旋镖似的循环引用,谁都不会释放就形成了循环引用,导致内存泄漏
把ListNode结构体中的_next和_prev改成weak_ptr,weak_ptr绑定到shared_ptr时不会增加它的引用计数,_next和_prev不参与资源释放管理逻辑,就成功打破了循环引用,解决了这里的问题
struct ListNode
{
int _data;
// 初始使用 shared_ptr 作为前后指针类型
std::shared_ptr<ListNode> _next;
std::shared_ptr<ListNode> _prev;
// 核心优化思路:将前后指针改为 weak_ptr
// 原因:当执行 n1->_next = n2 这类绑定操作时,weak_ptr 不会增加 n2 的引用计数
// 本质:weak_ptr 仅作为"弱引用"观察资源,不参与资源释放的所有权管理
// 效果:彻底打破 shared_ptr 互相引用导致的循环引用问题,确保节点能正常析构
/*std::weak_ptr<ListNode> _next;
std::weak_ptr<ListNode> _prev;*/
~ListNode()
{
cout << "~ListNode()" << endl; // 析构打印,用于验证资源是否释放
}
};
weak_ptr
weak_ptr不支持RAII,也不支持访问资源,所以我们看文档发现weak_ptr构造时不支持绑定到资源,只支持绑定到shared_ptr,绑定到shared_ptr时,不增加shared_ptr的引用计数,那么就可以解决上述的循环引用问题
weak_ptr也没有重载operator*和operator->等,因为它不参与资源管理,那么如果它绑定的shared_ptr已经释放了资源,那么它去访问资源就是很危险的。weak_ptr支持expired检查指向的资源是否过期,use_count也可获取shared_ptr的引用计数,weak_ptr想访问资源时,可以调用lock返回一个管理资源的shared_ptr,如果资源已经被释放,返回的shared_ptr是一个空对象,如果资源没有释放,则通过返回的shared_ptr访问资源是安全的。
template<class T>
class weak_ptr
{
public:
// 默认构造
weak_ptr() = default;
// 从 shared_ptr 构造(弱引用,不增加引用计数)
weak_ptr(const shared_ptr<T>& sp)
: _ptr(sp.get()),
_pcount(sp._pcount), // 关联 shared_ptr 的引用计数指针
_pmutex(sp._pmutex) // 关联 shared_ptr 的互斥锁(线程安全)
{}
// 赋值运算符(从 shared_ptr 赋值,不增加引用计数)
weak_ptr& operator=(const shared_ptr<T>& sp)
{
if (this != &sp) // 自我赋值检查(可选,更严谨)
{
_ptr = sp.get();
_pcount = sp._pcount;
_pmutex = sp._pmutex;
}
return *this;
}
// 核心接口1:判断资源是否已释放(expired = 过期)
bool expired() const
{
if (!_pmutex || !_pcount)
return true; // 无关联的计数或锁,视为已过期
std::lock_guard<std::mutex> lock(*_pmutex);
return *_pcount == 0; // 引用计数为0 → 资源已释放
}
// 核心接口2:升级为 shared_ptr(安全访问资源)
shared_ptr<T> lock() const
{
// 先判断资源是否有效,无效则返回空 shared_ptr
if (expired())
return shared_ptr<T>();
// 资源有效,创建 shared_ptr 并增加引用计数(线程安全)
std::lock_guard<std::mutex> lock(*_pmutex);
if (*_pcount > 0)
{
// 调用 shared_ptr 的私有构造(需将 weak_ptr 声明为 shared_ptr 的友元)
return shared_ptr<T>(_ptr, _pcount, _pmutex, false);
}
return shared_ptr<T>(); // 极端情况:判断后计数变为0,返回空
}
// 辅助接口:获取引用计数(弱引用不影响计数)
int use_count() const
{
if (!_pmutex || !_pcount)
return 0;
std::lock_guard<std::mutex> lock(*_pmutex);
return *_pcount;
}
private:
T* _ptr = nullptr; // 弱引用的资源指针(不主导资源生命周期)
int* _pcount = nullptr; // 关联 shared_ptr 的引用计数指针
std::shared_ptr<std::mutex> _pmutex; // 关联 shared_ptr 的互斥锁(线程安全)
// 关键:让 shared_ptr 能访问 weak_ptr 的私有成员(用于 lock() 升级)
friend class shared_ptr<T>;
};
722

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



