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

相关文章

C#数据结构之字符串(string)详解

《C#数据结构之字符串(string)详解》:本文主要介绍C#数据结构之字符串(string),具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录转义字符序列字符串的创建字符串的声明null字符串与空字符串重复单字符字符串的构造字符串的属性和常用方法属性常用方法总结摘

Java中StopWatch的使用示例详解

《Java中StopWatch的使用示例详解》stopWatch是org.springframework.util包下的一个工具类,使用它可直观的输出代码执行耗时,以及执行时间百分比,这篇文章主要介绍... 目录stopWatch 是org.springframework.util 包下的一个工具类,使用它

Java进行文件格式校验的方案详解

《Java进行文件格式校验的方案详解》这篇文章主要为大家详细介绍了Java中进行文件格式校验的相关方案,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录一、背景异常现象原因排查用户的无心之过二、解决方案Magandroidic Number判断主流检测库对比Tika的使用区分zip

Java实现时间与字符串互相转换详解

《Java实现时间与字符串互相转换详解》这篇文章主要为大家详细介绍了Java中实现时间与字符串互相转换的相关方法,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录一、日期格式化为字符串(一)使用预定义格式(二)自定义格式二、字符串解析为日期(一)解析ISO格式字符串(二)解析自定义

Linux samba共享慢的原因及解决方案

《Linuxsamba共享慢的原因及解决方案》:本文主要介绍Linuxsamba共享慢的原因及解决方案,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录linux samba共享慢原因及解决问题表现原因解决办法总结Linandroidux samba共享慢原因及解决

springboot security快速使用示例详解

《springbootsecurity快速使用示例详解》:本文主要介绍springbootsecurity快速使用示例,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝... 目录创www.chinasem.cn建spring boot项目生成脚手架配置依赖接口示例代码项目结构启用s

Python中随机休眠技术原理与应用详解

《Python中随机休眠技术原理与应用详解》在编程中,让程序暂停执行特定时间是常见需求,当需要引入不确定性时,随机休眠就成为关键技巧,下面我们就来看看Python中随机休眠技术的具体实现与应用吧... 目录引言一、实现原理与基础方法1.1 核心函数解析1.2 基础实现模板1.3 整数版实现二、典型应用场景2

一文详解SpringBoot响应压缩功能的配置与优化

《一文详解SpringBoot响应压缩功能的配置与优化》SpringBoot的响应压缩功能基于智能协商机制,需同时满足很多条件,本文主要为大家详细介绍了SpringBoot响应压缩功能的配置与优化,需... 目录一、核心工作机制1.1 自动协商触发条件1.2 压缩处理流程二、配置方案详解2.1 基础YAML

Python实现无痛修改第三方库源码的方法详解

《Python实现无痛修改第三方库源码的方法详解》很多时候,我们下载的第三方库是不会有需求不满足的情况,但也有极少的情况,第三方库没有兼顾到需求,本文将介绍几个修改源码的操作,大家可以根据需求进行选择... 目录需求不符合模拟示例 1. 修改源文件2. 继承修改3. 猴子补丁4. 追踪局部变量需求不符合很

java中反射(Reflection)机制举例详解

《java中反射(Reflection)机制举例详解》Java中的反射机制是指Java程序在运行期间可以获取到一个对象的全部信息,:本文主要介绍java中反射(Reflection)机制的相关资料... 目录一、什么是反射?二、反射的用途三、获取Class对象四、Class类型的对象使用场景1五、Class