融会贯通C++智能指针教程
目录
- 一、基础知识介绍
- 裸指针常出现以下几个问题:
- 二、不带引用计数的智能指针
- 不带引用计数的智能指针主要包括
- (1)auto_ptr源码
- (2)scoped_ptr
- (3)unique_ptr源码
- 三、带引用计数的智能指针
- 四、shared_ptr 和 weak_ptr
- 智能指针的交叉引用问题
- 五、多线程访问共享对象的线程安全问题
- 六、自定义删除器
一、基础知识介绍
裸指针常出现以下几个问题:
- 忘记释放资源,导致资源泄露(常发生内存泄漏问题)
- 同一资源释放多次,导致释放野指针,程序崩溃
- 写了释放资源的代码,但是由于程序逻辑满足条件,执行中间某句代码时程序就退出了,导致释放资源的代码未被执行到
- 代码运行过程中发生异常,随着异常栈展开,导致释放资源的代码未被执行到
template<typename T> class SmartPtr { public: SmartPtr(T* ptr = nullptr):_ptr(ptr) {} ~SmartPtr() {delete _ptr;} private: T* _ptr; }; int main(){ SmartPtr<int> ptr(new int); return 0; }
上面这段代码就是一个非常简单的智能指针,主要用到了这两点:
手动实现智能指针体现在把裸指针进行了面向对象的封装,在构造函数中初始化资源地址,在析构函数中负责释放资源
利用栈上的对象出作用域自动析构这个特点,在智能指针的析构函数中保证释放资源。
所以,智能指针一般都是定义在栈上的
面试官:能不能在堆上定义智能指针?
答:不能。就好比SmartPtr<int>* ptr = new SmartPtr<int>();
这段代码中,在堆空间定义一个智能指针,
这依然需要我们手动进行delete
,否则无法堆空间的对象无法释放,因为堆空间的对象无法自动调用析构函数。
一般而言智能指针还需要提供裸指针常见的*
和 ->
两种运算符的重载函数:
const T& operator*() const{return *_ptr;} T& operator*(){return *_ptr;} const T operator->() const{return _ptr;} T operator->(){return _ptr;}
二、不带引用计数的智能指针
int main(){ SmartPtr<int> ptr1(new int); SmartPtr<int> ptr2(ptr1); return 0; }
以上代码运行时,由于ptr2
拷贝构造时默认是浅拷贝,会出现同一资源释放两次的错误(释放野指针),这里需要解决两个问题:
智能指针的浅拷贝
多个智能指针指向同一个资源的时候,怎么保证资源只释放一次,而不是每个智能指针都释放一次
不带引用计数的智能指针主要包括
auto_ptr
,scoped_ptr
,unique_ptr
(1)auto_ptr源码
auto_ptr(auto_ptr& _Right) noexcept : _Myptr(_Right.release()) {} _Ty* release() noexcept { _Ty* _Tmp = _Myptr; _Myptr = nullptr; return _Tmp; }
使用auto_ptr
auto_ptr<int> ptr1(new int); auto_ptr<int> ptr2(ptr1);
从源码可以看到,auto_ptr
底层先是将ptr1
置空,然后将指向的资源再给ptr2
, auto_ptr
所做的就是使最后一个构造的指针指向资源,以前的指针全都置空,如果再去访问以前的指针就是访问空指针了,这很危险。所以一般不使用auto_ptr
(2)scoped_ptr
该智能指针底层私有化了拷贝构造函数和operator
=赋值函数,
从根本上杜绝了智能指针浅拷贝的发生,所以scoped_ptr
也是不能用在容器当中的。
- 如果容器互相进行拷贝或者赋值,就会引起
scoped_ptr
对象的拷贝构造和赋值,这是不允许的,代码会提示编译错误。 auto_ptr
和scoped_ptr
这一点上的区别,有些资料上用所有权的概念来描述,道理是相同的。auto_pt
r可以任意转移资源的所有权,而scoped_ptr
不会转移所有权(因为拷贝构造和赋值被禁止了)
一般也不推荐使用scoped_ptr
(3)unique_ptr源码
template <class _Dx2 = _Dx, enable_if_t<is_move_constructible_v<_Dx2>, int> = 0> unique_ptr(unique_ptr&& _Right) noexcept : _Mypair(_One_then_variadic_args_t{}, _STD forward<_Dx>(_Right.get_deleter()), _Right.release()) {} unique_ptr(unique_ptr<_Ty2, _Dx2>&& _Right) noexcept : _Mypair(_One_then_variadic_args_t{}, _STD forward<_Dx2>(_Right.get_deleter()), _Right.release()) {} // 拷贝构造或者赋值运算符的时候,用于将以前的智能指针置空 pointer release() noexcept { return _STD exchange(_Mypair._Myval2, nullptr); } unique_ptr(const unique_ptr&) = delete; unique_ptr& operator=(const unique_ptr&) = delete;
从源码可以看到,unique_ptr
直接delete
了拷贝构造函数和operator
=赋值重载函数
禁止用户对unique_ptr
进行显示的拷贝构造和赋值,防止智能指针浅拷贝问题的发生
但是unique_ptr提供了带右值引用参数的拷贝构造和赋值
即unique_ptr智能指针可以通过右值引用进行拷贝构造和赋值操作
unique_ptr<int> ptr1(new int); unique_ptr<int> ptr2(std::move(ptr1));// 使用右值引用的拷贝构造,由于执行了release,ptr1已经被置空 cout << (ptr1 == nullptr) << endl; // true ptr2 = std::move(ptr1); // 使用右值引用的operator=赋值重载函数 cout << (ptr2 == nullptr) << endl; // true
用临时对象构造新的对象时,也会调用带右值引用参数的函数
unique_ptr<int> get_unique_ptr() { unique_ptr<int> tmp(new int); return tmp; } int main(){ unique_ptr<int> ptr = get_unique_ptr(); // 调用带右值引用参数的拷贝构造函数,由tmp直接构造ptr return 0; }
unique_ptr从名字就可以看出来,最终也是只能有一个智能指针引用资源,其他智能指针全部置空
三、带引用计数的智能指针
#include <iostream> #include <memory> using namespace std; template<typename T> class RefCnt { public: RefCnt(T* ptr = nullptr){ mcount = (mptr == nullptr) ? 0 : 1; } void addRef() { mcount++; } int subRef() { return --mcount; } private: int mcount; // mptr指向某个资源的引用计数,线程不安全。使用atomic_int线程安全 T* mptr; //指向智能指针内部指向资源的指针,间接指向资源 }; template<typename T> class SmartPtr { public: SmartPtr(T* ptr = nullptr) :_ptr(ptr) { cout << "SmartPtr()" << endl; mpRefCnt = new RefCnt<int>(_ptr); // 用指向资源的指针初始化引用计数对象 } ~SmartPtr() { if (0 == mpRefCnt->subRef()) { cout << "释放资源析构~SmartPtr()" << endl; delete _ptr; } else { cout << "空析构~SmartPtr()" << endl; } } const T& operator*() const { return *_ptr; } T& operator*() { return *_ptr; } const T operator->() const { return _ptr; } T operator->() { return _ptr; } SmartPtr(const SmartPtr<T>& src) :_ptr(src._ptr), mpRefCnt(src.mpRefCnt) { cout << "SmartPtr(const SmartPtr<T>& src)" << endl; if (_ptr != nullptr) { // 用于拷贝的对象已经引用了资源 mpRefCnt->addRef(); } } SmartPtr<T>& operator=(const SmartPtr<T>& src) { cout << "SmartPtr<T>& operator=" << endl; if (this == &src) { return *this; } // 当前智能指针指向和src相同的资源,考虑是否释放之前的资源 if (0 == mpRefCnt->subRef()) { // 若之前指向的资源引用计数为1,释放之前的资源 delete _ptr; } _ptr = src._ptr; mpRefCnt = src.mpRefCnt; mpRefCnt->addRef(); return *this; } private: T* _ptr; // 指向资源的指针 RefCnt<T>* mpRefCnt; // 指向该资源引用计数的指针 }; int main(){ SmartPtr<int> ptr1(new int); SmartPtr<int> ptr2(ptr1); SmartPtr<int> ptr3; ptr3 = ptr2; *ptr2 = 100; cout << *ptr2 << " " << *ptr3 << endl; return 0; }
四、shared_ptr 和 weak_ptr
- shared_ptr:强智能指针,可以改变资源的引用计数
- weak_ptr:弱智能指针,不可改变资源的引用计数
weak_ptr
-> shared_ptr
-> 资源
智能指针的交叉引用问题
#include <iostream> #include <memory> using namespace std; class B; class A { public: A() { cout << "A()" << endl; } ~A() { cout << "~A()" << endl; } shared_ptr<B> _ptrb; }; class B { public: B() { cout << "B()" << endl; } ~B() { cout << "~B()" << endl; } shared_ptr<A> _ptra; }; int main() { shared_ptr<A> pa(new A()); shared_ptr<B> pb(new B()); pa->_ptrb = pb; pb->_ptra = pa; cout << pa.use_count() << endl; cout << pb.use_count() << endl; return 0; } /* A() B() 2 2 */
解决办法: 定义对象时用shared_ptr
,引用对象时用weak_ptr
class B; class A { public: A() { cout << "A()" << endl; } ~A() { cout << "~A()" << endl; } weak_ptr<B> _ptrb; }; class B { public: B() { cout << "B()" << endl; } ~B() { cout << "~B()" << endl; } weak_ptr<A> _ptra; }; /* A() B() 1 1 ~B() ~A() */
weak_ptr
只是用于观察资源,不能够使用资源,并没有实现operator*和operator->。可以使用weak_ptr对象的lock()方法返回shared_ptr
对象,这个操作会增加资源的引用计数。
五、多线程访问共享对象的线程安全问题
线程A和线程B访问一个共享对象,如果线程A已经析构这个对象
线程B又要调用该共享对象的成员方法,线程B再去访问该对象,就会发生不可预期的错误
#include <iostream> #include <memory> #include <thread> using namespace std; class A { public: A() { cout << "A()" << endl; } ~A() { cout << "~A()" << endl; } void test() { cout << "test()" << endl; } }; void handler01(A* q) { // 睡眠2s,使得主线程进行delete std::this_thread::sleep_for(std::chrono::seconds(2)); q->test(); } int main() { A* p = new A(); thread t1(handler01, p); delete p; t1.join(); return 0; }
在多线程访问共享对象的时候,往往需要lock检测一下对象是否存在。
开启一个新线程,并传入共享对象的弱智能指针。
#include <iostream> #include <memory> #include <thread> using namespace std; class A { public: A() { cout << "A()" << endl; } ~A() { cout << "~A()" << endl; } void test() { cout << "test()" << endl; } }; void handler01(weak_ptr<A> pw) { shared_ptr<A> ps = pw.lock(); // lock方法判定资源对象是否析构 if (ps != nullptr) { ps->test(); }else { cout << "A对象已经析构,无法访问" << endl; } } int main() { { shared_ptr<A> p(new A()); //开启一个新线程,并传入共享对象的弱智能指针 thread t1(handler01, weak_ptr<A>(p)); // 将子线程和主线程的关联分离,也就是说detach()后子线程在后台独立继续运行, // 主线程无法再取得子线程的控制权,即使主线程结束,子线程未执行也不会结束。 t1.detach(); } // 让主线程等待,给时间子线程执行,否则main函数最后会调用exit方法结束进程 std::this_thread::sleep_for(std::chrono::seconds(2)); return 0; }
六、自定义删除器
通常我们使用智能指针管理的资源是堆内存,当智能指针出作用域的时候,
在其析构函数中会delete释放堆内存资源,但是除了堆内存资源,智能指针还可以管理其它资源,
比如打开的文件,此时对于文件指针的关闭,就不能用delete了,
这时我们需要自定义智能指针释放资源的方法。
template<typename T> class ArrDeletor { public: // 对象删除的时候需要调用对应删除器的()重载函数 void operator()(T* ptr) const { cout << "ArrDeletor::operator()" << endl; delete[] ptr; } }; template<typename T> class FileDeletor { public: // 对象删除的时候需要调用对应删除器的()重载函数 void operator()(T* fp) const { cout << "FileDeletor::operator()" << endl; fclose(fp); } }; int main() { unique_ptr<int, ArrDeletor<int>> ptr1(new int[100]); unique_ptr<FILE, FileDeletor<FILE>> ptr2(fopen("1.cpp", "w")); // 使用lambda表达式 // function<返回值(参数)> // []叫做捕获说明符,表示一个lambda表达式的开始。接下来是参数列表,即这个匿名的lambda函数的参数 unique_ptr<int, function<void(int*)>> ptr1( new int[100], [](int* p)->void { cout << "call lambda release new int[]" << endl; delete[] p; } ); unique_ptr<FILE, function<void(FILE*)>> ptr2( fopen("1.cpp", "w"), [](FILE* p)->void { cout << "call lambda release fopen(\"1.cpp\", \"w\")" << endl; fclose(p); } ); return 0; }
到此这篇关于C++智能指针教程的文章就介绍到这了,更多相关C++智能指针内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!