【重学C++】02 脱离指针陷阱:深入浅出 C++ 智能指针

前言

大家好,今天是【重学C++】系列的第二讲,我们来聊聊C++的智能指针。

为什么需要智能指针

在上一讲《01 C++如何进行内存资源管理》中,提到了对于堆上的内存资源,需要我们手动分配和释放。管理这些资源是个技术活,一不小心,就会导致内存泄漏。

我们再给两段代码,切身体验下原生指针管理内存的噩梦。

void foo(int n) {
    int* ptr = new int(42);
    ...
    if (n > 5) {
	    return;
    }
    ...
    delete ptr;
}


void other_fn(int* ptr) {
	...
};
void bar() {
    int* ptr = new int(42);
    other_fn(ptr);
    // ptr == ?
}

foo函数中,如果入参n > 5, 则会导致指针ptr的内存未被正确释放,从而导致内存泄漏。

bar函数中,我们将指针ptr传递给了另外一个函数other_fn,我们无法确定other_fn有没有释放ptr内存,如果被释放了,那ptr将成为一个悬空指针,bar在后续还继续访问它,会引发未定义行为,可能导致程序崩溃。

上面由于原生指针使用不当导致的内存泄漏、悬空指针问题都可以通过智能指针来轻松避免。

C++智能指针是一种用于管理动态分配内存的指针类。基于RAII设计理念,通过封装原生指针实现的。可以在资源(原生指针对应的对象)生命周期结束时自动释放内存。

C++标准库中,提供了两种最常见的智能指针类型,分别是std::unique_ptrstd::shared_ptr
接下来我们分别详细展开介绍。

吃独食的unique_ptr

std::unique_ptr 是 C++11 引入的智能指针,用于管理动态分配的内存。每个 std::unique_ptr 实例都拥有对其所包含对象的唯一所有权,并在其生命周期结束时自动释放对象。

创建unique_ptr对象

我们可以std::unique_ptr的构造函数或std::make_unique函数(C++14支持)来创建一个unique_ptr对象,在超出作用域时,会自动释放所管理的对象内存。示例代码如下:

#include <memory>
#include <iostream>
class MyClass {
public:
    MyClass() {
        std::cout << "MyClass constructed" << std::endl;
    }

    ~MyClass() {
        std::cout << "MyClass destroyed" << std::endl;
    }
};
int main() {
	std::unique_ptr<MyClass> ptr1(new MyClass);
	// C++14开始支持std::make_unique
    std::unique_ptr<int> ptr2 = std::make_unique<int>(10);
    return 0;
}

代码输出:

MyClass constructed
MyClass destroyed
访问所管理的对象

我们可以像使用原生指针的方式一样,访问unique_ptr所指向的对象。也可以通过get函数获取到原生指针。

MyClass* naked_ptr = ptr1.get();
std::cout << *ptr2 << std::endl; // 输出 10

释放/重置所管理的对象

使用reset函数可以释放unique_ptr所管理的对象,并将其指针重置为nullptr或指定的新指针。reset`大概实现原理如下

template<class T> 
void unique_ptr<T>::reset(pointer ptr = pointer()) noexcept { 
	// 释放指针指向的对象
	delete ptr_; 
	// 重置指针
	ptr_ = ptr;
}

该函数主要完成两件事:

  1. 释放 std::unique_ptr 所管理的对象,以避免内存泄漏。
  2. std::unique_ptr 重置为nullptr或管理另一个对象。

code show time:

#include <iostream>
#include <memory>

class MyClass {
public:
    MyClass() {
        std::cout << "MyClass constructed" << std::endl;
    }

    ~MyClass() {
        std::cout << "MyClass destroyed" << std::endl;
    }
};

int main() {
    // 创建一个 std::unique_ptr 对象,指向一个 MyClass 对象
    std::unique_ptr<MyClass> ptr(new MyClass);

    // 调用 reset,将 std::unique_ptr 重置为管理另一个 MyClass 对象
    ptr.reset(new MyClass);
    return;
}

移动所有权

一个对象资源只能同时被一个unique_ptr管理。当尝试把一个unique_ptr直接赋值给另外一个unique_ptr会编译报错。

#include <memory>
int main() {
    std::unique_ptr<int> p1 = std::make_unique<int>(42);
    std::unique_ptr<int> p2 = p1; // 编译报错
    return 0;
}

为了把一个 std::unique_ptr 对象的所有权移动到另一个对象中,我们必须配合std::move移动函数。

#include <memory>
#include <iostream>
int main() {
    std::unique_ptr<int> p1 = std::make_unique<int>(42);
    std::unique_ptr<int> p2 = std::move(p1); // ok
    std::cout << *p2 << std::endl; // 42
    std::cout << (p1.get() == nullptr) << std::endl; // true
    return 0;
}

这个例子中, 我们把p1通过std::move将其管理对象的所有权转移给了p2, 此时p2接管了对象,而p1不再拥有管理对象的所有权,即无法再操作到该对象了。

乐于分享的shared_ptr

shared_ptr是C++11提供的另外一种常见的智能指针,与unique_ptr独占对象方式不同,shared_ptr是一种共享式智能指针,允许多个shared_ptr指针共同拥有同一个对象,采用引用计数的方式来管理对象的生命周期。当所有的 shared_ptr 对象都销毁时,才会自动释放所管理的对象。

创建shared_ptr对象

同样的,C++也提供了std::shared_ptr构造函数和std::make_shared函数来创建std::shared_ptr对象。

#include <memory>
int main() {
	std::shared_ptr<int> p1(new int(10));
	std::shared_ptr<int> p2 = std::make_shared<int>(20);
	return;
}
多个shared_ptr共享一个对象

可以通过赋值操作实现多个shared_ptr共享一个资源对象,例如

std::shared_ptr<int>p3 = p2;

shared_ptr采用引用计数的方式管理资源对象的生命周期,通过分配一个额外内存当计数器。

当一个新的shared_ptr被创建时,它对应的计数器被初始化为1。每当赋值给另外一个shared_ptr共享同一个对象时,计数器值会加1。当某个shared_ptr被销毁时,计数值会减1,当计数值变为0时,说明没有任何shared_ptr引用这个对象,会将对象进行回收。

image.png

C++提供了use_count函数来获取std::shared_ptr所管理对象的引用计数,例如

std::cout << "p1 use count: " << p1.use_count() << std::endl;
释放/重置所管理的对象

可以使用reset函数来释放/重置shared_ptr所管理的对象。大概实现原理如下(不考虑并发场景)

void reset(T* ptr = nullptr) {
	if (ref_count != nullptr) { 
		(*ref_count)--;
		if (*ref_count == 0) { 
			delete data; 
			delete ref_count; 
		} 
	} 
	data = ptr; 
	ref_count = (data == nullptr) ? nullptr : new size_t(1); 
}

data指针来存储管理的资源,指针ref_count 来存储计数器的值。

在 reset 方法中,需要减少计数器的值,如果计数器减少后为 0,则需要释放管理的资源,如果减少后不为0,则不会释放之前的资源对象。

如果reset指定了新的资源指针,则需要重新设置 data 和 ref_count,并将计数器初始化为 1。否则,将计数器指针置为nullptr

shared_ptr使用注意事项

避免循环引用

由于 shared_ptr 具有共享同一个资源对象的能力,因此容易出现循环引用的情况。例如:

struct Node { 
	std::shared_ptr<Node> next; 
};

int main() {
	std::shared_ptr<Node> node1(new Node);
	std::shared_ptr<Node> node2(new Node); 
	node1->next = node2; 
	node2->next = node1;
}

在上述代码中,node1node2 互相引用,在析构时会发现计数器的值不为0,不会释放所管理的对象,产生内存泄漏。

为了避免循环引用,可以将其中一个指针改为 weak_ptr 类型。weak_ptr也是一种智能指针,通常配合shared_ptr一起使用。

weak_ptr是一种弱引用,不对所指向的对象进行计数引用,也就是说,不增加所指对象的引用计数。当所有的shared_ptr都析构了,不再指向该资源时,该资源会被销毁,同时对应的所有weak_ptr都会变成nullptr,这时我们就可以利用expired()方法来判断这个weak_ptr是否已经失效。

我们可以通过weak_ptrlock()方法来获得一个指向共享对象的shared_ptr。如果weak_ptr已经失效,lock()方法将返回一个空的shared_ptr

下面是weak_ptr的基本使用示例:

#include <iostream>
#include <memory>

int main() {
    std::shared_ptr<int> sp = std::make_shared<int>(42);
    // 创建shared_ptr对应的weak_ptr指针
    std::weak_ptr<int> wp(sp);

	// 通过lock创建一个对应的shared_ptr
    if (auto p = wp.lock()) {
        std::cout << "shared_ptr value: " << *p << std::endl;
        std::cout << "shared_ptr use_count: " << p.use_count() << std::endl;
    } else {
        std::cout << "wp is expired" << std::endl;
    }

	// 释放shared_ptr指向的资源,此时weak_ptr失效
    sp.reset();
    std::cout << "wp is expired: " <<  wp.expired() << std::endl;
    return 0;
}

代码输出如下

shared_ptr value: 42
shared_ptr use_count: 2
wp is expired: 1

回到shared_ptr的循环引用问题,利用weak_ptr不会增加shared_ptr的引用计数的特点,我们将Node.next的类型改为weak_ptr, 避免node1和node2互相循环引用。修改后代码如下

```cpp
struct Node { 
	std::weak_ptr<Node> next; 
};

int main() {
	std::shared_ptr<Node> node1(new Node);
	std::shared_ptr<Node> node2(new Node); 
	node1->next = std::weak_ptr<Node>(node2); 
	node2->next = std::weak_ptr<Node>(node1); ;
}
避免裸指针与shared_ptr混用

先看看以下代码

int* q = new int(9);
{
	std::shared_ptr<int> p(new int(10));
	...
	q = p.get();
}
std::cout << *q << std::endl;

get函数返回 std::shared_ptr 所持有的指针,但是不会增加引用计数。所以在shared_ptr析构时,将该指针指向的对象给释放掉了,导致指针q变成一个悬空指针。

避免一个原始指针初始化多个shared_ptr
int* p = new int(10);
std::shared_ptr<int> ptr1(p);
// error: 两个shared_ptr指向同一个资源,会导致重复释放
std::shared_ptr<int> ptr2(p);

总结

避免手动管理内存带来的繁琐和容易出错的问题。我们今天介绍了三种智能指针:unique_ptrshared_ptrweak_ptr
每种智能指针都有各自的使用场景。unique_ptr用于管理独占式所有权的对象,它不能拷贝但可以移动,是最轻量级和最快的智能指针。shared_ptr用于管理多个对象共享所有权的情况,它可以拷贝和移动。weak_ptr则是用来解决shared_ptr循环引用的问题。

下一节,我们将自己动手,从零实现一个C++智能指针。敬请期待!

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

新媒体运营十大能力,让品牌闻达天下!

" 现在新媒体蓬勃发展&#xff0c;很多品牌都有新媒体运营这个岗位。新媒体运营好的话&#xff0c;可以提高公司品牌曝光、影响力。那新媒体运营具备什么能力&#xff0c;才能让品牌知名度如虎添翼呢&#xff1f;" 信息收集能力 在移动互联网时代&#xff0c;信息的…

你应该用USB还是或耳机插孔连接你的电脑耳机?这里有详细解释

​如果你有一套有线耳机&#xff0c;可以通过USB连接或传统耳机插孔连接到电脑&#xff0c;你可能想知道使用这两个端口选项的实际区别。 使用耳机插孔 如果你使用传统的模拟电缆将耳机连接到计算机&#xff08;或任何带耳机插孔的设备&#xff09;&#xff0c;则耳机的作用与…

英码科技算能系列边缘计算盒子再添新成员!搭载TPU处理器BM1688CV186AH,功耗更低、接口更丰富

在数据呈现指数级增长的今天&#xff0c;越来越多的领域和细分场景对实时、高效的数据处理和分析的需求日益增长&#xff0c;对智能算力的需求也不断增强。为应对新的市场趋势&#xff0c;英码科技凭借自身的硬件研发优势&#xff0c;携手算能相继推出了基于BM1684的边缘计算盒…

Google Earth Engine(GEE)深度学习入门教程-Python数据读入篇

Python数据读入篇 前置条件&#xff1a; GEE预处理影像导出保存为tfrecord的数据包&#xff0c;并下载到本地tensorflow的深度学习环境 本篇文章的目的主要是把Tfrecord格式的数据加载为tf可使用的数据集格式 设定超参数 首先需要设定导出时的波段名称和数据格式&#xff…

linux 排查java内存溢出(持续更新中)

场景 tone.jar 启动后内存溢出,假设pid 为48044 排查 1.确定java程序的pid(进程id) ps 或 jps 都可以 ps -ef | grep tone jps -l 2.查看堆栈信息 jmap -heap 48044 3.查看对象的实例数量显示前30 jmap -histo:live 48044 | head -n 30 4.查看线程状态 jstack 48044

于ThinkPHP开发的赛事报名小程序

基于ThinkPHP开发的赛事报名微信小程序 功能包括 1、参赛公告 2、会员中心&#xff08;会员注册、登录、成绩查询、资料管理、参赛记录管理&#xff09; 3、个人报名和企业报名 &#xff08;身份证验证防止重复报名&#xff09; 4、培训报名 5、查询是否在库人员&#xff0c;根…

深度学习之Pytorch框架垃圾分类智能识别系统

欢迎大家点赞、收藏、关注、评论啦 &#xff0c;由于篇幅有限&#xff0c;只展示了部分核心代码。 文章目录 一项目简介 二、功能三、系统四. 总结 一项目简介 一、项目背景 随着城市化进程的加快和人们环保意识的提高&#xff0c;垃圾分类已成为城市管理的重要一环。然而&am…

git拉取项目前需要操作哪些?

1.输入 $ ssh-keygen -t rsa -C "秘钥说明" 按enter键 2.出现 ssh/id_rsa&#xff1a;(输入也可以不输入也可以) 然后按enter键 3.出现empty for no passphrase&#xff1a;(输入也可以不输入也可以) 然后按enter键 4.出现same passphrase again: (输入也可以不输入也…

4. C++入门:内联函数、auto关键字、范围for及nullptr

内联函数 概念 以inline修饰的函数叫做内联函数&#xff0c;编译时C编译器会在调用内联函数的地方展开&#xff0c;没有函数调用建立栈帧的开销&#xff0c;内联函数提升程序运行的效率 对比C的宏 C语言不足&#xff1a;宏 #define ADD(x, y) ((x)(y))int main() {int ret…

java中的HashSet类

一、HashSet类 实现了Set接口&#xff0c;无法存储重复元素 特点&#xff1a;元素位置无序、无索引、底层是HashMap 1、构造方法 内部是HashMap的构造方法 2、add方法 (1)元素在底层存储使用到了三种数据结构&#xff1a;hash数组、链表、树 (2)添加流程&#xff08;根据…

arcgis 10.6 工具栏操作error 001143 后台服务器抛出异常

arcgis 10.6 工具栏操作error 001143 后台服务器抛出异常 环境 win10arcgis 10.6 问题 执行定义投影要素转线出现 Error: 001143:后台服务器抛出异常&#xff08;差点重装10.6&#xff09; 如下图所示&#xff1a; 解决方法 通过在菜单工具条上单击地理处理 > 地理处…

计算机网络学习小结_数据链路层

数据链路和帧 帧&#xff1a;数据链路层传输基本单元。链路层将网络层传过来的数据构成帧发到链路上&#xff0c;并将发到链路层的帧取出数据交给网络层 数据报/分组/包&#xff1a;网络层传输基本单元 三个基本问题 即封装成帧、透明传输、差错检测 封装成帧 概念&#…

尽微好物:从0到10亿+的抖音电商的TOP1“联盟团长”,如何使用NineData实现上云下云

杭州尽微供应链是抖⾳平台⽉均带货10E的TOP1“联盟团⻓”&#xff0c;是字节跳动⼀级代理商&#xff0c;巨量千川指定服务商&#xff0c;拥有商品库9万&#xff0c;是⾏业领先的电商供应链平台&#xff0c;达⼈陪跑机构。 杭州尽微供应链以天猫、京东抖音电商业务为依托&#x…

29.IO流(了解)

1. C语言的输入与输出 ​ C语言中我们用到的最频繁的输入输出方式就是scanf ()与printf()。 scanf(): 从标准输入设备(键 盘)读取数据&#xff0c;并将值存放在变量中。printf(): 将指定的文字/字符串输出到标准输出设备(屏幕)。注意宽度输出和精度输出控制。C语言借助了相应的…

Python全栈自动化测试-Python基础08--循环结构-while

目录 前言 一、while循环 二、while条件 1.while条件--固定值 2.while条件-运算符 三、避免死循环 1.条件不为恒定值 2.条件为恒定值 2.1.避免死循环-continue 2.2.避免死循环-break 2.3.避免死循环-breakcontinue 总结 前言 在Python编程中&#xff0c;循环结构是…

安科瑞汽车充电桩防火限流式保护器灭弧保护

充电桩防火限流式保护器 短路过载保护 充电桩防火限流式保护器 短路过载保护可有效克服传统断路器、空气开关和监控设备存在的短路电流大、切断短路电流时间长、短路时产生的电弧火花大&#xff0c;以及使用寿命短等弊端&#xff0c;发生短路故障时&#xff0c;能以微秒级速度…

软件工程期末复习(5)可行性研究

可行性研究 可行性研究的任务 可行性研究的目的&#xff1a; 用最小的代价&#xff0c;在尽可能短的时间内确定问题是否能够解决。 可行性研究的实质&#xff1a; 就是一次压缩、简化了的系统分析和设计的过程。 可行性研究的路线&#xff1a; 分析和澄清问题定义&#…

​学者观察 | 从区块链应用创新看长安链发展——CCF区块链专委会荣誉主任斯雪明

导语 2024年1月27日&#xff0c;斯雪明教授在长安链发布三周年庆暨生态年会上发表演讲&#xff0c;认为在区块链发展过程中&#xff0c;不仅需要技术创新&#xff0c;同时需要有价值、有特色、有示范意义的应用创新。斯雪明教授介绍了国内区块链技术与应用发展的现状、趋势与挑…

1721jsp网上银行管理系统Myeclipse开发mysql数据库web结构java编程计算机网页项目

一、源码特点 JSP 网上银行管理系统 是一套完善的web设计系统&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统采用web模式&#xff0c;系统主要采用B/S模式开发。开发环境为TOMCAT7.0,Myeclipse8.5开发&#xff0c;…

官宣:极海G32A1445汽车通用MCU通过TÜV莱茵ISO 26262 ASIL-B功能安全产品认证

2024年5月16日&#xff0c;极海宣布G32A1445汽车通用MCU正式通过德国TV莱茵ISO 26262 ASIL-B功能安全产品认证。 德国TV莱茵大中华区工业服务与信息安全总经理赵斌先生、德国莱茵TV助理大客户经理詹丽龙女士&#xff0c;珠海极海半导体有限公司总经理汪栋杰先生、副总经理曾豪…