C++进阶 | 智能指针详解

    技术2023-12-05  108

    目录

    智能指针自实现智能指针auto_ptr,scoped_ptr,unique_ptr1、auto_ptr(不推荐)2、scoped_ptr(不推荐)3、unique_ptr(推荐) 智能指针的交叉引用问题强智能指针的交叉引用问题 多线程访问共享对象问题

    智能指针

    首先来看一个裸指针:

    int *p=new int(10); //在堆内存上申请一个int型数组,大小为10,用指针p指向数组的起始地址。 *p=30; //这里可以通过指针解引用,来修改p指向地址的值。 delete p; //释放堆上申请的内存

    可以看到,裸指针在使用的时候必须手动释放掉,否则长期以往,多个指针指向的堆内存都不释放,内存很快消耗殆尽。

    人懒当然不想老是自己手动释放指针,我们希望这个指针在合适的地方自己把自己释放掉。这里便引入了智能指针这个概念。为了指针能自动释放,势必要用到栈上对象出栈自己析构的特点,用这个特点,我们自己先手写一个智能指针:

    template<typename T> class CSmartPtr { public: CSmartPtr(T* ptr = nullptr):mptr(ptr){} ~CSmartPtr () //对象出作用域自己释放掉mptr { delete mptr; } private: T* mptr; };

    第一版的智能指针很简单,直接利用上一特点、模板,在类的析构函数中delete掉这个类的指针mptr。用下面的方式调用即可:

    int main() { CSmartPtr<int> s(new int); return 0; }

    这里有一个问题:智能指针能不能放在堆上?如下写法:

    CSmartPtr<int>* p = new CSmartPtr<int>(new int);

    很显然,这里的指针p又变成了一个裸指针,用是可以用,但是没有必要,不然还写CSmartPtr干嘛呢?

    自实现智能指针

    当然目前的CSmartPtr功能还不完善,缺少指针解引用、指向运算符等功能,下面我们来继续完善。 完善指针和指向运算符:

    T& operator*() { return *mptr; } T* operator->() { return mptr; }

    这样我们就可以通过*、->来访问操作*mptr、mptr,进而操作数据。

    但是,目前我们的CSmartPtr还是不够完善,虽然开始更像一个指针了,但是请看下面的问题:

    int main() { CSmartPtr<int> p1(new int); CSmartPtr<int> p2(p1); return 0; }

    我们分析一个上面的代码,p1内的mptr指向一块堆内存,接着用p1拷贝构造p2。这段程序在执行的时候会报错。因为程序在出作用域时会先后执行p2、p1的析构函数,p2先将mptr释放,接着p1再去释放mptr,此时的mptr已经是一个野指针了。目前的问题就是这样,p1和p2同时在管理mptr。

    为了解决这类浅拷贝的问题,让我们自己的智能指针更适用。我们需要另想别的方法,我们先看下面官方给的解决方案。

    auto_ptr,scoped_ptr,unique_ptr

    为了解决浅拷贝问题,C++提供了3个不带引用计数的智能指针。

    1、auto_ptr(不推荐)

    对于auto_ptr这个智能指针,我们先上例子,看看它是否解决了我们上面遇到的问题:

    int main() { auto_ptr<int> ptr1(new int); auto_ptr<int> ptr2(ptr1); *ptr1 = 30; return 0; }

    下面是执行结果:

    程序直接崩溃,同时可以看到,ptr1内部的_M_ptr被置成了NULL。我们打开auto_ptr的拷贝构造方法:

    auto_ptr(auto_ptr& __a) throw() : _M_ptr(__a.release()) { }

    发现ptr2的_M_ptr用__a.release()初始化,而release如下:

    element_type* release() throw() { element_type* __tmp = _M_ptr; _M_ptr = 0; return __tmp; }

    可以发现,ptr1的_M_ptr被直接置成了NULL,也就是说auto_ptr在解决浅拷贝上,直接把被拷贝对象的封装的指针直接置空。这样auto_ptr只有最后一个持有原先_M_ptr值的智能指针是有效的,之前的所有ptr的底层指针已经被置成NULL。所以C++目前已经不推荐使用这个智能指针了。更不用说在容器里放auto_ptr了,非常危险,一旦做了浅拷贝构造,原来的资源就成空了。

    2、scoped_ptr(不推荐)

    相比于auto_ptr,scoped_ptr直接禁止了拷贝构造和拷贝赋值函数。那么对于scoped_ptr,就只能一个一个使用,不能做拷贝。使用频率也不高。

    scoped_ptr(const scoped_ptr<T>&) = delete; scoped_ptr<T>& operator=(const scoped_ptr<T>&) = delete;

    3、unique_ptr(推荐)

    unique_ptr同样对拷贝构造和拷贝赋值做了禁用处理:

    unique_ptr(const unique_ptr<T>&) = delete; unique_ptr<T>& operator=(const unique_ptr<T>&) = delete;

    那么对于浅拷贝,unique_ptr做了特殊处理,使用了右值引用–>std::move,来得到当前变量的右值类型。

    智能指针的交叉引用问题

    除了上面三个不带引用计数的智能指针,C++11以后还提供两个带引用计数的智能指针:shared_ptr、weak_ptr。而带引用计数的智能指针,其好处就是可以用多个智能指针来管理同一个资源,而上面三个都是用1个智能指针来管理资源。

    再来看引用计数的含义:就是给每一个对象的堆上资源,匹配一个引用计数,规则如下:

    1、智能指针在指向这个资源的时候,引用计数就会加1。 2、而不再指向资源的时候,引用计数就会减1。 3、当引用计数不等于0时,说明这个资源还有智能指针引用,不能析构掉。 4、当引用计数减为0时,说明这个资源没人用了,那么最后一个引用这个资源的智能指针就会把这个资源释放掉 。

    依据上面四条,我们重新优化一下上面我们自己的CSmarPtr:

    //对资源进行引用计数的类。 template<typename T> class RefCnt { public: RefCnt(T* ptr = nullptr) :mptr(ptr) { if (mptr != nullptr) { mcount = 1; //初始化如果mptr不是nullptr,就置mcount为1 } } void addRef() { mcount++; //增加资源的引用计数 } int delRef() { mcount--; //减少资源的引用计数并返回其mcount,来进一步判断是否需要释放资源 return mcount; } private: T* mptr; int mcount; }; template<typename T> class CSmartPtr { public: CSmartPtr(T* ptr = nullptr) :mptr(ptr) { mpRefCnt = new RefCnt<T>(mptr); } ~CSmartPtr() { if (0 == mpRefCnt->delRef()) { delete mptr; mptr = nullptr; } } T& operator*() { return *mptr; } T* operator->() { return mptr; } CSmartPtr(const CSmartPtr<T>& src) :mptr(src.mptr), mpRefCnt(src.mpRefCnt)//修改拷贝构造 { if (mptr != nullptr) { mpRefCnt->addRef(); //很显然一旦拷贝构造就是另起一个智能指针指向了同一个资源,那么其引用计数就要加1 } } CSmartPtr<T>& operator=(const CSmartPtr<T>& src) //修改拷贝赋值 { if (this == &src) { return *this; } if (0 == mpRefCnt->delRef()) //减为0,就要释放资源 { delete mptr; } mptr = src.mptr; mpRefCnt = src.mpRefCnt; mpRefCnt->addRef(); return *this; } private: T* mptr; //指向资源的指针 RefCnt<T>* mpRefCnt; //指向该资源引用计数对象的指针 };

    测试一下:

    int main() { CSmartPtr<int> ptr1(new int); CSmartPtr<int> ptr2(ptr1); CSmartPtr<int> ptr3; ptr3 = ptr2; *ptr1 = 20; cout << *ptr2 << "---" << *ptr3 << endl; return 0; }

    这里的运行结果已经显示,ptr1、ptr2、ptr3完全相等,目前解决了浅拷贝的问题: 但是目前我们的CSmartPtr还是不够好。因为基础数据增加和减少,对于多线程情况下的访问十分危险。我们可以使用官方的shared_ptr和weak_ptr

    强智能指针的交叉引用问题

    接着上面,我们再来了解一下shared_ptr、weak_ptr的情况。 1、shared_ptr:强智能指针 可以改变资源的引用计数 2、weak_ptr:弱智能指针 不会改变资源的引用计数,仅仅观察资源是否仍然存在,就看它计数有没有变成0,没有就行。只是一个观察者,只会观察资源,不能用资源。压根没有提供operator*和operator-> 3、weak_ptr可以通过shared_ptr访问资源 这里有一个问题:强智能指针的循环引用(交叉引用)问题?什么结果?怎么解决?

    我们先来看下面的情况:

    class B; class A { public: A(){cout<<"A()"<<endl;} ~A(){cout<<"~A()"<<endl;} shared_ptr<B> _ptrb; //A对象将直接持有一个智能指针_ptrb }; class B { public: B(){cout<<"B()"<<endl;} ~B(){cout<<"~B()"<<endl;} shared_ptr<A> _ptra; //B对象将直接持有一个智能指针_ptra }; int main() { shared_ptr<A> pa(new A()); shared_ptr<B> pb(new B()); cout<<pa.use_count()<<endl; cout<<pb.use_count()<<endl; }

    其结果运行如下: 可以看到,这是正常的,对于pa来说,引用了A的对象,里面引用了B。所以pa的引用计数是1,对于pb来说同理可得。

    再来看:

    int main() { shared_ptr<A> pa(new A()); shared_ptr<B> pb(new B()); pa->_ptrb=pb; //pa和pb各再次引用对方,造成循环引用,套娃 pb->_ptra=pa; cout<<pa.use_count()<<endl; cout<<pb.use_count()<<endl; }

    pa和pb开始了套娃,你引用我,我引用你。现在盲猜俩人的引用计数又都加了1,都变成2了。 结果如下: 完蛋,俩人计数都确实是2,在出main函数作用域时,俩人的引用计数都减1,从2变成了1,但是还在,没有变成0。这样导致俩智能指针最后都无法析构,导致资源无法释放。

    回答一下上面的问题:

    循环引用会造成new出来的资源无法释放!!资源会泄漏

    最好的情况要这么用:定义智能指针的时候用强智能指针,而引用对象的时候用弱智能指针。

    再来看把上面的情况稍作改动:

    class B; class A { public: A(){cout<<"A()"<<endl;} ~A(){cout<<"~A()"<<endl;} weak_ptr<B> _ptrb; //这里从shared_ptr改成了weak_ptr }; class B { public: B(){cout<<"B()"<<endl;} ~B(){cout<<"~B()"<<endl;} weak_ptr<A> _ptra; //这里从shared_ptr改成了weak_ptr };

    运行结果: 可见,weak_ptr没有令俩人的资源引用计数增加,这样就解决了循环引用的情况。

    但是假如说我们希望通过weak_ptr持有的资源访问一个方法,该怎么办呢?weak_ptr又没有operator*和operator->。答案是我们可以用weak_ptr的lock()方法来提升此智能指针的强度。如下:

    class B { public: B(){cout<<"B()"<<endl;} ~B(){cout<<"~B()"<<endl;} void func() { shared_ptr<A> ps=_ptra.lock(); //这里用lock()提升了weak_ptr的强度变成了shared_ptr } weak_ptr<A> _ptra; };

    多线程访问共享对象问题

    对于多线程的情况,我们先假想一个例子:

    假如对于main线程,定义了一个智能指针pa,管理资源a,我们新开一个线程thread,在thread里面调用pa指向的func方法。

    这里有一处非常危险的情况: thread在调用pa时是否知道,pa仍然还存活?

    所以在访问共享对象时,一定要判断这个指针是否是空指针

    Processed: 0.021, SQL: 9