2.C++中的智能指针
C++中的智能指针
智能指针的基础知识
智能指针能做到保证资源的自动释放
创新互联建站专注为客户提供全方位的互联网综合服务,包含不限于成都网站建设、成都网站设计、蕉城网络推广、微信平台小程序开发、蕉城网络营销、蕉城企业策划、蕉城品牌公关、搜索引擎seo、人物专访、企业宣传片、企业代运营等,从售前售中售后,我们都将竭诚为您服务,您的肯定,是我们最大的嘉奖;创新互联建站为所有大学生创业者提供蕉城建站搭建服务,24小时服务热线:028-86922220,官方网址:www.cdcxhl.com
利用栈上对象离开作用自动析构的特性保证自动释放。
//智能指针简单的实现
template
class CSmartPtr{
public:
CSmartPtr(T* ptr= nullptr):_mptr(ptr){}
~CSmartPtr(){
delete _mptr;
}
T& operator*(){//实现智能指针的解引用
return *_mptr;
}
T* operator->(){
return _mptr;
}
private:
T* _mptr;
};
不带引用计数的智能指针
在使用智能指针的使用要先引用memory库
stl中提供了三种不带引用计数的智能指针:auto_ptr
、scoped_ptr
和unique_ptr
auto_ptr在使用过程中,如果进行赋值和拷贝运算时,会将第一个指针置空,让拷贝或赋值的指针管理之前的内存。基本不使用这个。auto_ptr的源码:
template
class auto_ptr
{ // wrap an object pointer to ensure destruction
public:
typedef _Ty element_type;
explicit auto_ptr(_Ty * _Ptr = nullptr) noexcept
: _Myptr(_Ptr)
{ // construct from object pointer
}
/*这里是auto_ptr的拷贝构造函数,
_Right.release()函数中,把_Right的_Myptr
赋为nullptr,也就是换成当前auto_ptr持有资源地址
*/
auto_ptr(auto_ptr& _Right) noexcept
: _Myptr(_Right.release())
{ // construct by assuming pointer from _Right auto_ptr
}
_Ty * release() noexcept
{ // return wrapped pointer and give up ownership
_Ty * _Tmp = _Myptr;
_Myptr = nullptr;
return (_Tmp);
}
private:
_Ty * _Myptr; // the wrapped object pointer
};
一个面试题:可以在vector
中使用auto_ptr
吗?
答:最好不要,因为在使用容器的过程中,难免会发生拷贝(容器1复制到容器2),而auto_ptr的实现方式中为了避免浅拷贝,会把初始的指针置为空,让赋值的指针管理这块内存,之前的指针失去了堆内存的管理权,这样再访问容器1就会发现其中的都是空指针。例子如下:
int main()
{
vector> vec;
vec.push_back(auto_ptr(new int(10)));
vec.push_back(auto_ptr(new int(20)));
vec.push_back(auto_ptr(new int(30)));
// 这里可以打印出10
cout << *vec[0] << endl;
vector> vec2 = vec;
/* 这里由于上面做了vector容器的拷贝,相当于容器中
的每一个元素都进行了拷贝构造,原来vec中的智能指针
全部为nullptr了,再次访问就成访问空指针了,程序崩溃
*/
cout << *vec[0] << endl;
return 0;
}
【auto_ptr
总结】:auto_ptr智能指针不带引用计数,那么它处理浅拷贝的问题,是直接把前面的auto_ptr都置为nullptr,只让最后一个auto_ptr持有资源。
scoped_ptr
中将拷贝构造函数和赋值重载函数都删除了,从源码中禁止了拷贝和赋值。这个指针用的也比较少.源码如下:
template class scoped_ptr // noncopyable
{
private:
T * px;
/*
私有化拷贝构造函数和赋值函数,这样scoped_ptr的智能指针
对象就不支持这两种操作,从根本上杜绝浅拷贝的发生
*/
scoped_ptr(scoped_ptr const &);
scoped_ptr & operator=(scoped_ptr const &);
typedef scoped_ptr this_type;
/*
私有化逻辑比较运算符重载函数,不支持scoped_ptr的智能指针
对象的比较操作
*/
void operator==( scoped_ptr const& ) const;
void operator!=( scoped_ptr const& ) const;
public:
typedef T element_type;
explicit scoped_ptr( T * p = 0 ): px( p ) // never throws
{
#if defined(BOOST_SP_ENABLE_DEBUG_HOOKS)
boost::sp_scalar_constructor_hook( px );
#endif
}
#ifndef BOOST_NO_AUTO_PTR
/*支持从auto_ptr构造一个scoped_ptr智能指针对象,
但是auto_ptr因为调用release()函数,导致其内部指
针为nullptr*/
explicit scoped_ptr( std::auto_ptr p ) BOOST_NOEXCEPT : px( p.release() )
{
#if defined(BOOST_SP_ENABLE_DEBUG_HOOKS)
boost::sp_scalar_constructor_hook( px );
#endif
}
#endif
/*析构函数,释放智能指针持有的资源*/
~scoped_ptr() // never throws
{
#if defined(BOOST_SP_ENABLE_DEBUG_HOOKS)
boost::sp_scalar_destructor_hook( px );
#endif
boost::checked_delete( px );
}
};
从scoped_ptr的源码可以看到,该智能指针由于私有化了拷贝构造函数和operator=赋值函数,因此从根本上杜绝了智能指针浅拷贝的发生,所以scoped_ptr也是不能用在容器当中的,如果容器互相进行拷贝或者赋值,就会引起scoped_ptr对象的拷贝构造和赋值,这是不允许的,代码会提示编译错误。
推荐使用unique_ptr
,与scoped_ptr
相同,也是删除了拷贝构造和赋值重载函数,但是定义了移动构造函数,在调用过程中需要使用move
函数,可以对右值进行接管,好处是用户可以感知之前的指针不能再对内存进行管理了,心中有数。源码如下:
template // = default_delete<_Ty>
class unique_ptr
: public _Unique_ptr_base<_Ty, _Dx>
{ // non-copyable pointer to an object
public:
typedef _Unique_ptr_base<_Ty, _Dx> _Mybase;
typedef typename _Mybase::pointer pointer;
typedef _Ty element_type;
typedef _Dx deleter_type;
/*提供了右值引用的拷贝构造函数*/
unique_ptr(unique_ptr&& _Right) noexcept
: _Mybase(_Right.release(),
_STD forward<_Dx>(_Right.get_deleter()))
{ // construct by moving _Right
}
/*提供了右值引用的operator=赋值重载函数*/
unique_ptr& operator=(unique_ptr&& _Right) noexcept
{ // assign by moving _Right
if (this != _STD addressof(_Right))
{ // different, do the move
reset(_Right.release());
this->get_deleter() = _STD forward<_Dx>(_Right.get_deleter());
}
return (*this);
}
/*
交换两个unique_ptr智能指针对象的底层指针
和删除器
*/
void swap(unique_ptr& _Right) noexcept
{ // swap elements
_Swap_adl(this->_Myptr(), _Right._Myptr());
_Swap_adl(this->get_deleter(), _Right.get_deleter());
}
/*通过自定义删除器释放资源*/
~unique_ptr() noexcept
{ // destroy the object
if (get() != pointer())
{
this->get_deleter()(get());
}
}
/*unique_ptr提供->运算符的重载函数*/
_NODISCARD pointer operator->() const noexcept
{ // return pointer to class object
return (this->_Myptr());
}
/*返回智能指针对象底层管理的指针*/
_NODISCARD pointer get() const noexcept
{ // return pointer to object
return (this->_Myptr());
}
/*提供bool类型的重载,使unique_ptr对象可以
直接使用在逻辑语句当中,比如if,for,while等*/
explicit operator bool() const noexcept
{ // test for non-null pointer
return (get() != pointer());
}
/*功能和auto_ptr的release函数功能相同,最终也是只有一个unique_ptr指针指向资源*/
pointer release() noexcept
{ // yield ownership of pointer
pointer _Ans = get();
this->_Myptr() = pointer();
return (_Ans);
}
/*把unique_ptr原来的旧资源释放,重置新的资源_Ptr*/
void reset(pointer _Ptr = pointer()) noexcept
{ // establish new pointer
pointer _Old = get();
this->_Myptr() = _Ptr;
if (_Old != pointer())
{
this->get_deleter()(_Old);
}
}
/*
删除了unique_ptr的拷贝构造和operator=赋值函数,
因此不能做unique_ptr智能指针对象的拷贝构造和
赋值,防止浅拷贝的发生
*/
unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;
};
但是unique_ptr提供了带右值引用参数的拷贝构造和赋值,也就是说,unique_ptr智能指针可以通过右值引用进行拷贝构造和赋值操作,或者在产生unique_ptr临时对象的地方,如把unique_ptr作为函数的返回值时,示例代码如下:
// 示例1
unique_ptr ptr(new int);
unique_ptr ptr2 = std::move(ptr); // 使用了右值引用的拷贝构造 ptr此时就不持有资源了,无法访问
ptr2 = std::move(ptr); // 使用了右值引用的operator=赋值重载函数
// 示例2
unique_ptr test_uniqueptr()
{
unique_ptr ptr1(new int);
return ptr1;
}
int main()
{
/*
此处调用test_uniqueptr函数,在return ptr1代码
处,调用右值引用的拷贝构造函数,由ptr1拷贝构造ptr
*/
unique_ptr ptr = test_uniqueptr();
return 0;
}
unique_ptr还提供了reset重置资源,swap交换资源等函数,也经常会使用到。可以看到,unique_ptr从名字就可以看出来,最终也是只能有一个该智能指针引用资源,因此建议在使用不带引用计数的智能指针时,可以优先选择unique_ptr智能指针。
带引用计数的智能指针
自己实现:
template
class RefCnt {//计数类
public:
RefCnt(T *ptr = nullptr) : _mptr(ptr) {
if (_mptr != nullptr) {
_mcount = 1;
}
}
void addRef() { _mcount++; }
int delRef() { return --_mcount; }
private:
T *_mptr;
int _mcount;
};
template
class CSmartPtr {
public:
CSmartPtr(T *ptr = nullptr) : _mptr(ptr) {
_mRefPtr = new RefCnt(_mptr);
}
~CSmartPtr() {
if (0 == _mRefPtr->delRef()) {
delete _mptr;
_mptr = nullptr;
}
}
CSmartPtr(const CSmartPtr &src)
: _mptr(src._mptr), _mRefPtr(src._mRefPtr) {
if (_mptr != nullptr)
_mRefPtr->addRef();
}
CSmartPtr &operator=(const CSmartPtr &src) {
if (this == &src) {
return *this;
}
if (0 == _mRefPtr->delRef()) {
delete _mptr;
}
_mptr = src._mptr;
_mRefPtr = src._mRefPtr;
_mRefPtr->addRef();
return *this;
}
T &operator*() {
return *_mptr;
}
T *operator->() {
return _mptr;
}
private:
T *_mptr;
RefCnt *_mRefPtr;
};
C++stl里提供的带有引用计数的智能指针shared_ptr和weak_ptr的使用是线程安全的,底层的引用计数已经通过CAS操作。
shared_ptr交叉引用问题
stl中提供了两个带有引用计数的智能指针
shared_ptr
:强智能指针,可以改变资源的引用计数
weak_ptr
:弱智能指针,不会改变资源的引用计数
有这样一例子,是shared_ptr
的交叉引用:
#include
#include
using namespace std;
class B; // 前置声明类B
class A
{
public:
A() { cout << "A()" << endl; }
~A() { cout << "~A()" << endl; }
shared_ptr _ptrb; // 指向B对象的智能指针
};
class B
{
public:
B() { cout << "B()" << endl; }
~B() { cout << "~B()" << endl; }
shared_ptr _ptra; // 指向A对象的智能指针
};
int main()
{
shared_ptr ptra(new A());// ptra指向A对象,A的引用计数为1
shared_ptr ptrb(new B());// ptrb指向B对象,B的引用计数为1
ptra->_ptrb = ptrb;// A对象的成员变量_ptrb也指向B对象,B的引用计数为2
ptrb->_ptra = ptra;// B对象的成员变量_ptra也指向A对象,A的引用计数为2
cout << ptra.use_count() << endl; // 打印A的引用计数结果:2
cout << ptrb.use_count() << endl; // 打印B的引用计数结果:2
/*
出main函数作用域,ptra和ptrb两个局部对象析构,分别给A对象和
B对象的引用计数从2减到1,达不到释放A和B的条件(释放的条件是
A和B的引用计数为0),因此造成两个new出来的A和B对象无法释放,
导致内存泄露,这个问题就是“强智能指针的交叉引用(循环引用)问题”
*/
return 0;
}
代码打印结果:
A()
B()
2
2
可以看到,A和B对象并没有进行析构,通过上面的代码示例,能够看出来“交叉引用”的问题所在,就是对象无法析构,资源无法释放,那怎么解决这个问题呢?请注意强弱智能指针的一个重要应用规则:定义对象时,用强智能指针shared_ptr
,在其它地方引用对象时,使用弱智能指针weak_ptr
。
弱智能指针weak_ptr
区别于shared_ptr
之处在于:
weak_ptr
不会改变资源的引用计数,只是一个观察者的角色,通过观察shared_ptr
来判定资源是否存在weak_ptr
持有的引用计数,不是资源的引用计数,而是同一个资源的观察者的计数weak_ptr
没有提供常用的指针操作,无法直接访问资源,需要先通过lock
方法提升为shared_ptr
强智能指针,才能访问资源
那么上面的代码怎么修改,也就是如何解决带引用计数的智能指针的交叉引用问题,代码如下:
#include
#include
using namespace std;
class B; // 前置声明类B
class A
{
public:
A() { cout << "A()" << endl; }
~A() { cout << "~A()" << endl; }
weak_ptr _ptrb; // 指向B对象的弱智能指针。引用对象时,用弱智能指针
};
class B
{
public:
B() { cout << "B()" << endl; }
~B() { cout << "~B()" << endl; }
weak_ptr _ptra; // 指向A对象的弱智能指针。引用对象时,用弱智能指针
};
int main()
{
// 定义对象时,用强智能指针
shared_ptr ptra(new A());// ptra指向A对象,A的引用计数为1
shared_ptr ptrb(new B());// ptrb指向B对象,B的引用计数为1
// A对象的成员变量_ptrb也指向B对象,B的引用计数为1,因为是弱智能指针,引用计数没有改变
ptra->_ptrb = ptrb;
// B对象的成员变量_ptra也指向A对象,A的引用计数为1,因为是弱智能指针,引用计数没有改变
ptrb->_ptra = ptra;
cout << ptra.use_count() << endl; // 打印结果:1
cout << ptrb.use_count() << endl; // 打印结果:1
/*
出main函数作用域,ptra和ptrb两个局部对象析构,分别给A对象和
B对象的引用计数从1减到0,达到释放A和B的条件,因此new出来的A和B对象
被析构掉,解决了“强智能指针的交叉引用(循环引用)问题”
*/
return 0;
}
代码打印如下:
A()
B()
1
1
~B()
~A
可以看到,A和B对象正常析构,问题解决!
多线程访问共享对象的线程安全问题
有一个用C++写的开源网络库,muduo库,作者陈硕,大家可以在网上下载到muduo的源代码,该源码中对于智能指针的应用非常优秀,其中借助shared_ptr
和weak_ptr
解决了多线程访问共享对象的线程安全问题,解释如下:线程A和线程B访问一个共享的对象,如果线程A正在析构这个对象的时候,线程B又要调用该共享对象的成员方法,此时可能线程A已经把对象析构完了,线程B再去访问该对象,就会发生不可预期的错误。
先看如下代码:
#include
#include
using namespace std;
class Test
{
public:
// 构造Test对象,_ptr指向一块int堆内存,初始值是20
Test() :_ptr(new int(20))
{
cout << "Test()" << endl;
}
// 析构Test对象,释放_ptr指向的堆内存
~Test()
{
delete _ptr;
_ptr = nullptr;
cout << "~Test()" << endl;
}
// 该show会在另外一个线程中被执行
void show()
{
cout << *_ptr << endl;
}
private:
int *volatile _ptr;
};
void threadProc(Test *p)
{
// 睡眠两秒,此时main主线程已经把Test对象给delete析构掉了
std::this_thread::sleep_for(std::chrono::seconds(2));
/*
此时当前线程访问了main线程已经析构的共享对象,结果未知,隐含bug。
此时通过p指针想访问Test对象,需要判断Test对象是否存活,如果Test对象
存活,调用show方法没有问题;如果Test对象已经析构,调用show有问题!
*/
p->show();
}
int main()
{
// 在堆上定义共享对象
Test *p = new Test();
// 使用C++11的线程类,开启一个新线程,并传入共享对象的地址p
std::thread t1(threadProc, p);
// 在main线程中析构Test共享对象
delete p;
// 等待子线程运行结束
t1.join();
return 0;
}
运行上面的代码,发现在main主线程已经delete析构Test对象以后,子线程threadProc再去访问Test对象的show
方法,无法打印出*_ptr的值20。可以用shared_ptr
和weak_ptr
来解决多线程访问共享对象的线程安全问题,上面代码修改如下:
#include
#include
#include
using namespace std;
class Test
{
public:
// 构造Test对象,_ptr指向一块int堆内存,初始值是20
Test() :_ptr(new int(20))
{
cout << "Test()" << endl;
}
// 析构Test对象,释放_ptr指向的堆内存
~Test()
{
delete _ptr;
_ptr = nullptr;
cout << "~Test()" << endl;
}
// 该show会在另外一个线程中被执行
void show()
{
cout << *_ptr << endl;
}
private:
int *volatile _ptr;
};
void threadProc(weak_ptr pw) // 通过弱智能指针观察强智能指针
{
// 睡眠两秒
std::this_thread::sleep_for(std::chrono::seconds(2));
/*
如果想访问对象的方法,先通过pw的lock方法进行提升操作,把weak_ptr提升
为shared_ptr强智能指针,提升过程中,是通过检测它所观察的强智能指针保存
的Test对象的引用计数,来判定Test对象是否存活,ps如果为nullptr,说明Test对象
已经析构,不能再访问;如果ps!=nullptr,则可以正常访问Test对象的方法。
*/
shared_ptr ps = pw.lock();
if (ps != nullptr)
{
ps->show();
}
}
int main()
{
// 在堆上定义共享对象
shared_ptr p(new Test);
// 使用C++11的线程,开启一个新线程,并传入共享对象的弱智能指针
std::thread t1(threadProc, weak_ptr(p));
// 在main线程中析构Test共享对象
// 等待子线程运行结束
t1.join();
return 0;
}
运行上面的代码,show方法可以打印出20,因为main线程调用了t1.join()方法等待子线程结束,此时pw通过lock提升为ps成功,见上面代码示例。
如果设置t1为分离线程,让main主线程结束,p智能指针析构,进而把Test对象析构,此时show方法已经不会被调用,因为在threadProc方法中,pw提升到ps时,lock方法判定Test对象已经析构,提升失败!main函数代码可以如下修改测试:
int main()
{
// 在堆上定义共享对象
shared_ptr p(new Test);
// 使用C++11的线程,开启一个新线程,并传入共享对象的弱智能指针
std::thread t1(threadProc, weak_ptr(p));
// 在main线程中析构Test共享对象
// 设置子线程分离
t1.detach();
return 0;
}
该main函数运行后,最终的threadProc中,show方法不会被执行到。以上是在多线程中访问共享对象时,对shared_ptr和weak_ptr的一个典型应用。
自定义删除器
标准库中的智能指针中默认的析构方法是delete p
,如果使用智能指针指向数组或者是文件就需要自定义删除器。传统的方式是通过自定义函数对象作为删除器,这种方式会构造大量无关的代码,所以在C++11中引入了function
和lambda表达式,减少代码量。至于为什么要用函数对象:通过函数对象调用operator(),可以省略函数的调用开销,比通过函数指针调用函数(不能够inline内联调用)效率高。(见8.C++STL倒数第二段)
unique_ptr> ptr1(new int[100],
[](int *p)->void{//使用lambda表达式
cout<<"call lambda release int[100]"<> ptr2(fopen("data.txt","w"),
[](FILE* p)->void {
cout<<"call lambda release FILE*"<
名称栏目:2.C++中的智能指针
文章转载:http://ybzwz.com/article/dsoipcg.html