C++:共享指针(shared_ptr)详解

2024-08-28 18:36
文章标签 c++ 指针 详解 共享 shared ptr

本文主要是介绍C++:共享指针(shared_ptr)详解,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

shared_ptr是C++11提供的另外一种常见的智能指针,与unique_ptr独占对象所有权不同,shared_ptr允许多个指针指向同一个对象。

每个shared_ptr对象都有一个关联的计数器,被称为引用计数,用来记录有多少个shared_ptr指向所管理的内存对象。这个计数器是线程安全的。每当多一个智能指针一个对象时,指向该对象的所有智能指针内部的引用计数加1,每当减少一个智能指针指向该对象时,引用计数会减1,当计数为0的时候会自动的释放动态分配的资源。

创建shared_ptr对象

std::shared_ptr<int> p1(new int(1));
std::shared_ptr<int> p2=std::make_shared<int>(2);

默认初始化的智能指针中保存着一个空指针

共享对象

我们可以通过拷贝和赋值操作实现多个shared_ptr共享一个资源对象

std::shared_ptr<int> p2(new int(2));
std::shared_ptr<int> p3=p2;

当拷贝一个shared_ptr时,对于被拷贝的shared_ptr所指向的对象来说,其引用计数会增加,通常来说有3种常见的情况:

  • 使用一个shared_ptr去初始化另一个shared_ptr,会拷贝参数的shared_ptr对象。
  • 将它作为函数参数,传递给一个函数时。
  • 将它作为函数返回值,也会发生拷贝。

计数器较少的情况:

  • shared_ptr销毁时,比如离开其作用域,会触发其析构函数,这时所管理对象的引用计数会减一。
  • 当给shared_ptr赋予一个新值时,其原来所指向的对象的引用计数会减一

指定删除器

使用shared_ptr管理非new对象或者是没有析构函数的类时,应该为其传递合适的删除器

#include <iostream>#include <memory>using namespace std;void DeleteIntPtr(int *p) {cout << "call DeleteIntPtr" << endl;delete p;}int main(){std::shared_ptr<int> p(new int(1), DeleteIntPtr);std::shared_ptr<int> p2(new int(1), [](int *p) {cout << "call lambda1 delete p" << endl;delete p;});//p3没有显式指定类型为数组类型int[],shared_ptr默认调用delete//而非delete[]来删除他管理的对象,为了正确删除,需要自定义删除器std::shared_ptr<int> p3(new int[10], [](int *p) {cout << "call lambda2 delete p" << endl;delete [] p; // 数组删除});return 0;}

智能指针什么时候需要指定删除器:

在需要 delete 以外的析构行为的时候用. 因为 shared_ptr 在引用计数为 0 后默认调用 delete ptr; 如果不满足需求就要提供定制的删除器.

一些场景:

  • 资源不是 new 出来的(一般也意味着不能 delete), 比如可能是 malloc 出来的
  • 资源是被第三方库管理的 (第三方提供 资源获取 和 资源释放 接口, 那么要么写一个 wrapper 类要么就提供定制的 deleter)
  • 资源不是 RAII 的, 意味着析构函数不会把资源完全释放掉...也就是单纯 delete 还不够, 还得做额外的操作比如你的 end_connection 的例子. 虽然我觉得这个是 bad practice

循环引用

//定义A,拥有B类型指针
class A {
public:std::shared_ptr<B> pb;~A() {std::cout << "~A" << std::endl;}
};//定义B,拥有A类型的指针
class B {
public:std::shared_ptr<A> pa;~B() {std::cout << "~B" << std::endl;}
};
void Test(){std::shared_ptr<A> pA= std::make_shared<A>();std::shared_ptr<B> pB= std::make_shared<B>();//pA内部指向pBpA->pb = pB;//pb内部执行papB->pa = pA;
}int main(){//会导致循环引用,2个堆内存对象无法被释放Test();return 0;
}

Test函数结束时,局部变量的销毁是按照其创建顺序的相反顺序来进行销毁的。pB先于pA销毁。当pB销毁时,会先调用pB的析构函数,它会检测到它所指向的对象有2个引用者,即pB和pA的成员pb,引用计数为2,离开作用域后,pB的引用计数-1,并不是0,跟据shared_ptr的规则,pB所指向的内存不会被释放。pA同理。pA和pB所指向的内存都没有得到释放,会发生内存泄漏。

解决方法是将A或B的成员设定为weak_ptr

weak_ptr

weak_ptr也是一种智能指针,通常配合shared_ptr一起使用。

weak_ptr是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象。所以需要使用一个shared_ptr来初始化一个weak_ptr,并且将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数

它的最大特点是:一旦最后一个指向对象的shared_ptr被销毁,该对象就会被销毁,即使还有weak_ptr指向该对象,所有weak_ptr都会变成nullptr

所以,有时会出现weak_ptr还指向着对象,但是该对象已经被销毁了的情况。不能直接通过weak_ptr访问其所指向的对象。我们以利用expired()方法来判断这个weak_ptr是否已经失效。

我们可以通过weak_ptrlock()方法来获得一个指向共享对象的shared_ptr。如果weak_ptr已经失效,lock()方法将返回一个空的shared_ptr

std::shared_ptr<int> p1= std::make_shared<int>(2);std::weak_ptr<int> wp(p1);// 通过lock创建一个对应的shared_ptr
if (auto p = wp.lock()) {std::cout << "shared_ptr value: " << *p << std::endl;std::cout << "shared_ptr use_count: " << p.use_count() << std::endl;
} else {std::cout << "wp is expired" << std::endl;
}// 释放shared_ptr指向的资源,此时weak_ptr失效
p1.reset();
std::cout << "wp is expired: " <<  wp.expired() << std::endl;

注意事项

不要混用普通指针和智能指针

void TestShared(std::shared_ptr<int> p) {...}//离开作用域时,p会被销毁int main()
{int* p1 = new int(2);TestShared(std::shared_ptr<int> (p1));//指向的对象已经被delete,p1是一个空悬指针std::cout << *p1<< std::endl;return 0;
}

对象的引用计数是其shared_ptr的个数,当一个共享对象的shared_ptr为0时,即使有普通指针还在指向它,也会被释放

不要使用使用get初始化另一个智能指针或为智能指针赋值。

std::shared_ptr<int> p1(new int(1));
std::shared_ptr<int> p2(p1.get());
std::cout << p1.use_count() << " " << p2.use_count() <<std:: endl;

打印会发现它们的引用计数为1,因为引用计数是分开计数的,当其中一类的shared_ptr的引用计数为0时,就会释放对象内存,这时其他shared_ptr就是空悬指针了,此时会出现double free问题。

性能

  1. 内存占用高
    shared_ptr 的内存占用是裸指针的两倍。因为除了要管理一个裸指针外,还要维护一个引用计数。
    因此相比于 unique_ptr, shared_ptr 的内存占用更高

  2. 原子操作性能低
    考虑到线程安全问题,引用计数的增减必须是原子操作。而原子操作一般情况下都比非原子操作慢。

  3. 使用移动优化性能
    shared_ptr 在性能上固然是低于 unique_ptr。而通常情况,我们也可以尽量避免 shared_ptr 复制。
    如果,一个 shared_ptr 需要将所有权共享给另外一个新的 shared_ptr,而我们确定在之后的代码中都不再使用这个 shared_ptr,那么这是一个非常鲜明的移动语义。
    对于此种场景,我们尽量使用 std::move,将 shared_ptr 转移给新的对象。因为移动不用增加引用计数,性能比复制更好。

template<class T>
class SharedPtr{
private:T* m_p;int* m_count;void clear() {if(m_count&& --(*m_count)==0){delete m_p;m_p=nullptr;delete m_count;m_count=nullptr;}}public:SharedPtr(T* ptr=nullptr):m_p(p),m_count(new int(1)){}~SharedPtr(){clear();}//拷贝构造SharedPtr(const SharedPtr& that):m_p(that.m_p),m_count(that.m_count){++(*m_count);}//拷贝赋值SharedPtr& operator=(const SharedPtr& that){if(m_p!=that.m_p){clear();m_p=that.m_p;m_count=that.m_count;++(*m_count);  }return *this;}//移动构造SharedPtr(SharedPtr&& that):m_p(that.m_p),m_count(that.m_count) {that.m_p=nullptr;that.m_count=nullptr;}//移动赋值SharedPtr& operator=(SharedPtr&& that){clear();m_p=that.m_p;m_count=that.m_count;that.m_p=nullptr;that.m_count=nullptr;return *this;}T& operator*(){return *m_p;}T* operator->(){return m_p;}int get_count() const {return *m_count;}};

这篇关于C++:共享指针(shared_ptr)详解的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!


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

相关文章

python之uv使用详解

《python之uv使用详解》文章介绍uv在Ubuntu上用于Python项目管理,涵盖安装、初始化、依赖管理、运行调试及Docker应用,强调CI中使用--locked确保依赖一致性... 目录安装与更新standalonepip 安装创建php以及初始化项目依赖管理uv run直接在命令行运行pytho

Springboot项目构建时各种依赖详细介绍与依赖关系说明详解

《Springboot项目构建时各种依赖详细介绍与依赖关系说明详解》SpringBoot通过spring-boot-dependencies统一依赖版本管理,spring-boot-starter-w... 目录一、spring-boot-dependencies1.简介2. 内容概览3.核心内容结构4.

MySQL中REPLACE函数与语句举例详解

《MySQL中REPLACE函数与语句举例详解》在MySQL中REPLACE函数是一个用于处理字符串的强大工具,它的主要功能是替换字符串中的某些子字符串,:本文主要介绍MySQL中REPLACE函... 目录一、REPLACE()函数语法:参数说明:功能说明:示例:二、REPLACE INTO语句语法:参数

C++ vector越界问题的完整解决方案

《C++vector越界问题的完整解决方案》在C++开发中,std::vector作为最常用的动态数组容器,其便捷性与性能优势使其成为处理可变长度数据的首选,然而,数组越界访问始终是威胁程序稳定性的... 目录引言一、vector越界的底层原理与危害1.1 越界访问的本质原因1.2 越界访问的实际危害二、基

redis数据结构之String详解

《redis数据结构之String详解》Redis以String为基础类型,因C字符串效率低、非二进制安全等问题,采用SDS动态字符串实现高效存储,通过RedisObject封装,支持多种编码方式(如... 目录一、为什么Redis选String作为基础类型?二、SDS底层数据结构三、RedisObject

springboot整合mqtt的步骤示例详解

《springboot整合mqtt的步骤示例详解》MQTT(MessageQueuingTelemetryTransport)是一种轻量级的消息传输协议,适用于物联网设备之间的通信,本文介绍Sprin... 目录1、引入依赖包2、yml配置3、创建配置4、自定义注解6、使用示例使用场景:mqtt可用于消息发

Python Flask实现定时任务的不同方法详解

《PythonFlask实现定时任务的不同方法详解》在Flask中实现定时任务,最常用的方法是使用APScheduler库,本文将提供一个完整的解决方案,有需要的小伙伴可以跟随小编一起学习一下... 目录完js整实现方案代码解释1. 依赖安装2. 核心组件3. 任务类型4. 任务管理5. 持久化存储生产环境

详解Java中三种状态机实现方式来优雅消灭 if-else 嵌套

《详解Java中三种状态机实现方式来优雅消灭if-else嵌套》这篇文章主要为大家详细介绍了Java中三种状态机实现方式从而优雅消灭if-else嵌套,文中的示例代码讲解详细,感兴趣的小伙伴可以跟... 目录1. 前言2. 复现传统if-else实现的业务场景问题3. 用状态机模式改造3.1 定义状态接口3

Java集合中的链表与结构详解

《Java集合中的链表与结构详解》链表是一种物理存储结构上非连续的存储结构,数据元素的逻辑顺序的通过链表中的引用链接次序实现,文章对比ArrayList与LinkedList的结构差异,详细讲解了链表... 目录一、链表概念与结构二、当向单链表的实现2.1 准备工作2.2 初始化链表2.3 打印数据、链表长

Linux查询服务器 IP 地址的命令详解

《Linux查询服务器IP地址的命令详解》在服务器管理和网络运维中,快速准确地获取服务器的IP地址是一项基本但至关重要的技能,下面我们来看看Linux中查询服务器IP的相关命令使用吧... 目录一、hostname 命令:简单高效的 IP 查询工具命令详解实际应用技巧注意事项二、ip 命令:新一代网络配置全