目录
1.智能指针的应用场景
2. 内存泄漏
3.智能指针的使用及原理
3.1 RAII
3.2智能指针实例
4.STL 中的智能指针
4.1.C++11和boost中智能指针的关系
4.1.1Boost库的源起
4.1.2智能指针
4.2.std::auto_ptr
4.3.std::unique_ptr
4.4.1.使用及模拟实现
4.4.2.循环引用问题
4.5.std::weak_ptr
5.结语
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 抛异常会如何?p1申请的资源无法释放
// 3、如果div调用这里又会抛异常会如何?p1、p2申请资源无法释放
int* p1 = new int;
int* p2 = new int;
cout << div() << endl;
delete p1;
delete p2;
}
int main()
{
try
{
Func();
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}
在抛出异常后不会执行throw后的语句(如delete、fclose),会造成资源泄漏的问题。
什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内 存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对 该段内存的控制,因而造成了内存的浪费。
内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现 内存泄漏会导致响应越来越慢,最终卡死。
内存泄漏非常常见,解决方案分为两种:
1、事前预防型。如智能指针等。
2、事后查错型。如泄漏检测工具。
RAII(Resource Acquisition Is Initialization)即资源获取立即初始化,是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
它的原理是利用对象生命周期来控制程序资源,在对象构造时获取资源,在对象析构时释放资源。
借此,我们实际上把管理一份资源的责任托管给了一个对象。
这种做法有两大好处:
下面智能指针的设计将申请的资源放入一个对象类管理,对象声明周期结束的时候就会自动调用析构函数从而释放资源,有效避免了上文应用场景中资源泄漏的问题。
// 使用RAII思想设计的SmartPtr类
template
class SmartPtr {
public:
SmartPtr(T* ptr = nullptr)
: _ptr(ptr)
{}
~SmartPtr()
{
if (_ptr)
delete _ptr;
}
private:
T* _ptr;
};
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void Func()
{
ShardPtr sp1(new int);
ShardPtr sp2(new int);
cout << div() << endl;
}
int main()
{
try {
Func();
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}
SmartPtr模板类中还得需要将operator* 、operator->重载,可让其像指针一样去使用。
1998年,Robert Klarer、Beman Dawes和Dave Abrahams召集了第一个Boost邮件列表,旨在促进关于为C++编程语言创建高质量、经同行评审的库的讨论,Boost库由此诞生.当时C++标准库的功能有限,许多开发者认为需要额外的库来支持不断增长的软件开发需求,于是便发起了Boost项目.
1. C++ 98 中产生了第一个智能指针auto_ptr.
2. C++ boost给出了更实用的scoped_ptr和shared_ptr和weak_ptr.
3. C++ TR1,引入了shared_ptr等。不过注意的是TR1并不是标准版。
4. C++ 11,引入了unique_ptr和shared_ptr和weak_ptr。需要注意的是unique_ptr对应boost 的scoped_ptr。并且这些智能指针的实现原理是参考boost中的实现的。
https://cplusplus.com/reference/memory/auto_ptr/
C++98版本的库中就提供了auto_ptr的智能指针,包含于
#include
// 结论:auto_ptr是一个失败设计,很多公司明确要求不能使用auto_ptr
int main()
{
std::auto_ptr sp1(new int);
std::auto_ptr sp2(sp1); // sp1管理权转移给sp2
// sp1管理权悬空
*sp2 = 10;
cout << *sp2 << endl;
cout << *sp1 << endl;
return 0;
}
C++11中开始提供更靠谱的unique_ptr,unique_ptr对应boost库中的scoped_ptr
使用实例:
#include
class DeleteArray
{
public:
template
void operator()(T* ptr)
{
delete[] ptr;
}
};
int main()
{
std::unique_ptr sp1(new int);
//禁止拷贝
//std::unique_ptr sp2(sp1);
//
//DeleteArray为仿函数,用于定制释放资源的方式
std::unique_ptr sp3(new int[10]);
return 0;
}
http://www.cplusplus.com/reference/memory/unique_ptr/
unique_ptr的实现原理:简单粗暴的禁止拷贝。
模拟实现:
// 原理:简单粗暴 -- 防拷贝
namespace bit
{
template
class unique_ptr
{
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{}
~unique_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
// 像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
unique_ptr(const unique_ptr& sp) = delete;
unique_ptr& operator=(const unique_ptr& sp) = delete;
private:
T* _ptr;
};
}
C++11中开始提供更靠谱的并且支持拷贝的shared_ptr
使用实例:
#include
class Fclose
{
public:
template
void operator()(T* ptr)
{
fclose(ptr);
}
};
class Date
{
public:
Date(int year,int month,int day)
:_year(year)
,_month(month)
,_day(day)
{
}
private:
int _year;
int _month;
int _day;
};
int main()
{
shared_ptr sp1(new int);
shared_ptr sp2(sp1);
shared_ptr sp3(sp1);
shared_ptr sp4(new int);
//Fclose为定制的释放资源的方式
shared_ptr sp5(fopen("test.cpp","r"),Fclose());
shared_ptr spd = make_shared(2024, 11, 28);
//sp1 = sp1;
//sp1 = sp2;
//sp1 = sp4;
//sp2 = sp4;
//sp3 = sp4;
*sp1 = 2;
*sp2 = 3;
return 0;
}
https://cplusplus.com/reference/memory/shared_ptr/
shared_ptr的原理:是通过引用计数引用的方式来实现多个shared_ptr对象之间共享资源。例如: 比特老师晚上在下班之前都会通知,让最后走的学生记得把门锁下。
1. shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
2. 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源。
4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
模拟实现:
//优点:1.利用对象生命周期控制资源获取和释放(不需要显式地释放资源、资源在其生命周期内有效)
// 2.支持拷贝,使用了引用计数拷贝
//缺点:1.对同一个对象的地址拷贝,浅拷贝,会访问已释放的资源
// 2.循环引用会造成内存泄露
template
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
: _ptr(ptr)
, _count(new int(1))
{}
//Del为定制的释放资源的函数可以为:
//1.函数指针 2.仿函数 3.lambda
template
shared_ptr(T* ptr, Del del)
: _ptr(ptr)
, _count(new int(1))
, _del(del)
{}
shared_ptr(shared_ptr& sptr)
:_ptr(sptr._ptr)
, _count(sptr._count)
{
++*_count;
}
T* get()const
{
return _ptr;
}
int get_count()
{
return *_count;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
shared_ptr& operator=(const shared_ptr& sptr)
{
//首先要取消链接前一个指向对象
release();
_ptr = sptr._ptr;
_count = sptr._count;
++*_count;
return *this;
}
void release()
{
if (--*_count == 0)
{
//用包装器函数释放_ptr
_del(_ptr);
delete _count;
_ptr = nullptr;
_count = nullptr;
}
}
~shared_ptr()
{
release();
}
private:
//管理指针
T* _ptr;
//引用计数,所有shared_ptr指向同一个引用计数
int* _count;
//包装器,用于定制回收资源
function _del = [](T* ptr) {delete ptr; };
};
使用shared_ptr在循环引用是有内存泄露风险
下面来看循环引用的具体情景
#include
//循环引用问题
struct ListNode
{
int _data;
shared_ptr _prev;
shared_ptr _next;
~ListNode() { cout << "~ListNode()" << endl; }
};
int main()
{
shared_ptr node1(new ListNode);
shared_ptr node2(new ListNode);
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
//只有一个指向没问题,会打印~ListNode()
//互相指向,内存泄漏
node1->_next = node2;
node2->_prev = node1;
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
return 0;
}
这种情景下,n2先析构,prev调用其析构函数释放n1,而n1有反过来析构n2,最终资源无法释放
weak_ptr不同意于上述智能指针,不支持直接管理资源
配合解决shared_ptr的一个缺陷:循环引用导致内存泄露
// 简化版本的weak_ptr实现
//weak_ptr不同意于上述智能指针,不支持直接管理资源
//配合解决shared_ptr的一个缺陷:循环引用导致内存泄露
template
class weak_ptr
{
public:
weak_ptr()
:_ptr(nullptr)
{}
weak_ptr(const shared_ptr& sp)
:_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;
};
内存泄漏即不再使用的内存不释放。
正确使用智能指针,可以尽可能地避免这样的问题