C++智能指针「建议收藏」

C++智能指针「建议收藏」一、基础知识介绍二、不带引用计数的智能指针三、带引用计数的智能指针四、shared_ptr和weak_ptr五、多线程访问共享对象的线程安全问题六、自定义删除器

大家好,又见面了,我是你们的朋友全栈君。

一、基础知识介绍

裸指针常出现以下几个问题:

  1. 忘记释放资源,导致资源泄露(常发生内存泄漏问题)
  2. 同一资源释放多次,导致释放野指针,程序崩溃
  3. 写了释放资源的代码,但是由于程序逻辑满足条件,执行中间某句代码时程序就退出了,导致释放资源的代码未被执行到
  4. 代码运行过程中发生异常,随着异常栈展开,导致释放资源的代码未被执行到
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;
}

上面这段代码就是一个非常简单的智能指针,主要用到了这两点:

  1. 智能指针体现在把裸指针进行了面向对象的封装,在构造函数中初始化资源地址,在析构函数中负责释放资源
  2. 利用栈上的对象出作用域自动析构这个特点,在智能指针的析构函数中保证释放资源。所以,智能指针一般都是定义在栈上

面试官:能不能在堆上定义智能指针?
答:不能。就好比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;}

二、不带引用计数的智能指针auto_ptr、scoped_ptr、unique_ptr

int main(){ 
   
    SmartPtr<int> ptr1(new int);
    SmartPtr<int> ptr2(ptr1);
    return 0;
}

以上代码运行时,由于ptr2拷贝构造时默认是浅拷贝,两个对象底层的裸指针指向同一份资源,对象析构时,会出现同一资源释放两次的错误(释放野指针),这里需要解决两个问题:

  1. 智能指针的浅拷贝
  2. 多个智能指针指向同一个资源的时候,怎么保证资源只释放一次,而不是每个智能指针都释放一次

不带引用计数的智能指针主要包括:

  • C++库提供的auto_ptr
  • C++11提供的scoped_ptrunique_ptr

(1)auto_ptr

	auto_ptr<int> ptr1(new int);
    auto_ptr<int> ptr2(ptr1);

我们运行,发现程序崩溃
在这里插入图片描述
这是因为构造ptr2时将ptr1置空了,析构ptr1时出错

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底层先是将ptr1置空,然后将指向的资源再给ptr2auto_ptr所做的就是使最后一个构造的指针指向资源,以前的指针全都置空,如果再去访问以前的指针就是访问空指针了,这很危险。所以一般不使用auto_ptr

在这里插入图片描述
一般我们也不在容器中使用auot_ptr,以下程序会导致vec1中的指针元素全部为NULL

vector<auto_ptr<int>> vec1;
vector<auto_ptr> vec2(vec1);

(2)scoped_ptr

private:
    T * px;
    scoped_ptr(scoped_ptr const &);
    scoped_ptr & operator=(scoped_ptr const &);
    
    typedef scoped_ptr<T> this_type;
    void operator==( scoped_ptr const& ) const;
    void operator!=( scoped_ptr const& ) const;

scoped_ptr底层私有化了拷贝构造函数和operator=赋值函数,从根本上杜绝了智能指针浅拷贝的发生,所以scoped_ptr也是不能用在容器当中的。如果容器互相进行拷贝或者赋值,就会引起scoped_ptr对象的拷贝构造和赋值,这是不允许的,代码会提示编译错误。

auto_ptrscoped_ptr这一点上的区别,有些资料上用所有权的概念来描述,道理是相同的。auto_ptr可以任意转移资源的所有权,而scoped_ptr不会转移所有权(因为拷贝构造和赋值被禁止了)

由于scoped_ptr无法进行任何的拷贝构造函数和operator=赋值,一般也不推荐使用scoped_ptr

(3)unique_ptr

unique_ptr做的事:

  • delete左值引用拷贝构造和赋值
  • 提供右值引用拷贝构造或者赋值运算符
  • 右值引用拷贝构造或者赋值时,会release前一个对象的资源

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);
    }	
	// delete左值引用拷贝构造和赋值
	unique_ptr(const unique_ptr&) = delete;
    unique_ptr& operator=(const unique_ptr&) = delete;

从源码可以看到,unique_ptr直接delete了拷贝构造函数和operator=赋值重载函数,禁止用户对unique_ptr进行显示的拷贝构造和赋值,防止智能指针浅拷贝问题的发生

unique_ptr<int> p1(new int);
unique_ptr<int> p2(p1);

在这里插入图片描述
由于unique类delete了拷贝构造和赋值函数,所以以上代码是会报错的

但是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从名字就可以看出来,最终也是只能有一个智能指针引用资源,会release其他智能指针的资源

三、带引用计数的智能指针shared_ptr、weak_ptr

当多个智能指针指向同一个资源的时候,每一个智能指针都会给资源的引用计数加1,当一个智能指针析构时,同样会使资源的引用计数减1,最后一个智能指针把资源的引用计数从1减到0时,就说明该资源可以释放了,由最后一个智能指针的析构函数来处理资源的释放问题,这就是带引用计数的智能指针

要对资源的引用个数进行计数,那么大家知道,对于整数的++或者- -操作,它并不是线程安全的操作,因此shared_ptr和weak_ptr底层的引用计数已经通过CAS操作,保证了引用计数加减的原子特性,因此shared_ptr和weak_ptr本身就是线程安全的带引用计数的智能指针。

private:
    element_type* _Ptr{ 
   nullptr};
    _Ref_count_base* _Rep{ 
   nullptr};

智能指针的引用计数对象放在堆区

在shared_ptr和weak_ptr的基类_Ptr_base中,有两个和引用计数相关的成员,_Ptr是指向内存资源的指针,_Rep是指向new出来的计数器对象的指针

class shared_ptr : public _Ptr_base<_Ty>;
class weak_ptr : public _Ptr_base<_Ty>;
#include <iostream>
#include <memory>
using namespace std;
template<typename T>
class RefCnt { 

public:
RefCnt(T* ptr = nullptr) : mptr(ptr) { 

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:弱智能指针,不可改变资源的引用计数,只是一个观察者的角色,通过观察shared_ptr来判定资源是否存在。无法直接访问资源,需要先通过lock方法提升为shared_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;
}

在这里插入图片描述
可以看到,栈上pa和pb出作用域的时候,只能把引用计数从2减为1,无法执行析构函数。通过上面的代码示例,能够看出来交叉引用的问题所在,就是对象无法析构,资源无法释放,那怎么解决这个问题呢?
在这里插入图片描述
强弱智能指针的一个重要应用规则: 定义对象和访问对象时用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;
};

在这里插入图片描述
weak_ptr只是用于观察资源,不能够使用资源,并没有实现operator*operator->。我们可以使用weak_ptr对象的lock()方法返回shared_ptr对象,这个操作会增加资源的引用计数

四、多线程访问共享对象的线程安全问题

多线程环境中,线程A和线程B访问一个共享对象,如果线程A已经执行了这个对象的析构函数,释放了这个对象所有的资源。线程B又要调用该共享对象的成员方法,就会发生不可预期的错误

#include <iostream>
#include <memory>
#include <thread>
using namespace std;
class A { 

public:
A()
:ptr_(new int(11))
{ 

cout << "A()" << endl;
}
~A() { 

delete ptr_;
cout << "~A()" << endl;
}
void test() { 

cout << "*ptr = " << *ptr_ << endl;
}
private:
int* volatile ptr_;
};
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;
}

在这里插入图片描述
可以看到,由于裸指针p指向的A对象已经delete,对象的堆空间已经释放,访问对象的数据成员ptr_出问题

在多线程访问共享对象的时候,往往需要lock检测一下对象是否存在。开启一个新线程,并传入共享对象的弱智能指针

#include <iostream>
#include <memory>
#include <thread>
using namespace std;
class A { 

public:
A()
:ptr_(new int(11))
{ 

cout << "A()" << endl;
}
~A() { 

delete ptr_;
cout << "~A()" << endl;
}
void test() { 

cout << "*ptr_ = " << *ptr_ << endl;
}
private:
int* volatile ptr_;
};
void handler01(weak_ptr<A> pw) { 

shared_ptr<A> ps = pw.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));
t1.join();
return 0;
}

在这里插入图片描述
成功访问A对象的数据成员

#include <iostream>
#include <memory>
#include <thread>
using namespace std;
class A { 

public:
A()
:ptr_(new int(11))
{ 

cout << "A()" << endl;
}
~A() { 

cout << "~A()" << endl;
delete ptr_;
}
void test() { 

cout << "*ptr_ = " << *ptr_ << endl;
}
private:
int* volatile ptr_;
};
void handler01(weak_ptr<A> pw) { 

// 子线程等待,让main函数中的p出作用域析构
std::this_thread::sleep_for(std::chrono::seconds(1));
// 多线程环境中,需要检测当前对象是否存活,存活才能访问其方法和数据成员
shared_ptr<A> ps = pw.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;
}
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/151253.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)
blank

相关推荐

  • RabbitMQ入门:Hello RabbitMQ 代码实例[通俗易懂]

    在之前的一篇博客RabbitMQ入门:认识并安装RabbitMQ(以Windows系统为例)中,我们安装了RabbitMQ并且对其也有的初步的认识,今天就来写个入门小例子来加深概念理解并了解代码怎么实

  • 微信小程序宠物论坛2[通俗易懂]

    微信小程序宠物论坛2[通俗易懂]微信小程序宠物论坛2发帖模块界面展示填写标题、内容和选择图片之后,点击确定图片,然后点击发布即可。JS部分//import{promisify}from’../../utils/promise.util’import{$init,$digest}from’../../utils/common.util’//constwxUploadFile=promisify(wx.cloud.uploadFile)constdb=wx.cloud.databa

  • latex中如何正确输入 双引号「建议收藏」

    latex中如何正确输入 双引号「建议收藏」latex中输入双引号时,如果都直接用键盘上的双引号键,打出的是一顺撇的。左面引号的正确输入法是:按两次“Tab上面,数字1左面那个键”。至于后边的引号,与老方法是一样的,即按两次单引号键(或一次SHIFT+单引号键—也就是一次双引号键啦怎么输入左单引号、左双引号、右单引号、有双引号?左单引号:`(键盘上1旁边的那个);左双引号:“;右单引号:'(键盘分号的右边那个);右双引号:”或”。在

  • webpack基础打包命令_webpack打包原理

    webpack基础打包命令_webpack打包原理没有配置文件的打包如果我们没有使用配置文件webpack.config.js,那么我们就需要通过命令来打包案例我们首先创建一个webpackTest文件夹,然后在文件夹中再创建2个子文件夹dis

  • 适配器模式的理解和示例[通俗易懂]

    适配器模式的理解和示例[通俗易懂]一、是什么1.定义:让原来不兼容的两个接口协同工作2.分类:类适配器、对象适配器、接口适配器3.角色目标接口:Target,该角色把其他类转换为我们期望的接口被适配者:Adapte

  • Windows Phone 8.1 新功能 – 应用栏控件

    Windows Phone 8.1 新功能 – 应用栏控件

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号