🌈欢迎来到C++专栏~~智能指针
- (꒪ꇴ꒪(꒪ꇴ꒪ )🐣,我是Scort
- 目前状态:大三非科班啃C++中
- 🌍博客主页:张小姐的猫~江湖背景
- 快上车🚘,握好方向盘跟我有一起打天下嘞!
- 送给自己的一句鸡汤🤔:
- 🔥真正的大师永远怀着一颗学徒的心
- 作者水平很有限,如果发现错误,可在评论区指正,感谢🙏
- 🎉🎉欢迎持续关注!
下面我们先分析一下下面这段程序有没有什么内存方面的问题?提示一下:注意分析MergeSort
函数中的问题
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;//p1抛异常,直接跳catch,没毛病int* p2 = new int;//p2跑异常,程序从这一步跳catch,p1无法释放,资源泄露cout << div() << endl; // div抛异常,调到catch,p1, p2无法释放,资源泄露delete p1;delete p2;
}int main()
{try{Func();}catch (exception& e){cout << e.what() << endl;}return 0;
}
问题分析:上面的问题分析出来我们发现有什么问题?
对此我们需要重新捕获异常,捕获后先将之前申请的内存资源释放,然后再将异常重新抛出
还有一种方法就是:智能指针
//利用RAII思想设计delete资源的类
template
class Smartptr
{
public:Smartptr(T* ptr):_ptr(ptr){}~Smartptr(){delete _ptr;}T& operator*() //解引用{return *ptr;}T* operator->() //自定义类型{return ptr;}private:T* _ptr;
};void Func()
{int* p1 = new int;//p1抛异常,直接跳catch,没毛病Smartptr sp1(p1);//栈帧结束会调用析构函数Smartptr sp2(new int);cout << div() << endl; // div抛异常,调到catch,p1, p2无法释放,资源泄露
}
代码中将申请到的内存空间交给了一个SmartPtr
对象进行管理
SmartPtr
将传入的需要被管理的内存空间保存起来*
和->
运算符进行重载如此一来,无论是正常返回,抛异常的返回,只要SmartPtr对象的生命周期结束就会调用其对应的析构函数,进而完成内存资源的释放。
实现智能指针时需要考虑以下三个方面的问题:
*
和->
运算符进行重载,使得该对象具有像指针一样的行为运用了RAII的思想:RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术
我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
为什么要解决智能指针对象的拷贝问题呢?
对于当前实现的SmartPtr类,如果用一个SmartPtr对象来拷贝构造另一个SmartPtr对象,或是将一个SmartPtr对象赋值给另一个SmartPtr对象,都会导致程序崩溃
int main()
{SmartPtr sp1(new int);SmartPtr sp2(sp1); //拷贝构造SmartPtr sp3(new int);SmartPtr sp4(new int);sp3 = sp4; //拷贝赋值return 0;
}
编译器默认生成的拷贝构造对内置类型完成值拷贝,即浅拷贝,因此用 sp1 拷贝构造 sp2 后,相当于 sp1 和 sp2 管理了同一块内存空间,当 sp1 和 sp2 析构时就会导致这块空间被释放两次
💥 管理权转移:资源管理权转移,不负责任的拷贝,会导致被拷贝对象悬空
int main()
{auto_ptr ap1(new A);ap1->_a1++;ap1->_a2++;auto_ptr ap2(ap1);return 0;
}
但一个对象的管理权转移后也就意味着,该对象不能再用对原来管理的资源进行访问了,否则程序就会崩溃,因此使用auto_ptr
之前必须先了解它的机制,否则程序很容易出问题,很多公司也都明确规定了禁止使用auto_ptr
😎auto_ptr的模拟实现
实现步骤如下:
RAII
)*
和->
运算符进行重载,使auto_ptr对象具有指针一样的行为template
class auto_ptr
{
public:auto_ptr(T* ptr = nullptr):_ptr(ptr){}auto_ptr(auto_ptr& ap):_ptr(ap._ptr){ap._ptr = nullptr; //管理权转移后,ap置空}auto_ptr& operator=(auto_ptr& ap){//检测是否为自己给自己赋值if (this != &ap){// 释放当前对象中资源if (_ptr)delete _ptr;//转移ap中资源到当前对象中_ptr = ap._ptr;ap._ptr = nullptr;}return *this;}~auto_ptr(){delete _ptr;}//像指针一样使用T& operator*() //解引用{return *_ptr;}T* operator->() //自定义类型{return _ptr;}private:T* _ptr;
};
C++11中引入的智能指针,unique_ptr通过 防止拷贝(+ delete
)的方式解决智能指针的拷贝问题
简单粗暴,不让拷贝
void test_unique_ptr()
{std::unique_ptr up1(new A);//std::unique_ptr up2(up1);//出错
}
但是总会有需要拷贝的场景吧
模拟实现如下:
*
和->
运算符进行重载,使unique_ptr对象具有指针一样的行为C++11
的方式在这两个函数后面加上=delete
,防止外部调用template
class unique_ptr
{
public:unique_ptr(T* ptr = nullptr):_ptr(ptr){}//防止拷贝unique_ptr(unique_ptr& ap) = delete;unique_ptr& operator=(unique_ptr& ap) = delete;~unique_ptr(){delete _ptr;}//像指针一样使用T& operator*() //解引用{return *_ptr;}T* operator->() //自定义类型{return _ptr;}private:T* _ptr;
};
如果面试官要我们现场手撕,那我们就撕一个unique_ptr
是通过引用计数的方式来实现多个shared_ptr
对象之间共享资源
++
,每个对象释放时,--
计数通过这种引用计数的方式就能支持多个对象一起管理某一个资源,也就是支持了智能指针的拷贝,并且只有当一个资源对应的引用计数减为0时才会释放资源,因此保证了同一个资源不会被释放多次
void test_shared_ptr()
{ljj::shared_ptr sp1(new A);ljj::shared_ptr sp2(sp1);sp1->_a1++;sp1->_a2++;std::cout << sp2->_a1 << ":" << sp2->_a2 << std::endl;//1 1sp2->_a1++;sp2->_a2++;std::cout << sp1->_a1 << ":" << sp1->_a2 << std::endl;//2 2
}
shared_ptr的模拟实现步骤如下:
count
,表示智能指针对象管理的资源对应的引用计数++
--
(如果减为0则需要释放),然后再与传入对象一起管理它管理的资源,同时需要将该资源对应的引用计数++
(看下图)count--
,如果减为0,彻底释放资源*
和->
运算符进行重载,使shared_ptr对象具有指针一样的行为template
class shared_ptr
{
public://RAIIshared_ptr(T* ptr = nullptr):_ptr(ptr),_pCount(new int(1)) //构造的时候设为1{}//拷贝构造shared_ptr(shared_ptr& sp):_ptr(sp._ptr),_pCount(sp._pCount) //把引用指针给与,共同管理{(*_pCount)++;}// sp1 = sp5shared_ptr& operator=(shared_ptr& sp){//防止同一块资源之间赋值if (_ptr != sp._ptr){//sp的资源-- 并且判断是否要置空if ((*_pCount)-- == 0){cout << "delete: " << _ptr << endl;delete _ptr;delete _pt;}//一起管理新资源,++计数_ptr = sp._ptr;_pCount = sp._pCount;(*_pCount)++;return *this;}}~shared_ptr(){if (--(*_pCount) == 0 && _ptr){std::cout << "Delete" << _ptr << std::endl;delete _ptr;delete _pCount;}}//像指针一样使用T& operator*() //解引用{return *_ptr;}T* operator->() //自定义类型{return _ptr;}private:T* _ptr;int* _pCount; // 引用计数//int count;
};
为什么引用计数要设计成指针?放在堆区
首先count
不能设置成一个int类型的成员变量,这意味着每一个对象都有属于自己的count,如果多个对象要管理一个资源的时候,岂不是乱套了?
还有就是count
也不能定义成一个静态的成员变量,因为静态成员变量是所有类型对象共享的,所以无论什么类型的对象都是共用一个count
所以每个资源需要管理时,给构造函数,构造new一个引用计数指针,在堆区开辟一块空间用于存储其对应的引用计数,如果有其他对象也想要管理这个资源,那么除了将这个资源给它之外,还需要把这个引用计数也给它
后续补上
当智能指针对象的生命周期结束时,所有的智能指针默认都是以delete
的方式将资源释放,这是不太合适的,因为智能指针并不是只管理以new方式申请到的内存空间,智能指针管理的也可能是以new[]
的方式申请到的空间,或管理的是一个文件指针
struct Node
{int _val;ljj::weak_ptr _next;ljj::weak_ptr _prev;~Node(){cout << "~Node" << endl;}
};
void test_shared_ptr2()
{std::shared_ptr n1(new Node);std::shared_ptr n1(new Node[5]);//errorstd::shared_ptr sp2(fopen("test.cpp", "r")); //errorstd::shared_ptr n3(new int[5]);//内置类型没问题
}
这里为什么内置类型可以通过,但是自定义类型就会报错呢? 涉及指针偏移
malloc
,调用delete最终还是会调用到free
不管它底层崩不崩,我们要匹配好:new[]
的方式申请到的内存空间必须以delete[]
的方式进行释放,而文件指针必须通过调用fclose
函数进行释放
这时就需要用到定制删除器来控制释放资源的方式,C++标准库中的shared_ptr
提供了如下构造函数:
template
shared_ptr (U* p, D del);//构造函数
参数说明:
p
:需要让智能指针管理的资源del
:删除器,这个删除器是一个可调用对象,比如函数指针、仿函数、lambda表达式以及被包装器包装后的可调用对象//仿函数
template
struct DeleteArray
{void operator()(T* ptr){cout << "delete[]" << ptr << endl;delete[] ptr;}
};//定值删除器
void test_shared_ptr2()
{//仿函数对象std::shared_ptr n1(new Node);std::shared_ptr n2(new Node[5], DeleteArray());std::shared_ptr n3(new int[5], DeleteArray());//内置类型没问题//lambda对象std::shared_ptr l1(new Node);std::shared_ptr l2(new Node[5], [](Node* ptr) { delete[] ptr; });std::shared_ptr l3(new int[5], [](int* ptr) { delete[] ptr; });std::shared_ptr l4((int*)malloc(sizeof(12)), [](int* ptr) { free(ptr); });std::shared_ptr l5(fopen("test.txt", "w"), [](FILE* ptr) { fclose(ptr); });
}
最好用的肯定是lambda
对象
在unique_ptr
中却是在模板参数中给的,只能在模板参数中传类型,不能用lambda
对象
int main()
{//不能用lambdastd::unique_ptr up(new Node[5]);//error//模板中传的是类型,不是传对象不能加(),std::unique_ptr> up(new Node[5]);
}
🎃模拟删除器的实现:
shared_ptr
类再增加一个模板参数,在构造shared_ptr对象时就需要指定删除器的类型。然后增加一个支持传入删除器的构造函数,在构造对象时将删除器保存下来,在需要释放资源的时候调用该删除器进行释放即可。最好在设置一个默认的删除器,如果用户定义shared_ptr对象时不传入删除器,则默认以delete的方式释放资源namespace ljj
{//默认删除器templateclass Delete{void operator()(T* ptr){delete ptr;}};template>class shared_ptr{public:void Release(){if (--(*_pCount) == 0 && _ptr){//cout << "Delete" << _ptr << endl;//delete _ptr;//D del;//del(_ptr);D()(_ptr);//无参构造对象,operator()去决定,是free还是delete等等delete _pCount;}}~shared_ptr(){Release();}private:T* _ptr;int* _pCount; // 引用计数};
}
shared_ptr
的循环引用问题在一些特定的场景下才会产生。比如定义如下的结点类,并在结点类的析构函数中打印一句提示语句,便于判断结点是否正确释放
struct Node
{int _val;std::shared_ptr _next;std::shared_ptr _prev;~Node(){cout << "~Node" << endl;}
};
现在以new的方式在堆上构建两个结点,并将这两个结点连接起来,又为了防止抛异常我们把类型改成智能指针
struct Node
{int _val;std::shared_ptr _next;std::shared_ptr _prev;~Node(){cout << "~Node" << endl;}
};
int main()
{std::shared_ptr n1 (new Node);std::shared_ptr n2 (new Node);n1->_next = n2; //智能指针和原生指针不能直接赋值n2->_prev = n1;//...return 0;
}
此时两个结点都没有被释放,但如果去掉连接结点时的两句代码中的任意一句,那么这两个结点就都能够正确释放,根本原因就是因为这两句连接结点的代码导致了循环引用
下面来一探究竟吧
循环引用导致资源未被释放的原因: 很绕!
_next
析构,_next什么时候析构取决于左边节点的析构(作为成员);_prev
的析构,prev什么时候析构取决于右边节点的析构;右边的节点释放又取决于_next析构二者互相纠缠,对此shared_ptr也是无能为力,又要引进一员大将weak_ptr
weak_ptr
就是shared_ptr的小跟班,不是常规智能指针,没有RAII,不支持直接资源管理
weak_ptr
对象不参与资源释放管理,可以访问和修改资源,但不会增加这块资源对应的引用计数解决方案:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了
原理就是:node1->_next = node2;和node2->_prev = node1;时weak_ptr的_next和
_prev不会增加node1和node2的引用计数,引用计数都是1,就可以按先后顺序释放了
struct Node
{int _val;std::weak_ptr _next;std::weak_ptr _prev;~Node(){cout << "~Node" << endl;}
};//循环引用
void test_weak_ptr()
{std::shared_ptr n1 (new Node);std::shared_ptr n2 (new Node);cout << n1.use_count() << endl;cout << n2.use_count() << endl;n1->_next = n2; //智能指针和原生指针不能直接赋值;所以next变成智能指针n2->_prev = n1;cout << n1.use_count() << endl;cout << n2.use_count() << endl;
}
通过use_count获取这两个资源对应的引用计数就会发现,在结点连接前后这两个资源对应的引用计数就是1,根本原因就是weak_ptr不会增加管理的资源对应的引用计数
🎃weak_ptr的模拟实现
提供一个无参的构造函数、shared_ptr
对象拷贝构造、weak_ptr
对象拷贝赋值、shared_ptr对象拷贝赋值给weak_ptr
对*
和->
运算符进行重载,使weak_ptr对象具有指针一样的行为
//辅助型智能指针,配合解决shared_ptr的循环引用问题
template
class weak_ptr
{
public:weak_ptr() //无参:_ptr(nullptr){}weak_ptr(const weak_ptr& wp) //weak类型构造:_ptr(wp._ptr){}weak_ptr(const shared_ptr& sp) //shared_ptr类型构造:_ptr(sp.get()){}weak_ptr& operator= (const shared_ptr& sp){_ptr = sp.get();return *this;}//像指针一样使用T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;
};
注意: shared_ptr还会提供一个get
函数,用于获取其管理的_ptr
说明一下:boost库是为C++语言标准库提供扩展的一些C++程序库的总称,boost库社区建立的初衷之一就是为C++的标准化工作提供可供参考的实现,比如在送审C++标准库TR1中,就有十个boost库成为标准库的候选方案。