智能指针
智能指针
在c++中,我们需要注意对内存资源的管理,若在程序中发生内存泄漏,这是一个非常可怕的事情。刚开始可能无事发生,但是随着内存资源的泄漏,最后就会导致程序崩溃。
但是内存资源也是一件麻烦事,所以c++提供了智能指针,将申请到的内存对象交给智能指针对象来管理。
RAII和智能指针的设计思路
RAII是ResourceAcquisition Is Initialization,即资源申请立即初始化。利用对象的生命周期来管理内存资源、网络连接、互斥锁等。RAII在获取到资源后,立即将资源交付给对象,在对象生命周期内,资源始终有效,当对象析构时,资源释放,这样就避免了资源泄漏问题
cpp
#include<iostream>
#include<memory>
{
shared_ptr<string> sp1=make_shared<string>("111");
unique_ptr<string> sp1=make_shared<string>("111");
return 0;
}
什么是内存泄漏
内存泄漏指因为疏忽或错误 造成程序未能释放已经不再使⽤的内存 ,⼀般是忘记释 放或者发⽣异常释放程序未能执⾏导致的。
内存泄漏并不是指内存在物理上的消失,⽽是应⽤程序分 配某段内存后,因为设计错误,失去了对该段内存的控制,因⽽造成了内存的浪费。
内存泄漏的危害
内存泄漏的危害:普通程序运⾏⼀会就结束了出现内存泄漏问题也不⼤,进程正常结束,⻚表的映射 关系解除,物理内存也可以释放。⻓期运⾏的程序出现内存泄漏,影响很⼤,如操作系统、后台服 务、⻓时间运⾏的客⼾端等等,不断出现内存泄漏会导致可⽤内存不断变少,各种功能响应越来越 慢,最终卡死。
内存泄漏场景
下面以抛异常举例
cpp
void David(int x, int b)
{
if (b == 0)
throw ("b不能为0");
}
void func()
{
int* arr1 = new int[10];
int* arr2 = new int[10];
try
{
//若这里抛出除0异常,且当前作用域的catch不匹配
//程序会跳到main函数中,下面catch中的delete不会被执行
//资源不会被释放,造成资源泄漏
int len = 0, time = 0;
cin >> len >> time;
David(len, time);
}
catch(const char*errmsg)
{
//每次都要对异常进行捕捉,然后释放资源,再抛出异常
delete[] arr1;
delete[] arr2;
throw;
}
}
int main()
{
try
{
func();
}
catch (const char* errmsg)
{
cout << errmsg << endl;
}
catch (const exception& errmsg)
{
cout << errmsg.what() << endl;
}
catch (...)
{
cout << "未知错误" << endl;
}
return 0;
}
C++标准库智能指针的使⽤
C++标准库中的智能指针都在<memory>头⽂件下⾯,我们包含就可以是使⽤了, 智能指针有好⼏种,除了weak_ptr他们都符合RAII和像指针⼀样访问的⾏为,原理上而言主要是解决智能指针拷贝时的思路不同。
auto_ptr
auto_ptr是在c++98提出的,这是一个非常糟糕的设计,因为在拷贝时,会发生资源管理权转移的问题,造成智能指针对象悬空,访问报错的问题。
cpp
auto_ptr<int>ap1(new int(1));
ap1在拷贝构造ap2时,资源的管理权被转移到了ap2,ap1悬空。

所以对于auto_ptr,尽量不要用
unique_ptr
是C++11设计出来的智能指针,唯一指针,不支持拷贝,只支持移动。如果不需要拷⻉的场景就⾮常建议使⽤他。
它也会发生资源管理权的转移,但与auto_ptr不同的是,unique_ptr是否对资源管理权的转移,是由程序员决定的,而auto_ptr发生资源管理权转移时,程序员自己可能都不知道。
cpp
int main()
{
unique_ptr<Date>up1(new Date);
unique_ptr<Date>up2(move(up1));
return 0;
}

unique_ptr只支持移动构造,不支持拷贝构造,所以当发生资源管理权转移时,必定是程序员对unique_ptr对象move了。资源管理权转移变的可控了
shared_ptr
是C++11设计出来的智能指针,共享指针,支持拷贝构造也支持移动构造,需要进行拷贝的场景就要使用它。底层是⽤引⽤计数的⽅式实现的。
- 它允许多个shared_ptr指针指向同一块资源,底层用一个"计数器"来记录。

cpp
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
:_year(year),
_month(day),
_day(day)
{
}
private:
int _year;
int _month;
int _day;
};
int main()
{
shared_ptr<Date>sp1(new Date);
shared_ptr<Date>sp2(sp1);
return 0;
}

use_count()接口返回有多少shared_ptr在管理该资源
shared_ptr支持new type[]
cpp
shared_ptr<Date[]>sp1(new Date[]);
说到这个,就要提到另一个东西"定制删除器"
cpp
template <class U, class D> shared_ptr (U* p, D del);
删除器可以是:
- lambda
- 仿函数
- 函数指针
cpp
//传lambda
shared_ptr<Date>sp2(new Date[10], [](Date* ptr) {delete[]ptr; });
//传入放函数
shared_ptr<FILE>sp3(fopen("...","r"),Fclose());
//传函数指针
shared_ptr<Date>sp3(new Date[10],Del<Date>);
shared_ptr循环引用问题
我们知道,shared_ptr在底层,会维护一个计数器,来记录有多个shared_ptr在管理该资源,这个设计避免了因为一个对象析构,释放了资源导致其他管理该资源的对象悬空。但同时页引出了一个新问题"循环引用"
cpp
struct ListNode
{
shared_ptr<ListNode>*_next;
shared_ptr<ListNode>*_prev;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
shared_ptr<ListNode>sp1(new ListNode);
shared_ptr<ListNode>sp2(new ListNode);
sp1->_next = sp2;
sp2->_prev = sp1;
return 0;
}
- n1与n2是双向链表中的节点。sp1管理着n1节点,sp2管理着n2节点,此时让n1的_next指针指向sp2管理的资源,也就是n2节点,再让n2的_prev指针指向sp1管理的资源,也就是n1。
- 运行程序,当程序结束时,n1与n2因该被析构,终端会打印 "~ListNode()" ,但是结果是并没有打印。


这是因为n1与n2节点的_next、_prev是智能指针,这两个节点互相指向时,shared_ptr底层对应的计数器++,即使sp1与sp2析构了,计数器--,但由于_next、_prev的原因,资源对应的计数器始终不会减为0,导致资源无法正常释放,内存泄漏。这是为什么?
如果n1节点想要释放,就需要n2节点_prev对象析构,而_prev对象析构就需要n2节点释放,而n2节点释放,就需要n1节点的_next对象析构,而_next对象析构,就需要n1节点释放,此时,形成循环,导致n1与n2节点都无法释放

为了解决循环引用问题,c++提供了weak_ptr
weak_ptr和shared_ptr
weak_ptr其实并不是智能指针。它本质就是为了解决循环引用的问题而提出的。
- 当weak_ptr指向share_ptr管理的资源时,并不会增加引用计数。
- 但是,引用计数还被一个count维护,当weak_ptr指向shared_ptr管理的资源时,count++。
像上面提到的循环引用问题,为了解决该问题,我们需要将ListNode的代码该动一下
cpp
struct ListNode
{
//shared_ptr< ListNode> _prev;
//shared_ptr< ListNode> _next;
weak_ptr<ListNode>_prev;
weak_ptr<ListNode>_next;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
sp1管理n1节点,sp2管理n2节点,即使n1与n2节点互相指向,但是引用计数也不会++,因为ListNode的两个指针是weak_ptr,不会增加引用计数,只会增加count。
当sp1与sp2析构时,引用计数--,变为0,weak_ptr检测到计数器变为0,说明该资源过期了,就不会指向该资源了,count--,若此时count减为0,说明没有weak_ptr指向该资源了,就将资源释放了,反之就不释放。
shared_ptr线程安全问题
shared_ptr本身是线程安全的,但是指向的对象不是线程安全的,当多线程场景时,需要对临界资源进行加锁
cpp
struct AA
{
int _a1 = 0;
int _a2 = 0;
~AA()
{
cout << "~AA()" << endl;
}
};
int main()
{
shared_ptr<AA>p1(new AA);
mutex _mutex;
auto func = [&]() {
for (int i = 0; i < 10000; i++)
{
shared_ptr<AA>copy(p1);
{
unique_lock<mutex>lx(_mutex);
copy->_a1++;
copy->_a2++;
}
}
};
thread t1(func);
thread t2(func);
t1.join();
t2.join();
cout << p1->_a1 << endl;
cout << p1->_a2 << endl;
return 0;
}