智能指针shared_ptr的原理、用法和注意事项

  • 1 前言
  • 2 shared_ptr原理
  • 3 shared_ptr的基本用法
    • 3.1 初始化
    • 3.2 获取原始指针
  • 4 智能指针和动态数组
    • 4.1 c++17前需指定删除器
    • 4.2 `c++17`增加了`opreator[]`和使用`int[]`类的数组类型做模板参数
    • 4.3 `c++20`支持`std::make_shared`
  • 5 使用`shared_ptr`需要注意的问题

1 前言

在实际的C++开发过程中,我们经常会遇到诸如程序运行中突然崩溃、程序运行所用的内存越来越多最终不得不重启等问题,这些问题往往都是内存管理资源不当造成的。比如:
①有些内存资源已经释放,但指向它的指针并没有改变指向,最终成为了野指针,并且后续还在使用;
②有些内存资源已经被释放,后期又试图再释放一次,最终导致重复释放同一块内存会使程序运行崩溃;
③没有及时释放不再使用的内存资源,造成内存泄漏,程序占用的内存资源越来越多。
针对以上情况,C++提供了更友好的内存管理机制,让程序员更专注于开发项目的各个功能上,而不是自己进行内存管理。事实上,显示内存管理的替代方案很早就有了,早在1959年前后,就有人提出“垃圾自动回收”机制,“垃圾”指的是那些不再使用或者没有任何指针指向的内存空间,而“回收”指的是将这些“垃圾”收集起来以便再次利用。
在C++98/03标准中,支持使用anto_ptr智能指针来实现堆内存的自动回收,C++11新标准在废弃auto_ptr的同时,增加了nique_ptrshared_ptr 以及 weak_ptr 这 3 个智能指针来实现堆内存的自动回收。
智能指针和普通指针用法相似,智能指针的本质是一个模板类,对普通指针进行了封装,通过在构造函数中初始化分配内存,在析构函数中释放内存,达到自己管理内存,不需要手动管理内存的效果,因此智能指针可以在适当时机自动释放分配的内存,即使用智能指针可以很好地避免“忘记释放内存而导致内存泄漏”问题,由此可见,C++开始支持了垃圾回收机制,但是目前支持的程度有限。
接下来对shared_ptr的原理和用法做详细的解释说明。

2 shared_ptr原理

shared_ptr是以类模板的方式实现的,shared_ptr<T>(其中 T 表示指针指向的具体数据类型)的定义位于<memory>头文件,并位于 std 命名空间中,因此在使用该类型指针时,程序中应包含如下 2 行代码:

#include <memory>
using namespace std; //这一行代码不是必须的,如果不添加则在后续使用shared_ptr 智能指针时,就需要明确指明std::

shared_ptr的简单实现如下:

#include<iostream>
#include<mutex>
#include<thread>
using namespace std;template<class T>  //模板类
class Shared_Ptr{public://以普通指针进行构造Shared_Ptr(T* ptr = nullptr):_pPtr(ptr), _pRefCount(new int(1)), _pMutex(new mutex){}//析构函数~Shared_Ptr(){Release();}//拷贝构造函数Shared_Ptr(const Shared_Ptr<T>& sp):_pPtr(sp._pPtr), _pRefCount(sp._pRefCount), _pMutex(sp._pMutex){AddRefCount();}//重载赋值号,使得同一类型的shared_ptr智能指针可以相互赋值Shared_Ptr<T>& operator=(const Shared_Ptr<T>& sp){if (_pPtr != sp._pPtr){// 释放管理的旧资源Release();// 共享管理新对象的资源,并增加引用计数_pPtr = sp._pPtr;_pRefCount = sp._pRefCount;_pMutex = sp._pMutex;AddRefCount();}return *this;}//   重载 * 号,获取当前 shared_ptr 智能指针对象指向的数据T& operator*(){return *_pPtr;}//重载 -> 号,当智能指针指向的数据类型为自定义的结构体时,通过 -> 运算符可以获取其内部的指定成员T* operator->(){return _pPtr;}//返回同当前 shared_ptr 对象(包括它)指向相同的所有 shared_ptr 对象的数量int UseCount() { return *_pRefCount; }//返回 shared_ptr 对象内部包含的普通指针T* Get() { return _pPtr; }void AddRefCount(){_pMutex->lock();++(*_pRefCount);_pMutex->unlock();}
private:void Release(){bool deleteflag = false;_pMutex->lock();if (--(*_pRefCount) == 0){delete _pRefCount;delete _pPtr;deleteflag = true;}_pMutex->unlock();if (deleteflag == true)delete _pMutex;}
private:int *_pRefCount;  //定义一个引用计数指针T* _pPtr;   //定义一个存储指针mutex* _pMutex;  //定义一个锁指针,为了保证线程安全,防止资源未释放或程序崩溃
};

shared_ptr源码可以看出模板类Shared_Ptr有一个存储指针( _pPtr),一个锁指针( _pMutex)和一个引用计数指针( _pRefCount),共三个成员。为了方便用户使用 shared_ptr 智能指针,shared_ptr<T> 模板类还提供有一些实用的成员方法,它们各自的功能如下:

成员方法名 功 能
operator=() 重载赋值号,使得同一类型的 shared_ptr 智能指针可以相互赋值。
operator*() 重载 * 号,获取当前 shared_ptr 智能指针对象指向的数据。
operator->() 重载 -> 号,当智能指针指向的数据类型为自定义的结构体时,通过 -> 运算符可以获取其内部的指定成员。
swap() 交换 2 个相同类型 shared_ptr 智能指针的内容。
reset() 当函数没有实参时,该函数会使当前 shared_ptr 所指堆内存的引用计数减 1,同时将当前对象重置为一个空指针;当为函数传递一个新申请的堆内存时,则调用该函数的 shared_ptr 对象会获得该存储空间的所有权,并且引用计数的初始值为 1
get() 获得 shared_ptr 对象内部包含的普通指针。
use_count() 返回同当前 shared_ptr 对象(包括它)指向相同的所有 shared_ptr 对象的数量。
unique() 判断当前shared_ptr对象指向的堆内存,是否不再有其它 shared_ptr 对象再指向它。
operator bool() 判断当前 shared_ptr 对象是否为空智能指针,如果是空指针,返回 false;反之,返回 true。
make_shared(args) 返回一个shared_ptr,指向一个动态分配的类型为T的对象。使用args初始化次对

想系统的了解share_ptr的详细功能,可见shared_ptr官网:share_ptr官网

shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源
shared_ptr存储指针和引用计数指针是一一对应的,即shared_ptr里存的是存储指针,对应的引用计数指针就是对stored pointer的加一,因此shared_ptr在其内部,给每个资源都维护着一份计数,用来记录该份资源被几个对象共享
②在对象被销毁时,即调用了析构函数,说明自己不使用该资源了,对象的引用计数减一;
③如果引用计数是0,说明自己是最后一个使用该资源的对象,必须释放该对象
④如果不是0,说明除了自己还有其他对象在使用该份资源,不能释放资源,否则其他对象就成了野指针。
锁指针是为了保证线程安全。 智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时++--,这个操作不是原子的,引用计数原来是1,如果两个线程同时访问,++了两次可能还是2,这样的引用计数都是错乱的,会导致资源未释放或程序崩溃的问题。因此智能指针引用计数++--是需要加锁的,这样能够保证引用计数的操作是线程安全的。

3 shared_ptr的基本用法

3.1 初始化

可以通过构造函数、std::make_sharedreset初始化三种初始化方式

#include "stdafx.h"
#include <iostream>
#include <future>
#include <thread>using namespace std;
class Person
{public:Person(int v) {value = v;std::cout << "Cons" <<value<< std::endl;}~Person() {std::cout << "Des" <<value<< std::endl;}int value;
};int main()
{//构造函数初始化std::shared_ptr<Person> p1(new Person(1));// Person(1)的引用计数为1//std::make_shared初始化std::shared_ptr<Person> p2 = std::make_shared<Person>(2);//reset初始化p1.reset(new Person(3));// 首先生成新对象,然后引用计数减1,引用计数为0,故析构Person(1)// 最后将新对象的指针交给智能指针std::shared_ptr<Person> p3 = p1;//现在p1和p3同时指向Person(3),Person(3)的引用计数为2p1.reset();//Person(3)的引用计数为1p3.reset();//Person(3)的引用计数为0,析构Person(3)return 0;
}

初始化方式有很多,下面具体讲下每个初始化方式的用法
构造函数初始化方式

//1.通过如下两种方式,可以构造出 shared_ptr<T> 类型的空智能指针,对于空的指针,其初始引用计数方式为0,不是1
std::shared_ptr<int> p1;             //不传入任何实参
std::shared_ptr<int> p2(nullptr);    //传入空指针 nullptr//2.创建指针时,可以明确指向
std::shared_ptr<int> p3(new int(10)); //指向一个存有10这个int类型数据的堆内存空间//3.调用拷贝构造函数
std::shared_ptr<int> p4(p3);//或者 std::shared_ptr<int> p4 = p3; 如果P3为空,则P4也为空,其引用计数初始值为0,反之,则表明P4和P3指向同一块堆内存,同时堆内存的引用次数会加1.//4.调用移动构造函数
std::shared_ptr<int> p5(std::move(p4)); //或者 std::shared_ptr<int> p5 = std::move(p4); 即P5拥有了P4的堆内存,而P4则变成了空智能指针

std::make_shared 初始化方式,C++11 标准中提供了 std::make_shared<T> 模板函数,其可以用于初始化 shared_ptr 智能指针

//1.定义一个空的智能指针
std::shared_ptr<int> p6 = std::make_shared<int>(); //2.创建指针,并明确指向
std::shared_ptr<int> p7 = std::make_shared<int>(10);//3.auto关键字代替std::shared_ptr,p8指向一个动态分配的空vector<int>
auto p8 = make_shared<vector<int>>();

reset初始化

//创建了一个指针,并明确指向
//调用reset(new xxx())重新赋值时,智能指针首先是生成新对象,然后将旧对象的引用计数减1(当然,如果发现引用计数为0时,则析构旧对象),然后将新对象的指针交给智能指针保管。
std::shared_ptr<int> p8 = nullptr;
p8.reset(new int(1));//当智能指针中有值的时候,调用reset()会使引用计数减1,如果引用计数为0时,则析构旧对象
p8.reset();

3.2 获取原始指针

智能指针一般都提供了get()成员函数,用来执行显示转换,即返回智能指针内部的原始指针。

 std::shared_ptr<int> p9(new int(5));int *pInt = p9.get();

如果需要调用成员函数,由于几乎所有的智能指针都重载了 *->操作符,所以直接使用把智能指针当做一般的指针变量来使用就可以了。但是有时候需要传递参数,如果参数是 T*,那么传递一个智能指针类是无法识别的,因此需要使用原始指针。

4 智能指针和动态数组

c++17std::shared_ptr是不支持动态数组的,如下

#include<memory>std::shared_ptr<int[]> sp1(new int[10]()); // 错误,c++17前不能传递数组类型作为shared_ptr的模板参数
std::unique_ptr<int[]> up1(new int[10]()); // ok, unique_ptr对此做了特化std::shared_ptr<int> sp2(new int[10]()); // 错误,可以编译,但会产生未定义行为,请不要这么做

sp1错误的原因很明显,然而sp2的就没有那么好找了,究其原因,是因为std::shared_ptr对非数组类型都使用delete p释放资源,显然这对于new int[10]来说是不对的,对它应该使用delete [] p

其实c++17前的解决方案并不复杂,我们可以借助std::default_delete,它用于提供对应类型的正确的delete操作,即 指定删除器

4.1 c++17前需指定删除器

智能指针可以指定删除器,当智能指针的引用计数为0时,自动调用指定的删除器来释放内存。std::shared_ptr可以指定删除器的一个原因是在C++11标准中其默认删除器不支持数组对象,比如,对于申请的动态数组来说,shared_ptr 指针默认的释放规则是不支持释放数组的,只能自定义对应的释放规则,才能正确地释放申请的堆内存,这一点需要注意。对于申请的动态数组,释放规则可以使用C++11标准中提供的 default_delete<T> 模板类,我们也可以自定义释放规则:

//指定 default_delete 作为释放规则
std::shared_ptr<int> p6(new int[10], std::default_delete<int[]>());//自定义释放规则
void deleteInt(int*p) {delete []p;
}
//初始化智能指针,并自定义释放规则
std::shared_ptr<int> p7(new int[10], deleteInt);

以上,我们可以用默认的删除器,也可以用制定的删除器进行正确的delete操作,但是用默认删除器的缺点是明显的:

  • 我们想管理的值是int[]类型的,然而事实上传给模板参数的是int
  • 需要显示提供delete functor
  • 不能使用std::make_shared,无法保证异常安全
  • c++17shared_ptr未提供opreator[],所以当需要类似操作时不得不使用p7.get()[index]的形式

4.2 c++17增加了opreator[]和使用int[]类的数组类型做模板参数

以上的代码可以简化为:

std::shared_ptr<int[]> p7(new int[10]());

对于访问分配的空间,可以将p7.get()[index]替换为p7[index]。看个具体的例子:

#include <iostream>
#include <memory>int main()
{std::shared_ptr<int[]> p7(new int[5]());for (int i = 0; i < 5; ++i) {p7[i] = (i+1) * (i+1);}for (int i = 0; i < 5; ++i) {std::cout << p7[i] << std::endl;}
}

c++17缺点:无法使用std::make_shared,而我们除非指定自己的delete functor,否则我们应该尽量使用std::make_shared

4.3 c++20支持std::make_shared

auto up2 = std::make_unique<int[]>(10); // 从c++14开始,分配一个管理有10个int元素的动态数组的unique_ptr// c++20中你可以这样写,与上一句相似,只不过返回的是shared_ptr
auto sp3 = std::make_shared<int[]>(10);

5 使用shared_ptr需要注意的问题

①不要用一个原始指针初始化多个shared_ptr,原因在于,会造成二次销毁,如下所示:

 int *p5 = new int;std::shared_ptr<int> p6(p5);std::shared_ptr<int> p7(p5);// logic error

②不要在函数实参中创建shared_ptr。因为C++的函数参数的计算顺序在不同的编译器下是不同的。正确的做法是先创建好,然后再传入。

 function(shared_ptr<int>(new int), g());

③看了很多书籍或博客,都说shared_ptr不支持动态数组,但是本地编译器由通过了,这是为啥?

随着标准越来越新,C++17及以后是支持动态数组的,C++11/14是不支持的,只要是最新的编译器是没问题的。shared_ptr动态数组定义如下:std::shared_ptr<int[]> p(new int[10]);C++11shared_ptr默认调用的析构函数是default_delete(),而非default_delete<_Ty[]>,很显然,如果分配数组,当然应该使用delete[], 所以直到C++17才被支持。

④避免循环引用。智能指针最大的一个陷阱是循环引用,循环引用会导致内存泄漏。

⑤线程安全问题。
参考线程安全

假设一个简单的场景,有 3 个 shared_ptr 对象 x、g、n:

shared_ptr<Foo> g(new Foo); // 线程之间共享的 shared_ptr
shared_ptr<Foo> x; // 线程 A 的局部变量
shared_ptr<Foo> n(new Foo); // 线程 B 的局部变量

每个线程需要操作两个成员:new出来的对象以及计数器


因此,shared_ptr的线程不安全性在于它需要操作两个成员,即new出来的对象和计数器。在多线程下,不能保证new出来一个对象一定能被放入shared_ptr中,也不能保证智能指针管理的引用计数的正确性,这是因为shared_ptr操作不是一气呵成的。即存在以下情况:同一个shared_ptr对象可以被多线程同时读取。不同的shared_ptr对象可以被多线程同时修改。同一个shared_ptr对象不能被多线程直接修改,但可以通过原子函数完成。因此在创建一个shared_ptr时,需要使用C++11提供的make_shared模板,make_shared创建shared_ptr只申请一次内存,避免了上述错误,也提高了性能,同时在读写操作时,需要加锁。

智能指针shared_ptr的原理、用法和注意事项相关推荐

  1. 智能指针shared_ptr的用法

    智能指针shared_ptr的用法 2016-12-03 15:39 by jiayayao, 360 阅读, 0 评论, 收藏, 编辑 为了解决C++内存泄漏的问题,C++11引入了智能指针(Sma ...

  2. 智能指针(shared_ptr、unique_ptr、weak_ptr)的使用

    智能指针的使用 一.shared_ptr 1.创建一个shared_ptr 2.shared_ptr的常用成员函数 reset成员函数的使用 3.==注意事项== 二.unique_ptr 1.uni ...

  3. C++ 智能指针的简单原理

    C++ 智能指针的简单原理 为什么会有智能指针的原因 delete引起的内存泄漏 智能指针的使用及其原理 RAII auto_ptr std::unique_ptr std::shared_ptr(线 ...

  4. get方法报空指针_智能指针shared_ptr踩坑笔记

    平时写代码一直避免使用指针,但在某些场景下指针的使用还是有必要的.最近在项目中简单使用了一下智能指针(shared_ptr),结果踩了不少坑,差点就爬不出来了.痛定思痛抱着<Cpp Primer ...

  5. 智能指针shared_ptr

    如果有可能就使用unique_ptr,然后很多时候对象是需要共享的,因此shared_ptr也就会用得很多.shared_ptr允许多个指向同一个对象,当指向对象的最后一个shared_ptr销毁时, ...

  6. 智能指针shared_ptr的几个例子

    #include <string> #include <iostream> #include <memory> //智能指针定义在头文件memory中,例如shar ...

  7. C++智能指针shared_ptr、unique_ptr以及weak_ptr

    目录 shared_ptr类 shared_ptr和unique_ptr都支持的操作 shared_ptr独有的操作 make_shared函数 shared_ptr自动销毁所管理的对象 由普通指针管 ...

  8. C++ 使用智能指针shared_ptr/unique_ptr管理数组

    目录 零.要管理的类 一.使用shared_ptr管理数组 二.使用unique_ptr管理数组 1.第一种方式 2.第二种方式 关于shared_ptr/unique_ptr的基础,我不在本篇博客中 ...

  9. C++ 智能指针 shared_ptr、make_shared用法

    一.使用shared_ptr条件 C++版本11以上 需引入头文件 #include <memory> 否则编译会报错 error: 'shared_ptr' was not declar ...

最新文章

  1. [elixir! #0007] [译] 理解Elixir中的宏——part.5 重塑AST by Saša Jurić
  2. 腾讯SaaS生态战略再升级,“一云多端”助力企业数字化转型
  3. hdu 1116 欧拉路
  4. Atitit.多媒体区----web视频格式的选择总结
  5. 清华大学《操作系统》(六):非连续内存分配 段式、页式、段页式存储管理
  6. SAP License:SAP技术人员路在何方?
  7. 第一部分----HTML的基本结构与基本标签
  8. c++调用opencv库实现视频关键帧提取--灰度帧差法
  9. 交通信号灯的检测与识别
  10. WPS标题编号级别,根据上一级自动编号
  11. 《微微一笑很倾城》中肖奈大神说的平方根倒数速算法是什么鬼?三十分钟理解!
  12. 关于单链表中temp.next、head.next的理解
  13. 移动开发程序员的悲哀是什么?
  14. css 文字发光效果
  15. 服务器安全加固三件套
  16. 网络安全乱流,超级保护才是根本
  17. 访客wifi隔离实现
  18. PayPal收款手续费是多少钱?
  19. Ajax 完整教程-(二)
  20. アプリケーションコンポーネント(大分類)

热门文章

  1. Oracle EBS WebADI的配置(IE+Excel)
  2. 5 月全球数据库排名:PostgreSQL 有所回升;Fedora 开始支持 Google Chrome 和 Steam
  3. 爬虫百战穿山甲(4):帮学弟学妹们看看高考选科走班指南
  4. 【机器学习】准确率(Accuracy), 精确率(Precision), 召回率(Recall)和F1-Measure
  5. python画图简介
  6. 导向标识的定义和作用
  7. ZeusAutoCode代码生成工具(开源)
  8. android 英文日期格式,国际化-基于Android上用户区域设置的日期格式
  9. 支付宝证书模式(转账给其他支付宝)
  10. 微信公众号支付及提现