Modern C++ 智能指针

news2024/11/16 8:28:29

Why?

原始指针存在缺陷,不符合现代编程语言的需要。
原始指针的缺陷:

  • 指针指向一片内存,使用者无法得知到底是指向了什么,是数组还是对象?
  • 使用完指针是否需要销毁?什么时候销毁?如何进行销毁?并没有一个统一的定论。
    • 如果一个指针指向的地址没有被释放指针就被销毁了,会内存泄漏;
    • 如果指针指向的内存已经被释放了,指针没有被销毁,会产生悬空指针;
    • 如果指针指向的内存已经被释放,在销毁该指针时,又释放了一回,会二次释放,未定义行为,程序崩溃。
    • 如果指针指向了一个数组,使用了delete而非delete[]进行释放,会产生未定义行为,后果未知。

总之,原始指针过于简陋,虽然强大,但是需要考虑的太多了,对菜鸡不友好。
智能指针(smart pointers)是解决这些问题的一种办法。智能指针包裹原始指针,它们的行为看起来像被包裹的原始指针,但避免了原始指针的很多陷阱。

What

在C++11的智能指针之前,C++98中,智能指针通过一个模板类型auto_ptr来实现。auto_ptr以对象的方式管理堆分配的内存,并在适当的时间(比如析构)​,释放所获得的堆内存。
但是auto_ptr存在一些问题,在C++17中该方式已经被废弃。

在C++11中存在四种智能指针:std::auto_ptrstd::unique_ptrstd::shared_ptrstd::weak_ptr。都是被设计用来帮助管理动态对象的生命周期,在适当的时间通过适当的方式来销毁对象,以避免出现资源泄露或者异常行为。std::auto_ptr在C++17中已经弃用,尽量不要用,除非要与C++98代码兼容。

std::unique_ptr

指针独享它所指向的对象,在指针销毁时,对象一同被释放。

#include <iostream>
#include <memory>

class Test {
    public:
        Test() :number(0) {
            std::cout << "Test()" << std::endl;
        }
        ~Test() {
            std::cout << "~Test()" << std::endl;
        }
        int getNumber() {
            return number;
        }
    private:
        int number;
};


void print(std::unique_ptr<Test> ptr) {
    std::cout << ptr.get()->getNumber() << std::endl;

}
void print1(std::unique_ptr<Test> &ptr) {
    std::cout << ptr.get()->getNumber() << std::endl;
}
int main() {

    Test t; //不会内存泄漏,局部变量离开作用域会自动调用析构函数
    Test *t1 = new Test(); //会内存泄漏,new出来的对象存储在堆上,需要手动delete
    Test *t2 = new Test();
    std::unique_ptr<Test> ptr1(t2); //不会内存泄漏,unique_ptr会自动调用析构函数
    std::unique_ptr<Test> ptr(new Test()); //不会内存泄漏,unique_ptr会自动调用析构函数
    std::unique_ptr<Test> ptr2 = std::make_unique<Test>(); //C++14引入的std::make_unique,更加安全
    std::cout << ptr1.get()->getNumber() << std::endl; //使用时需要通过get()获取指针,然后就跟正常指针一样了
    print(std::move(ptr)); //传递时需要使用std::move,因为unique_ptr是不可拷贝的
    print1(ptr1); //传递时需要使用引用,因为unique_ptr是不可拷贝的
    return 0;
}

因为只有一个智能指针指向对象,所以unique_ptr智能指针的赋值函数,拷贝构造函数,都被禁用了,构造函数添加了explicit关键字修饰,不能使用转换函数,即:不能隐式转换构造unique_ptr指针,

原因也很好理解,如果有一个原始指针ptr,如果允许进行隐式转换构造,那么就可以根据这个原始指针隐式构造出来多个unique_ptr,这就不符合一个独享的原则了。

想要将unique_ptr转移,要么使用引用,要么使用std::move进行移动。
std::unique_ptr的常见用法是作为继承层次结构中对象的工厂函数返回类型。

std::shared_ptr

共享指针,允许多个指针指向同一个对象,当最后一个shared_ptr被销毁时自动进行内存释放。std::shared_ptr通过引用计数(reference count)来确保它是否是最后一个指向某种资源的指针,引用计数关联资源并跟踪有多少std::shared_ptr指向该资源。
引用计数会对性能有一定影响:

  • std::shared_ptr大小是原始指针的两倍,因为它内部包含一个指向资源的原始指针,还包含一个指向资源的控制块的原始指针。

    • 控制块的大小和位置都是不确定的,没有一个明确的规定非要放在哪里,有可能放到堆上面。
    • 在这里插入图片描述
  • 递增递减引用计数必须是原子性的,因为多个reader、writer可能在不同的线程。比如,指向某种资源的std::shared_ptr可能在一个线程执行析构(于是递减指向的对象的引用计数),在另一个不同的线程,std::shared_ptr指向相同的对象,但是执行的却是拷贝操作(因此递增了同一个引用计数)。原子操作通常比非原子操作要慢,所以即使引用计数通常只有一个word大小,你也应该假定读写它们是存在开销的。

shared_ptr和unique_ptr都支持自定义销毁器(仿函数或者匿名函数),不同的是unique_ptr把销毁器也看作指针类型的一部分,而shared_ptr不会,因此shared_ptr有更高的灵活性:

auto loggingDel = [](Widget *pw)        //自定义删除器
                  {                     //(和条款18一样)
                      makeLogEntry(pw);
                      delete pw;
                  };

std::unique_ptr<                        //删除器类型是
    Widget, decltype(loggingDel)        //指针类型的一部分
    > upw(new Widget, loggingDel);
std::shared_ptr<Widget>                 //删除器类型不是
    spw(new Widget, loggingDel);        //指针类型的一部分

不了解仿函数和匿名函数的可以看之前的这篇文章:Modern C++:函数的发展:从函数指针到匿名函数

当创造一个新的shared_ptr指针时,我们是不知道是否有其他的shared_ptr的,自然也就不知道是否有与该对象对应的control block,因此控制块的创建有以下规则:

  • 使用make_shared时,会创建控制块,因为make_shared是在创建一个新的对象。
  • 将unique_ptr转化为shared_ptr时会创建控制块,因为unique_ptr指向的对象不需要控制块,转为shared_ptr需要新建控制块,并将原来的unique_ptr置为null
  • 使用已有shared_ptr构造新的shared_ptr时不会新建控制块。
  • 使用原始指针创建shared_ptr时,会产生未定义行为,因为我们无法从原始指针得知指向的对象是否已经有了控制块。
#include <iostream>
#include <memory>

class Test {
    public:
        Test() :number(0) {
            std::cout << "Test()" << std::endl;
        }
        ~Test() {
            std::cout << "~Test()" << std::endl;
        }
        int getNumber() {
            return number;
        }
    private:
        int number;
};

int main(){
    std::shared_ptr<Test> ptr1 = std::make_shared<Test>(); //最推荐的初始化方式
    std::shared_ptr<Test> ptr2(new Test()); //也不是不可以
    std::shared_ptr<Test> ptr3 = ptr1; //shared_ptr可以拷贝
    Test *t = new Test();
    std::shared_ptr<Test> ptr4(t); //shared_ptr可以接受裸指针,但是会导致未定义行为,不推荐
    std::unique_ptr<Test> ptr5 = std::make_unique<Test>();
    std::shared_ptr<Test> ptr6 = std::move(ptr5); //unique_ptr可以转换为shared_ptr,原来的unique_ptr会变成nullptr
    std::cout << "Unique ptr has been moved:" << ptr5.get() << std::endl;
    std::cout << ptr1.get()->getNumber() << std::endl;

}

std::weak_ptr

在C++11标准中,除了unique_ptr和shared_ptr,智能指针还包括了weak_ptr这个类模板。weak_ptr的使用更为复杂一点,它可以指向shared_ptr指针指向的对象内存,却并不拥有该内存。而使用weak_ptr成员lock,则可返回其指向内存的一个shared_ptr对象,且在所指对象内存已经无效时,返回指针空值(nullptr,请参见7.1节)​。这在验证share_ptr智能指针的有效性上会很有作用.

  • std::weak_ptr的潜在使用场景包括:缓存、观察者列表、打破std::shared_ptr环状结构。
    • shared_ptr环状结构:即存在两个对象,互相持有对方的智能指针,如果使用shared_ptr,会因为相互持有,导致无法释放,内存泄漏。
#include <iostream>
#include <memory>

class Test {
    public:
        Test() :number(0) {
            std::cout << "Test()" << std::endl;
        }
        ~Test() {
            std::cout << "~Test()" << std::endl;
        }
        int getNumber() {
            return number;
        }
    private:
        int number;
};

int main(){
    std::shared_ptr<Test> ptr1 = std::make_shared<Test>();
    std::weak_ptr<Test> ptr2 = ptr1; //weak_ptr是shared_ptr的弱引用,不会增加引用计数
    if(!ptr2.expired()) { //判断weak_ptr是否指向了有效对象
        std::shared_ptr<Test> ptr3 = ptr2.lock(); //通过weak_ptr获取shared_ptr
        std::cout << ptr3.get()->getNumber() << std::endl;
    }else{
        std::cout << "ptr2 is expired" << std::endl;
    }
    return 0;
}

但是在考虑多线程时,有可能判断过weak_ptr的有效性后在另一个线程中对指向的对象进行了析构,所以更常见的做法是:

int main(){
int main(){
    std::shared_ptr<Test> ptr1 = std::make_shared<Test>();
    std::weak_ptr<Test> ptr2 = ptr1; //weak_ptr是shared_ptr的弱引用,不会增加引用计数
    std::shared_ptr<Test> ptr3 = ptr2.lock(); //通过weak_ptr获取shared_ptr
    if(ptr3) { //判断weak_ptr是否指向了有效对象
        std::cout << ptr3.get()->getNumber() << std::endl;
    }else{
        std::cout << "ptr2 is expired" << std::endl;
    }

    ptr1.reset(); //释放shared_ptr
    ptr3.reset();

    //或者
    try
    { 
        std::shared_ptr<Test> ptr4(ptr2); //如果已经析构,会抛出bad_weak_ptr异常
    }
    catch(const std::exception& e)
    {
        std::cerr << e.what() << '\n';
    }
    
    return 0;
}
}

使用建议

  • 如果不确定要使用哪种智能指针好,优先选用unique_ptr,性能和原始指针差不多,且从unique_ptr升级成shared_ptr很容易,反之不行。
  • 创建智能指针时,尽量使用make_xxx,如果不行再使用new,实在不行最后才选择使用原始指针,但是shared_ptr不要使用原始指针,会导致未定义行为。

参考文献:
《Effective Modern C++》
《深入理解C++11:C++11新特性解析与应用》

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

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

相关文章

B1.2 AArch64 执行状态下的寄存器

B1.2 AArch64 执行状态下的寄存器 在AArch64的执行状态下,在 EL0 上可见的寄存器如下: (1)、R0-R30 31 个通用寄存器,R0 到 R30。每个都可以做为: 一个 64 位的通用寄存器,命名为 X0 到 X30。 一个 32 位的通用寄存器,命名为 W0 到 W30。 (2)、LR X30 通用寄存器用…

文件的读写

一、IO 标准io &#xff08;输入输出&#xff09;站在计算机角度来确定输入输出&#xff0c;在linux里面io都是对文件操作。 so 动态库函数&#xff08;共享库&#xff09;&#xff0c;&#xff08;公共的&#xff0c;用的很多&#xff09;&#xff0c;在user里面存储。 man手…

Studying-代码随想录训练营day58| 拓扑排序精讲、dijkstra(朴素版)精讲

第58天&#xff0c;拓扑排序和最短路径算法讲解&#xff01;&#xff01;&#x1f4aa;(ง •_•)ง&#x1f4aa;&#xff0c;编程语言&#xff1a;C 目录 拓扑排序精讲 拓扑排序的背景 题目&#xff1a;117. 软件构建 (kamacoder.com) 拓扑排序的思路 模拟过程 有环…

基于K8S配置Jenkins主从节点实例

基于K8S配置Jenkins主从节点实例 1.配置Jenkins主节点1.确认 Jenkins Pod 名称2.进入 Jenkins Pod&#xff1a;3.生成SSH密钥对4.将公钥复制到目标节点&#xff1a; 2.配置Jenkins的node1节点1.安装java2.配置 Jenkins node1节点的 Java 路径1.添加Java环境变量2.生效Java环境变…

如何根据 EcoVadis 审核的评分标准改进企业社会责任表现?

要根据 EcoVadis 审核的评分标准改进企业社会责任表现&#xff0c;可以采取以下步骤&#xff1a; ​深入研究评分标准 详细了解每个主题&#xff08;环境、劳工与人权、商业道德、可持续采购&#xff09;及其子主题的具体要求和关键指标。 进行自我评估 对照评分标准&#xf…

未授权访问漏洞(非重点 上)

1.MongoDB 1.在fofo里搜索 port"27017 2.Memcached 1.用fofa语法 port"11211" 搜索资产 2.使用 telnet 连接 3.Zookeeper 1.在 fofa 中使用 port"2181" 获取资源 2.在 kali 中使用 echo envinc ip 2181 测试是否存在漏洞 4.Elasticsearch 1.在 …

⌈ 传知代码 ⌋ MSA+抑郁症模型总结(二)

&#x1f49b;前情提要&#x1f49b; 本文是传知代码平台中的相关前沿知识与技术的分享~ 接下来我们即将进入一个全新的空间&#xff0c;对技术有一个全新的视角~ 本文所涉及所有资源均在传知代码平台可获取 以下的内容一定会让你对AI 赋能时代有一个颠覆性的认识哦&#x…

Java语言程序设计——篇十一(5)

&#x1f33f;&#x1f33f;&#x1f33f;跟随博主脚步&#xff0c;从这里开始→博主主页&#x1f33f;&#x1f33f;&#x1f33f; 欢迎大家&#xff1a;这里是我的学习笔记、总结知识的地方&#xff0c;喜欢的话请三连&#xff0c;有问题可以私信&#x1f333;&#x1f333;&…

基于arcpro3.0.2的北斗网格生成简介

基于arcpro3.0.2的北斗网格生成简介 采用2000坐标系、可基于行政区范围 软件可生成第一级到第十级北斗网格经纬跨度 等分 约赤道处距离 第一级 6X4度 60 和A~V 660 km 第二级 30X30分 12X8 …

sql注入第一关和第二关

第一关&#xff1a; 输入?id1 正常 输入?id1 报错 .0 输入?id1-- 正常 判断他是字符型注入 闭合方式是: ?id1 and 12 union select 1,2,group_concat(schema_name) from information_schema.schemata-- 联合注入 爆出库&#xff1a;ctfshow,ctftraining,information_…

nvidia系列教程-AGX-Orin系统刷机及备份

目录 前言 一、准备工作 二、AGX Orin 系统刷机步骤 三、AGX Orin 系统备份 总结 前言 NVIDIA AGX Orin 是一款高性能的嵌入式计算平台&#xff0c;专为边缘计算和 AI 应用而设计。为了确保系统的稳定性和适应不同的应用场景&#xff0c;用户可能需要对 AGX Orin 进行系统刷…

SpringBoot集成阿里百炼大模型 原子的学习日记Day01

文章目录 概要下一章SpringBoot集成阿里百炼大模型&#xff08;多轮对话&#xff09; 原子的学习日记Day02 整体架构流程技术名词解释集成步骤1&#xff0c;选择大模型以及获取自己的api-key&#xff08;前面还有一步开通服务就没有展示啦&#xff01;&#xff09;2&#xff0c…

2024.8.05(glibc的安装及MySQL的安全用户角色权限)

一、glibc的安装 1、清空/etc目录下的my.cnf [rootlocalhost ~]# ls -l /etc/my.cnf -rw-r--r--. 1 root root 570 6月 8 2017 /etc/my.cnf [rootlocalhost ~]# rm -rf /etc/my.cnf 2、删除mariadb [rootlocalhost ~]# yum -y remove mariadb [rootlocalhost ~]# find / -na…

wps 最新 2019 专业版 下载安装教程,解锁全部功能,免费领取

文章目录 前言软件介绍软件下载安装步骤激活步骤小福利&#xff08;安卓APP&#xff09;软件介绍软件下载安装步骤 前言 本篇文章主要针对WPS2019专业版的安装下载进行详细讲解&#xff0c;软件已激活&#xff0c;可放心使用&#xff1b;并且可以进行账号登录&#xff0c;进行…

Router路由的使用

目录 一.Vue Router的使用&#xff1a; 二.使用vue-router来实现登录页面与主页面展示效果&#xff1a; 1.创建 index.js &#xff1a; 2.在 main.js 导入创建的路由器&#xff1a; 3.在App.vue声明标签&#xff1a; 三.子路由的使用&#xff1a; 1.添加五个组件 2.配置…

光线追踪(纹理映射)

最近在跟着ray trace in one week来学习光线追踪&#xff08;很多概念茅塞顿开&#xff09;做到一半想着记录一下&#xff08;比较随心&#xff09;上面是之前的效果。ray trace in one week Texture Coordinates for Spheres&#xff08;球体纹理坐标&#xff09; u, v 纹理…

K-means聚类算法原理解析

度量最小距离 对于 K-means 聚类算法而言&#xff0c;找到质心是一项既核心又重要的任务&#xff0c;找到质心才可以划分出距离质心最近样本点。从数学角度来讲就是让簇内样本点到达各自质心的距离总和最小。通过数学定义&#xff0c;我们将“质心”具象化&#xff0c;既然要使…

使用SpringBoot+Vue3开发项目(2)---- 设计文章分类的相关接口及页面

目录 一.所用技术栈&#xff1a; 二.后端开发&#xff1a; 1.文章分类列表渲染&#xff1a; 2.新增文章分类&#xff1a; 3.编辑文章分类&#xff1a; 4.删除文章分类 &#xff1a; 5.完整三层架构后端代码&#xff1a; &#xff08;1&#xff09;Controller层&#xff1a…

学习大数据DAY31 Python基础语法4和基于Python中的MySQL 编程

目录 Python 库 模块 time&datetime 库 连接 MySQL 操作 结构操作 数据增删改操作 数据查询操作 上机练习 7 面向对象 OOP 封装 继承 三层架构---面向对象思想模型层 数据层 业务逻辑显示层 上机练习 8 三层架构开发豆瓣网 关于我对 AI 写代码的看法&#xf…

大模型技术在企业应用中的实践与优化

【导读】大模型技术更新层出不穷&#xff0c;但对于众多企业及开发者而言&#xff0c;更为关键的命题则是如何进行应用落地&#xff0c;实现真正的智能化转型。本文系统且深入地探讨了大模型在企业应用中的关键环节和技术要点。从构建高质量的专属数据集、选择适宜的微调策略&a…