有的时候使用malloc或者new创建的对象忘记释放就会导致内存泄漏,又或者此时释放语句之前有一段代码是抛异常的话,那么执行流就会乱跳,导致内存也无法释放。
比如这一段代码,at越界访问会导致抛异常,导致执行流跳出从而没有释放指针p。
void Func()
{
int* p = new int;
vector<int> v;
v.at(0) = 10;//会抛异常
delete p;//导致p没有释放
}
因此引入了智能指针来防止这种问题导致的内存泄漏。
智能指针的思想就是RAII,就是利用对象的生命周期来管控资源的一种方式。
在对象构造时获取资源,把资源交给对象管理,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。实际上是把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr)
:_ptr(ptr)
{}
~SmartPtr()
{
cout <<"delete:"<< _ptr << endl;
delete _ptr;
}
private:
T* _ptr;
};
在对象的构造阶段把资源交给对象管理,对象的生命周期结束时会自动调用析构函数完成指针的释放。如图:抛出的异常被捕获,同时指针也释放了。
RAII思想可以扩展到锁上面,因为有时候加锁之后未解锁抛异常会导致执行流乱跳,而导致锁未释放,因此我们也可以基于此写一个智能锁,把这个锁交给一个对象管理,对象出了作用域就会自动调用析构函数完成锁的释放。但是这里需要注意的是,定义成员变量的时候要定义为引用,这样才能保证加锁的时候是加在同一个锁上的。
template<class T>//定义成模板就是如果有另外类型的锁来了也可以用这个SmartLock
class SmartLock
{
public:
SmartLock(T& lock)
:_lock(lock)
{
_lock.lock();
}
~SmartLock()
{
_lock.unlock();
}
private:
//使用引用就是为了保证加锁能加在同一个锁上面
T& _lock;//引用定义变量就是在定义它的时候初始化它
};
使用如下:
smtlock管理了这个mtx,出了作用域会自动调smtlock的析构函数完成锁的释放。
void add(int n, int *value)
{
SmartLock<mutex> smtlock(mtx);//用smtlock对象管理mtx这个锁,出了作用域会自动解锁
for (int i = 0; i < n; i++)
{
++(*value);
}
}
*
和->
)template<class T>
class SmartPtr
{
public:
//交给对象管理
SmartPtr(T* ptr)
:_ptr(ptr)
{}
~SmartPtr()
{
delete _ptr;
}
//像指针一样的行为
T& operator*()//对象出了作用域还在,所以返回引用
{
return *_ptr;//返回这个对象
}
T* operator->()
{
return _ptr;//返回原生指针
}
private:
T* _ptr;
};
C++98版本的库中提供了auto_ptr的智能指针。
想要实现一个智能指针就要实现这几个功能:RAII思想,像指针一样的行为。但是对象会有拷贝构造和赋值,auto_ptr的原理是进行了管理权的转移(管理权的转移就是,当把我的值给你之后,就把我置成空),这是一种带有缺陷的智能指针,会导致对象悬空。
实现的原理如下:
template <class T>
class auto_ptr
{
public:
auto_ptr(T* ptr)//构造
:_ptr(ptr)
{}
~auto_ptr()//析构
{
delete _ptr;
}
T& operator*(){return *_ptr;}
T* operator->(){return _ptr;}
//拷贝构造
//p1(p2)--->p2拷贝构造p1,此时把p2置空,管理权交给p1
auto_ptr(auto_ptr<T>& ptr)//this指针是p1,ptr是p2
:_ptr(ptr._ptr)
{
ptr._ptr = nullptr;
}
//赋值
//p1=p2--->p2赋值给p1,此时把p2置空,管理权转移给p1
auto_ptr& operator=(auto_ptr<T>& ptr)
{
if (this != &ptr)//不是自己给自己赋值
{
//如果p1不为空就把p1的资源先释放
if (_ptr)
{
delete _ptr;
}
//再进行权限的转移
_ptr = ptr._ptr;
ptr._ptr = nullptr;
}
return *this;//支持连续赋值
}
private:
T* _ptr;
};
auto_ptr是一种管理权转移的思想,由于转移之后原来的对象会被置空,会导致对象的悬空,如下:
void test_auto_ptr()
{
PTR::auto_ptr<int> p1(new int);
PTR::auto_ptr<int> p2(p1);//拷贝构造把p1置空了
//*p1 = 10;//p1悬空
*p2 = 20;
PTR::auto_ptr<int> p3(new int);
p3 = p2;//赋值把p2置空了
//*p2 = 30;//p2悬空
*p3 = 40;
}
这里导致原来的对象悬空了,因此很多公司都会禁用这个auto_ptr。
C++11中开始提供更靠谱的unique_ptr。
unique_ptr是简单粗暴的防止拷贝,这种比较简单,效率高,但是功能不全面,不支持拷贝和赋值操作。
具体实现思想如下:
template<class T>
class unique_ptr
{
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{}
~unique_ptr()
{
delete _ptr;
}
T& operator*(){return *_ptr;}
T* operator->() {return _ptr;}
//拷贝构造
unique_ptr(unique_ptr<T>& ptr) = delete;
//赋值
unique_ptr<T>& operator=(unique_ptr<T>& ptr) = delete;
private:
T* _ptr;
};
C++11中开始提供更靠谱的并且支持拷贝的shared_ptr。
shared_ptr它的原理是引用计数,它的功能更加全面,支持拷贝构造。但是设计复杂,会有循环引用的问题。
那说到引用计数,我们就很容易想到当指向同一块空间时,每个对象都要用同一份引用计数,所以就直接把这个引用计数定为静态的,那么这样其实是不对的,因为万一有多个对象呢?这个static定义的计数变量全局就只有一个,会出现错误的(メ`ロ´)/。如下图:
这种情况,如果释放p4引用计数变为3,释放p3引用计数变为2,释放p2引用计数变为1,释放p2引用计数变为1,最后释放p1把第一个指针释放了,但是第二个指针就没有释放,就会导致内存泄漏。所以这里的引用计数必须是动态开辟的,每创建一个新的指针就要对应的给它开辟一个引用计数,如下图。
在shared_ptr内部有会给每个资源维护一份计数,只要指向的是同一块空间,那么引用计数就会进行++,当有人要释放时,先判断引用计数是不是1,如果不是1,那就说明还有别人指向这块空间,那我就不能释放了,因为我要是释放了这块空间,那么其他指向这块空间的指针就变成了野指针,此时只需要把引用计数 - - 就可以了。
但是如果是1说明只有一个人管理这块空间,也就是说我就是最后一个管理这块空间的人,那么此时就要完成释放(这里就可以象形的理解为:我是最后一个离开教室的人,那我就要把灯关掉)。
初次实现代码如下:
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr)
:_ptr(ptr)
, _pcount(new int(1))
{}
~shared_ptr()
{
if (--(*_pcount) == 0)//引用计数--为0,此时要完成释放
{
cout << "delete:" << _ptr << endl;
delete _ptr;//释放指针
delete _pcount;//释放引用计数
}
}
T& operator*(){return *_ptr;}
T* operator->(){return _ptr;}
//p2(p1)
//拷贝构造-->你的就是我的,只要把你赋值给我就行,++引用计数即可
shared_ptr(const shared_ptr<T>& ptr)
:_ptr(ptr._ptr)
, _pcount(ptr._pcount)
{
++(*_pcount);
}
//p1=p2
//赋值-->
shared_ptr<T>& operator=(shared_ptr<T>& ptr)
{
//if (this != &ptr)不算错,但是可以优化
//两个对象的ptr如果相同就不进去了,否则--再++就是做无用功了
//要是相同的对象,ptr相同就不作操作
//要是不同的对象,你们指向同一块空间,ptr相同也不作操作
if (_ptr != ptr._ptr)
{
//判断要赋值对象的引用计数是不是1
//如果是1则需要释放这块空间,然后让该指针指向新的空间
if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
}
//两个指针指向同一块空间,再++引用计数
_ptr = ptr._ptr;
_pcount = ptr._pcount;
++(*_pcount);
}
return *this;
}
int use_count() {return *_pcount;}//查看引用计数是多少
private:
T* _ptr;
int* _pcount;//引用计数
};
基于上方的代码整体来看是没有问题,但是其实隐含了一个巨大的问题,那就是多线程的情况下会有线程安全的问题。因为这里的 ++操作和 - - 操作不是原子的操作 。所以我们还需要进一步完善shared_ptr。
看下面的代码我们就会发现多线程的时候释放的时候出问题了:
void copy_ptr(PTR::shared_ptr<int>& ptr, int n)
{
//拷贝n次ptr
for (int i = 0; i < n; i++)
{
PTR::shared_ptr<int> copy(ptr);
}
}
void test_shared_ptr_safe()
{
PTR::shared_ptr<int> p(new int);
thread t1(copy_ptr, p, 1000);
thread t2(copy_ptr, p, 1000);
cout << p.use_count() << endl;//打印引用计数
t1.join();
t2.join();
}
t1和t2是我们创建了两个线程,这两个线程都去执行拷贝ptr的函数,并且执行1000次,此时程序运行完并没有打印释放的ptr地址,并且打印出来的引用计数也是不确定的,说明此时的引用计数已经出现了问题,导致内存没有释放,会出现内存泄漏的问题。
因此多线程编程时,要给这里的引用计数进行加锁操作,基于上面的代码我们进行改进:
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr)
:_ptr(ptr)
, _pcount(new int(1))
, _pmtx(new mutex)
{}
~shared_ptr()
{
Release();
}
T& operator*(){return *_ptr;}
T* operator->(){return _ptr;}
void Addref()//++引用计数操作
{
_pmtx->lock();
++(*_pcount);
_pmtx->unlock();
}
void Release()//--引用计数操作
{
_pmtx->lock();
int flag = 0;
if (--(*_pcount) == 0)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
delete _pcount;
flag = 1;
}
_pmtx->unlock();
if (flag == 1){ delete _pmtx; }
}
//拷贝构造-->你的就是我的,然后把引用计数++
shared_ptr(const shared_ptr<T>& ptr)
:_ptr(ptr._ptr)
, _pcount(ptr._pcount)
, _pmtx(ptr._pmtx)
{
Addref();
}
//赋值-->要判断是否需要把赋值对象的空间释放掉
shared_ptr<T>& operator=(const shared_ptr<T>& ptr)
{
if (_ptr != ptr._ptr)
{
Release();//检查是否需要释放这个赋值对象
_ptr = ptr._ptr;
_pcount = ptr._pcount;
_pmtx = ptr._pmtx;
Addref();
}
return *this;
}
//查看引用计数
int use_count(){return *_pcount;}
private:
T* _ptr;
int* _pcount;//引用计数
mutex* _pmtx;
};
这样加锁之后,智能指针的引用计数的++和- - 是安全的,也支持拷贝构造,所以shared_ptr本身是安全的,智能指针的线程安全体现在引用计数的++和- -是线程安全的,但是它指向的对象不一定是线程安全的,因为智能指针指向的对象本身并不受它的管控。
struct ListNode
{
std::shared_ptr<ListNode> _prev;
std::shared_ptr<ListNode> _next;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
void test_shared_ptr_cycle_ref()
{
std::shared_ptr<ListNode> node1(new ListNode);
std::shared_ptr<ListNode> node2(new ListNode);
node1->_next = node2;
node2->_prev = node1;
}
int main()
{
test_shared_ptr_cycle_ref();
system("pause");
return 0;
}
这一段代码中用shared_ptr管理了node1和node2,同时也管理了node1和node2里面的_prev和_next,让智能指针share_ptr指向node1和node2时,两个智能指针的引用计数都是1,再让node1的_next指向了node2,让node2的_prev指向了node1,此时两个智能指针的引用计数都变成了2,出了test_shared_ptr_cycle_ref
函数之后,node1和node2应该去调析构函数,但是此时智能指针的引用计数不为1,所以两个智能指针的引用计数- -,那么此时就没有完成节点的释放,因为发生了循环引用。
具体是这样的:
那么这种情况下应该怎么办呢?
解决方案就是使用weak_ptr:
struct ListNode
{
std::weak_ptr<ListNode> _prev;
std::weak_ptr<ListNode> _next;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
因为在执行node1->_next = node2;
和node2->_prev = node1;
时weak_ptr的_next和_prev不会增加node1和node2的引用计数,这样就保证了正常的释放。
在上面写的代码中,都是一次只申请了一块空间,然后让智能指针管理,智能指针释放的时候直接调用delete即可,但是不排除有这么一种情况,那就是如果一次申请多块空间呢?那就要用delete[]来进行释放,因此可以利用仿函数来定制删除器。
具体代码如下:
template<class T>//定制删除器
struct DeleteArray
{
void operator()(T* ptr)//仿函数:重载了()
{
delete[] ptr;
}
};
struct test//定义一个用于测试的类
{
~test()
{
cout << "~test()" << endl;
}
};
void test_shared_ptr_delete()//测试的函数
{
DeleteArray<test> del;
std::shared_ptr<test> ptr(new test[10], del);
}
int main()
{
test_shared_ptr_delete();
system("pause");
return 0;
}