C++智能指针介绍

引言

为了充分利用RAII思想,C++ 11开始引入了智能指针,本文介绍RAII以及三种智能指针:

  • std::unique_ptr
  • std::shared_ptr
  • std::weak_ptr

除此之外,本文还会介绍智能指针的常用创建方法:

  • std::make_unique
  • std::make_shared

RAII

RAII指的是Resources Acquisition Is Initialization,其是一种C++编程思想,指的是在初始化的时候就完成资源的分配,而在析构的时候自动释放资源。

C++中有很多RAII思想的体现:

  • 多线程中自动获取与释放锁对象std::unique_lock等;
  • 内存的自动申请与释放,智能指针;
  • 自动执行joinstd::jthread等。

RAII的目的是为了更好的组织代码,减少程序员犯错的可能。例如程序员可能忘记释放已经申请的内存或者锁,而利用RAII在对象析构的时候会自动进行资源的释放。

智能指针

智能指针是用于实现内存资源RAII的相关类型。

在我们需要在堆上进行内存申请的时候,我们往往会通过new关键字来进行内存的申请:

class Base {
public:int num{10};Base() {}~Base() { std::cout << "~Base()" << std::endl; }
};
Base *b = new Base;
delete b;

而对于申请的内存我们需要通过delete进行释放以防止内存泄漏。

但是在实际开发过程中,我们很有可能忘记释放掉申请的内存,例如:

// memory leakage.
int test() {Base *b = new Base;if (!check()) { // do some check, but failed.// delete b; // this may be forgotten easily.return -1;}delete b;return 0;
}

我们对于正常的情况记住了释放内存,但在出错的时候,可能就忘记释放内存了。

而智能指针能够有效的防止上面的情况发生。

std::unique_ptr

对于上面的代码我们完全可以使用std::unique_ptr替换原始指针:

int test()
{std::unique_ptr<Base> b(new Base);if (!check()) { // do some check, but failed.return -1;}return 0;
}

这样上面的代码不论如何只要在b析构的时候便会释放掉申请的内存,执行上面的代码可以看到成功输出~Base()

智能指针的使用方式与普通的指针一样,通过*->可以对指针进行解引用和获取成员变量的值:

std::unique_ptr<Base> b(new Base);
std::unique_ptr<int> p(new int);
*p = 10;
std::cout << *p << std::endl;
std::cout << b->num << std::endl;

通过智能指针也可以创建数组:

std::unique_ptr<int[]> p(new int[10]);
for (int i = 0; i < 10; i++) {p[i] = i;
}
for (int i = 0; i < 10; i++) {std::cout << p[i] << std::endl;
}

智能指针同样可以直接绑定到一个原始指针上面,不过需要注意的如果智能指针声明周期结束,那么原始的指针则变成了野指针:

int *p = new int{0};
{std::unique_ptr<int> up(p);std::cout << *up << std::endl;
}
// we cannot use p here, it's a dangling pointer.
// the dereference of a dangling pointer is a UB.

与指针相同,智能指针也能用来表现多态:

class Base {
public:Base() { std::cout << "Base()" << std::endl; }virtual ~Base() { std::cout << "~Base()" << std::endl; }virtual void test() { std::cout << "Base test()" << std::endl; }
};
class Derived : public Base {
public:void test() { std::cout << "Derived test()" << std::endl; }
};
std::unique_ptr<Base> base(new Derived);
base->test(); // "Derived test()"
return 0;

需要注意的是,不能有两个unique_ptr绑定了同一块地址上的内存(所以std::unique_ptr只支持移动语义,而不支持拷贝语义),同时注意不要让unique_ptr去绑定栈上的内存(当然如果非要这样做的话,也可以把默认的deleter给替换掉即可,但是这样并没有什么意义)。

第二个模板参数

智能指针也能接收第二个模板参数,其类型是一个可执行对象,同时接收一个指针作为参数,用于释放指针的资源。

默认的deleter:如果第一个模板参数类型不是数组类型(即第一个模板参数不含有[]),那么默认的deleter通过delete关键字进行释放内存;如果第第一个模板参数是数组类型(即第一个模板参数含有[])那么默认的deleter通过调用delete []进行内存释放。

由于默认deleter的行为,这也是为什么不能传入指向栈上内存的指针的原因(而且栈上的内存会自动释放,也不需要使用只能指针来管理)。

高级用法 自定义RAII

智能指针除了能够实现对于指针的智能管理之外,其同样可以对于任意的需要申请以及释放的资源进行智能管理。

这里给出官网的打开文件的例子:

void close_file(std::FILE* fp) {std::fclose(fp);std::cout << "File closed" << std::endl;
}
{using unique_file_t = std::unique_ptr<std::FILE, decltype(&close_file)>;// make sure there is demo.txt in current directory.// otherwise the fp is nullptrunique_file_t fp(std::fopen("demo.txt", "r"), &close_file);
} // here fp is finalized, so the close_file() will be called.

上面通过自定义deleter通过对打开文件的自动关闭。

std::shared_ptr

std::shared_ptr也是智能指针,其与std::unique_ptr的一个不同是:可以有多个std::shared_ptr与同一个地址进行绑定。其常常用于多线程。

std::shared_ptr中保存着一个引用计数,用来表示当前的地址绑定到了多少个std::shared_ptr对象上,每有一个指向相同地址的std::shared_ptr对象被创建(需要保证从一个std::shared_ptr拷贝过来),其引用计数便会增加1,当被析构时,其引用计数会减少1,而引用计数减少到0的时候,指向的资源便会被deleter释放(默认deleterstd::unique_ptr中相同)。

下面给出一个例子:

int *p = new int;
std::shared_ptr<int> sp1(p);
{// this is wrong, when we bind p with a shared_ptr, its ref_count is 1.// so this will cause double free.// only copy from a shared_ptr can make the ref_count increase correctly.// sdt::shared_ptr<int> sp2(p);std::shared_ptr<int> sp2(sp1);std::cout << sp2.use_count() << std::endl; // 2std::cout << sp1.use_count() << std::endl; // 2
}
std::cout << sp1.use_count() << std::endl; // 1

由于std::shared_ptr本来是为多线程设计的,因此其保证了其内部的函数均为线程安全的,也就是use_count等函数不会出现不一致的问题。但是对于指针指向的数据的操作在多线程中往往需要额外的手段实现同步。

std::weak_ptr

std::weak_ptr严格意义上来讲并不是一个指针,其更像是一种弱引用,其可以延长数据的生命周期。

std::weak_ptr通常通过一个std::shared_ptr对象创建而来或者通过一个std::weak_ptr对象拷贝而来。

当通过一个std::sahred_ptr对象创建而来的时候,其并不会增加引用计数,例如下面的例子:

std::shared_ptr<Base> sp(new Base);
std::weak_ptr<Base> wp = sp;
std::cout << sp.use_count() << std::endl; // 1
std::cout << wp.use_count() << std::endl; // 1

可以通过std::weak_ptr延长声明周期指的是std::weak_ptr::lock方法能够创建一个新的std::shared_ptr对象(如果内存还没被释放)此时引用计数均会增加1

 std::shared_ptr<Base> sp(new Base);std::weak_ptr<Base> wp = sp;std::shared_ptr<Base> newSp = wp.lock();std::cout << newSp.use_count() << std::endl; // 2std::cout << wp.use_count() << std::endl; // 2std::cout << sp.use_count() << std::endl; // 2

由于此时已经有新的std::shared_ptr产生,那么将原来而std::shared_ptr释放后,并不会释放资源:

sp.reset();
std::cout << newSp.use_count() << std::endl; // 1
std::cout << wp.use_count() << std::endl; // 1

而对于lock方法如果在lock的时候,资源已经被释放了,那么此时创建的std::shared_ptr对象与nullptr绑定,因此使用lock之后我们往往需要先判断资源是否已经在lock之前被释放:

std::shared_ptr<Base> sp(new Base);
std::weak_ptr<Base> wp = sp;
sp.reset(); // "~Base()"
std::shared_ptr<Base> newSp = wp.lock(); // newSp is bind with nullptr;
if (newSp != nullptr) {// do something...
}
std::cout << sp.use_count() << std::endl; // 0

正如lock函数的名字一样,其同样是线程安全的,其线程安全指的是:如果lock返回的std::shared_ptr对象并不是与nullptr绑定,那么保证此时资源没有被释放,如果返回的std::shared_ptrnullptr绑定,那么保证此时资源已经被释放。同样地,对于数据的访问依然需要通过锁或者其他手段实现同步。

更为方便的创建方式

在之前介绍的智能指针中还是需要使用到new关键字,但是却没有使用delete关键字,这很不符合RAII,于是有了这两个函数的实现,能够完全脱离new关键字进行智能指针的创建。

std::make_unique

C++ 14开始支持。

使用方法非常简单,只需要要通过模板参数传入类型和构造器参数:

class Base {
public:int num1;int num2;Base() = default;Base(int i, int j) : num1(i), num2(j) {}
};
std::unique_ptr<Base> up = std::make_unique<Base>(1, 2); // new Base(1, 2);
// create an array.
// only one parameter is OK, the parameter is the size of the array.
// make sure that the Base() constructor exists.
std::unique_ptr<Base[]> upArray = std::make_unique<Base[]>(3); // new Base[3];

std::make_shared

C++ 11开始支持。

该方法使用于std::make_unique一样,只是返回的是std::shared_ptr,故此处不在赘述。

One Funny Thing

std::make_uniqueC++ 14才开始支持,而std::make_sharedC++ 11就已经支持了。据说是因为当时作者给搞忘了。
在这里插入图片描述

参考

std::unique_ptr cppreference
std::shared_ptr cppreference
std::weak_ptr cppreference
std::make_unique cppreference
std::make_shared cppreference

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

一键提取微信聊天记录,生成HTML、Word文档永久保存,还能生成微信年度聊天报告

不知道生活中你有没有遇到过这种情况&#xff0c;聊天记录不完整&#xff0c;有的在手机上&#xff0c;有的在电脑上&#xff0c;搜索起来很烦。那有没有一种办法可以把微信聊天记录统一呢&#xff1f;当然是有的。下面&#xff0c;就让我们一起来看一下怎么操作。 先看效果 操…

Solidity 代码执行漏洞原理

目录 1. 三种 call 方式 2. 两种 call 参数类型 3. 漏洞场景 3.1 delegatecall 3.2 call 1. 三种 call 方式 Solidity 中一个合约调用其他合约的函数有三种方式&#xff1a; <address>.call(...) returns (bool) <address>.callcode(...) returns (bool) &l…

【数据结构入门精讲 | 第一篇】打开数据结构之门

数据结构与算法是计算机科学中的核心概念&#xff0c;也与现实生活如算法岗息息相关。鉴于全网数据结构文章良莠不齐且集成度不高&#xff0c;故开设本专栏&#xff0c;为初学者提供指引。 目录 基本概念数据结构为何面世算法基本数据类型抽象数据类型使用抽象数据类型的好处 数…

自己动手写数据库: select 查询语句对应查询树的构造和执行

首先我们需要给原来代码打个补丁&#xff0c;在SelectScan 结构体初始化时需要传入 UpdateScan 接口对象&#xff0c;但很多时候我们需要传入的是 Scan 对象&#xff0c;因此我们需要做一个转换&#xff0c;也就是当初始化 SelectScan 时&#xff0c;如果传入的是 Scan 对象&am…

VUE笔试题精讲1

vue专题| ProcessOn免费在线作图,在线流程图,在线思维导图 VUE面试题视频 01-Vue组件之间通信方式有哪些? 1. 组件通信常⽤⽅式有以下8种: props $emit/$on $children/$parent $attrs/$listeners ref $root eventbus vuex 注意vue3中废弃的⼏个API https://v3-mig…

Python学习之——装饰器

Python学习之——装饰器 参考基础闭包概念装饰器系统自带的装饰器propertystaticmethodclassmethod 自定义装饰器函数的装饰器无参数有参数 类的装饰器无参数有参数 functools.wraps装饰器类 装饰器实现单例模式 参考 python装饰器的4种类型&#xff1a;函数装饰函数、函数装饰…

全志V3s之U-Boot

1、安装交叉编译器&#xff1a; ARM交叉编译器的官网&#xff1a;交叉编译器 a、使用wget下载&#xff1a; wget https://releases.linaro.org/components/toolchain/binaries/latest/arm-linux-gnueabihf/gcc-linaro-6.3.1-2017.05-x86_64_arm-linux-gnueabihf.tar.xzb、解…

mmseg上手自己的数据集

制作自己的数据集&#xff0c;VOC格式为例。 这三个文件包括数据集的名称。可以使用labelme脚本自动生成。 跟据预测类别修改配置文件 D:\projects\mmsegmentation-main\mmseg\datasets\voc.py 因为是voc格式的数据集&#xff0c;在这个文件里进行配置&#xff0c;修改成自己数…

每日分享,以元旦为题的诗词

元旦佳节即将来临&#xff0c;相信大家都会在朋友圈表达一下自己的情感&#xff0c;不管大家以前是怎么表达的&#xff0c;今天小编给你分享几首以元旦为题的几首诗&#xff0c;喜欢的朋友可以自取&#xff0c;想要更多免费的诗词&#xff0c;请自行百度或小程序搜索&#xff1…

SLAM算法与工程实践——相机篇:传统相机使用(2)

SLAM算法与工程实践系列文章 下面是SLAM算法与工程实践系列文章的总链接&#xff0c;本人发表这个系列的文章链接均收录于此 SLAM算法与工程实践系列文章链接 下面是专栏地址&#xff1a; SLAM算法与工程实践系列专栏 文章目录 SLAM算法与工程实践系列文章SLAM算法与工程实践…

C++共享和保护——(3)静态成员

归纳编程学习的感悟&#xff0c; 记录奋斗路上的点滴&#xff0c; 希望能帮到一样刻苦的你&#xff01; 如有不足欢迎指正&#xff01; 共同学习交流&#xff01; &#x1f30e;欢迎各位→点赞 &#x1f44d; 收藏⭐ 留言​&#x1f4dd; 信念&#xff0c;你拿它没办法&#x…

Java 第8章 本章作业

目录 4.通过继承实现员工工资核算打印功能 6.父类和子类中通过this和super都可以调用哪些属性和方法 8.扩展如下的BankAccount类 10.判断测试类中创建的两个对象是否相等 11.向上转型&向下转型 12.equals和的区别 15.什么是多态,多态具体体现有哪些? 16. java的动…

Spring Bean基础

写在最前面: 本文运行的示例在我github项目中的spring-bean模块&#xff0c;源码位置: spring-bean 前言 为什么要先掌握 Spring Bean 的基础知识&#xff1f; 我们知道 Spring 框架提供的一个最重要也是最核心的能力就是管理 Bean 实例。以下是其原因&#xff1a; 核心组件…

新版Spring Security6.2案例 - Authentication用户名密码

前言&#xff1a; 前面有翻译了新版Spring Security6.2架构&#xff0c;包括总体架构&#xff0c;Authentication和Authorization&#xff0c;感兴趣可以直接点链接&#xff0c;这篇翻译官网给出的关于Authentication的Username/Password这页。 首先呢&#xff0c;官网就直接…

前端如何使用express写一个简单的服务

相信不少前端平常在日常工作中肯遇见过后端API接口没开发出来的时候吧 前端提升小技巧 自己使用nodejs——express ,koa&#xff0c;egg开发接口吧(本人比较喜欢egg和express) 今天先分享一下express 下面是一个简单的demo 1、首先咱们可以新建一个文件夹,创建一个app.js 下…

【开源软件】最好的开源软件-2023-第18名 OpenTelemetry

自我介绍 做一个简单介绍&#xff0c;酒架年近48 &#xff0c;有20多年IT工作经历&#xff0c;目前在一家500强做企业架构&#xff0e;因为工作需要&#xff0c;另外也因为兴趣涉猎比较广&#xff0c;为了自己学习建立了三个博客&#xff0c;分别是【全球IT瞭望】&#xff0c;【…

Keepalived+Nginx实现高可用(下)

一、背景 上篇文章介绍了基本的Keepalived的简单入门&#xff0c;但是针对预留的问题还有优化的空间。分别是下面3个问题: 1、如果仅仅只提供一个VIP的方式&#xff0c;会存在只有1台服务器处于实际工作&#xff0c;另外1台处于闲置状态。 势必存在成本资源浪费问题&#xff0c…

LLM之RAG实战(四):Self-RAG如何革命工业LLM

论文地址&#xff1a;https://arxiv.org/pdf/2310.11511.pdf Github地址&#xff1a;https://github.com/AkariAsai/self-rag 尽管LLM&#xff08;大型语言模型&#xff09;的模型和数据规模不断增加&#xff0c;但它们仍然面临事实错误的问题。现有的Retrieval-Augmented Gen…

一文讲清 QWidget 大小位置

一文讲清 QWidget 大小位置 前言 ​ QWidget 的位置基于桌面坐标系&#xff0c;以左上角为原点&#xff0c;向右x轴增加&#xff0c;向下y轴增加。 一、图解 ​ ​ 如上图所示&#xff0c;当窗口为顶层窗口时&#xff08;即没有任何父窗口&#xff09;&#xff0c;系统会自…

JVM的五大分区

1.方法区 方法区主要用来存储已在虚拟机加载的类的信息、常量、静态变量以及即时编译器编译后的代码信息。该区域是被线程共享的。 2.虚拟机栈 虚拟机栈也就是我们平时说的栈内存&#xff0c;它是为java方法服务的。每个方法在执行的 时候都会创建一个栈帧&#xff0c;用于存…