前言引入
上节讲到:
这样的异常处理起来很麻烦:
p1抛异常, 要处理好p2,就需要先进行对p2的delete, 比较麻烦, 所以使用智能指针
智能指针的引入:
在抛异常之前, 中间的栈帧是正常销毁的, 对象可以正常调用析构函数, 所以智能指针就利用这一点特性.
RAII版智能指针
这样的情况, 在智能指针中叫做RAII(资源获得就进行初始化)
本质的使用方法是使用对操作符的重载进行使用
RAII是智能指针的一种体现(一般是分两个部分的就可以进行这样的处理)
对一个SmartPtr类
创建然后进行简单的使用:
此时就会出现错误
会发现因为浅拷贝, 析构两次, 但是不能写拷贝构造, 因为智能指针是管理这个内存资源, 这边赋值只能希望去共同管理这一份资源, 所以使用引用
auto_ptr
在c++98处理这个的方式是重载构造, 进行管理权转移, 将ptr1管理的权限交给ptr2
在c++98下智能指针叫做auto_ptr
意外访问会崩溃, 因此auto_ptr被不适用, 很多人吐槽
unique_ptr
因此在 c++98里面, 又使用了unique_ptr
他不让赋值这一步操作正确实现
对拷贝构造函数, 只声明不实现, 这样它编译能正确通过, 函数不会被链接. 但是不排除别人在类外面实现一个无法估量的拷贝构造
为彻底解决, 将他的声明限定为私有, 同时将赋值也设为私有, 就是防止拷贝(以后防止类拷贝的类内就这样写), 不需要实现
在最新的c++11里面, 对unique_ptr内部进行修改, 也不需要为私有, 直接delete就行
shared_ptr
但是, 在某些场景下需要拷贝, 所以又引入了share_ptr
, 与上述不同在于又引入了count变量在引用计数
version1
缺点在于:
析构会导致内存泄漏
version2
缺点在于, 当 ptr2 和ptr1都指向一个时没有问题, 此时若引入第三个智能指针, 那么这个static的count变量就会1, 导致原先的指针计数出现问题
version3
即shared_ptr内部实现原理:
template <class T>
class shared_ptr
{
public:
shared_ptr(T* ptr)//构造函数
:_ptr(ptr)
, _pcount(new int(1))
{}
shared_ptr(const shared_ptr<T>& sp)//拷贝构造内对计数器+1
:_ptr(sp._ptr)
, _pcount(sp._pcount)
{
++(*_pcount);
}
~shared_ptr()
{
if (--(*_pcount) == 0)//当这个计数器为0时进行析构
{
cout << "delete->" << _ptr << endl;
delete _ptr;
}
}
private:
T* _ptr;
int* _pcount;
};
main内:
OK::shared_ptr<string> sp1(new string("xxxxxxxx"));
OK::shared_ptr<string> sp2(sp1);
同样的,对=运算符进行重载
shared_ptr<T>& operator= (const shared_ptr<T>& sp)//重载=就是将sp赋值给this
{
if (--(*_pcount) == 0)//
{
delete _pcount;
delete _ptr;
}
_pcount = sp._pcount;
_ptr = sp._ptr;
++(*_pcount);
return *this;
}
这样的赋值重载不会排除自己给自己赋值, 这样本身也是不允许的,在刚才的main函数内部, sp1 = sp2, sp3 = sp3,这是不允许的
进行修改
对比auto_ptr unique_ptr shared_ptr
shared_ptr最实用
智能指针的发展历史
c++98
auto_ptr 管理权转移->设计不好,对象悬空 ( 不建议使用 )
在boost第三方准标准库( 由出++标准委员会推出, 但是比当前的c++语法超前 )
c++11可以说是抄了boost库中的unique_ptr和shared_ptr
但是在boost中叫把unique_ptr叫做scoped_ptr
c++11
unique_ptr ->简单粗暴, 防拷贝
c++11
shared_ptr ->引用计数, 最后一个释放对象的资源->复杂一些, 但是支持拷贝, 非常完美->问题:循环引用
weak_ptr
为此在shared_ptr引入了weak_ptr
专门解决循环引用的问题, 但是weak_>ptr不符合RAII, 单纯是解决循环引用的问题
什么是循环引用, 在某些场景下, 需要使用双向链表, 但是为了方便堆内存的释放, 所以使用RAII方式的智能指针来解决这问题, 如下:
导致内存泄漏:
分析,现在只有n1->next = n2;
n1节点的next指向n2, n2 n1是包装在shared_ptr内部的ListNode, 通过shared_ptr进行使用,
现在n1的next指向n2的内存
在析构时, 先析构n2这个节点, pcount–,然后再减n1没有问题
可以看到两个节点都被释放
现在
两个都有:
我们在运行代码后, 发现节点并没有被释放
直接崩溃
相比我们自己写的, 库内的shared_ptr也是这样的
为什么呢?
这样形成循环, 内存就会被遗漏, 怎么解决这问题?
库内方式(这边只是简单模拟)
目前可以理解为, 不增加这个引用计数, weak_ptr内部不适用RAII
它使用shared_ptr来进行赋值, 构造操作,专门来解决这个shared_ptr的循环引用问题
可以通过库内的use_count()查看引用计数的次数
使用shared_ptr
使用weak_ptr()
weak_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;
}
// 像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T* get() const
{
return _ptr;
}
~weak_ptr() {}
private:
T* _ptr;
};
有一份shared_ptr创建的资源, 后来也由weak_ptr指向他
当shared_ptr生命周期到了, weak_ptr没到就会导致野指针访问, 所以给weak_ptr也引入use_count, 这个计数不会增加, 只显示当前的weak_count的计数
也引入了其他方式, expired(过期)
检查有没有过期
也有像lock关键字
不让他过期,给他锁住,了解即可
总结
智能指针包含三个方面的东西:
1.RAII
2.像指针一样
3.拷贝问题
D模板引入(包装器的使用)
包括库内也好, 自己实现也好, 实现的析构不支持delete[], 那么要释放数组应该怎么办呢?
在库内提供这样的方法, 在构造时, 根据提供的模板, 有个D就是处理这个问题
这些个D叫做定制删除器, 他是一个可调用对象, 要给他一个指针, 然后释放资源, 怎么释放, 由我们自己决定
可以使用函数指针, 仿函数, lambda等
自己实现一下:
除了这样的删除, 还有其他的一些操作:
对文件的操作
且lambda更合适
我们自己实现一个, 首先构造函数有两个参数
因为这个参数是构造函数的, 所以在构造函数写一个重载实习一下
这个D是外部传入的, 所以引入一个包装器参数, 接受这些各种变化,
在析构时, 使用这个D即可
问题, 此时单参数就会出现问题,发生未知异常
为什么呢? 因为我们已经改了析构函数, 所以单参数时, 该对象对_del进行了未初始化的调用, 所以使用缺省值
而在标准库内不是使用这个方式, 而是将这个类进行另一个类的包装, 然后支持两个参数的构造
这边是包装器更好用, 推荐使用自己的方式包装器来完成, 更加整洁
当然库内的支持的方式更多, 所以也更复杂, 但是在这一个方面, 使用包装器更好
什么是内存泄漏?
不再使用的而(或)未能释放的内存
如何避免:
事后检测, 事前预防
shared_ptr有关事例:
当有两个线程对一个智能指针进行拷贝时, 会发生未知异常, 主要是这个引用计数不是原子操作, 所以他的内部实现, 对_pcount(new atomic(1)), 进行申请atomic的变量, 达到原子操作,也可以上锁, 但是原子操作更合适
如果c++11没有提供, atomic则要自己实现原子操作’
但是shared_ptr本身是线程安全的, 保护的资源不是线程安全的
处理方式就是加锁, 或者是进行原子操作
喜欢不防来个三连支持一下~~~