文章目录
C++智能指针
一、为什么需要智能指针
如下所示的代码存在异常安全的问题,就是资源没有被释放完毕,函数就结束了
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");// 会直接跳到main函数中的catch块中
return a / b;
}
void func()
{
int* p = new int;
cout << div() << endl;// 存在异常安全的问题
cout <<"delete: " << p << endl;
delete p;
}
int main()
{
try
{
func();
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}
如何解决?
第一种方式:重新抛出
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void func()
{
int* p = new int;
try
{
cout << div() << endl;// 存在异常安全的问题
}
catch (...)
{
cout << "delete: " << p << endl;
delete p;
throw;// 重新抛出
}
}
int main()
{
try
{
func();
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}
第二种方式:智能指针
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr)
:_ptr(ptr)
{}
~SmartPtr()
{
cout << "delete:" << _ptr << endl;
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()
{
int* p1 = new int;
SmartPtr<int> sp1(p1);//将p指针交给智能指针管理
int* p2 = new int;
SmartPtr<int> sp2(p2);
int* p3 = new int;
SmartPtr<int> sp3(p3);
cout << div() << endl;// 存在异常安全的问题
}// 出了func的作用域,自动调用析构函数,进行资源的释放
void func1()
{
// 匿名对象
//与上面的写法是等效的
SmartPtr<int> sp1(new int);
SmartPtr<int> sp2(new int);
SmartPtr<int> sp3(new int);
cout << div() << endl;
}
int main()
{
try
{
func();
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}
智能指针的使用及其原理
RAII
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句 柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的 时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
- 不需要显式地释放资源。
- 采用这种方式,对象所需的资源在其生命期内始终保持有效。
智能指针的原理
上述的SmartPtr还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过->
去访问所指空间中的内容,因此:AutoPtr模板类中还得需要将*
、->
重载下,才可让其像指针一样去使用。
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr)
:_ptr(ptr)
{}
~SmartPtr()
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
// 重载operator* operator->
T& operator*()// 解引用
{
return *_ptr;
}
T* operator->()// 箭头操作符
{
return _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);
SmartPtr<int> sp3(new int);
// 像指针一样使用
*sp1 = 10;
cout << "加加前:" << *sp1 << endl;
++(*sp1);
cout <<"加加后:" << *sp1 << endl;
cout << div() << endl;
}
int main()
{
try
{
func();
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}
存在问题:
拷贝的时候存在问题,我们不显式的写,是浅拷贝,同一块内存空间会被析构释放两次。
int main()
{
SmartPtr<int> sp1(new int);
SmartPtr<int> sp2(sp1);
return 0;
}
但是同时也没有办法做深拷贝!因为我们的要求就是两个指针共同管理一块内存空间。
auto_ptr
namespace sjj
{
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr)
:_ptr(ptr)
{}
// 管理权转移
auto_ptr(auto_ptr<T>& sp)
:_ptr(sp._ptr)
{
sp._ptr = nullptr;//将自己的指针置为空
}
~auto_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
// 重载operator* operator->
T& operator*()// 解引用
{
return *_ptr;
}
T* operator->()// 箭头操作符
{
return _ptr;
}
private:
T* _ptr;
};
}
int main()
{
sjj::auto_ptr<int> sp1(new int);
sjj::auto_ptr<int> sp2(sp1);// 管理权转移
// sp1悬空了
*sp1 = 10;
cout << *sp1 << endl;
*sp2 = 20;
cout << *sp2 << endl;
return 0;
}
最大的问题就是sp1指针悬空了。难免我们会忘记它悬空了,对它进行解引用等其他操作。
结论:当对象拷贝或者赋值以后,前面的对象就悬空了。auto_ptr是一个失败的产品,很多公司明确不能使用auto_ptr。
unique_ptr
C++11中才开始提供更加靠谱的unique_ptr。unique_ptr非常的暴力:直接防拷贝,不让拷贝和赋值操作。
namespace sjj
{
template<class T>
class unique_ptr
{
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{}
unique_ptr(const unique_ptr<T>& sp) = delete;
~unique_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
// 重载operator* operator->
T& operator*()// 解引用
{
return *_ptr;
}
T* operator->()// 箭头操作符
{
return _ptr;
}
private:
T* _ptr;
};
}
int main()
{
sjj::unique_ptr<int> sp1(new int);
sjj::unique_ptr<int> sp2(sp1);
return 0;
}
shared_ptr
C++11中提供了更加靠谱并且支持拷贝的shared_ptr。
shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。
- shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
- 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
- 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
- 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
采用static的问题:
namespace sjj
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr)
:_ptr(ptr)
{
_refCount = 1;
}
shared_ptr( shared_ptr<T>& sp)
:_ptr(sp._ptr)
{
++_refCount;
}
~shared_ptr()
{
if (--_refCount == 0 && _ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
// 重载operator* operator->
T& operator*()// 解引用
{
return *_ptr;
}
T* operator->()// 箭头操作符
{
return _ptr;
}
private:
T* _ptr;
static int _refCount;
};
template<class T>
int shared_ptr<T>::_refCount = 0;
}
int main()
{
sjj::shared_ptr<int> sp1(new int);
sjj::shared_ptr<int> sp2(sp1);
sjj::shared_ptr<int> sp3(sp1);
sjj::shared_ptr<int> sp4(new int);
return 0;
}
正确的做法:应该一个资源配一个引用计数!
我们在一块资源首次分配时,指定一个堆上的引用计数,在构造函数中处理。
namespace sjj
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr)
:_ptr(ptr)
,_pRefCount(new int(1))
{}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pRefCount(sp._pRefCount)
{
++(*_pRefCount);
}
~shared_ptr()
{
if (--(*_pRefCount) == 0 && _ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
delete _pRefCount;
_ptr = nullptr;
_pRefCount = nullptr;
}
}
// 重载operator* operator->
T& operator*()// 解引用
{
return *_ptr;
}
T* operator->()// 箭头操作符
{
return _ptr;
}
private:
T* _ptr;
int* _pRefCount;
};
}
int main()
{
sjj::shared_ptr<int> sp1(new int);
sjj::shared_ptr<int> sp2(sp1);
sjj::shared_ptr<int> sp3(sp1);
sjj::shared_ptr<int> sp4(new int);
return 0;
}
对于赋值函数的处理:
封装Release和AddRef是方便后续加锁操作!
namespace sjj
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr)
:_ptr(ptr)
,_pRefCount(new int(1))
{}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pRefCount(sp._pRefCount)
{
AddRef();
}
void Release()
{
if (--(*_pRefCount) == 0 && _ptr)// 引用计数减为0,且_ptr不能为空
{
cout << "delete:" << _ptr << endl;
delete _ptr;
delete _pRefCount;
_ptr = nullptr;
_pRefCount = nullptr;
}
}
void AddRef()
{
++(*_pRefCount);
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
//if (this != &sp) =>可能会遇到另类的自己给自己赋值
if (_ptr != sp._ptr)
{
Release();
// 赋值操作
_ptr = sp._ptr;
_pRefCount = sp._pRefCount;
AddRef();
}
return *this;
}
~shared_ptr()
{
Release();
}
// 重载operator* operator->
T& operator*()// 解引用
{
return *_ptr;
}
T* operator->()// 箭头操作符
{
return _ptr;
}
private:
T* _ptr;
int* _pRefCount;
};
}
int main()
{
sjj::shared_ptr<int> sp1(new int);
sjj::shared_ptr<int> sp2(sp1);
sjj::shared_ptr<int> sp3(sp1);
sjj::shared_ptr<int> sp4(new int);
sjj::shared_ptr<int> sp5(sp4);
sp1 = sp4;
sp2 = sp4;
sp3 = sp4;
return 0;
}
shared_ptr线程安全问题
在多线程场景下面,是存在问题的:
- 智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时++或- -,这个操作不是原子的,引用计数原来是1,++了两次,可能还是2,这样引用计数就错乱了。会导致资源未释放或者程序崩溃的问题。所以只能指针中引用计数++、–是需要加锁的,也就是说引用计数的操作是线程安全的。
- 智能指针管理的对象存放在堆上,两个线程中同时去访问,会导致线程安全问题。
问题演示:
struct Date
{
int _year=1;
int _month=1;
int _day=1;
};
void SharePtrFunc(sjj::shared_ptr<Date>& sp, size_t n)
{
cout << sp.Get() << endl;
for (size_t i = 0; i < n; ++i)
{
// 这里智能指针拷贝会++计数,智能指针析构会--计数,这里是线程安全的。
sjj::shared_ptr<Date> copy(sp);
// 这里智能指针访问管理的资源,不是线程安全的。所以我们看看这些值两个线程++了2n次,但
//是最终看到的结果,并一定是加了2n
copy->_year++;
copy->_month++;
copy->_day++;
}
}
int main()
{
sjj::shared_ptr<Date> p(new Date);
cout << p.Get() << endl;
const size_t n = 10000;
thread t1(SharePtrFunc, std::ref(p), n);
thread t2(SharePtrFunc, std::ref(p), n);
t1.join();
t2.join();
cout << p->_year << endl;
cout << p->_month << endl;
cout << p->_day << endl;
cout << p.use_count() << endl;
return 0;
}
加锁:
用锁的指针,因为mutex是不允许拷贝的,只有用指针,多个对象才能访问同一个锁。
namespace sjj
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr)
:_ptr(ptr)
,_pRefCount(new int(1))
,_pmtx(new mutex)
{}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pRefCount(sp._pRefCount)
,_pmtx(sp._pmtx)
{
AddRef();
}
void Release()
{
// 锁是new出来的,还未被释放
bool flag = false;
_pmtx->lock();
if (--(*_pRefCount) == 0 && _ptr)// 引用计数减为0,且_ptr不能为空
{
cout << "delete:" << _ptr << endl;
delete _ptr;
delete _pRefCount;
flag = true;
}
_pmtx->unlock();
if (flag == true)// 说明当前线程已经是最后一个管理该资源的对象了
{
delete _pmtx;
}
}
void AddRef()
{
_pmtx->lock();
++(*_pRefCount);
_pmtx->unlock();
}
int use_count()
{
return *_pRefCount;
}
T* Get()const
{
return _ptr;
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
//if (this != &sp) =>可能会遇到另类的自己给自己赋值
if (_ptr != sp._ptr)
{
Release();
// 赋值操作
_ptr = sp._ptr;
_pRefCount = sp._pRefCount;
_pmtx = sp._pmtx;
AddRef();
}
return *this;
}
~shared_ptr()
{
Release();
}
// 重载operator* operator->
T& operator*()// 解引用
{
return *_ptr;
}
T* operator->()// 箭头操作符
{
return _ptr;
}
private:
T* _ptr;
int* _pRefCount;
mutex* _pmtx;// 锁是不能拷贝的,所以用指针,来让多个对象来访问同一个锁
};
}
void SharePtrFunc(sjj::shared_ptr<Date>& sp, size_t n,mutex& mtx)
{
cout << sp.Get() << endl;
for (size_t i = 0; i < n; ++i)
{
// 这里智能指针拷贝会++计数,智能指针析构会--计数,这里是线程安全的。
sjj::shared_ptr<Date> copy(sp);
// 这里智能指针访问管理的资源,不是线程安全的。所以我们看看这些值两个线程++了2n次,但
//是最终看到的结果,并一定是加了2n
{
unique_lock<mutex> lk(mtx);
copy->_year++;
copy->_month++;
copy->_day++;
}
}
}
int main()
{
sjj::shared_ptr<Date> p(new Date);
cout << p.Get() << endl;
mutex mtx;// 外面加锁,保证指向资源的线程安全
const size_t n = 10000;
thread t1(SharePtrFunc, std::ref(p), n,std::ref(mtx));
thread t2(SharePtrFunc, std::ref(p), n,std::ref(mtx));
t1.join();
t2.join();
cout << p->_year << endl;
cout << p->_month << endl;
cout << p->_day << endl;
cout << p.use_count() << endl;
return 0;
}
面试问题:
智能指针(shared_ptr)是线程安全的吗?
是的,引用计数的加减是加锁保护的。但是指向资源不是线程安全的。
循环引用问题
什么是循环引用?
如下:
struct ListNode
{
int _val;
std::shared_ptr<ListNode> _next;
std::shared_ptr<ListNode> _prev;
~ListNode()
{
cout << "~ListNode" << endl;
}
};
int main()
{
std::shared_ptr<ListNode> n1 (new ListNode);
std::shared_ptr<ListNode> n2(new ListNode);
n1->_next = n2;
n2->_prev = n1;
return 0;
}
n2节点的释放取决于n1节点的_next,n1节点的释放取决于n2节点的_prev,而n2由_next管理,_next又属于n1节点,所以这叫做循环引用,陷入了死结,谁也不会释放谁。
解决方案:
在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了。
原理就是,n1->_next = n2和n2->_prev = n1时,weak_ptr的_next和_prev不会增加n1和n2的引用计数。但是weak_ptr中_next和_prev指针可以去访问节点资源,但是并不参与节点资源释放的管理。
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;
};
struct ListNode
{
int _val;
/*std::shared_ptr<ListNode> _next;
std::shared_ptr<ListNode> _prev;*/
std::weak_ptr<ListNode> _next;
std::weak_ptr<ListNode> _prev;
~ListNode()
{
cout << "~ListNode" << endl;
}
};
int main()
{
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;
return 0;
}
定制删除器
默认情况下,智能指针的底层是delete释放资源,当不是new出来的资源,如:new[ ]
、malloc
、fopen
等操作的资源,如何通过智能指针管理呢?
需要定制删除器——是一个可调用对象
释放的方式由删除器决定,不显式传就用默认的,显式传了就用手写的。
unique_ptr的删除器
//删除器
// unique_ptr 防拷贝 防赋值
namespace sjj
{
template<class T>
class default_delete
{
public:
void operator()(const T* ptr)
{
cout << "delete:" << ptr << endl;
delete ptr;
}
};
template<class T,class D=default_delete<T>>
class unique_ptr
{
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{}
unique_ptr(const unique_ptr<T>& sp) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;
~unique_ptr()
{
if (_ptr)
{
//cout << "delete:" << _ptr << endl;
//delete _ptr;
D del;
del(_ptr);
}
}
// 重载operator* operator->
T& operator*()// 解引用
{
return *_ptr;
}
T* operator->()// 箭头操作符
{
return _ptr;
}
private:
T* _ptr;
};
}
class A
{
public:
~A()
{
cout << "~A()" << endl;
}
private:
int _a = 1;
};
template<class T>
struct DeleteArray
{
void operator()(const A* ptr)
{
cout << "delete[]: " << ptr << endl;
delete[]ptr;
}
};
struct DeleteFile
{
void operator()(FILE* ptr)
{
cout << "fclose: " << ptr << endl;
fclose(ptr);
}
};
int main()
{
// 删除器在类模板给出——类型
sjj::unique_ptr<A> up1(new A);
sjj::unique_ptr<A, DeleteArray<A>> up2(new A[10]);
sjj::unique_ptr<FILE, DeleteFile> up3(fopen("test.txt", "w"));
return 0;
}
shared_ptr的删除器
int main()
{
// 删除器在构造函数给出——对象
std::shared_ptr<A> sp1(new A);
std::shared_ptr<A> sp2(new A[10], DeleteArray<A>());
std::shared_ptr<FILE> sp3(fopen("test.txt", "w"), DeleteFile());
std::shared_ptr<A> sp4(new A[10], [](A* p) {delete[] p; });
std::shared_ptr<FILE> sp5(fopen("test.txt", "w"), [](FILE* p) {fclose(p); });
return 0;
}