一、为什么要有智能指针
在new/malloc/fopen/lock与对应的delete/free/fclose/unlock操作之间使用抛异常,可能会造成内存泄漏的问题,当然了也可以使用异常的重新抛出来解决此类问题,但是如果有大量的资源申请和释放操作,使用重新抛出异常会非常繁琐且代码冗余.智能指针可以很好的解决这个问题.
#include<iostream>
using namespace std;
int Division(int a, int b) {return b == 0 ? throw "除0错误" : a = a / b; }
void func()
{
int* a1 = new int(1);
int* a2=nullptr;
//a2在new的时候就可能抛异常,造成开辟空间成功的a1内存未释放
//,所以处理一下a2抛异常时a1的资源释放
try{ a2 = new int(2); }
catch (...) { delete a1; throw; }
//division可能抛异常,造成开辟空间成功的a1和a2内存未释放
//,所以处理一下a1和a2资源释放再抛出
try { Division(*a1, *a2); }
catch (...) { delete a1; delete a2; throw; }
delete a1;
delete a2;
}
int main()
{
try{ func(); }
catch (const char* str) { cout << str << endl; }
catch (...) { cout << "未知异常" << endl; }
return 0;
}
二、智能指针的原理
1.智能指针基本结构
智能指针,学术名:RAII(Resource Acquisition Is Initialization)资源获取即初始化,是一种利用对象生命周期来控制资源的简单技术.是用封装指针,构造时保存指针,析构时释放资源的一个类来实现的,这样就不会发生因为异常跳过释放资源的代码,造成内存泄漏问题了.
#include<iostream>
using namespace std;
template <class T>
class smart_ptr
{
public:
smart_ptr(const T* ptr) :_ptr(ptr) {}
解引用可以使用操纵符重载来实现,
T& operator*() { return *_ptr; }
T* operator->() { return &*_ptr; }
~smart_ptr() { delete _ptr; }
private:
T* _ptr;
};
int Division(int a, int b) { return b == 0 ? throw "除0错误" : a = a / b; }
void func()
{
//如果因为异常跳出了此作用域,对象会调用析构自动释放资源
smart_ptr<int> a1(new int(1));
smart_ptr<int> a2(new int(2));
cout << Division(*a1, *a2) << endl;
}
int main()
{
try { func(); }
catch (const char* str) { cout << str << endl; }
catch (...) { cout << "未知异常" << endl; }
return 0;
}
2.智能指针的拷贝和赋值
智能指针解决了资源释放的问题,但是还要有原生指针的使用方法和功能,这就需要解决指针的解引用,拷贝和赋值问题
浅拷贝问题
如果不自定义拷贝构造函数,就会发生两个对象同时使用一个指针指向的空间,会析构两次造成程序崩溃.这个问题可以使用引用计数的思想解决,但是引用计数的具体实现也是个问题,如果使用静态变量做引用计数会造成所有对象使用同一个计数器,是不正确的,因为不同的对象可能使用的是同一个指针,但是也有可能不同对象使用的是不同的指针,这时计数器的值肯定也是不同的.
void func()
{
//如果因为异常跳出了此作用域,对象会调用析构自动释放资源
smart_ptr<int> a1(new int(1));
smart_ptr<int> a2(a1);
cout << Division(*a1, *a2) << endl;
}
auto_ptr
C++98版本的库中就提供了auto_ptr的智能指针,auto_ptr的实现原理:管理权转移的思想,但是这个智能指针是不被推广,它的原理是:a1和a2两个对象,a2用a1构造,a1的指针赋值给a2,a1直接置空,这样做坑能会导致a1被错误的使用.
class auto_ptr
{
public:
auto_ptr(T* ptr) :_ptr(ptr) {}
//管理权转移
auto_ptr(auto_ptr& ap) :_ptr(ap._ptr)
{ ap._ptr = nullptr; }
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()
{
if (_ptr)
{
delete _ptr;
}
}
T& operator*() { return *_ptr; }
T* operator->() { return &*_ptr; }
private:
T* _ptr;
};
unique_ptr
后来boost库提供了一些更好用的智能指针scoped_ptr/shared_ptr/weak_ptr,boost库是出++标准库的扩展库,它是一个可移植的库,作为c++的标准库的后备,是c++标准化进程的开发引擎之一.后来c++11将boost库中的智能指针吸收了,增加了更好用的智能指针unique_ptr,shared_ptr,weak_ptr.unique_ptr智能指针的实现思想是防止拷贝,在不需要拷贝的场景下可以使用这个智能指针.
template <class T>
class unique_ptr
{
public:
unique_ptr(T* ptr) :_ptr(ptr) {}
//强制不允许拷贝
unique_ptr(unique_ptr& ap) = delete;
//强制不允许赋值
unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete
T& operator*() { return *_ptr; }
T* operator->() { return &*_ptr; }
~unique_ptr() { delete _ptr; }
private:
T* _ptr;
};
shared_ptr
shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源
- shared_ptr在其内部,每分资源拿到的资源地址和计数器的地址是相同的,用来记录该份资源被几个对象共享,其中的一个对象对计数器加或者减,其他对象的计数器都会被改变。
- 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
- 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
- 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
#include<iostream>
using namespace std;
namespace kk
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr)
:_ptr(ptr), _pcount(new int(1))
{}
//引用加
void release()
{
if (-- (* _pcount) == 0)
{
cout << "delete _ptr" << endl;
delete _ptr;
delete _pcount;
}
}
//引用减
void addCount()
{
++(*_pcount);
}
//拷贝构造
shared_ptr(const shared_ptr& sp)
:_ptr(sp._ptr), _pcount(sp._pcount)
{
addCount();
}
//拷贝赋值
shared_ptr<T>& operator=(const shared_ptr& sp)
{
//当使用自己给自己赋值,或者自己给自己的拷贝者赋值不处理.
if (_ptr != sp._ptr)
{
//被赋值相当于赋值前的_ptr少了一个使用者所以--
release();
_ptr = sp._ptr;
_pcount = sp._pcount;
//赋值后的_ptr多了一个使用者所以++
addCount();
}
}
T& operator*() { return *_ptr; }
T* operator->() { return &(*_ptr); }
~shared_ptr()
{
release();
}
private:
T* _ptr;
int* _pcount;
};
int Division(int a, int b)
{
b == 0 ? throw "除0错误" : a = a / b;
return a;
}
void func()
{
//如果因为异常跳出了此作用域,对象会调用析构自动释放资源
shared_ptr<int> a1(new int(10));
shared_ptr<int> a2(new int(0));
cout << Division(*a1, *a2) << endl;
}
}
int main()
{
try { kk::func(); }
catch (const char* str) { cout << str << endl; }
catch (...) { cout << "未知异常" << endl; }
return 0;
}
3.引用计数线程安全问题
当同一个智能指针被两个以上的线程使用时,线程内部同时拷贝智能指针,会造成线程同时加加同一个引用计数,引用计数达不到逾期的值,析构时引用计数在最后一个对象没有析构时就已经减到了0调用了delete,造成二次delete.所以引用计数使用时要加锁,shared_ptr智能指针要增加一个mutex对象,mutex对象成员必须是指针,否者无法实现同一个对象使用同一个资源.在调用delete以后析构mutex对象,
#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
namespace kk
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
: _ptr(ptr)
, _pcount(nullptr)
, _pmutex(nullptr)
{
if (_ptr)
{
_pmutex= new mutex;
_pcount= new int(1);
}
}
shared_ptr(const shared_ptr<T>& sp)
: _ptr(sp._ptr)
, _pcount(sp._pcount)
, _pmutex(sp._pmutex)
{
if (_pCount)
addCount();
}
//引用加
void release()
{
_mtx->lock();
bool mutexflag = false;
if (-- (* _pcount) == 0)
{
cout << "delete _ptr" << endl;
delete _ptr;
delete _pcount;
mutexflag = true;
}
_mtx->unlock();
if (mutexflag)delete _mtx;
}
//引用减
void addCount()
{
_mtx->lock();
++(*_pcount);
_mtx->unlock();
}
//拷贝构造
shared_ptr(const shared_ptr& sp)
:_ptr(sp._ptr), _pcount(sp._pcount),_mtx(sp._mtx)
{
addCount();
}
//拷贝赋值
shared_ptr<T>& operator=(const shared_ptr& sp)
{
//当使用自己给自己赋值,或者自己给自己的拷贝者赋值不处理.
if (_ptr != sp._ptr)
{
//被赋值相当于赋值前的_ptr少了一个使用者所以--
release();
_ptr = sp._ptr;
_pcount = sp._pcount;
_mtx = sp._mtx;
//赋值后的_ptr多了一个使用者所以++
addCount();
}
return *this;
}
T& operator*() { return *_ptr; }
T* operator->() { return &(*_ptr); }
T* getPtr()
{
return _ptr;
}
int getPcount()
{
return *_pcount;
}
~shared_ptr()
{
release();
}
private:
T* _ptr;
int* _pcount;
mutex* _mtx;
};
struct Date
{
int year = 0;
int month = 0;
int day = 0;
~Date()
{}
};
void thread_func(const shared_ptr<Date>& sp,size_t n)
{
for (size_t i = 0; i < n; i++)
{
shared_ptr<Date> copy(sp);
}
}
void test_thread_safe()
{
size_t n = 10000;
kk::shared_ptr<Date> sp(new Date);
thread t1(thread_func, ref(sp), n);
thread t2(thread_func, ref(sp), n);
//连接线程回收资源,防止主线程先结束.
t1.join();
t2.join();
}
}
int main()
{
try { kk::test_thread_safe(); }
catch (const char* str) { cout << str << endl; }
catch (...) { cout << "未知异常" << endl; }
return 0;
}
4.循环引用
智能指针有些场景会造成循环引用的,循环引用,就是两个智能指针相互依赖,释放时互相制约,形成引用死循环,资源无法被释放,造成内存泄漏.以链表节点的链接为例,两个节点相连,节点1的next链接节点2,节点2的pre链接节点1,这两个节点的指针以及节点内部的指针都用希尔智能指针,这时节点1和节点2的引用计数都是2,出作用域时,节点2引用计数-1,节点1的引用计数-1,但是这两个节点都无法释放,因为节点1的释放需要节点2的pre先释放—>pre需要节点2先释放—>节点2需要节点1的next先释放—>next释放需要节点1先释放 weak_ptr智能指针可以解决这个问题,它的特点就是不管理资源,不增加引用计数,支持shared_ptr类型的拷贝构造.
template<class T>
class weak_ptr
{
public:
weak_ptr()
:_ptr(nullptr) {}
//拷贝构造
weak_ptr(shared_ptr<T>& sp)
:_ptr(sp.getPtr())
{}
T& operator*() { return *_ptr; }
T* operator->() { return &(*_ptr);}
T* getPtr()
{
return _ptr;
}
private:
T* _ptr;
};
template <class T>
struct listNode
{
weak_ptr<listNode> next;
weak_ptr<listNode> pre;
T _data=0;
};
void test()
{
shared_ptr<listNode<int>> l1(new listNode<int>);
shared_ptr<listNode<int>> l2(new listNode<int>);
l1->next = l2;
l2->pre = l1;
}
5.定制删除器
智能指针越有可能会是new的多个空间,所以不能将析构中的delete写死,多增加一个构造函数,用来接收定制删除器,定制删除器是一个根据智能指针的实际情况来特定释放方式的仿函数.析构时使用仿函数对象析构,所以要增加一个包装器对象成员,用来保存构造时接收的仿函数对象,供析构时使用,但是如果没有传递定制器,那么析构也需要默认的析构方式,所以包装器对象需要一个lambda,参数为智能指针类型,函数体默认空间的释放方式.
#include<iostream>
#include<thread>
#include<mutex>
#include<functional>
using namespace std;
namespace kk
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr=nullptr)
:_ptr(ptr), _pcount(new int(1)),_mtx(new mutex)
{}
template <class D>
shared_ptr(T* ptr ,D del)
: _ptr(ptr), _pcount(new int(1)), _mtx(new mutex),_del(del)
{}
//引用加
void release()
{
_mtx->lock();
bool mutexflag = false;
if (-- (* _pcount) == 0)
{
if (_ptr)_del(_ptr);
delete _pcount;
mutexflag = true;
}
_mtx->unlock();
if (mutexflag)delete _mtx;
}
//引用减
void addCount()
{
_mtx->lock();
++(*_pcount);
_mtx->unlock();
}
//拷贝构造
shared_ptr(const shared_ptr& sp)
:_ptr(sp._ptr), _pcount(sp._pcount),_mtx(sp._mtx)
{
addCount();
}
//拷贝赋值
shared_ptr<T>& operator=(const shared_ptr& sp)
{
//当使用自己给自己赋值,或者自己给自己的拷贝者赋值不处理.
if (_ptr != sp._ptr)
{
//被赋值相当于赋值前的_ptr少了一个使用者所以--
release();
_ptr = sp._ptr;
_pcount = sp._pcount;
_mtx = sp._mtx;
//赋值后的_ptr多了一个使用者所以++
addCount();
}
return *this;
}
T& operator*() { return *_ptr; }
T* operator->() { return &(*_ptr); }
T* getPtr()
{
return _ptr;
}
int getPcount()
{
return *_pcount;
}
~shared_ptr()
{
release();
}
private:
T* _ptr;
int* _pcount;
mutex* _mtx;
function<void(T*)> _del=[](T* ptr) {
cout << "delete _ptr" << endl;
delete ptr;
};
};
struct Date
{
int year = 0;
int month = 0;
int day = 0;
~Date()
{}
};
struct DateDelete
{
void operator()(Date* d)
{
cout << "delete[] d" << endl;
delete[] d;
}
};
void test()
{
//以为析构顺序是3,2,1,sp3和sp2不会调用_del(_ptr),
///只有sp1才会调用,所以拷贝构造和赋值构造不用拷贝包装器对象
shared_ptr<Date> sp1(new Date[10], DateDelete());
shared_ptr<Date> sp2(sp1);
shared_ptr<Date> sp3(sp1);
cout<<sp3.getPcount()<<endl;
shared_ptr<Date> sp4(new Date[10], DateDelete());
}
}
int main()
{
try { kk::test(); }
catch (const char* str) { cout << str << endl; }
catch (...) { cout << "未知异常" << endl; }
return 0;
}