【重学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_uniquestd::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); // okstd::cout << *p2 << std::endl; // 42std::cout << (p1.get() == nullptr) << std::endl; // truereturn 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_ptrif (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++智能指针。敬请期待!

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

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

相关文章

正点原子LWIP学习笔记(一)lwIP入门

lwIP入门 一、lwIP简介&#xff08;了解&#xff09;二、lwIP结构框图&#xff08;了解&#xff09;三、如何学习lwIP&#xff08;熟悉&#xff09; 一、lwIP简介&#xff08;了解&#xff09; lwIP是一个小型开源的TCP/IP协议栈 阉割的TCP/IP协议 TCP/IP协议栈结构&#xff0…

C语言游戏实战(12):植物大战僵尸(坤版)

植物大战僵尸 前言&#xff1a; 本游戏使用C语言和easyx图形库编写&#xff0c;通过这个项目我们可以深度的掌握C语言的各种语言特性和高级开发技巧&#xff0c;以及锻炼我们独立的项目开发能力&#xff0c; 在开始编写代码之前&#xff0c;我们需要先了解一下游戏的基本规则…

基础2 JAVA图形编程桌面:探索图形程序的抽象实现

嘿&#xff0c;大家好&#xff01;我非常高兴又一次有机会与大家相聚&#xff0c;分享新的知识和经验。对于热爱编程和探索新技术的朋友们来说&#xff0c;今天的内容绝对不容错过。我为大家准备了一个详尽的视频教程&#xff1a;《基础2 JAVA 图形编程&#xff1a;主程序调用…

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: (输入也可以不输入也…

20240516-Flyme AIOS 特种兵发布会

目录 1 Flyme AIOS 2 路演功能 2.1 拖拽流转 2.2 任务剧本自定义 2.3 智能体商店 2.4 实况通知 2.5 AI壁纸 3 MYVU 3.1 翻译功能 3.2 AR导航-骑行 3.3 AI语音转文字-科技向善 3.4 Flyme AR-提词器增强 1 Flyme AIOS 1&#xff09;目标&#xff1a;All in AI&#…

AI绘图Stable Diffusion,如何无损高清放大图片,保姆级教程建议收藏!

前言 我们在用 stable diffusion 制作AI图片时&#xff0c;默认生成图片的尺寸为512*512&#xff0c;即使是竖图一般也就是512*768&#xff0c;如果再把尺寸设置大一些&#xff0c;就会因为硬件算力不够而造成系统崩溃&#xff0c;今天就来跟大家聊一聊&#xff0c;如何将制作…

RocketMQ-Dashboard 控制台使用详解

1 安装部署 具体部署启动请参考&#xff1a;RocketMQ从安装、压测到运维一站式文档_rocketmq benchmark压测-CSDN博客 RocketMq的dashboard&#xff0c;有运维页面&#xff0c;驾驶舱&#xff0c;集群页面&#xff0c;主题页面&#xff0c;消费者页面&#xff0c;生产者页面&…

【Kubenetes】边缘计算KubeEdge架构设计详解

文章目录 前言KubeEdge云边通信方式云端架构设计EdgeController:云到边&#xff1a;边到云 DeviceController:云到边边到云 边缘端架构设计EdgedPod的管理部分Pod的监控部分Pod的卷管理Pod的垃圾回收Pod同步管理 MetaMangger从云到边缘的更新 (Update From Cloud To Edge)从边缘…

Covalent长期数据设施,支持基于 “blob” 、总锁仓54亿美元的L2

Covalent Network&#xff08;CQT&#xff09;是领先的历史数据可用性网络&#xff0c;通过其在 Web3 中超过 225 个区块链上的结构化数据基础设施&#xff0c;为数千名客户和开发人员提供支持。Covalent Network&#xff08;CQT&#xff09;正在与未来以太坊的进步需求相匹配&…

SQL慢查询学习篇

https://www.cnblogs.com/isyues/p/17733015.html 1. 对扫到的SQL慢查询语句执行 explain explain select task_id, channel, count(task_id) as count from tablename where send_time > "2024-05-10 16:13:59" and send_time < "2024-05-14 16:13:59…

api接口、api文档、api调试、api测试

应用程序接口是一组定义、程序及协议的集合&#xff0c;通过 API 接口实现计算机软件之间的相互通信。API 的一个主要功能是提供通用功能集。程序员通过调用 API 函数对应用程序进行开发&#xff0c;可以减轻编程任务。 API 同时也是一种中间件&#xff0c;为各种不同平台提供数…

展馆展厅设计施工流程

1、需求分析和确定&#xff1a; 与客户沟通&#xff0c;了解客户需求&#xff0c;对展馆展厅的用途、面积、功能、展品特点等进行分析&#xff0c;并确定设计方案。 2、方案设计 根据需求确定设计方案&#xff0c;包括平面布局、展品陈列、展示方式、照明等。设计师需要提供设计…

如何在Spring启动的时候执行一些操作

如何在Spring启动的时候执行一些操作 在Spring启动的时候执行一些操作有多种方式。你可以通过实现ApplicationRunner或者CommandLineRunner接口&#xff0c;在Spring Boot应用程序启动后执行特定操作。另外&#xff0c;你也可以使用PostConstruct注解&#xff0c;在Spring Bea…

【考研数学】张宇《1000题》强化阶段正确率多少算合格?

张宇1000题真的很练人心态.... 基础不好&#xff0c;建议别碰1000题 基础好&#xff0c;1000题建议在两个月以内刷完 如果自己本身在基础阶段学的比较水&#xff0c;自己的薄弱点刷了一小部分题没有针对性完全解决&#xff0c;转身去刷1000题就会发现&#xff0c;会的题目刷…

shell脚本-重定向与管道符

一、重定向 因为shell脚本有着批量操作的特殊性&#xff0c;大部分操作处于后台执行&#xff0c;不需要用户进行干预&#xff0c;所以提取、过滤并执行信息十分需要重定向和管道。重定向的意思是不输出到默认设备上&#xff0c;而是输出到你指定的位置&#xff08;文件、其他输…

1.微信小程序开发之准备工作

1.微信小程序账号注册 小程序开发 与 网页开发不一样&#xff0c;在开始微信小程序开发之前&#xff0c;需要访问 微信公众平台&#xff0c;注册一个微信小程序账号。 在拥有了小程序的账号以后&#xff0c;我们才可以开发和管理小程序&#xff0c;后续可以通过该账号进行开发…

国网电力分公司、税务企业如何向央媒投稿?

税务、电力、银行等单位如果想要将稿件发布到中央媒体&#xff0c;可以遵循为大家整理的以下步骤和建议&#xff1a; 了解央媒的定位与要求&#xff1a;中央媒体&#xff0c;如新华社、人民日报、中央电视台等&#xff0c;都有其独特的报道风格和关注重点。在投稿前&#xff0…

【Web后端】会话跟踪技术及过滤器

1.会话跟踪技术 1.1 会话的概念 在web应用中&#xff0c;浏览器和服务器在一段时间内发送请求和响应的连续交互的全过程 1.2 会话跟踪概念 对同一个用户跟服务器的连续请求和接收响应的监视过程 1.3 会话跟踪作用 浏览器和服务器是以http协议进行通信&#xff0c;http协议是…

算法工程师面试问题 | YOLOv8面试考点原理全解析(一)

本文给大家带来的百面算法工程师是深度学习目标检测YOLOv8面试总结&#xff0c;文章内总结了常见的提问问题&#xff0c;旨在为广大学子模拟出更贴合实际的面试问答场景。在这篇文章中&#xff0c;我们还将介绍一些常见的深度学习目标检测面试问题&#xff0c;并提供参考的回答…

AWS RDS ElasticCache 监控可观测最佳实践

在当今的电子商务时代&#xff0c;一个高效、稳定的电商平台对于保持竞争力至关重要。数据库作为电商平台的核心支撑&#xff0c;其性能直接影响到用户体验和业务流畅度。本文将深入探讨如何在电商场景下通过观测云对亚马逊云科技 RDS&#xff08;MySQL&#xff09; 和 Elastic…