C++ 智能指针内存泄漏问题

shared_ptr相互嵌套导致循环引用

代码示例

#include <iostream>
#include <memory>
using namespace std;class B;class A {
public:std::shared_ptr<B> b_ptr;~A() { std::cout << "A destroyed\n"; }
};class B {
public:std::shared_ptr<A> a_ptr;~B() { std::cout << "B destroyed\n"; }
};int main() {// 创建 shared_ptr 对象auto a = std::make_shared<A>();auto b = std::make_shared<B>();// 相互引用a->b_ptr = b;b->a_ptr = a;cout<<"use_count of a:"<<a.use_count()<<endl;cout<<"use_count of b:"<<b.use_count()<<endl;return 0;
}

解释说明

  1. 创建了两个 std::shared_ptr 对象 a 和 b
  2. a 持有 b 的 shared_ptrb 持有 a 的 shared_ptr
  3. 当 main 函数结束时,a 和 b 的引用计数不会减少到零,因此它们的析构函数不会被调用。
  4. 导致内存泄漏,因为对象 A 和 B 的内存不会被释放。

 解决方法

为了避免这种循环引用的问题,可以使用 std::weak_ptrstd::weak_ptr 是一种弱智能指针,它不会增加对象的引用计数。它可以用来打破循环引用,从而防止内存泄漏。

#include <iostream>
#include <memory>
using namespace std;
class B;  // 先声明类 B,使得 A 和 B 可以互相引用。class A {
public:std::shared_ptr<B> b_ptr; // A 拥有 B 的强引用~A() { std::cout << "A destroyed\n"; }
};class B {
public:std::weak_ptr<A> a_ptr; // B 拥有 A 的弱引用~B() { std::cout << "B destroyed\n"; }void safeAccess() {// 尝试锁定 a_ptr 获取 shared_ptrif (auto a_shared = a_ptr.lock()) {// 安全访问 a_shared 对象std::cout << "Accessing A from B\n";} else {std::cout << "A is already destroyed, cannot access A from B\n";}}
};int main() {// 创建 shared_ptr 对象auto a = std::make_shared<A>();auto b = std::make_shared<B>();// 互相引用a->b_ptr = b;b->a_ptr = a;// 安全访问b->safeAccess();cout<<"use_count of a:"<<a.use_count()<<endl;cout<<"use_count of b:"<<b.use_count()<<endl;return 0; // 在这里,a 和 b 的引用计数将会正确地减少到零,并且它们将会被销毁。
}

shared_ptr的层次使用没有导致循环引用

shared_ptr<vector<shared_ptr<pair<string, shared_ptr<string>>>>> jsFiles;

这个声明表示 jsFiles 是一个 std::shared_ptr,它指向一个 std::vector,向量中的每个元素是一个 std::shared_ptr,指向一个 std::pair 对象,而这个 std::pair 对象中包含一个 std::string 和一个 std::shared_ptr<std::string>。它们之间只是层次结构,没有跨层次的相互引用 。也就是说没有内存泄漏的问题。证明如下:

#include <iostream>
#include <vector>
#include <memory>
#include <string>using namespace std;
// 自定义 String 类,模拟 std::string
class MyString {
public:std::string data;MyString(const std::string& str) : data(str) {std::cout << "MyString created: " << data << std::endl;}~MyString() {std::cout << "MyString destroyed: " << data << std::endl;}// 添加输出操作符重载friend std::ostream& operator<<(std::ostream& os, const MyString& myStr) {os << myStr.data;return os;}
};// 自定义 Pair 类,模拟 std::pair
template<typename K, typename V>
class MyPair {
public:K first;V second;MyPair(const K& key, const V& value) : first(key), second(value) {std::cout << "MyPair created: {" << first << ", " << *second << "}" << std::endl;}~MyPair() {std::cout << "MyPair destroyed: {" << first << ", " << *second << "}" << std::endl;}
};int main() {// 创建 jsFiles,它是一个 shared_ptr,指向 vectorauto jsFiles = std::make_shared<std::vector<std::shared_ptr<MyPair<std::string, std::shared_ptr<MyString>>>>>();// 添加元素auto innerPair1 = std::make_shared<MyPair<std::string, std::shared_ptr<MyString>>>("file1", std::make_shared<MyString>("content of file1"));auto innerPair2 = std::make_shared<MyPair<std::string, std::shared_ptr<MyString>>>("file2", std::make_shared<MyString>("content of file2"));jsFiles->push_back(innerPair1);jsFiles->push_back(innerPair2);// 访问元素for (const auto& pairPtr : *jsFiles) {std::cout << "Filename: " << pairPtr->first << ", Content: " << *pairPtr->second << std::endl;}// 离开作用域时,智能指针会自动销毁它们管理的对象return 0;
}

同时也证明了一个结论,构造函数和析构函数的调用顺序是相反的。 

回调函数中的循环引用问题

值捕获

#include <iostream>
#include <memory>
#include <functional>class MyClass {
public:MyClass() { std::cout << "MyClass created" << std::endl; }~MyClass() { std::cout << "MyClass destroyed" << std::endl; }void setCallback(std::function<void()> cb) {callback_ = cb;}void executeCallback() {if (callback_) {callback_();}}private:std::function<void()> callback_;
};void createNoLeak() {auto myObject = std::make_shared<MyClass>();myObject->setCallback([=]() {std::cout << "Callback executed, myObject use count: " << myObject.use_count() << std::endl;});myObject->executeCallback();}int main() {createNoLeak();std::cout << "End of program" << std::endl;return 0;
}

可以看出myObject最后没有调用析构函数,是shared_ptr循环引用了。

引用捕获

如果换为引用捕获,则不会造成 shared_ptr循环引用。虽然这种方式不会增加引用计数,但需要特别注意捕获对象的生命周期,防止在 lambda 被调用时,对象已经被销毁,从而导致未定义行为。

如何解决 

#include <iostream>
#include <memory>
#include <functional>class MyClass : public std::enable_shared_from_this<MyClass> {
public:MyClass() { std::cout << "MyClass created" << std::endl; }~MyClass() { std::cout << "MyClass destroyed" << std::endl; }void setCallback(std::function<void()> cb) {callback_ = cb;}void executeCallback() {if (callback_) {callback_();}}private:std::function<void()> callback_;
};void createNoLeak() {auto myObject = std::make_shared<MyClass>();std::weak_ptr<MyClass> weakPtr = myObject;myObject->setCallback([weakPtr]() {if (auto sharedPtr = weakPtr.lock()) {std::cout << "Callback executed, object is valid" << std::endl;} else {std::cout << "Object already destroyed" << std::endl;}});myObject->executeCallback();// 这里 myObject 是按 weak_ptr 捕获,当 createNoLeak() 结束时,myObject 的生命周期也就结束了,并且引用计数=0
}int main() {createNoLeak();std::cout << "End of program" << std::endl;return 0;
}

  • weakPtr.lock() 的使用:持有 std::weak_ptr,并且需要检查或者使用其管理的对象。如果对象仍然存在(即它的 shared_ptr 引用计数大于零),我们希望获取一个 shared_ptr 来安全地使用该对象。否则,weak_ptr.lock() 返回一个空的 shared_ptr
  • std::enable_shared_from_this 是一个非常有用的标准库模板类,用于解决一个特定的问题: 当一个类的成员函数需要创建一个指向自己(this)的 std::shared_ptr 时,这类问题如何安全地实现。

std::enable_shared_from_this

背景问题

在使用 std::shared_ptr 管理对象时,有时会遇到需要在类的成员函数中获取该对象的 shared_ptr 的情况。例如,在一个类的成员函数中,如果想要得到一个指向该对象的 shared_ptr,不能简单地使用 std::shared_ptr<MyClass>(this),因为这会创建一个新的 shared_ptr,而不是增加现有的 shared_ptr 的引用计数。这可能导致对象被提前销毁或者多次销毁。

std::enable_shared_from_this 的作用

通过继承 std::enable_shared_from_this,类就能够安全地使用 shared_from_this 方法,从而获取一个 shared_ptr,该 shared_ptr 与其他 shared_ptr 共享所有权,而不会重复增加引用计数。

使用示例

#include <iostream>
#include <memory>// 定义 MyClass 继承 std::enable_shared_from_this<MyClass>
class MyClass : public std::enable_shared_from_this<MyClass> {
public:MyClass() { std::cout << "MyClass created" << std::endl; }~MyClass() { std::cout << "MyClass destroyed" << std::endl; }// 一个成员函数,它需要返回一个指向自身的 shared_ptrstd::shared_ptr<MyClass> getSharedPtr() {// 使用 shared_from_this 返回一个 shared_ptrreturn shared_from_this();}void doSomething() {auto ptr = shared_from_this(); // 获取 shared_ptrstd::cout << "Doing something with MyClass instance, ref count: " << ptr.use_count() << std::endl;}
};void exampleFunction() {// 创建 MyClass 对象的 shared_ptrauto myObject = std::make_shared<MyClass>();// 调用成员函数获取 shared_ptrauto mySharedPtr = myObject->getSharedPtr();std::cout << "Reference count after getSharedPtr: " << mySharedPtr.use_count() << std::endl;myObject->doSomething();
}int main() {exampleFunction();return 0;
}

注意

1.创建对象:
只有通过 std::shared_ptr 创建或管理的对象,才能安全地使用 shared_from_this

2. 保护避免使用 new 操作符:
直接使用 new 操作符创建的对象不能正确使用 shared_from_this,这样做可能会导致未定义行为(例如崩溃)。

为什么 std::enable_shared_from_this 是必要的?

std::enable_shared_from_this 内部维护了一个弱引用(std::weak_ptr)指向当前对象。这个弱引用确保不会增加引用计数,同时允许 shared_from_this 方法安全地获取 std::shared_ptr,从而真正共享管理的对象,避免不安全的重复引用计数增加。

通过这样做,C++ STL 提供了一种方便而安全的方式来管理对象的生命周期,特别是在需要从对象内部生成 shared_ptr的情境下。

总结

通过继承 std::enable_shared_from_thisMyClass 能够安全地在其成员函数中创建返回指向自身的 std::shared_ptr,避免不必要的重复引用计数,从而有效地管理和共享对象生命周期。这样既提升了代码的安全性,也使得对象生命周期管理变得更加简洁和直观。

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

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

相关文章

数据结构 1.1 数据结构的基本概念

本章总览&#xff1a; 一.什么是数据 1.数据 数据是信息的载体&#xff0c;是描述客观事物属性的数、字符及所有能输入到计算机中并被计算机程 序识别和处理的符号的集合。数据是计算机程序加工的原料。 早期计算机只能处理纯数值的问题&#xff0c;如世界第一题计算机ENI…

转让北京文化传媒公司带营业性演出经纪许可证

影视文化传播倡导将健康的影视文化有效传播给观众&#xff0c;从而构建观众与电影制作者的良 性沟通与互动&#xff0c;是沟通电影制作者与电影受众的重要桥梁。影视文化泛指以电影&#xff0c;电视方式所进行的全部文化创造&#xff0c;即体现为电影&#xff0c;电视全部的存在…

Java-List集合堆内存溢出

Java-List集合堆内存溢出 情况一情况二对照分析对照规定堆内存 情况一 往List<Object>的集合中不断插入元素&#xff0c;集合底层的数组会不断扩容&#xff0c;从0 -> 10 -> 10 10>>1…。最终出现堆内存溢出&#xff0c;是在扩容数组大小的时候。这里的过程…

【应届应知应会】SQL常用知识点50道

SueWakeup 个人主页&#xff1a;SueWakeup 系列专栏&#xff1a;借他一双眼&#xff0c;愿这盛世如先生所愿 个性签名&#xff1a;人生乏味啊&#xff0c;我欲令之光怪陆离 本文封面由 凌七七~❤ 友情提供 目录 数据库的概念 (什么是数据库) RDBMS NOSQL 数据库的分类 …

Qt涂鸦板

Qt版本&#xff1a;Qt6 具体代码&#xff1a; 头文件 dialog.h #ifndef DIALOG_H #define DIALOG_H#include <QDialog>QT_BEGIN_NAMESPACE namespace Ui { class Dialog; } QT_END_NAMESPACEclass Dialog : public QDialog {Q_OBJECTpublic:Dialog(QWidget *parent n…

0145__contain_of的原理与实现

contain_of的原理与实现_contain of-CSDN博客

从零开始!Jupyter Notebook的安装教程

引言 Jupyter Notebook作为一种交互式的开发环境&#xff0c;已经成为数据科学和机器学习领域中不可或缺的工具之一。它能够将代码、文本、图像和数据结合在一个灵活的文档中&#xff0c;使得数据分析和可视化变得更加直观和高效。 本文将详细介绍Jupyter Notebook的安装过程…

深入理解 Git `git add -p` 命令中的交互选项

个人名片 &#x1f393;作者简介&#xff1a;java领域优质创作者 &#x1f310;个人主页&#xff1a;码农阿豪 &#x1f4de;工作室&#xff1a;新空间代码工作室&#xff08;提供各种软件服务&#xff09; &#x1f48c;个人邮箱&#xff1a;[2435024119qq.com] &#x1f4f1…

500mA、低压差、低噪声、超快、无需旁路电容的CMOS LDO稳压器RT9013

一般描述 RT9013 SOT23-5封装的外观和丝印 RT9013 是一款高性能的 500mA LDO 稳压器&#xff0c;具有极高的 PSRR 和超低压差。非常适合具有苛刻性能和空间要求的便携式射频和无线应用。 RT9013的静态电流低至25μA&#xff0c;进一步延长了电池的使用寿命。RT9013 也适用于低…

mysql在部署时的问题

1.远程连接是否开放问题 DataGrip远程连接Ubuntu Linux MySQL服务器报错DBMS: MySQL (no ver.)-CSDN博客 【MySQL】DataGrip远程连接MySQL_datagrip连接远程mysql数据库-CSDN博客 一定要把对应端口规则打开 2.远程连接不适用3306作为默认运行端口 打开mysql的配置文件&…

音乐发行平台无加密开源源码

适用于唱片公司&#xff0c;用于接收物料&#xff0c;下载物料功能&#xff1a;个人或机构认证&#xff0c;上传专辑和歌曲&#xff0c;版税结算环境要求php7.4Nginx 1、导入数据库 2、/inc/conn.php里填写数据库密码等后台路径/admin&#xff08;可自行修改任意入口名称&…

AI在软件开发中的角色:助手还是取代者?

目录 前言 一、AI工具现状&#xff1a;高效助手的崛起 二、AI对开发者的影响&#xff1a;新技能与竞争力的重塑 三、AI开发的未来&#xff1a;共生而非取代 写在最后 前言 随着科技的飞速发展&#xff0c;生成式人工智能&#xff08;AIGC&#xff09;在软件开发领域的应用日…

【JS】过滤数组中空值——arr.filter(Boolean)

前言&#xff1a;过滤数组中的空值&#xff0c;包括 &#xff08;undefined、null、“”、0、false、NaN&#xff09; Boolean函数可以将一个值转换为布尔值&#xff0c;空值会被转换为false&#xff0c;非空值会被转换为true 方法&#xff1a; const arr [1, 2, ""…

【SQL常用日期函数(一)】

SQL 常用日期函数-基于impala 引擎 当前日期&#xff08;YYYY-MM-DD&#xff09; SELECT CURRENT_DATE(); -- 2024-06-30昨天 SELECT CURRENT_DATE(); -- 2024-06-30 SELECT CAST( DAYS_ADD(TO_DATE( CURRENT_DATE() ), -1 ) AS VARCHAR(10) ); -- 2024-06-29 SELECT CAST( …

Linux-页表如何对物理内存进行映射

1.1 页框和页帧 我们知道通过页表可以将虚拟内存映射到对应的物理内存&#xff0c;而操作系统对于物理内存的管理并不是以字节为单位的&#xff0c;而是将物理内存分为许多大小为4KB的块&#xff0c;称为页框或页帧&#xff0c;这就是为什么我们在创建共享内存是建议将大小设定…

LTSPICE仿真电路:(十九)磁珠的一些简单仿真

1.作用 简单来说就是用来滤波的&#xff0c;将高频信号转化为热量滤除掉&#xff0c;低频有用信号正常通过 2.参数 上图几个参数比较简单&#xff0c;就是字面上的意思&#xff0c;更重要的就是频率阻抗图 不同曲线代表不同型号的磁珠&#xff0c;实际上除了额定电流外&#…

基于springboot+vue+uniapp的语言课学习系统小程序

开发语言&#xff1a;Java框架&#xff1a;springbootuniappJDK版本&#xff1a;JDK1.8服务器&#xff1a;tomcat7数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09;数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/ideaMaven包&#…

艺活网DIY手工制作网站源码 工艺制作教程平台源码,带数据

帝国CMS仿《手艺活》DIY手工制作网源码&#xff0c;仿手艺活自适应手机版模板。 带数据库和图片资源&#xff0c;一共5个G大小&#xff0c;下载需耐心。 92开发 手艺活网DIY手工制作网站源码 创意手工艺品制作教程平台系统帝国h5自适应手机端 是一套展示各种 DIY 小物品精美又…

@react-google-maps/api实现谷歌地图中添加多边围栏,并可编辑,编辑后可获得围栏各个点的经纬度

先上一张效果图 看看是不是大家想要的效果&#xff5e; ❤️ 由于该功能微微复杂一点&#xff0c;为了让大家精准了解 我精简了一下地图代码 大家根据自己的需求将center值和paths&#xff0c;用setState做活就可以了 1.第一步要加入项目package.json中或者直接yarn install它…

[激光原理与应用-97]:激光焊接焊中检测系统系列介绍 - 1 - 什么是焊接以及传统的焊接方法

目录 一、什么是焊接 1.1 概述 1.2 基本原理 二、传统的焊接技术与方法 2.1 手工电弧焊&#xff1a; 1、定义与原理 2、特点 3、焊条类型 4、应用领域 5、安全注意事项 2.2 气体保护焊&#xff1a; 1、原理与特点 2、应用领域 3、气体选择 4、注意事项 2.3 电阻…