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

相关文章

Debezium 与 Apache Kafka 的集成方式步骤详解

《Debezium与ApacheKafka的集成方式步骤详解》本文详细介绍了如何将Debezium与ApacheKafka集成,包括集成概述、步骤、注意事项等,通过KafkaConnect,D... 目录一、集成概述二、集成步骤1. 准备 Kafka 环境2. 配置 Kafka Connect3. 安装 D

Java中ArrayList和LinkedList有什么区别举例详解

《Java中ArrayList和LinkedList有什么区别举例详解》:本文主要介绍Java中ArrayList和LinkedList区别的相关资料,包括数据结构特性、核心操作性能、内存与GC影... 目录一、底层数据结构二、核心操作性能对比三、内存与 GC 影响四、扩容机制五、线程安全与并发方案六、工程

C++初始化数组的几种常见方法(简单易懂)

《C++初始化数组的几种常见方法(简单易懂)》本文介绍了C++中数组的初始化方法,包括一维数组和二维数组的初始化,以及用new动态初始化数组,在C++11及以上版本中,还提供了使用std::array... 目录1、初始化一维数组1.1、使用列表初始化(推荐方式)1.2、初始化部分列表1.3、使用std::

C++ Primer 多维数组的使用

《C++Primer多维数组的使用》本文主要介绍了多维数组在C++语言中的定义、初始化、下标引用以及使用范围for语句处理多维数组的方法,具有一定的参考价值,感兴趣的可以了解一下... 目录多维数组多维数组的初始化多维数组的下标引用使用范围for语句处理多维数组指针和多维数组多维数组严格来说,C++语言没

Spring Cloud LoadBalancer 负载均衡详解

《SpringCloudLoadBalancer负载均衡详解》本文介绍了如何在SpringCloud中使用SpringCloudLoadBalancer实现客户端负载均衡,并详细讲解了轮询策略和... 目录1. 在 idea 上运行多个服务2. 问题引入3. 负载均衡4. Spring Cloud Load

Springboot中分析SQL性能的两种方式详解

《Springboot中分析SQL性能的两种方式详解》文章介绍了SQL性能分析的两种方式:MyBatis-Plus性能分析插件和p6spy框架,MyBatis-Plus插件配置简单,适用于开发和测试环... 目录SQL性能分析的两种方式:功能介绍实现方式:实现步骤:SQL性能分析的两种方式:功能介绍记录

在 Spring Boot 中使用 @Autowired和 @Bean注解的示例详解

《在SpringBoot中使用@Autowired和@Bean注解的示例详解》本文通过一个示例演示了如何在SpringBoot中使用@Autowired和@Bean注解进行依赖注入和Bean... 目录在 Spring Boot 中使用 @Autowired 和 @Bean 注解示例背景1. 定义 Stud

如何通过海康威视设备网络SDK进行Java二次开发摄像头车牌识别详解

《如何通过海康威视设备网络SDK进行Java二次开发摄像头车牌识别详解》:本文主要介绍如何通过海康威视设备网络SDK进行Java二次开发摄像头车牌识别的相关资料,描述了如何使用海康威视设备网络SD... 目录前言开发流程问题和解决方案dll库加载不到的问题老旧版本sdk不兼容的问题关键实现流程总结前言作为

SQL 中多表查询的常见连接方式详解

《SQL中多表查询的常见连接方式详解》本文介绍SQL中多表查询的常见连接方式,包括内连接(INNERJOIN)、左连接(LEFTJOIN)、右连接(RIGHTJOIN)、全外连接(FULLOUTER... 目录一、连接类型图表(ASCII 形式)二、前置代码(创建示例表)三、连接方式代码示例1. 内连接(I

Go路由注册方法详解

《Go路由注册方法详解》Go语言中,http.NewServeMux()和http.HandleFunc()是两种不同的路由注册方式,前者创建独立的ServeMux实例,适合模块化和分层路由,灵活性高... 目录Go路由注册方法1. 路由注册的方式2. 路由器的独立性3. 灵活性4. 启动服务器的方式5.