- 前言:
- 普通指针:
- 安全隐患:
- 其他函数异常:
- new函数异常:
- 智能指针:
- RAII原理:
- smart_ptr:
- auto_ptr:
- 管理权转移:
- 优点:
- 缺点:
- unique_str:
- 防拷贝:
- 缺点:
- shared_ptr:
- 静态引用计数器:
- 代码:
- 漏洞:
- 计数器混乱问题:
- 直接指针构造问题:
- 动态引用计数器:
- 代码:
- 优点:
- 缺点:
- 对象赋值拷贝时的计数器错误:
- 多线程下的计数器安全问题:
- 真·智能指针:
- 代码:
- 优点:
- 缺点:
- 漏洞:循环引用
- weak_ptr:
- 原理:
- 使用:
- 定制删除器:
- 背景:
- 删除器类型:
- 模拟实现:
- STL中的删除器:
- 模板传入仿函数类:
- 传参传入删除函数:
Vue框架:
从项目学Vue
OJ算法系列:
神机百炼 - 算法详解
Linux *** 作系统:
风后奇门 - linux
- 查看下面这段存在异常的函数:
int div(){
int a, b;
cin >>a >>b;
if(b == 0)
throw invalid_argument("除0错误");
return a/b;
}
void func(){
int *p = new int;
cout<<div() <<endl;
delete p;
}
int main(){
try{
func();
}catch(const exception &e){
cout<< e.what() <<endl;
}
return 0;
}
- 异常来源:
如果div()函数异常,由于不设置try catch机制,申请的p指针无法被delete释放 - 处理策略:try catch
int div(){
int a, b;
cin >>a >>b;
if(b == 0)
throw invalid_argument("除0错误");
return a/b;
}
void func(){
int *p = new int;
try{
cout<<div() <<endl;
}catch(const exception &e){
cout<< e.what() <<endl;
}
delete p;
}
int main(){
try{
func();
}
catch(const exception &e){
cout<< e.what() <<endl;
}
return 0;
}
new函数异常:
-
多次为指针申请空间,之后统一释放。
当一次申请出错时,前面所有申请的资源无法释放:
int main(){
int *p1 = new int;
int *p2 = new int;
int *p3 = new int;
delete p1;
delete p2;
delete p3;
return 0;
}
- 处理策略:
- try + catch
- 设置初始值为nullptr,申请失败后值还为nullptr
int main(){
int *p1 = nullptr;
int *p2 = nullptr;
int *p3 = nullptr;
try{
int *p1 = new int;
int *p2 = new int;
int *p3 = new int;
}catch(...){
if(p2 == nullptr)
delete p1;
if(p3 == nullptr){
delete p1;
delete p2;
}
}
delete p1;
delete p2;
delete p3;
return 0;
}
智能指针:
RAII原理:
-
RAII:resource acquisition is initialization
- 利用对象生命周期控制程序资源
- 在对象构造时获取资源
- 在对象析构时释放资源
- 实际上将一份资源的管理职责交给了一个对象
-
好处:
- 不需要显式释放资源
- 对象所需资源在对象的生命周期内始终有效
-
实质:
一个成员变量是模板指针的类
模板指针指向new函数的返回指针
资源无效后析构时释放模板指针所指资源
- 析构函数存在bug的版本:
template <class T>
class smart_ptr{
private:
T *ptr;
public:
smart_ptr(T *_ptr){
ptr = _ptr;
}
T& operator*(){
return *ptr;
}
T* operator->(){
return ptr;
}
//有bug的析构函数
~smart_ptr(){
cout<<ptr <<" " <<*ptr <<endl;
delete ptr;
}
}
int main(){
smart_ptr<int> sp1(new int);
smart_ptr<int> sp2(new int);
smart_ptr<int> sp3(new int);
//析构函数bug体现:同一空间被析构两次,肯定报错
int *p = new int;
smart_ptr<int> sp4 = p;
smart_ptr<int> sp5 = p;
cout<<div() <<endl;
return 0;
}
- 很明显,这一版的SmartPtr不是存在缺点,而是存在析构漏洞,根本不能使用
- 下面介绍C++98和C++11中几种防止SmartPtr()多次析构的不同设计
- 出现年代:c++98
- 解决多次delete同一资源的方案:管理权转移
每指向资源的指针多一个时- 将对资源的管理权交给最新的指针
- 同时原来的指针都指向空
- 代码:
template <class T>
class auto_ptr{
private:
T *ptr;
public:
auto_ptr(T *_ptr){
ptr = _ptr;
}
auto_ptr(auto_ptr<T> &ap){
ptr = ap.ptr;
ap.ptr = nullptr;
}
auto_ptr<T>& operator=(auto_ptr<T> *ap){
if(this != &ap){
if(ptr)
delete ptr;
ptr = ap.ptr;
ap.ptr = nullptr;
}
return *this;
}
T& operator*(){
return *ptr;
}
T* operator->(){
return ptr;
}
~auto_ptr(){
delete ptr;
}
};
优点:
-
优点:
直接了当的解决了双重析构的问题
-
缺点1:
新的auto_ptr到来会覆盖旧的auto_ptr
对于auto_ptr使用不熟悉的同学可能出这样的错
int main(){
auto_ptr<int> sp1(new int);
auto_ptr<int> sp2(sp1);
*sp2 = 10;
cout<< *sp2 <<endl;
cout<< *sp1 <<endl;
return 0;
}
-
缺点2:
管理权转移只存在于auto_ptr对象之间拷贝
当存在两个auto_ptr对象都从一个指针初始化而来时,还是会出现双重析构问题:
int main(){
int *p = new int(10);
auto_ptr<int> ap1(p);
auto_ptr<int> ap2(p);
return 0;
}
unique_str:
- 出现年代:boost库,同期三个智能指针为scoped_ptr / shared_ptr / weak_ptr
- C++11对boost库的借鉴产物:unique_ptr / shared_ptr / weak_ptr
- 解决多次delete同一资源的方案:防拷贝
每个指针开辟空间之后,只能有一个unique_ptr/scoped_ptr对象封装该指针
- 防止拷贝函数:
- 法一:delete关键词
- 法二:私有 + 空实现(只声明不实现)
- 代码:
template <class T>
class unique_ptr{
private:
T *ptr;
//c++11中delete关键字屏蔽函数
unique_ptr(unique_ptr<T> const &) = delete;
unique_ptr& operator=(unique_ptr<T> const &) = delete;
//c++98中私有 + 只声明不实现
unique_ptr(unique_ptr<T> const &);
unique_ptr& operator=(unique_ptr<T> const &);
public:
unique_ptr(T *_ptr){
ptr = _ptr;
}
~unique_ptr(){
delete ptr;
}
void Show(){
cout<<*ptr <<endl;
}
};
缺点:
-
理论上每个指针都只能存在一个unique_ptr对象
-
当不采用unique_ptr对象拷贝赋值,
而是直接使用指针初始化两个unique_ptr对象时,还是存在双重析构
int *p = new int(10);
unique_ptr<int> uq1(p);
unique_ptr<int> uq2(p);
shared_ptr:
- 原理:
- 记录有多少个对象管理着同一块资源
- 每个对象析构时计数器 - -
- 每个对象构造时计数器 ++
- 最后一个析构的对象负责释放资源
- 代码:
template <class T>
class shared_ptr{
private:
static int refCount;
T *ptr;
public:
shared_ptr(T *_ptr){
refCount = 0;
ptr = _ptr;
}
shared_ptr(auto_ptr &ap){
refCount++;
ptr = ap.ptr;
}
~shared_ptr(){
refCount--;
if(refCount == 0 && ptr){
delete ptr;
}
}
};
int main(){
shared_ptr<int> sp1(new int);
shared_ptr<int> sp2(sp1);
shared_ptr<int> sp3(sp1);
shared_ptr<int> sp4(new int);
return 0;
}
漏洞:
计数器混乱问题:
-
应该是每个资源独立使用一个自己的计数器
如果所有资源都使用同一个引用计数器,那么会产生如下结果
-
初始化时:
- sp1 借助 int* 初始化时,计数器refCount == 0
- sp2 借助 sp1 初始化时,计数器refCount == 1
- sp3 借助 sp1 初始化时,计数器refCount == 2
- sp4 借助 int* 初始化时,计数器refCount == 0
-
析构时:
sp1 & sp2 & sp3都对同一块内存析构,引发多重析构异常
- 继续不按套路出牌,直接使用指针构造两个对象:
int *p = new int(10);
shared_ptr<int> sp1(p); //静态计数器refCount == 0
shared_ptr<int> sp2(p); //静态计数器refCount == 0
/*
析构时直接双重析构,异常
*/
-
静态引用计数器本身存储漏洞,且不能解决指针直接构造对象问题,
下面来看看动态引用计数器能不能同时解决两个问题?
- 代码:
template <class T>
class shared_ptr{
private:
T *ptr;
int *refCount; //动态引用计数器
public:
shared_ptr(T *_ptr){
ptr = _ptr;
refCount = (new int(1));
//从这步开始已经决定了智能指针只能走对象拷贝路线,不能走指针直接构造路线
}
shared_ptr(const shared_ptr<T> &sp){
ptr = sp.ptr;
refCount = sp.refCount;
(*refCount)++;
}
shared_ptr<T>& operator=(const shared_ptr<T> &sp){
if(ptr != sp.ptr){
ptr = sp.ptr;
refCount = sp.refCount;
(*refCount)++;
}
}
~shared_ptr(){
if(--(*refCount) == 0 && ptr){
delete ptr;
delete refCount;
}
}
};
int main(){
shared_ptr<int> sp1(new int);
shared_ptr<int> sp2(sp1);
shared_ptr<int> sp3(sp1);
shared_ptr<int> sp4(new int);
return 0;
}
优点:
-
每个资源独立使用自己的计数器
不同的资源计数器之间互不干扰
- 两个指针所指不同的对象,又发生赋值拷贝时:
class shared_ptr{
shared_ptr<T>& operator=(const shared_ptr<T> &sp){
if(ptr != sp.ptr){
ptr = sp.ptr;
refCount = sp.refCount;
(*refCount)++;
}
}
};
- 发生资源引用数只增不减,可能最终导致无法析构释放:
- 对策:每次对象拷贝构造时,先让原资源引用数–,再让现资源引用数++
class shared_ptr{
shared_ptr<T>& operator=(const shared_ptr<T> &sp){
if(ptr != sp.ptr){
//原资源引用数--
if(--(*refCount) == 0){
delete ptr;
delete refCount;
}
//先资源引用数++
ptr = sp.ptr;
refCount = sp.refCount;
(*refCount)++;
}
}
};
多线程下的计数器安全问题:
- 当多线程同时含有对同一块资源的智能指针时,可能出现下列情况:
- 计数器++时少加:
1. 主线程中智能指针被创建出来,refCount = 1;
2. 子线程1中对主线程智能指针对象拷贝,refCount还未++完成(++分三个原子步骤)
3. 子线程2中对主线程智能指针对象拷贝,refCount++完成
4. 子线程1中refCount++完成
5. 此时对同一资源的引用计数器本来应该是3,但是只有2 - 计数器- -时少减:
1. 所有线程中只有两个智能指针对象时:
2. 子线程1中使用完毕智能指针对象,开始析构,但是refCount–未完成(–分三个原子步骤)
3. 子线程2中也使用完毕智能指针对象,开始析构,refCount- -完成
4. 子线程2本来refCount- -之后,refCount==0,开始delete。但是此时refCount不为0
5. 子线程1完成refCount–,资源无人delete,造成内存泄露
- 计数器++时少加:
- 线程安全对策:
- 加锁
- 锁也要借鉴refCount的原理,使用指针完成“一人一把锁”
-
吸收动态引用计数的两大缺点之后,
我们终于可以写出基本没有安全问题的智能指针类了:
#include
#include
using namespace std;
template <class T>
class SharedPtr{
private:
T *ptr;
int *refCount;
mutex *mtx;
private:
void AddRefCount(){
mtx.lock();
*refCount++;
mtx.unlock();
}
void SubRefCount(){
bool flag = 0;
mtx.lock();
if (--(*refCount) == 0){
delete ptr;
delete refCount;
flag = true;
}
mtx.unlock();
if(flag == 1)
delete mtx;
}
public:
SharedPtr(T *_ptr){
ptr = _ptr;
refCount = new int(0);
mtx = new mutex;
}
//默认采用拷贝构造的对象暂无ptr/refCount/mtx
SharedPtr(SharedPtr<T> &sp){
ptr = sp.ptr;
refCount = sp.refCount;
mtx = sp.mtx;
AddRefCount();
}
//默认采用赋值构造的对象已有ptr/refCount/mtx
SharedPtr<T>& operator=(SharedPtr<T> &sp){
if(ptr != sp.ptr){
SubRefCount();
ptr = sp.ptr;
refCount = sp.refCount;
mtx = sp.mtx;
AddRefCount();
}
}
~ShardPtr(){
SubRefCount();
}
};
优点:
-
类的基本优点:指针所指资源不用时自动析构释放
-
多个智能指针对象共享一块资源:
每一块资源都独立使用一个引用计数器
直接指针构造 / 拷贝构造 / 赋值构造 / 析构,都不会出现多重析构
-
线程安全:不会出现引用计数器少++ / 少- -的情况
- 多个智能指针对象直接使用指针构造对象时,还是存在多重析构异常:
int *p = new int(10);
shared_ptr<int> sp1(p);
shared_ptr<int> sp2(p);
shared_ptr<int> sp3(p);
- 关于拷贝构造是否要先减少原有资源引用计数器,需要单独为使用者说明:
- 如果拷贝构造只能使用在空白对象上,则不需要减少原有资源引用计数器
- 如果拷贝构造只能使用已经赋值过的对象上,则需要减少原有资源引用计数器
- 查看下面的链表节点智能指针,分析析构过程:
struct ListNode{
int data;
shared_ptr<ListNode> prev;
shared_ptr<LIstNode> next;
~ListNode(){
cout<<"~ListNode()"<<endl;
}
}
int main(){
shared_ptr<ListNode> node1(new ListNode);
shared_ptr<ListNode> node2(new ListNode);
node1->next = node2;
node2->next = node1;
return 0;
}
-
析构一个节点需要析构三部分
- 值date
- 前驱指针
- 后继指针
-
当前两块资源的引用计数器状态:
-
要释放node1,需要资源引用计数器==0
node1完成refCount–后,refCount==1
要想refCount继续–,需要释放node2的next
要释放node2的next,需要释放node2
-
要释放node2,需要资源引用计数器==0
node2完成refCount–后,refCount==1
要想refCount继续–,需要释放node1的prev
要想释放node1的prev,需要释放node1
-
发现出现了类似锁套锁的死锁情况,不过这里叫循环引用
下面来看c++11中解决循环引用的weak_ptr<>
- 循环引用出现的根本原因:
node1->next = node2;
node2->next = node1;
//各自资源引用计数器数目 +1 了
- weak_ptr避免循环引用的对策:
- 增加引用对象时,不会增加资源计数器
- 析构引用对象时,也不会发生双重析构
- 我们就不自己手动模拟实现了:#include < memory>
struct ListNode{
int _data;
weak_ptr<ListNode> _prev;
weak_ptr<ListNode> _next;
~ListNode(){
cout << "~ListNode()" << endl;
}
};
int main()
{
shared_ptr<ListNode> node1(new ListNode);
shared_ptr<ListNode> node2(new ListNode);
node1->_next = node2;
node2->_prev = node1;
return 0;
}
定制删除器:
背景:
-
不论是功能完整的shared_ptr还是特殊场景下的weak_ptr,
既然称为智能指针,就可以接收很多类型的指针
但是不同类型指针所指资源释放方式不同:
- new开辟的空间的指针 -> delete删除
- new[]开辟的空间的指针 -> delete[]删除
- malloc开辟的空间的指针 -> free释放
- fopen()打开的文件的指针 -> fclose()关闭
-
所以我们在单纯为这些智能STL类传入指针的同时,也应该传入删除指针的方式,这就叫做定制删除器
-
删除功能本质还是一个可调用对象:
- 函数名 / 函数指针
- 仿函数类对象
- lambda表达式
-
重提一下new delete 和 new[] delete[]的区别:
- new出来的空间全部都是存储内容,delete按照类型大小+内存开头地址删除即可
- new[]出来的空间开头存储的是元素个数,delete按照元素个数+类型大小+内存开头地址进行删除
- 本质上两组关键字看待内存空间的方式不一致,即使是一个bit的区别,造成的差异也很大
- 但是现在很多编译器做了优化,new[]出来的含有个数的空间,也可以被delete删除释放
- 由于类内要保存传入的删除器函数及其类型,所以只能采用模板仿函数类,而不能采用函数参数:
template <class T>
class default_delete{
public:
void operator()(const T*ptr){
cout<<"delete:"<<ptr<<endl;
delete ptr;
}
};
template <class T, class D = default_delete<T>>
class del_ptr{
private:
T *ptr;
public:
unique_ptr(T *_ptr){
ptr = _ptr;
}
~unique_ptr(){
if(ptr){
D del;
del(ptr);
}
}
};
struct DeleteArray{
void operator()(A* ptr){
cout<< "delete[] : "<<ptr <<endl;
delete[] ptr;
}
};
struct DeleteFile{
void operator()(FILE* ptr){
cout<< "fclose[] : "<<ptr <<endl;
fclose(ptr);
}
};
int main(){
del_ptr<A> sp1(new A); //默认删除器
del_ptr<A, DeleteArray> sp2(new A[10]);
del_ptr<A, DeleteFile> sp3(fopen("test.txt", "r"));
}
STL中的删除器:
- STL实现较为复杂,同时支持:
- 函数参数传入删除器函数
- 模板类内保存删除器模板仿函数类
- 模板仿函数类:
struct DeleteArray{
void operator()(A* ptr){
cout<< "delete[] : "<<ptr <<endl;
delete[] ptr;
}
};
int main(){
unique_ptr<A> sp1(new A);
unique_ptr<A, DeleteArray> sp1(new A);
return 0;
}
传参传入删除函数:
- 传参传入删除函数:
int main(){
unique_ptr<A> sp1(new A[10], [](A* p){
delete[] p;
},);
unique_ptr<A> sp2(fopen("test.txt","r"), [](FILE *p){
fclose(p);
});
}
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)