目录
前言:智能指针在C++11出了以后就很好用了,新增的unique_ptr和shared_ptr都很好用。
一.智能指针出现的原因
1.为什么需要智能指针
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 抛异常会如何?
// 3、如果div调用这里又会抛异常会如何?
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;
}
这里因为抛异常导致无法delete,因此就导致内存泄漏,出现问题。这个想要解决是可以解决的,不过如果当new的数量更多时,会很难去解决,因此出现了智能指针。
2.内存泄漏
(1)内存泄漏的概念
内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
(2)内存泄漏的危害
长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。
(3)内存泄漏分类
① 堆内存泄漏
堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。
② 系统资源泄漏
指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放
掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
(4)如何避免内存泄漏
① 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。(这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要用智能指针来管理才有保证)
② 采用RAII思想或者智能指针来管理资源。
③ 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项
④ 出问题了使用内存泄漏工具检测。
二.智能指针的使用及原理
1.RAII
RAII是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
① 不需要显式地释放资源。
② 采用这种方式,对象所需的资源在其生命期内始终保持有效
// 使用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr {
public:
SmartPtr(T* ptr = nullptr)
: _ptr(ptr)
{}
~SmartPtr()
{
if(_ptr)
delete _ptr;
}
private:
T* _ptr;
};
2.智能指针原理
上述的SmartPtr还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过->去访问所指空间中的内容,因此:还得需要将* 、->重载下,才可让其像指针一样去使用。
智能指针就是在RAII的原理指导下形成的。
智能指针的原理:①RAII特性 ②重载了operator*和operator->,具有像指针一样的行为。
template<class T>
class SmartPtr
{
public:
// RAII思想
SmartPtr(T* ptr)
: _ptr(ptr)
{}
~SmartPtr()
{
cout << "delete" << _ptr << endl;
delete _ptr;
_ptr = nullptr;
}
// 像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T* Get()
{
return _ptr;
}
private:
T* _ptr;
};
3.auto_ptr
C++98版本的库中就提供了auto_ptr的智能指针。但是它有一些问题。
auto_ptr的实现原理:管理权转移的思想。
模拟实现:
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr)
: _ptr(ptr)
{}
~auto_ptr()
{
if (_ptr)
{
cout << "delete" << _ptr << endl;
delete _ptr;
_ptr = nullptr;
}
}
auto_ptr(auto_ptr<T>& sp)
: _ptr(sp._ptr)
{
sp._ptr = nullptr;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T* get()
{
return _ptr;
}
private:
T* _ptr;
};
auto_ptr如果拷贝给另一个对象后,会导致原对象悬空,这时如果对原对象进行操作,就会报错,因此auto_ptr很少会有人去使用它,它也是C++一个失败的设计。
4.unique_ptr
C++11提供了unique_ptr、shared_ptr和weak_ptr,这几个比较好用。
unique_ptr的实现原理:简单粗暴的防拷贝(不允许拷贝)。
模拟实现:
template<class T>
class unique_ptr
{
public:
unique_ptr(T* ptr)
: _ptr(ptr)
{}
~unique_ptr()
{
cout << "delete" << _ptr << endl;
delete _ptr;
_ptr = nullptr;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T* get()
{
return _ptr;
}
unique_ptr(const unique_ptr<T>& sp) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;
private:
T* _ptr;
};
unique_ptr是不允许拷贝的,如果确定这个不需要拷贝,就可以使用unique_ptr。
5.shared_ptr
shared_ptr的原理:通过引用计数的方式来实现多个shared_ptr对象之间共享资源。
① shared_ptr在其内部,给每个资源都维护了一份计数,用来记录该份资源被几个对象共享。
② 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
③ 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
④ 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
模拟实现:
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr)
: _ptr(ptr)
, _pCount(new int(1))
{}
void Release()
{
if (--(*_pCount) == 0 && _ptr)
{
cout << "delete" << _ptr << endl;
delete _ptr;
_ptr = nullptr;
delete _pCount;
_pCount = nullptr;
}
}
~shared_ptr()
{
Release();
}
shared_ptr(const shared_ptr<T>& sp)
: _ptr(sp._ptr)
, _pCount(sp._pCount)
{
(*_pCount)++;
}
shared_ptr operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp._ptr)
{
Release();
_ptr = sp._ptr;
_pCount = sp._pCount;
++(*_pCount);
}
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T* get()
{
return _ptr;
}
private:
T* _ptr;
int* _pCount;
};
shared_ptr的线程安全问题
拷贝是线程安全的,但是访问资源不是线程安全的。
#include<memory>
namespace bit
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _pRefCount(new int(1))
, _pMutex(new mutex)
{}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pRefCount(sp._pRefCount)
, _pMutex(sp._pMutex)
{
AddRef();
}
void Release()
{
bool flag = false;
_pMutex->lock();
if (--(*_pRefCount) == 0 && _ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
delete _pRefCount;
flag = true;
}
_pMutex->unlock();
if (flag)
delete _pMutex;
}
void AddRef()
{
_pMutex->lock();
++(*_pRefCount);
_pMutex->unlock();
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp._ptr)
{
Release();
_ptr = sp._ptr;
_pRefCount = sp._pRefCount;
_pmtx = sp._pmtx;
AddRef();
}
return *this;
}
int use_count()
{
return *_pRefCount;
}
~shared_ptr()
{
Release();
}
// 像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T* get() const
{
return _ptr;
}
private:
T* _ptr;
int* _pRefCount;
mutex* _pMutex;
};
}
int main()
{
std::shared_ptr<double> sp1(new double(1.11));
std::shared_ptr<double> sp2(sp1);
mutex mtx;
vector<thread> v(2);
int n = 100000;
for (auto& t : v)
{
t = thread([&](){
for (size_t i = 0; i < n; ++i)
{
// 拷贝是线程安全的
std::shared_ptr<double> sp(sp1);
// 访问资源不是(所以要自己加锁)
mtx.lock();
(*sp)++;
mtx.unlock();
}
});
}
for (auto& t : v)
{
t.join();
}
cout << sp1.use_count() << endl;
cout << *sp1 << endl;
return 0;
}
shared_ptr是允许拷贝的,功能是这几个中最强大的,不过shared_ptr也有一个不足的地方:如果出现了循环引用就会出现内存泄漏,这时就需要用weak_ptr来解决了。
6.weak_ptr
(1)循环引用
struct ListNode
{
hb::shared_ptr<ListNode> _next = nullptr;
hb::shared_ptr<ListNode> _prev = nullptr;
int _val = 0;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
// 循环引用
hb::shared_ptr<ListNode> p1(new ListNode);
hb::shared_ptr<ListNode> p2(new ListNode);
cout << p1.use_count() << endl;
cout << p2.use_count() << endl;
p1->_next = p2;
p2->_prev = p1;
cout << p1.use_count() << endl;
cout << p2.use_count() << endl;
return 0;
}
循环引用分析:
① p1和p2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动delete。
② p1的_next指向p2,p2的_prev指向p2,引用计数变成2。
③ p1和p2析构,引用计数减到1,但是_next指向下一个节点,并且_prev还指向上一个节点。
④ 也就是说_next析构了,p2就释放了;_prev析构了,p1就释放了。
⑤ 但是_next属于p的成员,p1释放了,_next才会析构,而p1由_prev管理,_prev
属于p2成员,所以这就叫循环引用,谁也不会释放。
(2)weak_ptr
weak_ptr与上面的智能指针不同,weak_ptr是专门用来解决shared_ptr循环引用导致内存泄漏的问题的,并且weak_ptr的参数也是shared_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)
{
if (_ptr != sp.get())
{
_ptr = sp.get();
}
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
上述循环引用解决方案:
确定是循环引用后,就使用weak_ptr来解决。
struct ListNode
{
hb::weak_ptr<ListNode> _next;
hb::weak_ptr<ListNode> _prev;
int _val = 0;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
// 循环引用
hb::shared_ptr<ListNode> p1(new ListNode);
hb::shared_ptr<ListNode> p2(new ListNode);
cout << p1.use_count() << endl;
cout << p2.use_count() << endl;
p1->_next = p2;
p2->_prev = p1;
cout << p1.use_count() << endl;
cout << p2.use_count() << endl;
return 0;
}
三.定制删除器
如果不是new出来的对象如何通过智能指针管理呢?在unique_ptr和shared_ptr各设计了一个删除器来解决这个问题
1.unique_ptr删除器
unique_ptr的删除器是通过自己实现的仿函数,并在模板参数列表中显式的写出该对应的仿函数,就可以在析构时去调用对应的仿函数析构。
class Date
{
public:
~Date()
{
cout << "~Date()" << endl;
}
private:
int _year = 1;
int _month = 1;
int _day = 1;
};
template<class T>
struct DeleteArray
{
void operator()(T* ptr)
{
cout << "delete[]" << ptr << endl;
delete[] ptr;
}
};
template<class T>
struct Free
{
void operator()(T* ptr)
{
cout << "free" << ptr << endl;
free(ptr);
}
};
struct Fclose
{
void operator()(FILE* ptr)
{
cout << "fclose" << ptr << endl;
fclose(ptr);
}
};
// 定制删除器
int main()
{
// 传类型
hb::unique_ptr<Date> up1(new Date);
hb::unique_ptr<Date, DeleteArray<Date>> up2(new Date[10]);
hb:unique_ptr<Date, Free<Date>> up3((Date*)malloc(sizeof(Date)* 10));
hb::unique_ptr<FILE, Fclose> up4((FILE*)fopen("Test.cpp", "r"));
return 0;
}
2.shared_ptr删除器
shared_ptr不是通过模板参数的仿函数来实现,而是通过传对象来实现,这里因为是对象,所以我们不仅可以用仿函数也可以用lambda表达式
// 定制删除器
int main()
{
// 传对象
std::shared_ptr<Date> sp1(new Date[10], DeleteArray<Date>());
std::shared_ptr<Date> sp2(new Date[10], [](Date* ptr){delete[] ptr; });
return 0;
}