智能指针
为什么C++11要提出智能指针的概念?
- new申请内存后,如果触发异常,则就不会处理后面的delete,这样就会导致内存泄露的产生;若要是捕获异常再重新抛出话,代码比较繁杂
- 所有的内存申请new与释放delete之间都要捕获,代码繁杂,代码的可读性变得很差
智能指针的使用及其原理
提及智能指针,就必须提到RAII,
RAII是什么呢?
RAII是一种利用对象生命周期控制资源的技术。
对象构造时获取资源,接着控制对资源的访问使它在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。本质上就是把资源托管给一个对象。
智能指针:
- 将指向动态开辟资源的指针,交给RAII的类管理起来
- 像指针一样的使用,支持operator* 和 operator->
- 解决拷贝问题
智能指针的发展历史
名称 | 产生 | 作用 |
---|---|---|
auto_ptr | C++98产生 | 管理权转移,导致指针悬空 |
scoped_ptr/scoped_array | boost库中的 | 防拷贝 ,在不需要拷贝的情况下,建议使用它 |
shared_ptr/weakptr | boost库中的 | 引出引用计数的概念,支持拷贝,拷贝时增加技术,析构时减少计数,引用计数为0时释放资源 |
unique_ptr | C++11产生的 | 强制的防拷贝,在不需要拷贝的情况下,建议使用它 |
shared_ptr/weak_ptr | C++11产生的 | 引用计数,支持拷贝,拷贝时增加计数,析构时减少计数 ,引用计数为0时释放资源 |
C++11中的shared_ptr、weak_ptr就是对boost库中的进行模仿重写,我们这篇就看从auto_ptr到C++11的unque_ptr,再到shared_ptr/weak_ptr的底层实现。
auto_ptr
我们先来实现一个简易版的auto_ptr,来看看它带来的问题
//1.RAII管理资源
//2. 像指针一样使用
namespace lth
{
template <class T>
class auto_ptr
{
public:
auto_ptr(T* ptr)
:_ptr(ptr)
{
}
~auto_ptr()
{
cout << "delete" << " "<< _ptr << endl;
delete[] _ptr;
}
T& operator*()
{
return *_ptr;
}
T*operator->()
{
return _ptr;
}
private:
T* _ptr;
};
// auto_ptr的使用
struct A
{
int _a1;
int _a2;
};
//像指针一样使用
void test_auto_ptr()
{
auto_ptr<int> ap1(new int);
auto_ptr<int> ap2(new int);
*ap1 = 10;
A* pa = new A;
pa->_a1 = 20;
auto_ptr<A> apa(new A);
(*apa)._a1 = 20;
apa->_a2 = 100;
}
void test_auto_ptr2()
{
auto_ptr<int>ap1(new int);
auto_ptr<int> copy(ap1); // error, 默认的是浅拷贝,内存被管理了两次,最后内存才能也释放了两次
}
}
使用auto_ptr的时候的注意事项:
- auto_ptr不可以深拷贝。原因是auto_ptr copy(ap1)默认是浅拷贝,内存资源被管理了两次,最后调用析构函数,内存释放了两次。
- 那么深拷贝可以吗? 也是不可以的
- 资源不是属于智能指针,智能指针只是托管这个资源,并没有权利去深拷贝。
- 智能指针,行为也要像指针一样,如果可以支持深拷贝就不像指针了,指针指向的是托管资源,又重新拷贝出一块资源,就不是对原来空间的进行托管的了。
我们知道了auto_ptr存在的问题,但是auto_ptr针对以上的问题,C++98采用的是管理权转移的方法,支持深拷贝。是将现在管理这个资源的类拷贝出新的类之后,将这个类的指针置空实现的。
auto_ptr的完整版模拟实现
template <class T>
class auto_ptr
{
public:
auto_ptr(T* ptr)
:_ptr(ptr)
{
}
auto_ptr(auto_ptr<T>& ap)
: _ptr(ap._ptr)
{
ap._ptr = nullptr; // 管理权转移,就把原来管理这块空间的智能指针置空
}
//ap1 = ap2;
auto_ptr<T>& operator=(auto_ptr<T>& ap)
{
if (this != &ap)
{
if (_ptr) // 如果还有资源,先把资源释放掉
{
delete _ptr;
}
_ptr = ap._ptr;
ap._ptr = nullptr;
}
return *this;
}
~auto_ptr()
{
cout << "delete" << " " << _ptr << endl;
delete[] _ptr;
}
T& operator*()
{
return *_ptr;
}
T*operator->()
{
return _ptr;
}
private:
T* _ptr;
};
void test1()
{
auto_ptr<int> ap1 = new int;
auto_ptr<int> ap = new int[10];
auto_ptr<int> copy(ap1); //拷贝构造copy1,管理权转移,交给别的类管理
//这样的问题: 拷贝构造给别人了,转交出去的对象全部都悬空。不能在访问
*ap1 = 10; // error, 访问空指针,崩溃。
}
但是管理权转移也带来了新的问题,我们把auto_ptr copy(ap1);我们把对象ap1深拷贝出对象copy,本来是由ap1管理这个类的,而现在是交给copy对象去管理资源。但是auto_ptr的做法是将ap1这个类管理资源的指针置为空,这样访问ap1这个类就会程序崩溃。
所以C++11又引入了 unique_ptr。
unique_ptr
简单粗暴,不允许拷贝。通过将拷贝构造函数定义为私有实现的。
unique_ptr的模拟现实
template <class T>
class unique_ptr
{
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{
}
~unique_ptr()
{
cout << "delete" << " " << _ptr << endl;
delete[] _ptr;
}
unique_ptr(auto_ptr<T>& ap) = delete;
unique_ptr<T>& operator=(const auto_ptr<T>& ap) = delete;
T& operator*()
{
return *_ptr;
}
T*operator->()
{
return _ptr;
}
private:
T* _ptr;
};
void test_unique_ptr()
{
unique_ptr<int> up1 = new int();
unique_ptr<int> copy(up1); //error 不支持拷贝构造
}
unique_ptr简单粗暴,通过将拷贝构造函数和赋值函数通过delete来禁止掉来实现
shared_ptr
shared_ptr之引用计数
功能:支持拷贝
解决的问题:两个对象或者多个对象管理这块空间的时候,若都调用析构函数,那么这块空间会被释放多次。
实现原理:引入了一个引用计数的概念。
引用计数:记录多少个对象管理着这块资源。管理对象的这个类若要释放调用析构函数的时候要先检查引用计数,如果引用计数是1,则表示最后一个管理者管理着对象,那么就会释放这个资源。否则引用数–。
那么引用计数怎么实现的呢?
引用计数属于多个对象的,是静态全局的,所有对象都可以看见。实现方法有两种:
- 使用static变量实现引用计数。
- 在堆上,为第一个管理资源的智能指针对象中为资源创建一个引用计数。
但是第一种方法有明显的问题,当托管的资源时两个或多个的时候,全局只有一个static变量的引用计数,那么就不知道什么时候一个资源被释放,有可能引用计数还是>0。
比方:
引用计数static int count = 0;
shared_ptr< int > p1 = new int(); // 引用计数 count++ count=1
shared_ptr< int > p2 = new int(); // 引用计数 count++ count=2
shared_ptr< int > p3 = new int(); // 引用计数 count++ count=3
此时应该每一块空间的引用计数为1, 但是引用计数全都是3。
这样子的话,当释放p1的时候,应该是p1的引用计数减小到0,但是还是2
所以我们的引用计数应该在堆上建立。
但是我们的shared_ptr的引用计数是在堆上的,堆是公共空间,不是线程安全的,所以我们要在对临界资源引用计数修改的时候,要进行加锁。所以我们还要定义互斥锁变量,互斥锁变量也必须是在堆上的。如果不是在堆上的话,互斥锁加锁和解锁操作的就不会是同一个锁。
shared_ptr的模拟实现
template <class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _pCount(new int(1))
, Mutex(new mutex)
{
}
~shared_ptr()
{
ReleaseRef();
}
shared_ptr(shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pCount(sp._pCount)
, Mutex(sp.Mutex)
{
AddRef();
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (this != &sp && _ptr != sp._ptr)
{
// 先释放原对象的资源
ReleaseRef();
_ptr = sp._ptr;
_pCount = sp._pCount; // 引用计数赋值过去
Mutex = sp.Mutex; // 互斥锁变量也要赋值过去
AddRef();
}
return *this;
}
// 引用计数的线程安全问题
// 加引用计数
void AddRef()
{
Mutex->lock();
++(*_pCount);
Mutex->unlock();
}
// 减引用计数
void ReleaseRef()
{
bool deleteflag = false;
Mutex->lock();
if (--(*_pCount) == 0)
{
delete _pCount;
delete _ptr;
deleteflag = true; //如果资源减到0的话, true
}
Mutex->unlock();
if (deleteflag == true) //释放堆上的锁资源
{
delete Mutex;
}
}
T& operator*()
{
return *_ptr;
}
T*operator->()
{
return _ptr;
}
int GetRefCount()
{
return *_pCount;
}
T* GetPtr() const
{
return _ptr;
}
private:
T* _ptr;
int* _pCount;
mutex* Mutex;
};
//测试模块
void test_shared_ptr()
{
shared_ptr<int> sp1 = new int;
shared_ptr<int> copy1(sp1);
shared_ptr<int> copy2(copy1); //三个对象管理一块空间,引用计数是3
}
//多线程测试模块
void test_threads()
{
shared_ptr<int> sp(new int);
vector<thread> vthreads;
for (size_t i = 0; i < 2; ++i)
{
vthreads.push_back(
thread([&]()
{
for (int i = 0; i <100000; i++)
{
shared_ptr<int> copy(sp);
}
}
));
}
for (size_t i = 0; i < 2; ++i)
{
vthreads[i].join();
}
cout << sp.GetRefCount() << endl;
}
但是shared_ptr也会带来一个问题。循环引用的问题。
循环引用的问题
我们需要弄清楚一下两点问题:
- 导致的问题是什么? 内存泄漏
- 如何解决的,怎么解决的?
循环引用的例子:
struct ListNode
{
shared_ptr<ListNode> _next;
shared_ptr<ListNode> _prev;
int val;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
void test_cycle_ref()
{
shared_ptr<ListNode> node1(new ListNode);
shared_ptr<ListNode> node2(new ListNode);
node1->_next = node2;
node2->_prev = node1;
}
两者间形成相互依赖的关系
最初的状态:node1管理这块空间,空间的引用计数为1,node2也是管理一块空间,空间的引用计数为1.
之后: node1的_next也是一个智能指针,指向node2,就相当于node1和node2都管理这node2这块空间,node2的引用计数增加到2,node2的_prev指向node1, 也就是node1和node2也都管理着node1这块空间。所以node1的引用计数也增加到2
释放时:node2,node1都要进行析构,所以node1的空间释放空间,计数-1,现在的引用计数为1, node2的空间的释放,计数-1,引用计数为1。
这时候,两块空间的引用计数都不是0,都是1,所以两块空间的资源没有被释放。 node1节点的释放取决于node2->prev释放,node2->prev释放取决于node2节点的释放,但是node2节点的释放,此时还要取决于node1->next什么时候释放,node1->next什么时候释放,又取决于node1什么时候释放。 所以就是一个死循环,导致两块空间大家的引用计数都不为0,都释放不了这两个空间。
如何解决循环引用?
- weak_ptr 可以像指针一样去使用,但是他不会增加shared_ptr的引用计数,他不参与对资源的管理。
- 可以访问管理的资源,但是不参与对资源的释放。
weak_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) //注意这里参数是shared_ptr,所以要先搭配着上面实现的shared_ptr一起使用
:_ptr(sp.GetPtr())
{
}
weak_ptr<T>& operator=(const shared_ptr<T>& sp)//这里也是shared_ptr
{
_ptr = sp.GetPtr(); // shared_ptr类的接口,上面已经实现过了。
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
然后搭配着weak_ptr一起这样使用就不会引起循环引用了
struct ListNode
{
weak_ptr<ListNode> _next;
weak_ptr<ListNode> _prev;
int val;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
void test_cycle_ref()
{
weak_ptr<ListNode> node1(new ListNode);
weak_ptr<ListNode> node2(new ListNode);
node1->_next = node2;
node2->_prev = node1;
}
最后的最后,我们来看锁资源管理的概念:
C++11的锁资源管理
lock_guard的模拟实现
作用:管理C++11的锁资源。
LockGuard的模拟实现:
template <class Lock>
class LockGuard
{
public:
LockGuard(Lock&lock)
:_lock(lock)
{
_lock.lock();
}
~LockGuard()
{
_lock.unlock();
}
LockGuard(const LockGuard&) = delete;
private:
Lock& _lock;
};
多线程加锁的时候可以使用我们锁资源管理这个LockGuard类
int main()
{
int n = 0;
//cout << &n << endl;
mutex mtx;
vector<thread> vthreads;
for (int i = 0; i < 4; i++)
{
vthreads.push_back(thread([&n, &mtx]()
{
LockGuard<mutex> lg(mtx);
//lock_guard<mutex> lg(mtx); // C++11 库里实现的
//unique_lock<mutex> lg(mtx);// C++11 库里的锁资源管理类
for (size_t i = 0; i < 10000; i++)
{
++n;
}
}
));
}
for (size_t i = 0; i < 4; i++)
{
vthreads[i].join();
}
cout << n << endl;
system("pause");
}
问题:
C++11的lock_guard和unique_lock的区别与联系?
- 都是管理锁的自动释放,解决异常安全问题的RAII类。
- unique_lock处理RAII类,还支持手动加锁解锁,比lock_guard多了几个接口。