当前位置: 首页 > news >正文

Modern C++ 智能指针

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出来的对象存储在堆上,需要手动deleteTest *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会变成nullptrstd::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_ptrstd::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_ptrif(ptr3) { //判断weak_ptr是否指向了有效对象std::cout << ptr3.get()->getNumber() << std::endl;}else{std::cout << "ptr2 is expired" << std::endl;}ptr1.reset(); //释放shared_ptrptr3.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新特性解析与应用》

相关文章:

Modern C++ 智能指针

Why&#xff1f; 原始指针存在缺陷&#xff0c;不符合现代编程语言的需要。 原始指针的缺陷&#xff1a; 指针指向一片内存&#xff0c;使用者无法得知到底是指向了什么&#xff0c;是数组还是对象&#xff1f;使用完指针是否需要销毁&#xff1f;什么时候销毁&#xff1f;如…...

Python的100道经典练习题,每日一练,必成大神!!!

Python的100道经典练习题是一个广泛而深入的学习资源&#xff0c;可以帮助Python初学者和进阶者巩固和提升编程技能 完整的100多道练习题可在下面图片免沸获取哦~ 整理了100道Python的题目&#xff0c;如果你是一位初学者&#xff0c;这一百多道题可以 帮助你轻松的使用Python…...

代码回滚命令

定位到当前分支 git branch回滚到指定的commit git reset --hard 85da0cb8322accad143cpush到远程分支 git push --force...

[ASIS 2019]Unicorn shop1

打开题目 随便输入信息看一下 操作失败&#xff0c;只让输入一个字符 不妨抓包看一下&#xff0c;信息&#xff0c;发现 从中可以发现源代码是如何处理price的 使用的是unicodedata.numeric() 但我们查看页面源码时&#xff0c;看到源码处理方式是utf-8 所以&#xff0c;前…...

LangChain与泛型编程:探索代码生成的新维度

LangChain与泛型编程&#xff1a;探索代码生成的新维度 在软件开发领域&#xff0c;泛型编程是一种允许创建可重用组件的技术&#xff0c;这些组件可以在多种数据类型上工作的编程范式。LangChain作为一个假设的编程辅助工具&#xff0c;如果存在&#xff0c;它可能会支持泛型…...

day25

一、进程间通信&#xff08;IPC&#xff09; 1.1 进程间通信的引入 1> 对于多个线程之间通信&#xff0c;我们可以使用临界资源来完成&#xff0c;通过一个线程任务对临界资源进行修改&#xff0c;另一个线程也可以使用已经修改过的临界资源&#xff0c;但是要注意使用…...

红黑树的概念和模拟实现[C++]

文章目录 红黑树的概念一、红黑树的性质红黑树原理二、红黑树的优势和比较 红黑树的模拟实现构建红黑树的数据结构定义节点的基本结构和初始化方式插入新节点插入新节点的颜色调整颜色和结构以满足红黑树性质 红黑树的应用场景 红黑树的概念 一、红黑树的性质 红黑树是一种自平…...

网络安全应急响应概述

前言 在网络安全领域&#xff0c;有一句广为人知的话&#xff1a;“没有绝对的安全”。这意味着任何系统都有可能被攻破。安全攻击的发生并不可怕&#xff0c;可怕的是从头到尾都毫无察觉。当系统遭遇攻击时&#xff0c;企业的安全人员需要立即进行应急响应&#xff0c;以将影响…...

【C++】链表操作技巧综合:重排链表(带你理顺链表的做题思路)

1.题目 2.算法思路 这是一道关于链表的综合题&#xff0c;一共涉及到三个步骤&#xff0c;其中每个步骤单拎出来就可以当一道单独的题目。所以需要大家对链表的操作十分熟悉&#xff0c;否则可能需要大量的时间做这道题目&#xff0c;而且还要很多的bug。 第一个步骤&#xf…...

行为型设计模式2:观察者/职责链/中介者/访问者

设计模式&#xff1a;观察者/职责链/中介者/访问者 (qq.com)...

叛逆,批判

1、对以往说法的批判之一&#xff08;第一次这么公开批判是2004-2005年&#xff09;&#xff1a; 这部英文版的《数学百科全书》似乎是从俄语版翻译过来的&#xff1f;我查了三本引用的图书文献&#xff0c;都没有关于“nonsingular”和“singular”的类似下面的说法&#xff…...

Linux 命令,mkdir说明与使用

1&#xff1a;mkdir命令功用&#xff1a; 用于创建一个或多个目录&#xff0c;创建目录&#xff0c;必须在父目录中写上权限。 新目录的默认模式为0777&#xff0c;可以由系统或用的umask来修改。 2&#xff1a;命令构件: mkdir [options] directories 3:参数选项: -m&#x…...

24. 两两交换链表中的节点(Java)

目录 题目描述&#xff1a;示例 &#xff1a;代码实现&#xff1a; 题目描述&#xff1a; 给你一个链表&#xff0c;两两交换其中相邻的节点&#xff0c;并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题&#xff08;即&#xff0c;只能进行节点交换&am…...

linux虚拟机设置固定ip

修改/etc/sysconfig/network-scripts目录下ifcfg-eth0文件&#xff0c;各虚拟机这个文件名不一致&#xff0c;ifcfg-XX格式 vim /etc/sysconfig/network-scripts/ifcfg-eth0BOOTPROTO设置为static&#xff0c;然后在最后添加固定IP地址和默认网关、DNS等配置&#xff0c;IP地址…...

mysql问题解决

1.etl数据同步时&#xff0c;发现连接不上要同步的数据库 解决方法&#xff1a;关闭mysql的ssl&#xff0c;步骤如下&#xff1a; 在MySQL中禁用SSL连接涉及修改服务器的配置文件&#xff08;通常是my.cnf或my.ini&#xff0c;取决于你的操作系统和MySQL版本&#xff09;。以…...

类和对象(下)C++

1.初始化列表 1.为什么有初始化列表&#xff0c;它的作用&#xff1f; ->初始化列表&#xff0c;是构造函数初始化的另一种形式。 ->在语法上面理解&#xff0c;初始化列表可以认定为是每个成员变量定义初始化的地方. ->引用成员变量&#xff0c;const成员变量&am…...

常用在线 Webshell 查杀工具推荐

一、简介 这篇文章将介绍几款常用的在线 Webshell 查杀工具&#xff0c;包括长亭牧云、微步在线云沙箱、河马和VirusTotal。每个工具都有其独特的特点和优势&#xff0c;用于帮助用户有效检测和清除各类恶意 Webshell&#xff0c;保障网站和服务器的安全。文章将深入探讨它们的…...

RPC远程调用框架Dubbo

一、分布式服务调用_什么是RPC RPC(Remote Procedure Call)远程过程调用&#xff0c;它是一种通过网络从远程计算机程序上请求服务。 大白话理解就是&#xff1a;RPC让你用别人家的东西就像自己家的一样。 RPC两个作用&#xff1a; 屏蔽远程调用跟本地调用的区别&#xff0c…...

基于STM32的智能灌溉系统

目录 引言环境准备工作 硬件准备软件安装与配置系统设计 系统架构硬件连接代码实现 初始化代码传感器读取和控制代码应用场景 农业灌溉花园自动灌溉常见问题及解决方案 常见问题解决方案结论 1. 引言 智能灌溉系统通过实时监测土壤湿度和环境温度&#xff0c;自动控制灌溉设…...

Datawhale AI 夏令营 Task3(半成品,仍在学习理解

课程链接 / 知识点整理 &#xff08;一&#xff09;...

细腻呵护静音生活缓冲器,家具中的隐形侍者

在忙碌的生活节奏中&#xff0c;家是我们寻找宁静与放松的避风港。而家具缓冲器&#xff0c;就像一位隐形的侍者&#xff0c;在不经意间为我们营造出温馨、宁静的居住环境。它们静静地工作&#xff0c;细腻地呵护着每一处细节&#xff0c;让家的每一次触碰成为一次尊享体验。 细…...

【MATLAB源码-第243期】基于simulink的CUK斩波电路仿真,输出各节点波形。

操作环境&#xff1a; MATLAB 2022a 1、算法描述 CUK电路是一种高效的直流-直流转换器&#xff0c;它以其独特的能量传递方式和高效的电压转换能力&#xff0c;在许多电力电子应用中得到了广泛的使用。下面将详细描述CUK电路的工作原理、各个组成部分以及其在实际应用中的优…...

springboot项目不能同时跑junit4和junit5的解决方法

springboot项目的maven test只会跑junit4 RunWith注解的测试类&#xff0c;而不会跑junit5 ExtendWith的测试类 解决方法&#xff1a;pom加上以下plugin&#xff0c;版本号需要3.0.0-M5及以上 <plugin><groupId>org.apache.maven.plugins</groupId><art…...

【IO】使用消息队列完成两个进程之间相互通信

目录 1、使用消息队列完成两个进程之间相互通信 2、共享内存实现两个进程之间的通信 3、思维导图 1、使用消息队列完成两个进程之间相互通信 //msgsnd.c #include <myhead.h>// 要发送的消息类型 struct msgbuf {long mtype;char mtext[1024]; };// 定义一个宏&#…...

Web开发:用C#的逻辑理解VUE语法(VUE + Webapi小白开发笔记)

适用阅读对象&#xff1a;需要兼顾前端的C#后端开发人员&#xff08;基础笔记&#xff09; 目录 一、后端交互-获取实体数据 二、变量 1.声明 2.作用域 三、字符串的处理 四、数组(列表)的处理 1.数组中的SELECT语法&#xff08;提取特定字段到新数组&#xff09; 2.数…...

操作系统文件位置指针

文件位置指针 与标准IO的文件读写位置指针一样&#xff0c;系统IO时也会有一个表示位置的指针在移动&#xff0c;会随着读写操作的执行向后自动移动 当需要随机位置进行读写操作时&#xff0c;那么需要移动位置指针的位置 off_t lseek(int fd, off_t offset, int whence); 功…...

设计模式的概念

设计模式主要分为三类&#xff1a;创建类的设计模式、结构型设计模式、行为型设计模式。 创建类的设计模式&#xff1a;简单工厂&#xff0c;工厂模式&#xff0c;抽象工厂&#xff0c;建造者&#xff0c;单例&#xff0c;原型 结构型设计模式&#xff1a;代理模式、享元模式 行…...

VMware17下载与安装

1.下载 通过百度网盘分享的文件&#xff1a;VMware17 链接&#xff1a;https://pan.baidu.com/s/1gCine3d3Rp_l3NYAu5-ojg 提取码&#xff1a;ek25 --来自百度网盘超级会员V3的分享 2.安装...

mv命令学习

移动和重命名文件 mv mv命令的作用就是将文件系统的文件从一个地方移动到另一个地方。 $ pwd /home/scott/libby $ ls libby_arrowrock.jpg libby_bak.jpg libby.jpg ➥libby_on_couch.jpg on_floor $ ls ~/pictures/dogs libby_on_floor_01.jpg libby_on_floor_03.jpg li…...

西北航天基地采用Infortrend NAS存储做影视后期及共享

用户背景&#xff1a; 创建最早的综合型航空航天基地&#xff0c;占地5万平方米&#xff0c;每年约300天进行航天试验 挑战&#xff1a; 西北航天基地规模大任务多&#xff0c;分别有不同的项目组负责试验&#xff0c;项目组需要获取试验任务影像资料&#xff0c;用于分析总…...