在谈及智能指针指针之前,我先来介绍一下 RAII 这个技术:RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源的简单技术。在对象构造时获取对象资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。这样子做实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
- 不需要显式地释放资源。
- 采用这种方式,对象所需的资源在其生命期内始终保持有效。
智能指针
智能指针有两个要素:
-
1、利用 RAII 技术管理资源。
-
2、能够像普通指针一样去使用(比如支持:->、*)。
-
3、实现拷贝赋值问题。
struct person { string _name; int _age; }; template<class T> class SmartPtr { public: SmartPtr(T* ptr) :_ptr(ptr) {} T*& operator->() { return _ptr; } T& operator*() { return *_ptr; } ~SmartPtr() { cout << _ptr << endl; delete _ptr; } private: T* _ptr; }; void test_SmarePtr() { SmartPtr<int> sp(new int); *sp = 100; cout << *sp << endl; SmartPtr<person> sp1(new person); sp1->_name = "wzf"; sp1->_age = 21; cout << sp1->_age << endl; cout << sp1->_name << endl; } int main() { test_SmarePtr(); system("pause"); return 0; }
智能指针不仅需要支持->、*,还需要像普通指针一样支持互相的赋值与拷贝。当然在上面那个类中,拷贝构造函数与复制运算符重载编译器都生成默认的了。
可以看见程序崩溃了,因为编译器默认生成的拷贝构造函数是浅拷贝,浅拷贝完成后 cpy 与 sp 会管理同一块资源。对象生命周期结束后就会对同一块资源进行多次释放,所以造成程序崩溃。此处该如何解决呢?在以前我们是会显示实例化出一个深拷贝来解决。但是对于智能指针并没有必要,我们仍然需要进行一个浅拷贝,让多个对象管理同一块资源,就像普通指针那样(int * p1 = new int ; int * p2 = p1;)。所以我们应该解决的是如何避免多个对象对同一块资源进行多次释放的问题。于是就有了下面这几种智能指针。
智能指针- - - std::auto_ptr
- auto_ptr是通过管理权转移的思想来解决的:
template<class T>
class AutoPtr
{
public:
AutoPtr(T* ptr)
:_ptr(ptr)
{}
//一旦发生拷贝,就将sp中资源转移到当前对象中,然后ap与其所管理资源断开联系,
// 这样就解决了一块空间被多个对象使用而造成程序奔溃问题
AutoPtr(AutoPtr<T>& sp)
:_ptr(sp._ptr)
{
sp._ptr = nullptr;
}
AutoPtr<T>& operator=(AutoPtr<T>& sp)
{
if (this != &sp)
{
if (_ptr)
delete _ptr;
_ptr = sp._ptr;
sp._ptr = nullptr;
}
return *this;
}
T*& operator->()
{
return _ptr;
}
T& operator*()
{
return *_ptr;
}
~AutoPtr()
{
if (_ptr)
{
cout << _ptr << endl;
delete _ptr;
}
}
private:
T* _ptr;
};
void test_AutoPtr()
{
AutoPtr<int> sp(new int);
AutoPtr<int> cpy(sp);//管理权转给cpy,将sp对象里的资源置空
AutoPtr<int> cpy1 = cpy;//管理权转给cpy1,将cpy对象里的资源置空
}
int main()
{
test_AutoPtr();
system("pause");
return 0;
}
但是 auto_ptr 带来的一个问题是:会使被转走资源的对象悬空,访问崩溃。
所以这种智能指针的设计并不是完美的,一般严禁使用 。
智能指针- - - std::unique_ptr
unique_ptr的实现原理,简单粗暴的防拷贝,也就是不让拷贝和赋值 :
-
C++98防拷贝的方式:只声明不实现+声明成私有
-
C++11防拷贝的方式:delete
template<class T> class UniquePtr { public: UniquePtr(T* ptr) :_ptr(ptr) {} T*& operator->() { return _ptr; } T& operator*() { return *_ptr; } ~UniquePtr() { if (_ptr) { cout << _ptr << endl; delete _ptr; } } private: // C++98防拷贝的方式:只声明不实现+声明成私有 UniquePtr(UniquePtr<T> const &); UniquePtr & operator=(UniquePtr<T> const &); // C++11防拷贝的方式:delete UniquePtr(UniquePtr<T> const &) = delete; UniquePtr & operator=(UniquePtr<T> const &) = delete; private: T* _ptr; };
智能指针- - - std::shared_ptr
shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。
-
shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
-
在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
-
如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源
-
如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指 针了。
template<class T> class SharedPtr { public: SharedPtr(T* ptr) :_ptr(ptr) { ++(*_count); } SharedPtr(SharedPtr<T>& sp) :_ptr(sp._ptr) , _count(sp._count) { ++(*_count); } SharedPtr<T>& operator=(SharedPtr<T>& sp) { //此处条件为了避免 s1 = s2; s2 =s1;这样的情况发生,可以添加一个条件 _ptr != sp._ptr //其实可以直接省略 this != &sp 这个条件 //if (this != &sp && _ptr != sp._ptr) if (_ptr != sp._ptr) { if (--(*_count) == 0)//此处已经对*count进行了--操作 { delete _count; if (_ptr) { delete _ptr; } } _ptr = sp._ptr; _count = sp._count; ++(*_count); } return *this; } T*& operator->() { return _ptr; } T& operator*() { return *_ptr; } ~SharedPtr() { if (--(*_count) ==0) { delete _count; if (_ptr) { cout << _ptr << endl; delete _ptr; } } } private: T* _ptr; int* _count = new int(0);//引用计数 }; void test_SharedPtr() { SharedPtr <int> sp1(new int); SharedPtr <int> cpy1(sp1); SharedPtr <int> cpy2 = cpy1; SharedPtr <int> sp2(new int); SharedPtr <int> sp3(sp2); sp1 = sp3; sp3 = sp1; } int main() { test_SharedPtr(); system("pause"); return 0; }
在上面模拟实现的 shared_ptr 在对引用计数 *_count 进行 ++ 、- - 并不是线程安全的,所以我们需要实现一个像 std::shared_ptr 库中的一样对引用计数的操作是线程安全的。
①:将对引用计数 _count 的操作单独写一个函数。
②:对引用计数操作函数实现一个互斥锁。
#include<thread>
#include<vector>
#include<mutex>
template<class T>
class SharedPtr
{
public:
SharedPtr(T* ptr)
:_ptr(ptr)
, _count(new int(1))
, _mutex(new mutex)
{}
SharedPtr(SharedPtr<T>& sp)
:_ptr(sp._ptr)
, _count(sp._count)
, _mutex(sp._mutex)
{
AddRefCount();
}
void AddRefCount()
{
_mutex->lock();
++(*_count);
_mutex->unlock();
}
void ReleaseRefCount()
{
bool deleteMutexFlag = false;
_mutex->lock();
if (--(*_count) == 0)
{
delete _count;
if (_ptr)
{
deleteMutexFlag = true;
delete _ptr;
}
}
_mutex->unlock();
if (deleteMutexFlag == true){
delete _mutex;
}
}
SharedPtr<T>& operator=(SharedPtr<T>& sp)
{
//此处条件为了避免 s1 = s2; s2 =s1;这样的情况发生,虽然影响不大,但是可以添加一个条件 _ptr != sp._ptr更好
//其实可以直接省略 this != &sp 这个条件
//if (this != &sp && _ptr != sp._ptr)
if (_ptr != sp._ptr)
{
ReleaseRefCount();
_ptr = sp._ptr;
_count = sp._count;
AddRefCount();
}
return *this;
}
T*& operator->()
{
return _ptr;
}
T& operator*()
{
return *_ptr;
}
~SharedPtr()
{
ReleaseRefCount();
}
int GetCount()
{
return *_count;
}
private:
T* _ptr;//指向管理资源的指针
int* _count;//引用计数
mutex* _mutex;//互斥锁
};
void test_SharedPtr()
{
SharedPtr <int> sp1(new int);
vector<thread> thread_array;
const size_t threadNum = 4;
for (size_t i = 0; i < threadNum; i++)
{
thread_array.push_back(thread([&]()
{
for (size_t i = 0; i < 100000; i++)
{
SharedPtr<int> cpy1(sp1);
}
}));
}
for (size_t i = 0; i < threadNum; i++)
{
thread_array[i].join();
}
cout << sp1.GetCount() << endl;//输出为 1 时才是正确的,因为只剩下一个 sp1 对象,其他拷贝的对象出了作用域他就销毁了。
}
int main()
{
test_SharedPtr();
system("pause");
return 0;
}
//经过测试后该模拟实现的 shared_ptr 是线程安全的。
关于智能指针是否是线程安全的的问题?
shared_ptr 是线程安全的,需要注意的是shared_ptr的线程安全分为两方面:
- 1、shared_ptr 拷贝/析构时对引用计数的++,- - 是线程安全的。
- 2、shared_ptr 管理的指针指向的资源不是线程安全的。比如:
std::shared_ptr的循环引用
什么是循环引用呢?代码举例:
#include<memory>
struct ListNode
{
int _data;
shared_ptr<ListNode> _prev;
shared_ptr<ListNode> _next;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
void test_cycleRef()
{
shared_ptr<ListNode> node1(new ListNode);
shared_ptr<ListNode> node2(new ListNode);
node1->_next = node2;
node2->_prev = node1;
}
循环引用分析:
-
node1和node2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动delete。
-
node1的_next指向node2,node2的_prev指向node1,引用计数变成2。
-
node1和node2析构,引用计数减到1,但是_next还指向下一个节点。但是_prev还指向上一个节点。
-
也就是说_next析构了,node2就释放了。
-
也就是说_prev析构了,node1就释放了。
-
但是_next属于node的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev属于node2 成员,所以这就叫循环引用,谁也不会释放,程序结束后谁也不会释放就会造成内存泄漏
但是如何解决循环引用呢?可用weak_ptr 来解决:解决方案:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以
原理就是:node1->_next = node2;和node2->_prev = node1;时weak_ptr的_next和_prev不会增加 node1和node2的引用计数。
struct ListNode
{
int _data;
weak_ptr<ListNode> _prev;
weak_ptr<ListNode> _next;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
#include<memory>
void test_cycleRef()
{
shared_ptr<ListNode> node1(new ListNode);
shared_ptr<ListNode> node2(new ListNode);
node1->_next = node2;
node2->_prev = node1;
}
什么是循环引用呢,带来的问题是什么?如何解决循环引用?
- 循环引用就是上面这个例子,循环引用带来的问题就是最后谁都不会释放资源,导致资源泄漏。
- 为了解决循环引用,就可以用 weak_ptr 来解决,weak_ptr 不会增加 shared_ptr 的引用计数,可以访问管理的资源,但是它不参与对资源释放的管理。
shared_ptr 的删除器(了解即可)
当我们需要 shared_ptr 管理的指针是指向多个连续空间、不是new出来的对象空间等等,智能指针析构函数默认是完成不了对多个空间的释放的(因为底层就为 delete _ptr;),所以实 shared_ptr 设计了一个删除器来解决这个问题。
class Type
{
public:
~Type()
{
cout << "~Type()" << endl;
}
private:
int _num;
};
template<class T>
class DeleteFunc
{
public:
void operator()(T* ptr)
{
delete[] ptr;
}
};
#include<memory>
void test_delereFunc()
{
//通过仿函数实现删除器
shared_ptr<Type> sp(new Type[5],DeleteFunc<Type>());
//铜过lambda表达式实现删除器
shared_ptr<Type> sp1(new Type[5], [](Type* ptr){delete[]ptr; });
shared_ptr<Type> sp3((Type*)malloc(sizeof(Type)), [](Type* ptr){free(ptr); });
shared_ptr<FILE> sp4(fopen("test.cpp", "r"), [](FILE* ptr){fclose(ptr); });
}
随机插入:了解一下智能指针的发展历史. . . . . .
RAII带来的其他应用
RAII思想除了可以用来设计智能指针,还可以用来设计守卫锁,防止异常安全导致的死锁问题。