智能指针shared_ptr的原理、用法和注意事项
智能指针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_ptr
、shared_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_shared
和reset
初始化三种初始化方式
#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++17
前std::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++17
前shared_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_pt
r动态数组定义如下:std::shared_ptr<int[]> p(new int[10])
;C++11
中shared_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的原理、用法和注意事项相关推荐
- 智能指针shared_ptr的用法
智能指针shared_ptr的用法 2016-12-03 15:39 by jiayayao, 360 阅读, 0 评论, 收藏, 编辑 为了解决C++内存泄漏的问题,C++11引入了智能指针(Sma ...
- 智能指针(shared_ptr、unique_ptr、weak_ptr)的使用
智能指针的使用 一.shared_ptr 1.创建一个shared_ptr 2.shared_ptr的常用成员函数 reset成员函数的使用 3.==注意事项== 二.unique_ptr 1.uni ...
- C++ 智能指针的简单原理
C++ 智能指针的简单原理 为什么会有智能指针的原因 delete引起的内存泄漏 智能指针的使用及其原理 RAII auto_ptr std::unique_ptr std::shared_ptr(线 ...
- get方法报空指针_智能指针shared_ptr踩坑笔记
平时写代码一直避免使用指针,但在某些场景下指针的使用还是有必要的.最近在项目中简单使用了一下智能指针(shared_ptr),结果踩了不少坑,差点就爬不出来了.痛定思痛抱着<Cpp Primer ...
- 智能指针shared_ptr
如果有可能就使用unique_ptr,然后很多时候对象是需要共享的,因此shared_ptr也就会用得很多.shared_ptr允许多个指向同一个对象,当指向对象的最后一个shared_ptr销毁时, ...
- 智能指针shared_ptr的几个例子
#include <string> #include <iostream> #include <memory> //智能指针定义在头文件memory中,例如shar ...
- C++智能指针shared_ptr、unique_ptr以及weak_ptr
目录 shared_ptr类 shared_ptr和unique_ptr都支持的操作 shared_ptr独有的操作 make_shared函数 shared_ptr自动销毁所管理的对象 由普通指针管 ...
- C++ 使用智能指针shared_ptr/unique_ptr管理数组
目录 零.要管理的类 一.使用shared_ptr管理数组 二.使用unique_ptr管理数组 1.第一种方式 2.第二种方式 关于shared_ptr/unique_ptr的基础,我不在本篇博客中 ...
- C++ 智能指针 shared_ptr、make_shared用法
一.使用shared_ptr条件 C++版本11以上 需引入头文件 #include <memory> 否则编译会报错 error: 'shared_ptr' was not declar ...
最新文章
- [elixir! #0007] [译] 理解Elixir中的宏——part.5 重塑AST by Saša Jurić
- 腾讯SaaS生态战略再升级,“一云多端”助力企业数字化转型
- hdu 1116 欧拉路
- Atitit.多媒体区----web视频格式的选择总结
- 清华大学《操作系统》(六):非连续内存分配 段式、页式、段页式存储管理
- SAP License:SAP技术人员路在何方?
- 第一部分----HTML的基本结构与基本标签
- c++调用opencv库实现视频关键帧提取--灰度帧差法
- 交通信号灯的检测与识别
- WPS标题编号级别,根据上一级自动编号
- 《微微一笑很倾城》中肖奈大神说的平方根倒数速算法是什么鬼?三十分钟理解!
- 关于单链表中temp.next、head.next的理解
- 移动开发程序员的悲哀是什么?
- css 文字发光效果
- 服务器安全加固三件套
- 网络安全乱流,超级保护才是根本
- 访客wifi隔离实现
- PayPal收款手续费是多少钱?
- Ajax 完整教程-(二)
- アプリケーションコンポーネント(大分類)
热门文章
- Oracle EBS WebADI的配置(IE+Excel)
- 5 月全球数据库排名:PostgreSQL 有所回升;Fedora 开始支持 Google Chrome 和 Steam
- 爬虫百战穿山甲(4):帮学弟学妹们看看高考选科走班指南
- 【机器学习】准确率(Accuracy), 精确率(Precision), 召回率(Recall)和F1-Measure
- python画图简介
- 导向标识的定义和作用
- ZeusAutoCode代码生成工具(开源)
- android 英文日期格式,国际化-基于Android上用户区域设置的日期格式
- 支付宝证书模式(转账给其他支付宝)
- 微信公众号支付及提现