C++ 智能指针

article/2025/9/19 12:21:48

shared_ptr

智能指针也是模板类,因此当我们创建一个智能指针是要提供额外的信息——指针可以指向的类型。默认初始化的智能指针保存着一个空指针。shared_ptr允许多个指针指向同一对象。

shared_ptr<string> p1;	//可指向string
shared_ptr<list<int>> p2;	//可指向int的list

在这里插入图片描述

make_shared

最安全的分配和使用动态内存的方法是调用一一个名为make_shared 的标准库函数。此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr与智能指针一样,make_shared也定义在头文件memory中。当要用make_shared时,必须指定想要创建的对象的类型。定义方式与模板类相同,在函数名之后跟一个尖括号,在其中给出类型:

//指向一个值为42的int的shared_ptr
shared_ptr<int> p3 = make_shared<int> (42);
// p4指向一个值为"999999999"的string
shared_ptr<string> p4 = make_shared<string>(10, '9');
// p5指向一个值初始化的int,即,值为0
shared_ptr<int> p5 = make_shared<int>() ;

类似顺序容器的emplace成员make_shared 用其参数来构造给定类型的对象。例如,调用make_shared时 传递的参数必须与string 的某个构造函数相匹配,调用make_shared< int>时传递的参数必须能用来初始化一个int,依此类推。如果我们不传递任何参数,对象就会进行值初始化当然,我们通常用auto定义一个对象来保存make_ shared的结果,这种方式较为简单:

// p6指向一个动态分配的空vector<string>
auto p6 = make_shared<vector<string>>() ;

拷贝和赋值

当进行拷贝或赋值操作时,每个shared_ ptr都会记录有多少个其他shared_ptr指向相同的对象:

auto p = make_ shared<int>(42); // p指向的对象只有p一个引用者
auto q(p); // p和q指向相同对象,此对象有两个引用者

我们可以认为每个shared_ptr都有一个关联的计数器,通常称其为引用计数(reference count)。无论何时我们拷贝一个shared_ptr, 计数器都会递增。例如,当用一个shared_ptr初始化另一个shared_ptr,或将它作为参数传递给一个函数以及作为函数的返回值时,它所关联的计数器就会递增。当我们给shared_ptr赋予一个新值或是shared_ptr被销毁(例如一个局部的shared_ptr离开其作用域)时,计数器就会递减。一旦一个shared_ptr的计数器变为0,它就会自动释放自己所管理的对象:

auto r = make_ shared<int>(42); // r指向的int只有一个引用者
r = q;//给r赋值,令它指向另一个地址
//递增q指向的对象的引用计数
//递减r原来指向的对象的引用计数
// r原来指向的对象已没有引用者,会自动释放

自动销毁所管理的对象

shared_ ptr的析构函数会递减它所指向的对象的引用计数。如果引用计数变为0,shared_ptr的析构函数就会销毁对象,并释放它占用的内存。如果两个对象共享底层数据,当某个对象被销毁时,我们不能单方面的销毁数据。

和new使用

如前所述,如果我们不初始化一个智能指针,它就会被初始化为一个空指针。如表所示,我们还可以用new返回的指针来初始化智能指针:

shared ptr<double> p1; // shared ptr可以指向一-个double
shared_ ptr<int> p2(new int(42)); // p2指向一个值为42的int

接受指针参数的智能指针构造函数是explicit的。因此,我们不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化形式来初始化一个智能指针:

shared ptr<int> p1 = new int (1024);//错误:必须使用直接初始化形式
shared ptr<int> p2 (new int (1024));//正确:使用了直接初始化形式

p1的初始化隐式地要求编译器用一个new返回的int*来创建一个shared_ptr。由于我们不能进行内置指针到智能指针间的隐式转换,因此这条初始化语句是错误的。出于相同的原因,一个返回shared_ptr的函数不能在其返回语句中隐式转换一个普通指针:

shared_ptr<int> clone(int p) {return new int(p); //错误:隐式转换为shared_ ptr<int>
}

我们必须将shared_ ptr 显式绑定到一个想要返回的指针上:

shared_ptr<int> clone(int p) {return shared_ ptr<int> (new int (p));//正确:显式地用int*创建shared ptr<int>
}

默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用delete释放它所关联的对象。我们可以将智能指针绑定到一个指向其他类型的资源的指针上,但是为了这样做,必须提供自己的操作来替代delete。
在这里插入图片描述

不要混合使用普通指针和智能指针…

shared_ptr可以协调对象的析构,但这仅限于其自身的拷贝(也是shared_ ptr)之间。这也是为什么我们推荐使用make_shared 而不是new的原因。这样,我们就能在分配对象的同时就将shared_ptr与之绑定,从而避免了无意中将同一块内存绑定到多个独立创建的shared_ptr上。

考虑下面对shared_ ptr进行操作的函数:

//在函数被调用时ptr被创建并初始化
void process (shared_ _ptr<int> ptr)
//使用ptr
} // ptr离开作用域,被销毁

process的参数是传值方式传递的,因此实参会被拷贝到ptr中。拷贝一个shared_ptr会递增其引用计数,因此,在process运行过程中,引用计数值至少为2。当process结束时,ptr 的引用计数会递减,但不会变为0。因此,当局部变量ptr被销毁时,ptr指向的内存不会被释放。

使用此函数的正确方法是传递给它一一个 shared_ptr:

shared_ptr<int> p(new int(42)) ; // 引用计数为1
process(p); //拷贝p会递增它的引用计数;在process中引用计数值为2
int i=*p;//正确:引用计数值为1

虽然不能传递给process 一个内置指针,但可以传递给它一个(临时的)shared_ptr,这个shared_ptr是用一个内置指针显式构造的。但是,这样做很可能会导致错误:

int *x(new int (1024)) ;//1危险: x是一个普通指针,不是一个智能指针
process(x); // 错误:不能将int*转换为一个shared_ _ptr<int>
process (shared_ptr<int>(x)); // 合法的,但内存会被释放!
int j=*x;//未定义的:x是一个空悬指针!

在上面的调用中,我们将一个临时 shared_ptr 传递给process.当这个调用所在的表达式结束时,这个临时对象就被销毁了。销毁这个临时变量会递减引用计数,此时引用计数就变为0了。因此,当临时对象被销毁时,它所指向的内存会被释放。但x继续指向(已经释放的)内存,从而变成一个空悬指针。如果试图使用x的值,其行为是未定义的。当将一个shared_ptr 绑定到一个普通指针时,我们就将内存的管理责任交给了这个shared_ptr一旦这样做了 ,我们就不应该再使用内置指针来访问shared_ ptr 所指向的内存了。

…也不要使用get初始化另一个智能指针或为智能指针赋值

智能指针类型定义了一个名为get的函数,它返回一个内置指针,指向智能指针管理的对象。此函数是为了这样一种情况而设计的:我们需要向不能使用智能指针的代码传递一个内置指针。使用get返回的指针的代码不能delete此指针。虽然编译器不会给出错误信息,但将另一个智能指针也绑定到get返回的指针上是错误的:

shared ptr<int> p(new int(42)); // 引用计数为1
int *q = p.get(); //正确:但使用q时要注意,不要让它管理的指针被释放
{ //新程序块
//未定义:两个独立的shared_ ptr指向相同的内存
shared_ptr<int> (q) ;
} //程序块结束,q被销毁,它指向的内存被释放
int foo = *p; //未定义: p指向的内存已经被释放了

在本例中,p和q指向相同的内存。由于它们是相互独立创建的,因此各自的引用计数都是1。当q所在的程序块结束时,q被销毁,这会导致q指向的内存被释放。从而p变成一个空悬指针,意味着当我们试图使用p时,将发生未定义的行为。而且,当p被销毁时,这块内存会被第二次delete。

删除器

默认情况下,shared_ptr 假定它们指向的是动态内存。因此,当一个shared_ptr被销毁时,它默认地对它管理的指针进行delete操作。为了用shared_ptr 来管理一个connection,我们必须首先定义一个函 数来代替delete 这个删除器(deleter) 函数必须能够完成对shared_ptr 中保存的指针进行释放的操作。在本例中,我们的删除器必须接受单个类型为connection*的参数:

void end_ connection (connection *p) { disconnect(*p); }

当我们创建一一个shared_ptr时,可以传递一个(可选的)指向删除器函数的参数:

void f (destination &d /*其他参数*/)
connection C = connect (&d) ;
shared_ptr<connection> P(&C, end_connection);
//使用连接
//当f退出时(即使是由于异常而退出),connection会被正确关闭

当p被销毁时,它不会对自己保存的指针执行delete,而是调用end_connection接下来,end_connection会调用disconnect,从而确保连接被关闭。如果f正常退出,那么p的销毁会作为结束处理的一部分。如果发生了异常,p同样会被销毁,从而连接被关闭。

陷阱

智能指针可以提供对动态分配的内存安全而又方便的管理,但这建立在正确使用的前提下。为了正确使用智能指针,我们必须坚持一些基本规范:

  • 不使用相同的内置指针值初始化(或reset)多个智能指针。
  • 不delete get()返回的指针。
  • 不使用get()初始化或reset另一个智能指针。
  • 如果你使用get()返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了。
  • 如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器。

unique_ptr

一个unique_ ptr “拥有”它所指向的对象。与shared_ ptr不同,某个时刻只能有一个unique_ptr指向一个给定对象。当unique_ptr被销毁时,它所指向的对象也被销毁。当我们定义一个unique_ptr 时,需要将其绑定到一个new返回的指针上。类似shared ptr,初始化unique_ ptr必须采用直接初始化形式:

unique_ptr<double> pl; // 可以指向一个double的unique_ ptr
unique_ptr<int> p2 (new int(42)); // p2指向一个值为42的int

由于一个unique_ptr 拥有它指向的对象,因此unique_ptr不支持普通的拷贝或赋值操作:

unique_ptr<string> p1 (new string ("Stegosaurus"));
unique_ptr<string> p2(p1); // 错误: unique_ptr 不支持拷贝
unique_ptr<string> p3;
p3 = p2;//错误: unique_ptr不支持赋值

在这里插入图片描述
虽然我们不能拷贝或赋值unique_ptr,但可以通过调用release或reset将指针的所有权从一个(非const) unique_ptr 转移给另一个unique:

//将所有权从p1 (指向string Stegosaurus)转移给p2
unique_ptr<string> p2(pl.release()); // release 将p1置为空
unique_ptr<string> p3 (new string ("Trex")) ;
//将所有权从p3转移给p2
p2.reset (p3.release()); // reset 释放了p2原来指向的内存

release成员返回unique_ ptr当前保存的指针并将其置为空。因此,p2被初始化为p1原来保存的指针,而p1被置为空。

reset成员接受一个可选的指针参数,令unique_ptr 重新指向给定的指针。如果unique_ ptr不为空,它原来指向的对象被释放。因此,对p2调用reset 释放了用"Stegosaurus"初始化的string所使用的内存,将p3对指针的所有权转移给p2,并将p3置为空。

调用release会切断unique_ptr 和它原来管理的对象间的联系。release返回的指针通常被用来初始化另一个智能指针或给另一个智能指针赋值。在本例中,管理内存的责任简单地从一个智能指针转移给另一个。但是,如果我们不用另一个智能指针来保存release返回的指针,我们的程序就要负责资源的释放:

p2.release();
//错误: p2不会释放内存,而且我们丢失了指针.
auto P = p2.release();
//正确,但我们必须记得delete (p)

传递unique_ptr 参数和返回unique_ptr

不能拷贝unique_ptr的规则有一个例外:我们可以拷贝或赋值一个将要被销毁的unique_ptr。 最常见的例子是从函数返回一个unique_ ptr:

unique_ ptr<int> clone(int p) {
//正确:从int*创建一个unique_ptr<int>return unique_ptr<int> (new int(p)) ;
}

还可以返回一个局部对象的拷贝:

unique_ptr<int> clone(int p) {unique_ptr<int> ret(new int (p)) ;return ret;
}

对于两段代码,编译器都知道要返回的对象将要被销毁。在此情况下,编译器执行一种特殊的“拷贝”。

unique_ptr中的删除器

重载一个unique_ptr中的删除器会影响到unique_ptr类型以及如何构造(或reset)该类型的对象。与重载关联容器的比较操作类似,我们必须在尖括号中unique_ptr 指向类型之后提供删除器类型。在创建或reset一个这种unique_ ptr 类型的对象时,必须提供一个指定类型的可调用对象(删除器):

// p指向一个类型为objT的对象,并使用一个类型为delT的对象释放objT对象
//它会调用一个名为fcn的delT类型对象
unique_ptr<objT, delT> p (new objT, fcn) ;

作为一个更具体的例子,我们将重写连接程序,用unique_ptr 来代替shared_ptr,如下所示:

void f (destination &d /*其他需要的参数*/)
{connection c = connect(&d); // 打开连接//当p被销毁时,连接将会关闭unique_ptr<connection, decltype (end_connection)*>P(&C, end_connection);//使用连接//当f退出时(即使是由于异常而退出), connection会被正确关闭
}

在本例中我们使用了decltype 来指明函数指针类型。由于decltype (end_connection) 返回一个函数类型,所以我们必须添加一个*来指出我们正在使用该类型的一个指针。

weak_ptr

weak_ptr是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象。将一个weak_ptr 绑定到一个shared_ptr不会改变shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr 被销毁,对象就会被释放。即使有weak_ptr指向对象,对象也还是会被释放,因此,weak ptr 的名字抓住了这种智能指针“弱”共享对象的特点。
在这里插入图片描述
当我们创建一个weak_ptr 时,要用一个shared_ptr 来初始化它:

auto p = make_shared<int>(42);
weak_ptr<int> wp(p); // wp 弱共享p; p的引用计数未改变

本例中wp和p指向相同的对象。由于是弱共享,创建wp不会改变p的引用计数: wp指向的对象可能被释放掉。

由于对象可能不存在,我们不能使用weak_ptr直接访问对象,而必须调用lock。此函数检查weak_ptr 指向的对象是否仍存在。如果存在,lock返回一个指向共享对象的shared_ptr。 与任何其他shared_ptr类似,只要此shared_ptr存在,它所指向的底层对象也就会一直存在。 例如:

if (shared_ptr<int> np = wp.lock()) { //如果np不为空则条件成立
//在if中,np与p共享对象
}

在这段代码中,只有当lock调用返回true时我们才会进入if语句体。在if中,使用np访问共享对象是安全的。


http://chatgpt.dhexx.cn/article/gGa91aU8.shtml

相关文章

【C++】智能指针详解

今天我们来讲一下c中的智能指针。 目录 1. 智能指针初识1.1 什么是智能指针1.2 智能指针发展历史1.3 为什么需要智能指针 3. 智能指针原理3.1 RALL3.2 智能指针的分类3.2.1 auto_ptr3.2.2 unique_ptr3.2.3 shared_ptr3.2.3.1 shared_ptr 原理3.2.3.2 shared_ptr 的模拟实现3.2.…

智能指针详解

目录 前言 1、为什么需要智能指针&#xff1f; 2、智能指针的原理 3、智能指针的分类 3.1 auto_ptr 3.2 unique_ptr 3.3 shared_ptr 前言 C11中引入了智能指针的特性&#xff0c;本文将详细介绍智能指针的使用。 1、为什么需要智能指针&#xff1f; 我们来看一段代码&…

【c++复习笔记】——智能指针详细解析(智能指针的使用,原理分析)

&#x1f482; 个人主页:努力学习的少年&#x1f91f; 版权: 本文由【努力学习的少年】原创、在CSDN首发、需要转载请联系博主&#x1f4ac; 如果文章对你有帮助、欢迎关注、点赞、收藏(一键三连)和订阅专栏哦 目录 一. 智能指针的基本概念 二. 智能指针的定义和使用 三. a…

C++ 智能指针 - 全部用法详解

为什么要学习智能指针&#xff1f; 咳咳&#xff0c;这个问题不是问大家的&#xff0c;是询问我自己的&#xff01; 我依稀记得刚离校出来找实习工作那会儿(2020年7月)&#xff0c;去面试一份工作&#xff0c;面试官问了我许多问题&#xff0c;其中有一个问题就是问&#xff1a…

AndroidStudio编译时 CXX1405 错误解决

AndroidStudio 编译带C的native库项目时报错&#xff0c; [CXX1405] error when building with cmake using D:\WorkSpace\AndroidXXXX\app\src\main\cpp\CMakeLists.txt: Build command failed.解决方法&#xff1a; cmd 到 Android SDK的cmake路径下&#xff0c;执行 cmake …

硬件速攻-AT24CXX存储器

AT24C02是什么&#xff1f; AT24CXX是存储芯片&#xff0c;驱动方式为IIC协议 实物图&#xff1f; 引脚介绍&#xff1f; A0 地址设置角 可连接高电平或低电平 A1 地址设置角 可连接高电平或低电平 A2 地址设置角 可连接高电平或低电平 1010是设备前四位固定地址 &#xf…

【dbus-cxx】libsigc++ 和 dbus-cxx 在 Ubuntu 中的编译和配置

文章目录 参考资料配置环境cmakelibsigcdbus-cxx 例程server.cppclient.cppMakefile运行结果 此帖作为自己学习记录使用&#xff0c;尚未使用交叉编译&#xff0c;仅在本地使用 参考资料 需先了解DBUS基础知识&#xff0c;建议看DBUS官方文档 DBUS-CXX也是建议看官方文档&…

正点原子IIC例程讲解笔记(三)——24cxx.c中函数理解

目录 一、24C02 简介 二、在 AT24CXX 指定地址写入一个数据&#xff1a; 三、在ATC24XX指定地址读出一个数据 四、检查AT24CXX是否正常&#xff1a;u8 AT24CXX_Check(void) 五、在 AT24CXX 里面的指定地址开始写入长度为 Len 的数据 六、在 AT24CXX 里面的指定地址开始读出…

windows用VS2019下编译log4cxx日志库

一、下载相关库文件 获取log4cxx源码包&#xff1a;http://logging.apache.org/log4cxx/index.html 获取依赖库apr和apr-util源码包:http://archive.apache.org/dist/apr/apr-1.2.11-win32-src.zip http://archive.apache.org/dist/apr/apr-util-1.2.10-win32-src.zip 编译apr…

log4cxx编译

本人进行过win7 64位操作系统和win10家庭版的log4cxx编译&#xff0c;使用的是vs2015&#xff0c;下面是详情。 1.sed下载 sed-4.2.1-bin.zip、sed-4.2.1-dep.zip下载地址&#xff1a;http://gnuwin32.sourceforge.net/packages/sed.htm 下载后&#xff0c;将sed的两个压缩包解…

【RT-Thread Master】at24cxx软件包使用笔记

硬件介绍 RT-Thread版本&#xff1a;V4.1.0软件包名称&#xff1a;at24cxxMCU型号&#xff1a;AT32F407VET7EEPROM型号&#xff1a;AT24C16 使用说明 1、使用menuconfig将软件包添加进入工程&#xff0c;路径如下所示。 2、把IIC总线打开&#xff0c;这里使用软件IIC&#…

linux下编译和安装log4cxx,ubuntu下log4cxx安装使用

需要安装log4cxx&#xff0c;安装的过程中可是充满了坎坷。。。最大的问题是在make log4cxx时&#xff0c;总是报undefined XML什么什么的错误&#xff0c;查了一下也没解决了&#xff0c;然后把apr-utils删了重新装了一下就好了。。 log4cxx现在是apache的一个项目&#xff0c…

linux下编译和安装log4cxx,RedHat如何安装log4cxx日志库

log4cxx日志库是一种动态库&#xff0c;用于记录c的日志&#xff0c;那么RedHat系统下要如何安装log4cxx日志库呢&#xff1f;下面小编就给大家介绍下RedHat安装log4cxx日志库的步骤&#xff0c;感兴趣的朋友不妨来了解下吧。 首先&#xff0c;我得到信息&#xff0c;安装这个库…

AT24Cxx读写全面理解

AT24Cxx - 电可擦可写E2PROM 芯片介绍 基础介绍\引脚介绍 AT24Cxx系列EEPROM是由美国Mcrochip公司出品&#xff0c;1-512K位的支持I2C总线数据传送协议的串行CMOS E2PROM&#xff0c;可用电擦除&#xff0c;可编程自定时写周期&#xff08;包括自动擦除时间不超过10ms&#…

mongodb-cxx-driver使用

mongocxx driver 是构建在 MongoDB C driver 之上的 1.首先需要安装mongo-c-driver wget https://github.com/mongodb/mongo-c-driver/releases/download/ 1.23.1/mongo-c-driver-1.23.1.tar.gz tar xzf mongo-c-driver-1.23.1.tar.gz cd mongo-c-driver-1.23.1 mkdir cmak…

老胡的周刊(第095期)

老胡的信息周刊[1]&#xff0c;记录这周我看到的有价值的信息&#xff0c;主要针对计算机领域&#xff0c;内容主题极大程度被我个人喜好主导。这个项目核心目的在于记录让自己有印象的信息做一个留存以及共享。 &#x1f3af; 项目 tabby[2] 自托管的 AI 编码助手&#xff0c;…

程序员养生指北

吴小胖第八次推送 阅读时间预计3分钟~ 熬夜篇 互联网人熬夜是不能避免的&#xff0c;原因却各不相同。 不加班的时候&#xff0c;总会对自己说&#xff0c;今天一定早睡&#xff0c;然鹅... 午休篇 熬夜的程序员总想依靠午休补觉&#xff0c;然鹅... 更不幸的是&#xff0c;互联…

老杨说运维 | 中国IT运维市场的现状与趋势

文章内容来源《第一新声》 对擎创科技CEO杨辰(老杨)的专访 前言&#xff1a; 中国目前正面临百年未有之大变局&#xff0c;在这个变局中&#xff0c;不稳定性和不确定性正在增强。疫情持续反复、国际形势变化多端&#xff0c;导致国内多个行业出现发展增速下降、产供销节奏打…

老杨说运维 | 非常重要,事关转型

《荀子》有云&#xff1a;“水能载舟&#xff0c;亦能覆舟。”在公司日常运营过程中&#xff0c;数据指标就像是水&#xff0c;孕育着生命&#xff0c;承载着万物。科学的数据指标能指引公司在正确的道路上不断前进&#xff0c;使平淡无常的业务焕发新生&#xff0c;而不合理的…

学习springcloud的一些心得体会——老卫的天气预报系统

1&#xff1a;建立天气预报springboot系统 首先先建立一个天气预报的springboot系统&#xff0c;具体流程如下&#xff1a; &#xff08;1&#xff09;从cityList.xml中获取城市信息&#xff0c; &#xff08;2&#xff09;然后根据下面的链接获取各个城市的天气预报信息&am…