c++11 | 细说智能指针
💓个人主页:mooridy
💓专栏地址:C++
关注我🌹,和我一起学习更多计算机的知识
🔝🔝🔝
什么是智能指针?
智能指针是 C++ 中一种用于管理动态内存的机制。它提供了一种更安全、更方便的方式来处理对象的生命周期,自动释放不再需要的内存,从而避免内存泄漏和悬空指针等问题。
为什么要有智能指针?
你可能会想,智能指针虽然方便,但也不是必须的呀?我只要自己记得释放就好了。但有的时候,我们会遇到如下面代码这样无解的问题。
我们在int* p1 = new int
,int* p2 = new int
,div()
这三个地方都有可能发生抛异常导致无法执行后续代码,即无法释放掉申请的空间,造成内存泄露。
这时我们就需要引入智能指针。
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void Func()
{
// 1、如果p1这里new 抛异常会如何?
// 2、如果p2这里new 抛异常会如何?
// 3、如果div调用这里又会抛异常会如何?
int* p1 = new int;
int* p2 = new int;
cout << div() << endl;
delete p1;
delete p2;
}
int main()
{
try
{
Func();
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}
RAII思想
RAII
是`是⼀种管理资源的类的设计思想,本质是⼀种利⽤对象⽣命周期来管理获取到的动态资源,避免资源泄漏,这⾥的资源可以是内存、⽂件指针、⽹络连接、互斥锁等等。RAII在获取资源时把资源委托给⼀个对象,接着控制对资源的访问,资源在对象的⽣命周期内始终保持有效,最后在对象析构的时候释放资源,这样保障了资源的正常释放,避免资源泄漏问题。
智能指针的设计就是依据RAII思想。
智能指针的基本思想
我们知道当对象出了作用域会自动调用析构,那么我们就把申请资源放在构造函数里,把释放资源放在析构函数里。
// 使用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr {
public:
SmartPtr(T* ptr = nullptr)
: _ptr(ptr)
{}
~SmartPtr()
{
if(_ptr!=nullptr)
delete _ptr;
}
T& operator*() {return *_ptr;}
T* operator->() {return _ptr;}
private:
T* _ptr;
};
C++标准库中的几种智能指针
• C++标准库中的智能指针都在这个头⽂件下面。
• auto_ptr
是C++98时设计出来的智能指针,他的特点是拷⻉时把被拷⻉对象的资源的管理权转移给
拷⻉对象,这是⼀个⾮常糟糕的设计,因为他会到被拷⻉对象悬空,访问报错的问题。强烈建议不要使⽤auto_ptr
。
#include <iostream>
#include <memory>
int main() {
// 创建一个 std::auto_ptr 并让它管理一个 int 对象
std::auto_ptr<int> ptr1(new int(42));
// 输出 ptr1 所指向的值
std::cout << "ptr1 value before copy: " << *ptr1 << std::endl;
// 拷贝 ptr1 到 ptr2,此时 ptr1 的资源管理权转移给 ptr2
std::auto_ptr<int> ptr2(ptr1);
// 尝试访问 ptr1 所指向的值,这会导致未定义行为
// 因为 ptr1 已经失去了对资源的管理权,变成了悬空指针
std::cout << "ptr1 value after copy: " << *ptr1 << std::endl;
return 0;
}
• unique_ptr
是C++11设计出来的智能指针,他的特点的不⽀持拷⻉,只⽀持移动。
• shared_ptr
是C++11设计出来的智能指针,他的特点是⽀持拷⻉,也⽀持移动。底层是⽤引⽤计数的⽅式实现的。
• weak_ptr
是C++11设计出来的智能指针,他完全不同于上⾯的智能指
针,他不⽀持RAII,也就意味着不能⽤它直接管理资源,weak_ptr的产⽣本质是要解决shared_ptr
的⼀个循环引⽤导致内存泄漏的问题。(这个问题文章后面会谈到)
shared_ptr的模拟实现
这段代码经常面试手撕,一定要会!
这⾥⼀份资源就需要⼀个引⽤计数,多个资源时引⽤计数⽤静态成员的⽅式是⽆法实现的,所以要使⽤堆上动态开辟的⽅式,构造智能指针对象时来⼀份资源,就要new⼀个引⽤计数出来。多个shared_ptr
指向资源时就++引⽤计数,shared_ptr
对象析构时就–引⽤计数,引⽤计数减到0时代表当前析构的shared_ptr
是最后⼀个管理资源的对象,则析构资源。
#include <iostream>
#include <mutex>
template<class T>
class Smart_Ptr // 实现的 C++11 的 shared_ptr 版本
{
public:
// 构造函数,默认参数为 nullptr
// 接收一个指向类型 T 对象的指针 ptr,用于初始化智能指针所管理的资源
// 同时创建一个新的计数器,初始值为 1,表示当前只有一个智能指针管理该资源
Smart_Ptr(T* ptr = nullptr)
: _ptr(ptr)
, _pcount(new int(1))
{}
// 析构函数
// 当智能指针对象生命周期结束时,调用 Release 函数来处理资源的释放
~Smart_Ptr()
{
Release();
}
// 拷贝构造函数
// 接收另一个 Smart_Ptr 对象的引用 sp
// 将当前智能指针的 _ptr 和 _pcount 指向 sp 的对应成员
// 并调用 Addcount 函数将引用计数加 1
Smart_Ptr(const Smart_Ptr<T>& sp)
: _ptr(sp._ptr)
, _pcount(sp._pcount)
{
Addcount();
}
// 赋值运算符重载
// 接收另一个 Smart_Ptr 对象的引用 sp
// 首先检查是否是自我赋值(即 this 和 sp 是否指向同一个对象)
// 如果不是,则先调用 Release 函数释放当前智能指针管理的资源
// 然后将 _ptr 和 _pcount 指向 sp 的对应成员
// 最后调用 Addcount 函数将引用计数加 1
Smart_Ptr<T>& operator=(const Smart_Ptr<T>& sp)
{
if (this != &sp) // 避免自我赋值
{
Release();
_ptr = sp._ptr;
_pcount = sp._pcount;
Addcount();
}
return *this;
}
// 释放资源的函数
// 首先对引用计数进行减 1 操作
// 如果减 1 后引用计数变为 0,表示没有智能指针再管理该资源
// 此时释放 _ptr 指向的资源、_pcount 指向的计数器
void Release()
{
if (--(*_pcount) == 0) // 销毁最后一个变量时才释放资源
{
delete _ptr;
delete _pcount;
delete _pmtx;
}
}
// 增加引用计数的函数
// 对引用计数进行加 1 操作
void Addcount()
{
(*_pcount)++;
}
void Subcount()
{
Release();
}
private:
T* _ptr; // 指向实际管理的资源的指针
int* _pcount; // 指向引用计数的指针,用于记录有多少个智能指针管理该资源
};
shared_ptr
的循环引用问题
下面这段代码演示的就是循环引用问题。
struct ListNode
{
int _data;
std::shared_ptr<ListNode> _next;
std::shared_ptr<ListNode> _prev;
// 这⾥改成weak_ptr,当n1->_next = n2;绑定shared_ptr时
// 不增加n2的引⽤计数,不参与资源释放的管理,就不会形成循环引⽤了
/*std::weak_ptr<ListNode> _next;
std::weak_ptr<ListNode> _prev;*/
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
// 循环引⽤ -- 内存泄露
std::shared_ptr<ListNode> n1(new ListNode);
std::shared_ptr<ListNode> n2(new ListNode);
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
n1->_next = n2;
n2->_prev = n1;
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
// weak_ptr不⽀持管理资源,不⽀持RAII
// weak_ptr是专⻔绑定shared_ptr,不增加他的引⽤计数,作为⼀些场景的辅助管理
//std::weak_ptr<ListNode> wp(new ListNode);
return 0;
}
这里我画了个图帮助理解。
对于这种场景,最好的解决方案就是不要在这里使用智能指针!!!
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
访问资源是安全的。