Modern C++ 智能指针
Why?
原始指针存在缺陷,不符合现代编程语言的需要。
原始指针的缺陷:
- 指针指向一片内存,使用者无法得知到底是指向了什么,是数组还是对象?
- 使用完指针是否需要销毁?什么时候销毁?如何进行销毁?并没有一个统一的定论。
- 如果一个指针指向的地址没有被释放指针就被销毁了,会内存泄漏;
- 如果指针指向的内存已经被释放了,指针没有被销毁,会产生悬空指针;
- 如果指针指向的内存已经被释放,在销毁该指针时,又释放了一回,会二次释放,未定义行为,程序崩溃。
- 如果指针指向了一个数组,使用了delete而非delete[]进行释放,会产生未定义行为,后果未知。
总之,原始指针过于简陋,虽然强大,但是需要考虑的太多了,对菜鸡不友好。
智能指针(smart pointers)是解决这些问题的一种办法。智能指针包裹原始指针,它们的行为看起来像被包裹的原始指针,但避免了原始指针的很多陷阱。
What
在C++11的智能指针之前,C++98中,智能指针通过一个模板类型auto_ptr
来实现。auto_ptr以对象的方式管理堆分配的内存,并在适当的时间(比如析构),释放所获得的堆内存。
但是auto_ptr存在一些问题,在C++17中该方式已经被废弃。
在C++11中存在四种智能指针:std::auto_ptr
,std::unique_ptr
,std::shared_ptr
, std::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? 原始指针存在缺陷,不符合现代编程语言的需要。 原始指针的缺陷: 指针指向一片内存,使用者无法得知到底是指向了什么,是数组还是对象?使用完指针是否需要销毁?什么时候销毁?如…...

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

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

[ASIS 2019]Unicorn shop1
打开题目 随便输入信息看一下 操作失败,只让输入一个字符 不妨抓包看一下,信息,发现 从中可以发现源代码是如何处理price的 使用的是unicodedata.numeric() 但我们查看页面源码时,看到源码处理方式是utf-8 所以,前…...
LangChain与泛型编程:探索代码生成的新维度
LangChain与泛型编程:探索代码生成的新维度 在软件开发领域,泛型编程是一种允许创建可重用组件的技术,这些组件可以在多种数据类型上工作的编程范式。LangChain作为一个假设的编程辅助工具,如果存在,它可能会支持泛型…...

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

红黑树的概念和模拟实现[C++]
文章目录 红黑树的概念一、红黑树的性质红黑树原理二、红黑树的优势和比较 红黑树的模拟实现构建红黑树的数据结构定义节点的基本结构和初始化方式插入新节点插入新节点的颜色调整颜色和结构以满足红黑树性质 红黑树的应用场景 红黑树的概念 一、红黑树的性质 红黑树是一种自平…...
网络安全应急响应概述
前言 在网络安全领域,有一句广为人知的话:“没有绝对的安全”。这意味着任何系统都有可能被攻破。安全攻击的发生并不可怕,可怕的是从头到尾都毫无察觉。当系统遭遇攻击时,企业的安全人员需要立即进行应急响应,以将影响…...

【C++】链表操作技巧综合:重排链表(带你理顺链表的做题思路)
1.题目 2.算法思路 这是一道关于链表的综合题,一共涉及到三个步骤,其中每个步骤单拎出来就可以当一道单独的题目。所以需要大家对链表的操作十分熟悉,否则可能需要大量的时间做这道题目,而且还要很多的bug。 第一个步骤…...

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

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

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

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

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

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

类和对象(下)C++
1.初始化列表 1.为什么有初始化列表,它的作用? ->初始化列表,是构造函数初始化的另一种形式。 ->在语法上面理解,初始化列表可以认定为是每个成员变量定义初始化的地方. ->引用成员变量,const成员变量&am…...

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

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

基于STM32的智能灌溉系统
目录 引言环境准备工作 硬件准备软件安装与配置系统设计 系统架构硬件连接代码实现 初始化代码传感器读取和控制代码应用场景 农业灌溉花园自动灌溉常见问题及解决方案 常见问题解决方案结论 1. 引言 智能灌溉系统通过实时监测土壤湿度和环境温度,自动控制灌溉设…...
Datawhale AI 夏令营 Task3(半成品,仍在学习理解
课程链接 / 知识点整理 (一)...

MongoDB学习和应用(高效的非关系型数据库)
一丶 MongoDB简介 对于社交类软件的功能,我们需要对它的功能特点进行分析: 数据量会随着用户数增大而增大读多写少价值较低非好友看不到其动态信息地理位置的查询… 针对以上特点进行分析各大存储工具: mysql:关系型数据库&am…...

DIY|Mac 搭建 ESP-IDF 开发环境及编译小智 AI
前一阵子在百度 AI 开发者大会上,看到基于小智 AI DIY 玩具的演示,感觉有点意思,想着自己也来试试。 如果只是想烧录现成的固件,乐鑫官方除了提供了 Windows 版本的 Flash 下载工具 之外,还提供了基于网页版的 ESP LA…...

Spring Cloud Gateway 中自定义验证码接口返回 404 的排查与解决
Spring Cloud Gateway 中自定义验证码接口返回 404 的排查与解决 问题背景 在一个基于 Spring Cloud Gateway WebFlux 构建的微服务项目中,新增了一个本地验证码接口 /code,使用函数式路由(RouterFunction)和 Hutool 的 Circle…...

算法笔记2
1.字符串拼接最好用StringBuilder,不用String 2.创建List<>类型的数组并创建内存 List arr[] new ArrayList[26]; Arrays.setAll(arr, i -> new ArrayList<>()); 3.去掉首尾空格...

SAP学习笔记 - 开发26 - 前端Fiori开发 OData V2 和 V4 的差异 (Deepseek整理)
上一章用到了V2 的概念,其实 Fiori当中还有 V4,咱们这一章来总结一下 V2 和 V4。 SAP学习笔记 - 开发25 - 前端Fiori开发 Remote OData Service(使用远端Odata服务),代理中间件(ui5-middleware-simpleproxy)-CSDN博客…...

RabbitMQ入门4.1.0版本(基于java、SpringBoot操作)
RabbitMQ 一、RabbitMQ概述 RabbitMQ RabbitMQ最初由LShift和CohesiveFT于2007年开发,后来由Pivotal Software Inc.(现为VMware子公司)接管。RabbitMQ 是一个开源的消息代理和队列服务器,用 Erlang 语言编写。广泛应用于各种分布…...
Git常用命令完全指南:从入门到精通
Git常用命令完全指南:从入门到精通 一、基础配置命令 1. 用户信息配置 # 设置全局用户名 git config --global user.name "你的名字"# 设置全局邮箱 git config --global user.email "你的邮箱example.com"# 查看所有配置 git config --list…...
为什么要创建 Vue 实例
核心原因:Vue 需要一个「控制中心」来驱动整个应用 你可以把 Vue 实例想象成你应用的**「大脑」或「引擎」。它负责协调模板、数据、逻辑和行为,将它们变成一个活的、可交互的应用**。没有这个实例,你的代码只是一堆静态的 HTML、JavaScript 变量和函数,无法「活」起来。 …...

STM32---外部32.768K晶振(LSE)无法起振问题
晶振是否起振主要就检查两个1、晶振与MCU是否兼容;2、晶振的负载电容是否匹配 目录 一、判断晶振与MCU是否兼容 二、判断负载电容是否匹配 1. 晶振负载电容(CL)与匹配电容(CL1、CL2)的关系 2. 如何选择 CL1 和 CL…...
Linux系统部署KES
1、安装准备 1.版本说明V008R006C009B0014 V008:是version产品的大版本。 R006:是release产品特性版本。 C009:是通用版 B0014:是build开发过程中的构建版本2.硬件要求 #安全版和企业版 内存:1GB 以上 硬盘…...