目录
前言:
1.知识引入
1.1.异常安全问题
1.2.RALL和智能指针雏形
2.智能指针的发展
2.1.auto_ptr的引入
2.2.unique_ptr的引入
2.4.weak_ptr的引入(重点)
2.5.测试函数
3.定制删除器
3.2.定制删除器的实现
前言:
这是一篇又臭又长又精华的博客,需要每一个模块认真学习,仔细理解,这一部分也是C++面试常考的内容,那么废话不多说,just do it!
在这一篇章中我们需要重点学习:RAII思想、shared_ptr的实现和相关问题。
1.知识引入
1.1.异常安全问题
在C++学习进阶:异常-CSDN博客我们引入这一段代码是为了提出异常的重新抛出这个作法,但是异常重新抛出一定就是解决异常安全问题的最优法吗?
#include<iostream>
#include<string.h>
using namespace std;
int Exception(int i)
{
int arr[5] = { 0, 1, 2, 3, 4 };
if (i >=5)
{
throw "数组越界";
}
else
{
return arr[i];
}
}
void Print()
{
int* array = new int[10];
try
{
int i = 0;
cin >> i;
cout << Exception(i) << endl;
// 不出现异常也是需要释放空间
delete[] array;
cout<<"释放array资源"<<endl;
}
catch(...)
{
delete[] array;
cout<<"释放array资源"<<endl;
}
}
int main()
{
try
{
Print();
}
catch (const char* errmsg)
{
cout << errmsg << endl;
}
catch (...)
{
cout << "未知异常" << endl;
}
}
我们看回这段代码,对于Print中,我们发现当不发生异常时我们也是需要继续的释放掉array这段空间,也就是在代码简洁上重新抛出显然是不太适合这个场景的。那么如何解决这个问题呢?先不着急,我们先引入一个新概念RAII。
1.2.RALL和智能指针雏形
RAII(Resource Acquisition Is Initialization)是C++编程中的一种技术,用于管理资源(如内存、文件句柄、网络连接等)的生命周期。RAII的核心思想是,将资源的获取(即初始化)与资源的释放(即析构)绑定到对象的生命周期上。通过这样做,可以确保资源在不再需要时得到正确的释放,从而避免资源泄漏和其他相关问题。
显然十分抽象,接下来我们结合上一段代码来体会一下RAII思想。
#include<iostream>
using namespace std;
// 智能指针的引入
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr)
:_ptr(ptr)
{}
~SmartPtr()
{
delete _ptr;
cout << "~SmartPtr() 已经释放传入的指针资源" << endl;
}
private:
T* _ptr;
};
int Exception(int i)
{
int arr[5] = { 0, 1, 2, 3, 4 };
if (i >= 5)
{
throw "数组越界";
}
else
{
return arr[i];
}
}
void Print()
{
int* array = new int[10];
SmartPtr<int> s(array);
int i = 0;
cin >> i;
cout << Exception(i) << endl;
}
int main()
{
try
{
Print();
}
catch (const char* errmsg)
{
cout << errmsg << endl;
}
catch (...)
{
cout << "未知异常" << endl;
}
}
- 我们引入一个类,来实现传入指针的释放,这里我们传入了外部资源的指针,当我们在类内部释放这个指针,外部资源的指针同时也被释放(共用一块地址)。
- 而这个类SmartPtr就是智能指针,不过这是我们自己手搓的一个雏形,功能较少
- RAII思想:就是将具有资源的、且不方便管理资源的对象绑定到容易管理资源的另一个对象上,来实现资源的管理。而智能指针是这个思想的一种体现
另外我们通过黑窗口打印,也发现无论出现异常还是不出现异常,这个指针的资源最终都得到释放
通过上面的例子我们大概知道智能指针本质上就是实现一个对指针进行操作的对象,而在C++中封装为了在原生指针的基础上的一个具有多样功能的指针类,因而可以构造出“智能”指针对象……
2.智能指针的发展
智能指针是C++中用于自动管理内存生命周期的类模板。它们被设计为像原生指针一样使用,但提供了额外的功能来确保在适当的时候自动删除所指向的对象。智能指针通过封装原生指针并重载相关的操作符(如
*
和->
)来实现这一点,使得它们可以像使用普通指针一样方便。
智能指针主要有四种,分别是:auto_ptr
(C++98标准,已弃用)、unique_ptr
(C++11标准)、shared_ptr
(C++11标准)和weak_ptr
(C++11标准) ,为什么需要这几种智能指针呢?接下来我们通过几个场景来讲一下智能指针的发展史。
2.1.auto_ptr的引入
template<class T>
class SmartPtr
{
// RAII思想
public:
SmartPtr(T* ptr)
:_ptr(ptr)
{}
~SmartPtr()
{
delete _ptr;
cout << "~SmartPtr() 资源已经释放" << endl;
}
// 模拟指针功能
// 实现解引用
T& operator*() { return *_ptr; }
// 对多成员类型访问
T* operator->() { return _ptr; }
private:
T* _ptr;
};
int main()
{
zhong::SmartPtr<int> sp(new int(1));
cout << *sp << endl;
*sp = 2;
cout << *sp << endl;
// zhong::SmartPtr<int> sp2(sp);
}
首先我们定义了一个智能指针类,然后我们可以对他进行操作,比如值的修改和打印,左上角为注释了// zhong::SmartPtr<int> sp2(sp);调用函数的结果。
因为我们是通过共用同一块地址空间来实现智能指针的,而当我们析构时,因为有两个对象 ,所以这两个对象都需要被析构,而用的是同一份地址,也就是这块地址被析构了两次,因此崩溃了。
这里就是智能指针雏形的缺陷所在,为了解决这个问题C++98对这个智能指针做了一定的优化。
// C++98的智能指针
// 进行管理权的转移
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr)
:_ptr(ptr)
{}
// 管理权转移 将旧对象的指针置空 防止二次析构
auto_ptr(auto_ptr<T>& ap)
:_ptr(ap._ptr)
{
ap._ptr = nullptr;
}
~auto_ptr()
{
delete _ptr;
cout << "~auto_ptr() 资源已经释放" << endl;
}
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
private:
T* _ptr;
};
我们通过它的拷贝构造函数可知,对于拷贝对象,直接把自身地址传给被拷贝对象,然后将自身地址置为空。这样子我们解决:同一份地址被析构两次的问题。通过析构一份为空的地址和析构一份具有意义的地址来实现的。
2.2.unique_ptr的引入
但是这样子会不会有点捞!!!实际场景下我们可能同时对这两个对象都需要进行操作!而其中一个对象我们已经置空了,那么就会产生对空指针的使用错误,显然是不符合实际应用。这时unique_ptr就横空出世了!
template<class T>
class unique_ptr
{
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{}
~unique_ptr()
{
delete _ptr;
cout << "~unique_ptr() 资源已经释放" << endl;
}
unique_ptr(const unique_ptr<T>& up) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
private:
T* _ptr;
};
为了解决拷贝构造导致的共用同一块地址析构两次和auto_ptr将其中一个对象值为空的这两个问题,unique_ptr察觉到了问题的根本来源,就是拷贝构造的问题,所以秉着不失败就是最大的成功这个伟大真理,直接删除了智能指针的拷贝构造功能。
2.3.救世主shared_ptr(重点)
这样确实是能够从根源上解决问题,但是这是合理的吗??? 所以这时主角shared_ptr闪亮登场!
// 合理的智能指针
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr)
:_ptr(ptr)
, _p_r_count(new int(1))
{}
~shared_ptr()
{
release();
cout << "~shared_ptr() 资源已经释放" << endl;
}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
,_p_r_count(sp._p_r_count)
{
++(*_p_r_count);
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
// 自己给自己赋值
if (sp._ptr != this->_ptr)
{
// = 本质上替换左边的智能指针
// 需要考虑是否需要释放
release();
_ptr = sp._ptr;
_p_r_count = sp._p_r_count;
++(*_p_r_count);
return *this;
}
}
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
private:
// 引用计数为0时对资源的释放
void release()
{
// 当引用计数为0时才释放资源
// 判断的同时已经对引用计数--了
if (--(*_p_r_count) == 0)
{
delete _ptr;
delete _p_r_count;
}
}
private:
T* _ptr;
// 维护引用计数的指针
int* _p_r_count;
};
在讲shared_ptr是怎么实现之前,我们先来回顾智能指针雏形的问题
退出当前调用栈时,两个对象对应的同一块空间无法避免析构两次!!!
为了解决这个问题:shared_ptr维护了一个int*类型的指针,用来存放这个智能指针对象的地址被引用了几次,即这个地址维护了几个智能指针的问题。
具体实现:
- 通过析构某一个智能指针对象时,先判断当前智能指针的引用计数是否大于1,如果大于1说明这个地址仍被其他对象使用中,不能释放这个地址,但是可以释放当前智能指针对象,并将这个智能指针的引用数减一。
- 我们在进行拷贝构造,也就是增加该地址对应的智能指针对象时,需要增加引用计数。
这样子我们就完善了智能指针并且解决了问题。
我们已经知道用引用计数可以解决问题,但是为什么一定是int*类型呢?我们画个图来体会一下
那么假如只是在智能指针中维护int类型呢?这时由于对象不同,所以他们各自的int也是不同的,其他的智能指针对象就算拥有同一块_ptr,但是int的地址却是各自开辟的,无法实现三者独立操作。
那么可能有人会提出:如果维护static int这个成员变量时,在静态区中独立开辟,总是可以满足独立操作的需求了吧? 当然如果只是满足独立操作的需求,通过静态区是能够实现的,然而如果我们有另一块内存开辟的资源,就会导致上一块资源对应的static int和这一块对应的static int冲突。
2.4.weak_ptr的引入(重点)
shared_ptr的缺陷:
然而shared_ptr并不是万能的,shared_ptr会出现循环引用的问题,也就是两个智能指针内部维护智能指针对象时可能会出现互相进行指向的问题。
在这段代码中我们直接使用C++原生的shared_ptr来展示shared_ptr存在的缺陷
// shared_ptr的缺陷
struct ListNode
{
int _val;
std::shared_ptr<ListNode> _next;
std::shared_ptr<ListNode> _prev;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
std::shared_ptr<ListNode> sp1 = new ListNode;
std::shared_ptr<ListNode> sp2 = new ListNode;
// 这里会出现循环引用
sp1->_next = sp2;
sp2->_prev = sp1;
}
运行这段代码时,我们发现我们显性的进行ListNode的析构,但是却没有析构的打印,而是没有任何输出结果的正常退出程序。这也就表示了这一段代码中的ListNode结构体并没有被释放资源,存在内存泄漏的问题!!!
为什么会出现这种问题,已经循环引用是什么意思呢? 如图所示:
当我们运行到函数结束前,我们会出现左图的现象,智能指针的指向图,而当函数退出时,乡音函数内部的sp1、sp2作为临时变量,编译器自动调用它的析构函数进行回收,然而此时我们发现这两个ListNode的资源仍然未被释放,这是为什么呢?
因为ListNode内部维护的智能指针的引用计数仍然不为0,这里因为上面的ListNode中的智能指针指向下一个ListNode,而下面的智能指针指向上一个ListNode,即:如果想要ListNode资源得到释放,就需要内部维护的互相指向的智能指针的引用计数为0,但是他们各自为1。
那么现在我们就要研究一下_next和_prev什么时候释放的问题?
- 对于_next而言,如果他需要被释放,也就是引用计数为0,那么就是需要下面的ListNode节点被释放,这时维护_next的ListNode(上面的ListNode)才会释放。
- 然而下面的ListNode节点被释放的条件,就是_prev的引用计数为0,而_prev引用计数为0的条件就是上面的ListNode节点被释放。
这时是不是发现:1的条件需要2为结果,2的条件需要1为结果。就产生了因果循环!
所以这时我们发现shared_ptr并不是万能的,他会出现循环引用的问题! 这时C++11就引入了weak_ptr这一个智能指针来解决这个问题。
// 循环引用的问题就是ListNode中维护了shared_ptr指针
struct ListNode
{
int _val;
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);
// 这里会出现循环引用
n1->_next = n2;
n2->_prev = n1;
}
这时问题就完美解决了!!!
weak_ptr的实现:
- weak_ptr是专门为了解决shared_ptr循环引用问题而产生的
- 不支持RAII
- 不参与shared_ptr的引用计数
template<class T>
class weak_ptr
{
public:
weak_ptr()
:_ptr(nullptr)
{}
weak_ptr(const shared_ptr<T>& sp)
:_ptr(sp.get())
{}
weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
_ptr = sp.get();
return *this;
}
weak_ptr<T>& operator=(const weak_ptr<T>& sp)
{
_ptr = sp._ptr;
return *this;
}
// 像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
// 注意weak_ptr也是拥有引用计数的
// 这里我们只是实现简单的逻辑,就不实现了
};
在这一部分中,我们没有实现weak_ptr的引用计数,只是根据std库大致实现了,解决循环引用的weak_ptr具有的功能
2.5.测试函数
附:为了不与库中的智能指针冲突,建议在自己的命名空间实现这几个智能指针
void test1()
{
zhong::SmartPtr<int> sp(new int(1));
cout << *sp << endl;
*sp = 2;
cout << *sp << endl;
//zhong::SmartPtr<int> sp2(sp);
}
void test2()
{
zhong::auto_ptr<int> sp(new int(1));
cout << *sp << endl;
*sp = 2;
cout << *sp << endl;
zhong::auto_ptr<int> sp2(sp);
cout << *sp2 << endl;
// auto_ptr对原对象进行访问 会出现访问空指针错误
// cout << *sp << endl;
}
void test3()
{
zhong::unique_ptr<int> sp(new int(1));
cout << *sp << endl;
*sp = 2;
cout << *sp << endl;
// unique_ptr直接就不允许拷贝构造
// zhong::unique_ptr<int> sp2(sp);
}
void test4()
{
zhong::shared_ptr<int> sp(new int(1));
*sp = 2;
zhong::shared_ptr<int> sp2(sp);
cout << "sp对应值为:" << *sp << endl;
cout << "sp2对应值为:" << *sp2 << endl;
}
3.定制删除器
这里我们主要介绍一下shared_ptr的定制删除器,因为其他的智能指针实现也是一致的。为什么需要定制删除器?我们先看回我们的release函数
3.1.shared_ptr中删除的漏洞
void release()
{
// 当引用计数为0时才释放资源
// 判断的同时已经对引用计数--了
if (--(*_p_r_count) == 0)
{
delete _ptr;
delete _p_r_count;
}
}
我们发现我们释放智能指针指向资源的指针是通过delete _ptr,但是实际场景中我们传入的可能是数组对象,那么这时就需要delete[ ] _ptr,不然就仅仅释放了数组对象对应的首个对象的资源,数组后面的资源并没有的到释放。也就是说release有点写死了。
int main()
{
// 我们传入数组对象给shared_ptr,然后析构时程序崩溃
std::shared_ptr<string> n1(new string[10]);
}
C++库中是删除数组指针,通过传入删除器对象来实现的,而这个删除器一般就是函数指针、仿函数、lambda表达式这种可调用对象 。需要注意的是:类型需要匹配。
// 定制删除器
template<class T>
struct Delete
{
void operator()(T* ptr)
{
delete[] ptr;
}
};
int main()
{
// 通过仿函数
std::shared_ptr<string> n1(new string[10], Delete<string>());
// 通过lambda表达式
std::shared_ptr<string> n2(new string[10], [](string* ptr) {delete[] ptr; });
}
3.2.定制删除器的实现
接下来我们学习一下shared_ptr是如何实现定制删除器的。
- 我们增加数组指针和传入删除器的构造函数
- 接着需要重写一下release函数的逻辑,增加删除器删除的代码实现
- 用包装器统一传入的函数指针、仿函数、lambda表达式类型
// 合理的智能指针
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _p_r_count(new int(1))
{}
template<class D>
shared_ptr(T* ptr, D del)
: _ptr(ptr)
, _p_r_count(new int(1))
, _del(del)
{}
~shared_ptr()
{
release();
// cout << "~shared_ptr() 资源已经释放" << endl;
}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _p_r_count(sp._p_r_count)
{
++(*_p_r_count);
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
// 自己给自己赋值
if (sp._ptr != this->_ptr)
{
// = 本质上替换左边的智能指针
// 需要考虑是否需要释放
release();
_ptr = sp._ptr;
_p_r_count = sp._p_r_count;
++(*_p_r_count);
return *this;
}
}
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
// 返回引用计数长度
int use_count() const { return *_p_r_count; }
// 提供指向资源的地址
T* get() const { return _ptr; }
private:
// 引用计数为0时对资源的释放
void release()
{
// 当引用计数为0时才释放资源
// 判断的同时已经对引用计数--了
if (--(*_p_r_count) == 0)
{
// 通过删除器删除指向对象的指针
_del(_ptr);
delete _p_r_count;
}
}
private:
T* _ptr;
// 维护引用计数的指针
int* _p_r_count;
// 增加包装器来接受定制删除器
// 并且默认初始化,当没有传入删除器,也就是删除普通指针
function<void(T*)> _del = [](T* ptr) {delete ptr; };
};
对这段代码我们只要求能够理解即可,需要注意的点:
- 包装器我们需要进行初始化,当没有传入删除器时,我们通过lambda表达式将删除器设置为删除普通指针,当传入删除器时,我们在release中通过可调用的删除器对象_delete删除。
- 传入含有资源指针类型和删除器的构造函数,也需要满足shared_ptr正常的功能和拥有的成员变量
那么到了这里我们智能指针的篇章就告一段落了。