首先,我们来看一段代码:
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void Func()
{
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;
}
我们会发现,上述代码中,在div函数中抛出了一个异常,此时就会直接跳转至main函数的catch位置,p1和p2此时new出来的空间并没有得到释放,就会造成内存泄漏。
而我们如果要解决这个问题,就要在p1,p2开辟空间的位置分别抛出异常,然后在catch(…)接收,这样就显得特别的麻烦,所以也就有了智能指针;
什么是内存泄漏
内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
内存泄漏的危害
长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。
C/C++程序中一般我们关心两种方面的内存泄漏:
总结一下:
内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具。
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
template<class T>
class Smartptr
{
public:
//构造函数
Smartptr(T* ptr = nullptr)
:_ptr(ptr)
{}
//析构函数
~Smartptr()
{
cout << "delete[]:" << _ptr << endl;
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()
{
Smartptr<int> sp1(new int);
Smartptr<int> sp2(new int);
cout << div() << endl;
}
int main()
{
try
{
Func();
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}
代码中将申请到的内存空间交给了一个SmartPtr对象进行管理:
上述的SmartPtr还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过->
去访问所指空间中的内容,因此:AutoPtr模板类中还得需要将*
、->
重载下,才可让其像指针一样去使用。
template<class T>
class Smartptr
{
public:
//构造函数
Smartptr(T* ptr = nullptr)
:_ptr(ptr)
{}
//析构函数
~Smartptr()
{
cout << "delete[]:" << _ptr << endl;
if(_ptr)
delete _ptr;
}
T& operator*()
{
return (*_ptr);
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
我们的智能指针还需要解决对象的拷贝问题,我们来看下面这段代码:
int main()
{
Smartptr<int> sq1(new int);
Smartptr<int> sq2(sq1);//拷贝构造
Smartptr<int> sq3(new int);
Smartptr<int> sq4(new int);
sq3 = sq4;//拷贝赋值
return 0;
}
我们会发现对于内置类型发生的都是浅拷贝,而上述代码中就会出现同一块空间被释放两次,或者是失去对一块空间的控制,没有进行是否而造成内存泄漏。
而我们的智能指针作为一个原生指针,就是想让两个指针指向同一片空间,本就应该是浅拷贝,但单纯的浅拷贝又会导致空间被多次释放,因此根据解决智能指针拷贝问题方式的不同,从而衍生出了不同版本的智能指针。
管理权转移
auto_ptr是C++98中引入的智能指针,auto_ptr通过管理权转移的方式解决智能指针的拷贝问题,保证一个资源在任何时刻都只有一个对象在对其进行管理,这时同一个资源就不会被多次释放了。
int main()
{
std::auto_ptr<int> sq1(new int);
std::auto_ptr<int> sq2(sq1);
std::auto_ptr<int> sq3(new int);
std::auto_ptr<int> sq4(new int);
sq3 = sq4;//拷贝赋值
return 0;
}
但是我们需要注意的是管理权转移以后,我们就不能对对原来的资源进行访问了,否则程序就会崩溃,因此使用auto_ptr之前必须先了解它的机制,否则程序很容易出问题:
auto_ptr的模拟实现
auto_ptr的实现步骤如下:
*
和->
运算符进行重载,使auto_ptr对象具有指针一样的行为。namespace gtt {
template<class T>
class auto_ptr
{
public:
//构造函数
auto_ptr(T* ptr = nullptr)
:_ptr(ptr)
{}
//拷贝构造函数
auto_ptr(auto_ptr<T>& ap)
:_ptr(ap._ptr)
{
ap._ptr = nullptr;
}
//拷贝赋值函数
auto_ptr& operator=(auto_ptr<T>& ap)
{
if (this != &ap)
{
delete _ptr;
_ptr = ap._ptr;
ap._ptr = nullptr;
}
return *this;
}
//析构函数
~auto_ptr()
{
cout << "delete[]:" << _ptr << endl;
if (_ptr)
delete _ptr;
}
T& operator*()
{
return (*_ptr);
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
}
int main()
{
gtt::auto_ptr<int> sq1(new int);
gtt::auto_ptr<int> sq2(sq1);
gtt::auto_ptr<int> sq3(new int);
gtt::auto_ptr<int> sq4(new int);
sq3 = sq4;//拷贝赋值
return 0;
}
防拷贝
C++11中开始提供更靠谱的unique_ptr,unique_ptr的实现原理:简单粗暴的防拷贝。
int main()
{
std::unique_ptr<int> sq1(new int);
//std::unique_ptr sq2(sq1); error
std::unique_ptr<int> sq3(new int);
std::unique_ptr<int> sq4(new int);
//sq3 = sq4; error
return 0;
}
unique_ptr的模拟实现:
namespace gtt {
template<class T>
class unique_ptr
{
public:
//构造函数
unique_ptr(T* ptr = nullptr)
:_ptr(ptr)
{}
//析构函数
~unique_ptr()
{
cout << "delete[]:" << _ptr << endl;
if (_ptr)
delete _ptr;
}
T& operator*()
{
return (*_ptr);
}
T* operator->()
{
return _ptr;
}
//防拷贝
unique_ptr(const unique_ptr<T>& up) = delete;
unique_ptr& operator=(const unique_ptr<T>& up) = delete;
private:
T* _ptr;
};
}
C++11中开始提供更靠谱的并且支持拷贝的shared_ptr,shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源:
int main()
{
std::shared_ptr<int> sq1(new int);
std::shared_ptr<int> sq2(sq1);
cout << sq2.use_count() << endl;//2
std::shared_ptr<int> sq3(new int);
std::shared_ptr<int> sq4(new int);
sq3 = sq4;
cout << sq3.use_count() << endl;//2
return 0;
}
通过这种引用计数的方式就能支持多个对象一起管理某一个资源,也就是支持了智能指针的拷贝,并且只有当一个资源对应的引用计数减为0时才会释放资源,因此保证了同一个资源不会被释放多次。
首先,shared_ptr中的count并不能定义为一个普通的int类型的变量,因为定义成一个普通的int就会导致每一个shared_ptr对象都拥有一个自己的count,我们需要的是多个对象管理同一个资源,所以要是同一个引用计数,如下图所示:
同样,shared_ptr中的引用计数count也不能定义成一个静态的成员变量,因为静态成员变量是所有类型对象共享的,这会导致管理相同资源的对象和管理不同资源的对象用到的都是同一个引用计数,如下图:
而如果将shared_ptr中的引用计数count定义成一个指针,当一个资源第一次被管理时就在堆区开辟一块空间用于存储其对应的引用计数,如果有其他对象也想要管理这个资源,那么除了将这个资源给它之外,还需要把这个引用计数也给它。
这时管理同一个资源的多个对象访问到的就是同一个引用计数,而管理不同资源的对象访问到的就是不同的引用计数了,相当于将各个资源与其对应的引用计数进行了绑定。
需要注意,由于引用计数的内存空间也是在堆上开辟的,因此当一个资源对应的引用计数减为0时,除了需要将该资源释放,还需要将该资源对应的引用计数的内存空间进行释放。
namespace gtt {
template<class T>
class shared_ptr
{
public:
//构造函数
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
,_count(new int(1))
{}
//析构函数
~shared_ptr()
{
if (--(*_count) == 0)
{
if (_ptr != nullptr)
{
cout << "delete[]:" << _ptr << endl;
delete _ptr;
_ptr = nullptr;
}
delete _count;
_count = nullptr;
}
}
//拷贝构造函数
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _count(sp._count)
{
(*_count)++;
}
//拷贝赋值函数
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp._ptr)
{
if (--(*_count) == 0)
{
cout << "delete[]:" << _ptr << endl;
delete _ptr;
delete _count;
}
_ptr = sp._ptr;
_count = sp._count;
(*_count)++;
}
return *this;
}
T& operator*()
{
return (*_ptr);
}
T* operator->()
{
return _ptr;
}
//获取引用计数
int use_count()
{
return *_count;
}
T* get()
{
return _ptr;
}
private:
T* _ptr;
int* _count;
};
}
++
或--
,这个操作不是原子的,引用计数原来是1,++
了两次,可能还是2。这样引用计数就错乱了,会导致资源未释放或者程序崩溃的问题。所以只能指针中引用计数++
、--
是需要加锁的,也就是说引用计数的操作是线程安全的。比如下面这段程序,用一个shared_ptr管理一个整型变量,然后用两个线程分别对这个shared_ptr对象进行1000次拷贝操作,这些对象被拷贝出来后又会立即被销毁。
struct Data
{
int _year = 0;
int _month = 0;
int _day = 0;
};
void func(gtt::shared_ptr<Data>& sp, int n)
{
for (int i = 0; i < n; i++)
{
shared_ptr<Data> copy(sp);
}
}
void test()
{
gtt::shared_ptr<Data> p(new Data);
cout << p.get() << endl;
const size_t n = 10000;
thread t1(func, ref(p), n);
thread t2(func, ref(p), n);
t1.join();
t2.join();
cout << p.use_count() << endl;
}
在这个过程中两个线程会不断对引用计数进行自增和自减操作,理论上最终两个线程执行完毕后引用计数的值应该是1,因为拷贝出来的对象都被销毁了,只剩下最初的shared_ptr对象还在管理这个整型变量,但每次运行程序得到引用计数的值可能都是不一样的,根本原因就是因为对引用计数的自增和自减不是原子操作。
要解决引用计数的线程安全问题,本质就是要让对引用计数的自增和自减操作变成一个原子操作,因此可以对引用计数的操作进行加锁保护,也可以用原子类atomic对引用计数进行封装,这里以加锁为例:
代码优化如下:
namespace gtt {
template<class T>
class shared_ptr
{
public:
//构造函数
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _count(new int(1))
,_mtx(new mutex)
{}
//析构函数
~shared_ptr()
{
Release();
}
void Release()
{
_mtx->lock();
bool flag = false;
if (--(*_count) == 0) //count为0就需要释放资源
{
if (_ptr != nullptr)
{
cout << "delete[]:" << _ptr << endl;
delete _ptr;
_ptr = nullptr;
}
//释放引用计数资源
delete _count;
_count = nullptr;
flag = true;
}
_mtx->unlock();
if (flag)//锁资源最终也需要释放掉
{
delete _mtx;
_mtx = nullptr;
}
}
void AddCount()
{
_mtx->lock();
(*_count)++;
_mtx->unlock();
}
//拷贝构造函数
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _count(sp._count)
,_mtx(sp._mtx)
{
AddCount();
}
//拷贝赋值函数
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp._ptr)
{
Release();
_ptr = sp._ptr;
_count = sp._count;
_mtx = sp._mtx;
AddCount();
}
return *this;
}
T& operator*()
{
return (*_ptr);
}
T* operator->()
{
return _ptr;
}
//获取引用计数
int use_count()
{
return *_count;
}
T* get()
{
return _ptr;
}
private:
T* _ptr;
int* _count;
mutex* _mtx;
};
}
注意:
shared_ptr只需要保证引用计数的线程安全问题,而不需要保证管理的资源的线程安全问题,就像原生指针管理一块内存空间一样,原生指针只需要指向这块空间,而这块空间的线程安全问题应该由这块空间的操作者来保证。
我们看下面这段代码:
struct Data
{
int _year = 0;
int _month = 0;
int _day = 0;
};
void func(gtt::shared_ptr<Data>& sp, int n, mutex& mtx)
{
for (int i = 0; i < n; i++)
{
shared_ptr<Data> copy(sp);
sp->_year++;
sp->_month++;
sp->_day++;
}
}
void test()
{
gtt::shared_ptr<Data> p(new Data);
cout << p.get() << endl;
const size_t n = 50000;
thread t1(func, ref(p), n);
thread t2(func, ref(p), n);
t1.join();
t2.join();
cout << p.use_count() << endl;
cout << p->_month << endl;
cout << p->_year << endl;
cout << p->_day << endl;
}
尽管我们在shared_ptr内部已经实现了引用计数安全,但是我们最终在调用Date成员变量进行++
操作时,依旧不是线程安全的,因为我们是在shared_ptr外部实现++
操作的,此时就需要我们自己进行加锁和解锁操作来实现线程安全。
struct Data
{
int _year = 0;
int _month = 0;
int _day = 0;
};
void func(gtt::shared_ptr<Data>& sp, int n, mutex& mtx)
{
for (int i = 0; i < n; i++)
{
shared_ptr<Data> copy(sp);
mtx.lock();
sp->_year++;
sp->_month++;
sp->_day++;
mtx.unlock();
}
}
void test()
{
gtt::shared_ptr<Data> p(new Data);
cout << p.get() << endl;
mutex mtx;
const size_t n = 50000;
thread t1(func, ref(p), n, ref(mtx));
thread t2(func, ref(p), n, ref(mtx));
t1.join();
t2.join();
cout << p.use_count() << endl;
cout << p->_month << endl;
cout << p->_year << endl;
cout << p->_day << endl;
}
我们来看一段代码:
struct ListNode
{
gtt::shared_ptr<ListNode> _prev;
gtt::shared_ptr<ListNode> _next;
int _data;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
void test2()
{
gtt::shared_ptr<ListNode> n1(new ListNode);
gtt::shared_ptr<ListNode> n2(new ListNode);
n1->_next = n2;
n2->_prev = n1;
}
上面程序正常情况下,调用结束以后n1, n2释放就会自动调用析构函数,释放空间,但是我们运行以后发现并没有,但是当我们注释掉n1->_next = n2
和n2->_prev = n1;
中任意一句的时候,就会发现他就会调用析构函数来释放掉空间了,那么这是为什么呢?
如下图所示:
如果我们此时只让n1->_next = n2
或者是n2->_prev = n1
,其中一个资源的引用计数就变为了2,我们以n1->_next = n2
为例,此时资源2的引用计数就变为了2:
当函数调用结束以后,n1,n2的生命周期就到了,此时n2先释放,资源2引用计数--
为1,释放完之后n1在释放,资源1引用计数--
为0,因为n1->_next = n2
,此时资源2引用计数在--
为0,就完成了所有资源的释放。
如果我们将两个节点都链接起来,资源1当中的_next成员与n2一同管理资源2,资源2中的_prev成员与n1一同管理资源1,此时这两个资源对应的引用计数都被加到了2。
当出了main函数的作用域后,n1和2的生命周期也就结束了,因此这两个资源对应的引用计数最终都减到了1。如下图:
循环引用导致资源未被释放的原因:
上述问题是如何解决的呢?此时就出现了我们的weak_ptr。
weak_ptr是C++11中引入的智能指针,weak_ptr不是用来管理资源的释放的,它主要是用来解决shared_ptr的循环引用问题的。
将ListNode中的next和prev成员的类型换成weak_ptr就不会导致循环引用问题了,此时当node1和node2生命周期结束时两个资源对应的引用计数就都会被减为0,进而释放这两个结点的资源。
struct ListNode
{
std::weak_ptr<ListNode> _prev;
std::weak_ptr<ListNode> _next;
int _data;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
void test2()
{
std::shared_ptr<ListNode> n1(new ListNode);
std::shared_ptr<ListNode> n2(new ListNode);
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
n1->_next = n2;
n2->_prev = n1;
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
}
通过use_count获取这两个资源对应的引用计数就会发现,在结点连接前后这两个资源对应的引用计数就是1,根本原因就是weak_ptr不会增加管理的资源对应的引用计数。
weak_ptr的模拟实现
简易版的weak_ptr的实现步骤如下:
namespace
{
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;
}
private:
T* _ptr;
};
}
当智能指针对象的生命周期结束时,所有的智能指针默认都是以delete的方式将资源释放,这是不太合适的,因为智能指针并不是只管理以new方式申请到的内存空间,智能指针管理的也可能是以new[]的方式申请到的空间,或管理的是一个文件指针。
struct ListNode
{
ListNode* _prev;
ListNode* _next;
int _data;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
void test2()
{
gtt::shared_ptr<ListNode> sp1(new ListNode[10]);
gtt::shared_ptr<FILE> sp2(fopen("test.cpp", "r"));
}
这时当智能指针对象的生命周期结束时,再以delete的方式释放管理的资源就会导致程序崩溃,因为以new[]的方式申请到的内存空间必须以delete[]的方式进行释放,而文件指针必须通过调用fclose函数进行释放。
此时,我们就需要定制删除器来控制释放资源的方式,C++标准库中的shared_ptr提供了如下构造函数:
template <class U, class D>
shared_ptr (U* p, D del);
参数说明:
当shared_ptr对象的生命周期结束时就会调用传入的删除器完成资源的释放,调用该删除器时会将shared_ptr管理的资源作为参数进行传入。
因此当智能指针管理的资源不是以new的方式申请到的内存空间时,就需要在构造智能指针对象时传入定制的删除器。
template<class T>
struct DeleteArray
{
void operator()(T* ptr)
{
cout << "delete[] ptr" << endl;
delete[] ptr;
}
};
void test2()
{
std::shared_ptr<ListNode> sp1(new ListNode[10], DeleteArray<ListNode>());
std::shared_ptr<FILE> sp2(fopen("test.cpp", "r"), [](FILE* ptr)
{
cout << "lambda fclose" << ptr << endl;
fclose(ptr);
});
std::shared_ptr<Date> sp3(new Date[10], [](Date* ptr)
{
cout << "lambda delete" << ptr << endl;
delete[] ptr;
});
}
定制删除器的实现问题:
namespace gtt {
template<class T>
class shared_ptr
{
public:
//构造函数
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _count(new int(1))
,_mtx(new mutex)
{}
template<class D>
shared_ptr(T* ptr, D del)
: _ptr(ptr)
, _count(new int(1))
, _mtx(new mutex)
,_del(del)
{}
//析构函数
~shared_ptr()
{
Release();
}
void Release()
{
_mtx->lock();
bool flag = false;
if (--(*_count) == 0) //count为0就需要释放资源
{
if (_ptr != nullptr)
{
//cout << "delete[]:" << _ptr << endl;
//delete _ptr;
//_ptr = nullptr;
//删除器删除
_del(_ptr);
}
//释放引用计数资源
delete _count;
_count = nullptr;
flag = true;
}
_mtx->unlock();
if (flag)//锁资源最终也需要释放掉
{
delete _mtx;
_mtx = nullptr;
}
}
void AddCount()
{
_mtx->lock();
(*_count)++;
_mtx->unlock();
}
//拷贝构造函数
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _count(sp._count)
,_mtx(sp._mtx)
{
AddCount();
}
//拷贝赋值函数
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp._ptr)
{
Release();
_ptr = sp._ptr;
_count = sp._count;
_mtx = sp._mtx;
AddCount();
}
return *this;
}
T& operator*()
{
return (*_ptr);
}
T* operator->()
{
return _ptr;
}
//获取引用计数
int use_count()
{
return *_count;
}
T* get()const
{
return _ptr;
}
private:
T* _ptr;
int* _count;
mutex* _mtx;
//设置为包装器类型,可以接收任意类型
function<void(T*)> _del = [](T* ptr) {
cout << "lambda delete:" << ptr << endl;
delete ptr;
};
};
}