目录
一. 智能指针初识
1.1 什么是智能指针
1.2 智能指针历史历程
1.3 为什么需要智能指针
1.3.1 内存泄漏
1.3.2 防止内存泄漏
1.3.3 异常的重新捕获
二. 智能指针的原理与使用
2.1 智能指针的原理
2.2 智能指针的使用
2.3 智能指针的拷贝问题
三. 智能指针的众多版本
3.1 auto_ptr
3.2 unique_ptr
3.3.1 基础实现
四. 定制删除器
4.1 定制删除器的使用
4.2 定制删除器的模拟实现
4.2.1 按照库的实现
4.2.2 不按照库的实现
一. 智能指针初识
1.1 什么是智能指针
智能指针不是指针,是一个管理指针的类,用来存储指向动态分配对象的指针,负责自动释放动态分配的对象,防止堆内存泄漏和空悬指针等等问题。
动态分配的资源,交给一个类对象去管理,当类对象声明周期结束时,自动调用析构函数释放资源。
1.2 智能指针历史历程
- C++ 98 中产生了第一个智能指针auto_ptr。
- C++boost给出了更加实用的scoped_ptr(防止拷贝) 和 shared_ptr(引进引用计数) 和 weak_ptr。
- C++ 11 引入了unquie_ptr 和 shared_ptr 和 weak_ptr .需要注意的是,unique_ptr对应的是boost中的scoped_ptr。并且这些智能指针的实现是参照boost中的实现的。
1.3 为什么需要智能指针
1.3.1 内存泄漏
我们在讲为什么之前先来了解一下什么是内存泄漏。
内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。
内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对 该段内存的控制,因而造成了内存的浪费。
内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现 内存泄漏会导致响应越来越慢,最终卡死。
1.3.2 防止内存泄漏
我们来看看这一个代码:
void fxx()
{
int* p1 = new int[10];
int* p2 = new int[20];
int* p3 = new int[30];
//...
delete[] p1;
delete[] p2;
delete[] p3;
}
如果指针p2或者p3开辟空间new错误,这里就会导致后面的delete不会被执行,这就导致了指针p1的内存泄漏。这里我们可以用异常来解决,但是很难看:
void fxx()
{
int* p1 = new int[10];
int* p2, *p3;
try
{
p2 = new int[20];
try {
p3 = new int[30];
}
catch (...)
{
delete[] p1;
delete[] p2;
throw;
}
}
catch (...)
{
delete[] p1;
throw;
}
//...
delete[] p1;
delete[] p2;
delete[] p3;
}
1.3.3 异常的重新捕获
我们之前一个博客(http://t.csdnimg.cn/6jA1U)在异常的描述中说了,在异常的重新抛出与捕获中,可以用智能指针解决。我们来看看:
double Division(int a, int b)
{
// 当b == 0时抛出异常
if (b == 0)
{
throw "Division by zero condition!";
}
return (double)a / (double)b;
}
void fyy() noexcept
{
int len, time;
cin >> len >> time;
cout << Division(len, time) << endl;
}
void func()
{
//这里可以看到如果发生除0错误抛出异常,下面的array数组就没有得到释放
//所以这里捕获异常但是不处理异常,异常还是交给外面处理,这里捕获了再抛出去
//就能delete array了
int* array = new int[10];
try
{
fyy();
}
catch (...)
{
//捕获异常不是为了处理异常
//是为了释放内存,然后异常再重新抛出
cout << "delete[]" << array << endl;
delete[] array;
throw;//捕到什么抛什么
}
cout << "delete[]" << array << endl;
delete[] array;
}
但是当有很多个变量要new和delete呢?就跟上面一样,会导致代码的繁琐嵌套,所以我们要用智能指针来解决。
二. 智能指针的原理与使用
2.1 智能指针的原理
智能指针的基本原理是利用RAII。
RAII:RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在 对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做 法有两大好处:
- 不需要显式地释放资源。
- 采用这种方式,对象所需的资源在其生命期内始终保持有效。
template<class T>
class Smartptr
{
public:
//RAII
Smartptr(T* ptr)
:_ptr(ptr)
{}
~Smartptr()
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
//像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
int main()
{
Smartptr<int> sp1(new int(1));
Smartptr<int> sp2(new int(2));
*sp1 += 10;
Smartptr<pair<string, int>> sp3(new pair<string, int>);
sp3->first = "apple";
sp3->second = 1;
return 0;
}
2.2 智能指针的使用
template<class T>
class Smartptr
{
public:
//RAII
Smartptr(T* ptr)
:_ptr(ptr)
{}
~Smartptr()
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
//像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
{
throw invalid_argument("除0错误");
}
return a / b;
}
void func()
{
Smartptr<int> sp1(new int(1));
Smartptr<int> sp2(new int(2));
*sp1 += 10;
cout << div() << endl;
}
int main()
{
try
{
func();
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}
通过SmartPtr对象,无论程序是正常执行结束,还是因为某些中途原因进行返回,或者抛出异常等开始所面临的困境,只要SmartPtr对象的生命周期结束就会自动调用对应的析构函数,不会造成内存泄漏,完成资源释放。
2.3 智能指针的拷贝问题
如果我们用一个智能指针拷贝构造一个智能指针,或者用一个智能指针赋值给另一个智能指针。这样的操作都会导致程序崩溃。
void test()
{
SmartPtr<int> sp1(new int);
SmartPtr<int> sp2(sp1);//拷贝构造
SmartPtr<int> sp3(new int);
SmartPtr<int> sp4 = sp3;//赋值
}
因为对于我们的智能指针来说,将sp1拷贝给sp2操作是浅拷贝,是将两个指针的指向统一到一块空间。当sp1和sp2释放时,会导致这块空间释放两次。同样的道理,将sp3赋值给sp4的时候,也只是单纯的将指针的指向指到同一块空间,这样在析构的时候也会导致析构两次。
所以对于如何解决这个问题,智能指针分为了很多版本。
三. 智能指针的众多版本
C++中存在4种智能指针:auto_ptr,unquie_ptr,shared_ptr,weak_ptr,他们各有优缺点,以及对应的实用场景。
3.1 auto_ptr
auto_ptr :管理权转移,被拷贝对象把资源管理权转移给拷贝对象,导致被拷贝对象悬空。
auto_ptr是C++98的,通过管理权转移的方式解决智能指针拷贝问题,保证了一个资源只有一个对象对其进行管理,这时候一个资源就不会被多个释放:
int main()
{
yjy::auto_ptr<int> ap1(new int(1));
yjy::auto_ptr<int> ap2(ap1);
*ap2 += 10;
//ap1悬空
//*ap1 += 10;
return 0;
}
auto_ptr的模拟实现为:
namespace yjy
{
template<class T>
class auto_ptr
{
public:
//RAII
auto_ptr(T* ptr)
:_ptr(ptr)
{}
//ap2(ap1)
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;
// 转移ap中资源到当前对象中
_ptr = ap._ptr;
ap._ptr = NULL;
}
return *this;
}
~auto_ptr()
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
//像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
}
构造对象获取资源,析构对象释放资源。对*和->运算符进行重载,使其像指针一样。拷贝构造函数,用传入的对象的资源来构造当前对象,并将传入对象管理资源指针悬空。
3.2 unique_ptr
需要引用memory库来使用。
unique_ptr是C++11中的智能指针,unique_ptr来的更直接:直接防止拷贝的方式解决智能指针的拷贝问题,简单而又粗暴,防止智能指针对象拷贝,保证资源不会被多次释放,但是防止拷贝也不是解决问题的好办法,因为在很多场景下是需要拷贝的。
int main()
{
yjy::unique_ptr<int> sp1(new int(1));
yjy::unique_ptr<int> sp2(new int(10));
sp1 = sp2;
return 0;
}
模拟实现如下:
namespace yjy
{
template<class T>
class unique_ptr
{
public:
//RAII
unique_ptr(T* ptr)
:_ptr(ptr)
{}
//up2(up1)
unique_ptr(const unique_ptr<T>& up) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;
~unique_ptr()
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
//像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
}
为了禁止拷贝,所以我们C++98的方式是将拷贝构造函数和拷贝赋值函数声明为私有;C++11的方式就直接在这两个函数后面加上=delete
。
3.3 shared_ptr
3.3.1 基础实现
shared_ptr是C++11的智能指针,通过引用计数的方式解决智能指针的拷贝问题。
- shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共 享。
- 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减 一。如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
- 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对 象就成野指针了。
引用计数的方式能够支持多个对象一起管理一个资源,也就支持智能指针的拷贝,只有当资源的引用计数减为0时才会释放,保证了同一个资源不会被多次释放:
int main()
{
yjy::shared_ptr<int> sp1(new int(1));
cout << sp1.use_count() << endl;
yjy::shared_ptr<int> sp2(sp1);
cout << sp2.use_count() << endl;
*sp2 += 10;
*sp1 += 10;
yjy::shared_ptr<int> sp3(new int(2));
yjy::shared_ptr<int> sp4(sp2);
cout << sp3.use_count() << endl;
return 0;
}
模拟实现如下:
namespace yjy
{
template<class T>
class shared_ptr
{
public:
// RAII
// 保存资源
shared_ptr(T* ptr)
:_ptr(ptr)
, _pcount(new int(1))
{}
// 释放资源
~shared_ptr()
{
Release();
}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
{
++(*_pcount);
}
void Release()
{
if (--(*_pcount) == 0)
{
delete _pcount;
delete _ptr;
}
}
//sp1 = sp1;
//sp1 = sp2;//sp2如果是sp1的拷贝呢?
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp._ptr)//资源地址不一样
{
Release();
_pcount = sp._pcount;
_ptr = sp._ptr;
++(*_pcount);
}
return *this;
}
int use_count()
{
return *_pcount;
}
// 像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T& operator[](size_t pos)
{
return _ptr[pos];
}
private:
T* _ptr;
int* _pcount;
};
}
- 构造函数获取资源时,同时将对应于的引用计数设为1,表示当前一个对象在管理这块资源。
- 析构函数,将管理资源对应的引用计数--,如果为0就需要进行释放。
- 拷贝构造函数中,与传入对象一起管理资源,将该资源的引用计数++。
- 对于拷贝赋值:先将当前对象管理的资源对应的引用计数–,为0时需要释放,然后在传入对象一起管理资源。将该资源对应的引用计数++。
为什么引用计数要用指针?
- 首先引用计数可不能用int整型来表示,这样的话,每个对象都有一个单独的引用计数。而我们要求当多个对象管理一个资源的时候,应该是引用的同一个引用计数。
- 其次引用计数也不能设置为静态的,这样的话,结果是同一个类创建的对象都是同一个引用计数,即管理不同资源的对象引用了同一个引用计数。而我们要求的是一个资源对应一个引用计数。
3.3.2 shared_ptr的循环引用
我们来讲一下shared_ptr的美中不足的地方:循环引用
struct ListNode
{
int _val;
yjy::shared_ptr<ListNode> _next;
yjy::shared_ptr<ListNode> _prev;
ListNode(int val = 0)
:_val(val)
{}
~ListNode()
{
cout << "ListNode" << endl;
}
};
int main()
{
yjy::shared_ptr<ListNode> n1(new ListNode(10));
yjy::shared_ptr<ListNode> n2(new ListNode(20));
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
n1->_next = n2;
n2->_next = n1;
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
return 0;
}
我们可以看到定义了两个对象,对象里面的prev和next对应指向另一个对象,这时候我们的shared_ptr就会存在缺陷。
在我们出作用域销毁的时候,会发生下面的情况:
n2对象销毁时-》_prev指针释放-》n1对象销毁-》_next指针释放-》n2对象销毁
可以看看运行情况:
可以看见销毁时是出错了的。
可以看到这个销毁的过程是一个互相影响的过程,是一个死循环。这样的结构就是我们的循环引用。该怎么办呢?
这里就要用到我们的weak_ptr:
//不支持RAII,不参与资源管理
template<class T>
class weak_ptr
{
public:
//RAII
weak_ptr()
:_ptr(nullptr)
{}
weak_ptr(const shared_ptr<T>& wp)
{
_ptr = wp.get();
}
weak_ptr<T>& operator=(const shared_ptr<T>& wp)
{
_ptr = wp.get();
return *this;
}
// 像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
这里的weak_ptr就不涉及RAII,不参与资源管理,从根源上杜绝了这个问题
struct ListNode
{
int _val;
yjy::weak_ptr<ListNode> _next;
yjy::weak_ptr<ListNode> _prev;
ListNode(int val = 0)
:_val(val)
{}
~ListNode()
{
cout << "ListNode" << endl;
}
};
int main()
{
yjy::shared_ptr<ListNode> n1(new ListNode(10));
yjy::shared_ptr<ListNode> n2(new ListNode(20));
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
n1->_next = n2;
n2->_next = n1;
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
return 0;
}
这样就好了。
四. 定制删除器
4.1 定制删除器的使用
智能指针该如何辨别我们的资源是用new int开辟的还是new int[]开辟的呢,要知道[]必须与delete[]匹配否则会有未知错误的,这个问题我们就交给定制删除器来解决:
这个del参数就是定制删除器,是一个可调用对象,比如函数指针、仿函数、lambda表达式以及被包装器包装后的可调用对象。
当shared_ptr对象的生命周期结束时就会调用传入的删除器完成资源的释放,调用该删除器时会将shared_ptr管理的资源作为参数进行传入。
所以当资源不是以new的形式开辟的时候,就要传特定的定制删除器。
比如:
template<class T>
struct DeleteArray
{
void operator()(T* ptr)
{
delete[] ptr;
}
};
struct ListNode
{
int _val;
yjy::weak_ptr<ListNode> _next;
yjy::weak_ptr<ListNode> _prev;
ListNode(int val = 0)
:_val(val)
{}
~ListNode()
{
cout << "ListNode" << endl;
}
};
int main()
{
yjy::shared_ptr<ListNode, DeleteArray<ListNode>> n2(new ListNode[10]);
return 0;
}
4.2 定制删除器的模拟实现
4.2.1 按照库的实现
这是按照库的实现方法来的。可以看到模板参数定制删除器是在构造函数的。
template<class T>
class shared_ptr
{
public:
template<class D>//为了跟库保持一致,我们在此处定义模板
shared_ptr(T* ptr, D del)//定制删除器
:_ptr(ptr)
, _pcount(new int(1))
, _del(del)
{}
//RAII
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _pcount(new int(1))
{}
//sp2(sp1)
shared_ptr(const shared_ptr<int>& sp)
{
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
}
void release()
{
//说明最后一个管理对象析构了,可以释放资源了
if (--(*_pcount) == 0)
{
cout << "delete:" << _ptr << endl;
//delete _ptr
_del(_ptr);
delete _pcount;
}
}
//sp1=sp4
//sp4=sp4
//sp1=sp2
shared_ptr<T>& operator=(const shared_ptr& sp)
{
//if(this!=&sp)
if (_ptr != sp._ptr)
{
release();
_ptr = sp._ptr;
--(*_pcount);
_pcount = sp._pcount;
//拷贝时++计数
++(*_pcount);
}
return *this;
}
~shared_ptr()
{
//析构时,--计数,计数减到0
release();
}
int use_count()
{
return *_pcount;
}
// 像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T* get() const
{
return _ptr;
}
private:
T* _ptr;
int* _pcount;
function<void(T*)> _del = [](T* ptr) {delete ptr; };
};
int main()
{
yjy::shared_ptr<ListNode> p1(new ListNode(10));
yjy::shared_ptr<ListNode> p2(new ListNode[10], DeleteArray<ListNode>());
yjy::shared_ptr<FILE> p3(fopen("Test.cpp", "r"), [](FILE* ptr) {fclose(ptr); });
return 0;
}
nwe[]申请内存空间必须以delete[]方式进行释放,文件指针要fclost进行释放。
4.2.2 不按照库的实现
下面是不按照库的实现方法:
template <class T>
struct Delete
{
void operator()(T* ptr)
{
delete ptr;
}
};
template<class T,class D=Delete<T>>
class shared_ptr
{
public:
shared_ptr(T* ptr)
:_ptr(ptr)
,_pcount(new int(1))
{}
shared_ptr(const shared_ptr<T>& sp)
{
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
}
void release()
{
//说明最后一个管理对象析构了,可以释放资源了
if (--(*_pcount) == 0)
{
cout << "delete:" << _ptr << endl;
//delete _ptr
_del(_ptr);
delete _pcount;
}
}
//sp1=sp4
//sp4=sp4
//sp1=sp2
shared_ptr<T>& operator=(const shared_ptr& sp)
{
//if(this!=&sp)
if(_ptr!=sp._ptr)
{
release();
_ptr = sp._ptr;
--(*_pcount);
_pcount = sp._pcount;
//拷贝时++计数
++(*_pcount);
}
return *this;
}
~shared_ptr()
{
//析构时,--计数,计数减到0
release();
}
int use_count()
{
return *_pcount;
}
// 像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T* get() const
{
return _ptr;
}
private:
T* _ptr;
int* _pcount;
D _del;
};
那我们main函数传参的时候就要变换一下:
int main()
{
yjy::shared_ptr<ListNode, DeleteArray<ListNode>> n2(new ListNode[10]);
return 0;
}
总结:
好了,到这里今天的知识就讲完了,大家有错误一点要在评论指出,我怕我一人搁这瞎bb,没人告诉我错误就寄了。
祝大家越来越好,不用关注我(疯狂暗示)