C++11 shared_ptr---面试常考

shared_ptr简介

  • 共享对其所指堆内存空间的所有权,当最后⼀个指涉到该对象的shared_ptr不再指向他时,shared_ptr会⾃动析构所指对象
  • 如何判断⾃⼰是否指涉到该资源的最后⼀个?《引⽤计数》
    • shared_ptr构造函数,使引⽤计数++
    • 析构函数,–
    • 赋值运算符,sp1 = sp2; sp1++,sp2–
    • 移动构造函数,会将源shared_ptr置空,所引引⽤计数不变,所以移动操作⽐复制快(复制要递增引⽤计数,移动不需要)
  • 引⽤计数使得shred_ptr⼤小是裸指针的2倍(包含:指向资源的裸指针**+**控制块的裸指针)
  • ⾃定义析构器:
    • 对于unique_ptr来说析构器型别是智能指针⼀部分,而shared_ptr不是;
    • ⾃定义析构器不会改变shared_ptr⼤小,这部分内存不属于shared_ptr,位于堆上
  • ⼀个对象的控制块是由第⼀个指涉到该对象的shared_ptr函数来确定
    • make_shared总会创建⼀个控制块
    • ⽤裸指针作为shared_ptr构造函数的实参
    • unique_ptr出发构造⼀个shared_ptr
为什么建议使用make_share()??

举例:

用法简单

std::shared_ptr<Test> ptr = std::make_shared<Test>(1);
std::shared_ptr<Test> ptr2 = std::make_shared<Test>(2, 3);
std::shared_ptr<Test> ptr3 = std::make_shared<Test>(4, 5, 6);
直接构造的问题?

由于智能指针指针和引用计数的内存块是分开的,直接构造过程中出现问题,没办法保证两个内存块都能够一起分配成功。

make_shared的优缺点:

  • 优点:
  • 引用计数内存块和指针内存块是一起开辟的,在一块内存上。内存分配效率高了,同时也能够防止资源泄露的风险。
  • 缺点
  • 由于是一起开辟的,那么在引用计数为0时,资源也不一定能够释放,只有当观察计数也为0,资源才能够释放,造成了延迟释放的问题。
  • 无法自定义删除器。

在这里插入图片描述

  • 从同⼀个裸指针出发构造多个shared_ptr,会产⽣未定义⾏为
int* ptr = new int;
std::shared_ptr<int> p1(ptr);
std::shared_ptr<int> p2 = p1;
std::shared_ptr<int> p3 = p1;
std::shared_ptr<int> p2(p1);
std::shared_ptr<int> p3(p1);
// 都是可以的 具体看以下shared_ptr简单实现
int* ptr = new int;
std::shared_ptr<int> p1(ptr);
std::shared_ptr<int> p2(ptr);//错误

因为⽤裸指针作为shared_ptr构造函数的实参会创建⼀个控制块,那么被指向的对象就拥有多个控制块,也就是有多个引⽤计数,每个引⽤计数最后都会变为0,从而导致析构多次,产⽣未定义⾏为。

  • this指针创建⼀个shared_ptr
    • ⾸先继承⾃ class A : public std::enable_shared_from_this<A>
    • 成员函数`` shared_from_this 创建⼀个shared_ptr对象指向当前对象(内部实现,查询当前对象的控制块并创建⼀个shared_ptr`指向当前对象,前提是当前对象已有控制块)
    • 为了避免⽤⼾再shared_ptr`指向该对象之前就调⽤shared_from_this`
      • 将类的构造函数声明为private
      • 只允许⽤⼾调⽤返回shared_ptr的⼯⼚函数来创建对象

为什么多线程读写 shared_ptr 要加锁?/ shared_ptr是否是线程安全的?

(shared_ptr)的引用计数本身是安全且无锁的,但对象的读写则不是,因为 shared_ptr 有两个数据成员,读写操作不能原子化。 shared_ptr 的线程安全级别和内建类型、标准库容器、std::string 一样,即:

  • 一个 shared_ptr 对象实体可被多个线程同时读取;
  • 两个 shared_ptr 对象实体可以被两个线程同时写入,“析构”算写操作;
  • 如果要从多个线程读写同一个 shared_ptr 对象,那么需要加锁。

shared_ptr的线程安全问题?

shared_ptr本身不是线程安全的

  • 多线程代码操作同一个shared_ptr的对象的时候不是线程安全的
  • 当多个线程操作同一个shared_tr对象的时候,如果发生赋值操作,那么就会涉及到引用计数的加一和减一操作,由于这两个操作本身并不是线程安全的,所以就可能出现不安全的情况
  • 多线程操作不是同一个对象的shared_ptr对象,由于shared_ptr本身的引用计数是原子的,所以是线程安全的。

shared_from_this解决了什么问题?

参考上述内容,从同⼀个裸指针出发构造多个shared_ptr,会产⽣未定义⾏为

那么返回指向自身对象的shared_ptr指针也会有这个问题

// A类提供了一个成员方法,返回指向自身对象的shared_ptr智能指针。
shared_ptr<A> getSharedPtr() { /*注意:不能直接返回this,在多线程环境下,根本无法获知this指针指向的对象的生存状态,通过shared_ptr和weak_ptr可以解决多线程访问共享		对象的线程安全问题,参考我的另一篇介绍智能指针的博客*/return shared_ptr<A>(this); 
}

也就相当于从一个裸指针构造两个shared_ptr指针,所以也会引发未定义行为,(也就是拷贝了两份引用计数,两份引用计数都是1;而不是引用计数增加到2。)

在这里插入图片描述
在这里插入图片描述

shared_from_this解决了这个问题

为什么解决了这个问题?原理是什么?

本质是一个weak_ptr,如果对象存活才会提升到shared_ptr

template<class _Ty2>
bool _Construct_from_weak(const weak_ptr<_Ty2>& _Other)
{	// implement shared_ptr's ctor from weak_ptr, and weak_ptr::lock()
// if通过判断资源的引用计数是否还在,判定对象的存活状态,对象存活,提升成功;
// 对象析构,提升失败!if (_Other._Rep && _Other._Rep->_Incref_nz()){_Ptr = _Other._Ptr;_Rep = _Other._Rep;return (true);}return (false);
}

所有过程都没有再使用shared_ptr的普通构造函数,没有在产生额外的引用计数对象,不会存在把一个内存资源,进行多次计数的过程;更关键的是,通过weak_ptrshared_ptr的提升,还可以在多线程环境中判断对象是否存活或者已经析构释放,在多线程环境中是很安全的,通过this裸指针进行构造shared_ptr,不仅仅资源会多次释放,而且在多线程环境中也不确定this指向的对象是否还存活。

shared_from_this有什么问题?注意事项?
  • 智能指针管理的是堆上的对象,所以用如果管理栈对象的话会内存泄漏!!!

  • 注意智能指针的循环引用问题!(类中有个成员变量,通过成员函数赋值为shared_from_this()),就会造成循环引用问题。

weak_ptr简介

  • weak_ptr⼀般由shared_ptr初始化,两者指向相同位置,但weak_ptr不会影响引⽤计数

  • weak_ptr可以⽤expired函数检测空悬

  • 多线程场景下,在expired函数和访问之间,另⼀个线程可能析构最后⼀个指向该对象的shared_ptr,导致该对象被析构,引发未定义⾏为,所以需要⼀个源⾃操作来完成校验与访问

//1-通过lock函数获取weak_ptr检测的shared_ptr对象,如果指针空悬,结果shared_ptr为空
std::shared_ptr<A> sp1 = wp.lock();
//2-将weak_ptr对象作为shared_ptr的构造函数的型参传⼊,如果指针空悬,抛出bad weak_ptr的异常
std::shared_ptr<A> sp2(wp);
  • weak_ptr解决循环引⽤的问题

存在ABC三个类,AC都持有⼀个指向Bshared_ptr,此时⼀个指针从B指向A,应该采⽤什么型别?

    • 采⽤裸指针,若A析构,C仍指向BB中有A的空悬指针,但是却⽆法检测到,若进⾏访问,出现未定义⾏为
    • 采⽤shared_ptrA<-- ->B形成环路,引⽤计数始终不能为0,资源⽆法回收
    • 采⽤weak_ptr,若A析构,B中有A的空悬指针,但是B可以检测到空悬,同时weak_ptr不会增加引⽤计数,不会阻⽌析构。
  • (AB两个类也行,这里仅仅举一个例子)

unique_ptr和shared_ptr在析构函数中都使用delete而不是delete[],所以不应该new一个数组

std::shared_ptr<int> ii(new [1024]);  //错误的方式!delete[]一个数组

多线程访问共享对象问题

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的线程类,开启一个新线程,并传入共享对象的地址pstd::thread t1(threadProc, p);// 在main线程中析构Test共享对象delete p;// 等待子线程运行结束t1.join();return 0;
}

发现在main主线程已经delete析构Test对象以后,子线程threadProc再去访问Test对象的show方法,无法打印出*_ptr的值20

可以通过weak_ptrshared_ptr解决

void threadProc(weak_ptr<Test> 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<Test> ps = pw.lock();if (ps != nullptr){ps->show();}
}
int main()
{// 在堆上定义共享对象shared_ptr<Test> p(new Test);// 使用C++11的线程,开启一个新线程,并传入共享对象的弱智能指针std::thread t1(threadProc, weak_ptr<Test>(p));// 在main线程中析构Test共享对象// 等待子线程运行结束t1.join();return 0;
}

因为main线程调用了t1.join()方法等待子线程结束,此时pw通过lock提升为ps成功

如果设置t1为分离线程,让main主线程结束,p智能指针析构,进而把Test对象析构,此时show方法已经不会被调用,因为在threadProc方法中,pw提升到ps时,lock方法判定Test对象已经析构,提升失败

int main()
{// 在堆上定义共享对象shared_ptr<Test> p(new Test);// 使用C++11的线程,开启一个新线程,并传入共享对象的弱智能指针std::thread t1(threadProc, weak_ptr<Test>(p));// 在main线程中析构Test共享对象// 设置子线程分离t1.detach();return 0;
}
//或者在main中将p reset了
int main()
{// 在堆上定义共享对象shared_ptr<Test> p(new Test);// 使用C++11的线程,开启一个新线程,并传入共享对象的弱智能指针std::thread t1(threadProc, weak_ptr<Test>(p));p.reset();// 在main线程中析构Test共享对象// 设置子线程分离t1.join();return 0;
}

只要在子线程执行前将指针释放了,那么weak_ptr就不会提升成功,即判断shared_ptr判断存活失败。

实现

注意点:

  • ref_count的实现仅仅是一个简单版本,其内的引用计数不仅有user还有weak。
  • 对象的第一次构造,引用计数为1
  • 对象的拷贝构造,引用计数+1
  • 对象的移动构造,原对象为nullptr、引用计数为0;所有权转移的对象继承了源对象的引用计数和指针
  • 对象的拷贝赋值,引用计数+1
  • 对象的移动赋值,类似移动构造
  • use_count()需要判断裸指针是否存在
#include <iostream>
#include <atomic>
#include <mutex>
#include <memory>
using namespace std;class ref_count{
public:ref_count():count_(1){}void add_ref(){++count_;}int reduce_ref(){--count_;return use_count();}int use_count() const{return count_.load();}
private:atomic_int count_;
};template<typename T>
class Shared_Ptr{
public:Shared_Ptr() : ptr_(nullptr), ref_count_(nullptr){}explicit Shared_Ptr(T* ptr) :ptr_(ptr){std::cout << " default constructor ." << std::endl;if(ptr){ref_count_ = new ref_count();}}~Shared_Ptr(){std::cout << " ~ constructor ." << std::endl;release();}Shared_Ptr(Shared_Ptr<T>& obj): ptr_(obj.ptr_), ref_count_(obj.ref_count_){std::cout << " copy constructor ." << std::endl;ref_count_->add_ref();}Shared_Ptr(Shared_Ptr<T>&& obj){std::cout << " move constructor ." << std::endl;swap(*this, obj);obj.ptr_ = nullptr;obj.ref_count_ = nullptr;}Shared_Ptr<T>& operator=(Shared_Ptr<T> &obj){std::cout << " copy = constructor ." << std::endl;if(this != &obj){release();ptr_ = obj.ptr_;ref_count_ = obj.ref_count_;ref_count_->add_ref();}return *this;}Shared_Ptr<T>& operator=(Shared_Ptr<T>&& obj){std::cout << " move = constructor ." << std::endl;release();swap(*this, obj);obj.ptr_ = nullptr;obj.ref_count_ = nullptr;return *this;}T* operator ->(){return ptr_;}T& operator *(){return *ptr_;}T* get(){return ptr_;}int use_count() const{if(ptr_){return ref_count_->use_count();}return 0;}void reset(T* ptr = nullptr){if(ptr_){ref_count_->reduce_ref();ptr_ = nullptr;ref_count_ = nullptr;}if(ptr){ptr_ = ptr;ref_count_ = new ref_count();}}
private:T* ptr_;ref_count* ref_count_;void release(){if(ptr_ && ref_count_->reduce_ref() == 0){delete ref_count_;delete ptr_;}}friend void swap(Shared_Ptr<T> &lhs, Shared_Ptr<T> &rhs) noexcept{using std::swap;swap(lhs.ptr_, rhs.ptr_);swap(lhs.ref_count_, rhs.ref_count_);}
};// g++ ../MySharedPtr.cpp -o mshared_ptr -g -fsanitize=address
void testMySharedPtr(){
Shared_Ptr<int> test = Shared_Ptr<int>(new int(0));{// 拷贝构造Shared_Ptr<int> test2 = test;std::cout << test2.use_count() << std::endl;}std::cout << test.use_count() << std::endl;{// 移动构造Shared_Ptr<int> test2 = std::move(test);std::cout << test2.use_count() << std::endl;}test.reset(new int(1));std::cout << test.use_count() << std::endl;{// 拷贝操作符Shared_Ptr<int> test3(new int(2));Shared_Ptr<int> test2 = test3;std::cout <<" before test3.count:"<< test3.use_count() << std::endl;test2 = test;std::cout <<" after test3.count:"<< test3.use_count() << std::endl;}std::cout << test.use_count() << std::endl;{// 移动操作符Shared_Ptr<int> test3(new int(2));std::cout << test.use_count() << std::endl;test3 = std::move(test);std::cout << test.use_count() << std::endl;std::cout <<" after test3.val:"<< *test3.get() << std::endl;}// std::cout << test.use_count() << std::endl;
}
void testSharedPtr(){shared_ptr<int> test = shared_ptr<int>(new int(0));{// 拷贝构造shared_ptr<int> test2 = test;std::cout << test2.use_count() << std::endl;}std::cout << test.use_count() << std::endl;{// 移动构造shared_ptr<int> test2 = std::move(test);std::cout << test2.use_count() << std::endl;}test.reset(new int(1));std::cout << test.use_count() << std::endl;{// 拷贝操作符shared_ptr<int> test3(new int(2));shared_ptr<int> test2 = test3;std::cout <<" before test3.count:"<< test3.use_count() << std::endl;test2 = test;std::cout <<" after test3.count:"<< test3.use_count() << std::endl;}std::cout << test.use_count() << std::endl;{// 移动操作符shared_ptr<int> test3(new int(2));test3 = std::move(test);std::cout << test.use_count() << std::endl;std::cout <<" after test3.val:"<< *test3.get() << std::endl;}std::cout << test.use_count() << std::endl;
}
int main(){testMySharedPtr();cout << "----------------------" << endl;testSharedPtr();return 0;
}

参考:大秦坑王的智能指针详解

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/369313.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【YOLOv5/v7改进系列】改进池化层为ASPP

一、导言 Atrous Spatial Pyramid Pooling (ASPP)模块是一种用于多尺度特征提取的创新技术&#xff0c;旨在提升深度学习模型在语义图像分割任务中的表现。ASPP模块通过在不同的采样率下应用空洞卷积&#xff0c;可以捕获不同大小的对象以及图像的上下文信息&#xff0c;从而增…

Astro新前端框架首次体验

Astro新前端框架首次体验 1、什么是Astro Astro是一个静态网站生成器的前端框架&#xff0c;它提供了一种新的开发方式和更好的性能体验&#xff0c;帮助开发者更快速地构建现代化的网站和应用程序。 简单来说就是&#xff1a;Astro这个是一个网站生成器&#xff0c;可以直接…

Hyper-V克隆虚拟机教程分享!

方法1. 使用导出导入功能克隆Hyper-V虚拟机 导出和导入是Hyper-V服务器备份和克隆的一种比较有效的方法。使用此功能&#xff0c;您可以创建Hyper-V虚拟机模板&#xff0c;其中包括软件、VM CPU、RAM和其他设备的配置&#xff0c;这有助于在Hyper-V中快速部署多个虚拟机。 在…

前端引用vue/element/echarts资源等引用方法Blob下载HTML

前端引用下载vue/element/echarts资源等引用方法 功能需求 需求是在HTML页面中集成Vue.js、Element Plus&#xff08;Element UI的Vue 3版本&#xff09;、ECharts等前端资源&#xff0c;使用Blob下载HTML。 解决方案概述 直接访问线上CDN地址&#xff1a;简单直接&#xff0c…

计算机网络(2

计算机网络续 一. 网络编程 网络编程, 指网络上的主机, 通过不同的进程, 以编程的方式实现网络通信(或网络数据传输). 即便是同一个主机, 只要不同进程, 基于网络来传输数据, 也属于网络编程. 二. 网络编程套接字(socket) socket: 操作系统提供的网络编程的 API 称作 “soc…

【数据结构与算法】堆排序算法原理与实现:基于堆实现的高效排序算法

&#x1f493; 博客主页&#xff1a;倔强的石头的CSDN主页 &#x1f4dd;Gitee主页&#xff1a;倔强的石头的gitee主页 ⏩ 文章专栏&#xff1a;《数据结构与算法》 期待您的关注 ​ 目录 一、引言 堆排序的简介 堆排序的特点 二、堆的概念 三、堆排序算法的原理 四、堆…

软件测试面试1000问(含答案)

1、自动化代码中,用到了哪些设计模式? 单例设计模式工厂模式PO设计模式数据驱动模式面向接口编程设计模式 2、什么是断言( Assert) ? 断言Assert用于在代码中验证实际结果是不是符合预期结果&#xff0c;如果测试用例执行失败会抛出异常并提供断言日志 3、什么是web自动化…

数据结构预科

在堆区申请两个长度为32的空间&#xff0c;实现两个字符串的比较【非库函数实现】 要求&#xff1a; 1> 定义函数&#xff0c;在对区申请空间&#xff0c;两个申请&#xff0c;主函数需要调用2次 2> 定义函数&#xff0c;实现字符串的输入&#xff0c;void input(char …

Jenkins容器的部署

本文主要是记录如何在Centos7上安装docker,以及在docker里面配置tomcat、mysql、jenkins等环境。 一、安装docker 1.1 准备工作 centos7、VMware17Pro 1.2 通过yum在线安装dokcer yum -y install docker1.3 启动docker服务 systemctl start docker.service1.4 查看docke…

Java传引用问题

本文将介绍 Java 中的引用传递&#xff0c;包括其定义、实现方式、通过引用修改原来指向的内容和通过引用修改当前引用的指向的区别 目录 1、引用传递的概念 2、引用传递的实现方式 3、传引用会发生的两种情况&#xff1a; 通过引用修改当前引用的指向 通过引用修改原来指…

《数据仓库与数据挖掘》 总复习

试卷组成 第一章图 第二章图 第三章图 第四章图 第五章图 第六章图 第九章图 第一章 DW与DM概述 &#xff08;特点、特性&#xff09; DB到DW 主要特征 &#xff08;1&#xff09;数据太多&#xff0c;信息贫乏&#xff08;Data Rich&#xff0c; Information Poor)。 &a…

H2 Database Console未授权访问漏洞封堵

背景 H2 Database Console未授权访问&#xff0c;默认情况下自动创建不存在的数据库&#xff0c;从而导致未授权访问。各种未授权访问的教程&#xff0c;但是它怎么封堵呢&#xff1f; -ifExists 很简单&#xff0c;启动参数添加 -ifExists &#xff0c;它的含义&#xff1a…

【机器学习】机器学习的重要方法——线性回归算法深度探索与未来展望

欢迎来到 破晓的历程博客 引言 在数据科学日益重要的今天&#xff0c;线性回归算法以其简单、直观和强大的预测能力&#xff0c;成为了众多领域中的基础工具。本文将详细介绍线性回归的基本概念、核心算法&#xff0c;并通过五个具体的使用示例来展示其应用&#xff0c;同时探…

CASS7.0按方向和距离绘制图形

1、绘制工具 2、按方向和距离绘制 &#xff08;1&#xff09;切换方向 &#xff08;2&#xff09;距离输入

Python函数缺省参数的 “ 坑 ” (与C++对比学习)

我们都知道Python函数的缺省参数可以降低我们调用函数的成本&#xff0c;但是一般我们的缺省参数都是不可变对象&#xff0c;如果是可变对象&#xff0c;我们对其多次调用会发生什么呢&#xff1f; def func(arr[]):arr.append(Hello)print(arr)func() func() func() 这貌似…

MongoDB-社区版-本地安装

系统&#xff1a;win10 1. 下载server:Download MongoDB Community Server | MongoDB 我选的zip包 2. 下载shell&#xff1a;MongoDB Shell Download | MongoDB 我选的zip包 3. 启动server 4. 启动shell, 完成

MYSQL函数进阶详解:案例解析(第19天)

系列文章目录 一、MySQL的函数&#xff08;重点&#xff09; 二、MySQL的窗口函数&#xff08;重点&#xff09; 三、MySQL的视图&#xff08;熟悉&#xff09; 四、MySQL的事务&#xff08;熟悉&#xff09; 文章目录 系列文章目录前言一、MySQL的函数1. 聚合函数2. group_c…

Redis 多数据源自定义配置 Spring Boot 升级版

文章目录 1.前言2.git 示例地址3.需求4.代码实现4.1 application.properties 配置文件4.2 获取 application.properties 中的 redis 配置4.2.1 Environment 对象来获取自定义 redis 配置 4.3 初始化 RedisTemplate 对象&#xff0c;并注册到 Spring IOC 容器4.3.1 初始化方法4.…

spring boot (shiro)+ websocket测试连接不上的简单检测处理

1、用前端连接测试的demo一切正常&#xff0c;但是到了项目中连接不上了 一开始以为是地址错&#xff0c;但是换了apifox测试也是不可以。 2、考虑是shiro进行了拦截了&#xff0c;所以就访问不到了地址&#xff0c;那么就放行。 3、再次用apifox测试&#xff0c;成功了。 当然…

马拉松报名小程序的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;用户管理&#xff0c;赛事信息管理&#xff0c;赛事报名管理&#xff0c;活动商城管理&#xff0c;留言板管理&#xff0c;系统管理 微信端账号功能包括&#xff1a;系统首页&#xff0c;赛事信息&…