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_ptr
        if (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,指向 vector
    auto 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_ptr
    std::shared_ptr<MyClass> getSharedPtr() {
        // 使用 shared_from_this 返回一个 shared_ptr
        return shared_from_this();
    }

    void doSomething() {
        auto ptr = shared_from_this(); // 获取 shared_ptr
        std::cout << "Doing something with MyClass instance, ref count: " << ptr.use_count() << std::endl;
    }
};

void exampleFunction() {
    // 创建 MyClass 对象的 shared_ptr
    auto myObject = std::make_shared<MyClass>();

    // 调用成员函数获取 shared_ptr
    auto 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.mfbz.cn/a/776228.html

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

相关文章

百日筑基第十二天-入门Elasticsearch

百日筑基第十二天-入门Elasticsearch Elasticsearch 是什么 Elasticsearch 是一个分布式、RESTful 风格的搜索和数据分析引擎。 安装 Elasticsearch 下载&#xff1a;https://www.elastic.co/cn/downloads/elasticsearch Elasticsearch 是免安装的&#xff0c;只需要把 zip…

实在智能对话钉钉:宜搭+实在Agent,AI时代的工作方式

比起一个需求需要等产品、技术排期&#xff0c;越来越多的人开始追求把自己武装成「全能战士」&#xff0c;通过低代码工具一搭&#xff0c;一个高效的工作平台便产生了。 宜搭是钉钉自研的低代码应用构建平台&#xff0c;无论是专业开发者还是没有代码基础的业务人员&#xf…

Nuxt3 的生命周期和钩子函数(十一)

title: Nuxt3 的生命周期和钩子函数&#xff08;十一&#xff09; date: 2024/7/5 updated: 2024/7/5 author: cmdragon excerpt: 摘要&#xff1a;本文详细介绍了Nuxt3中几个关键的生命周期钩子和它们的使用方法&#xff0c;包括webpack:done用于Webpack编译完成后执行操作…

Linux运维:MySQL备份,物理冷备份,热备,完备+二进制日志

备份类型 完全备份、增量备份、差异备份 完全备份&#xff1a;整个数据集都备份 增量备份&#xff1a;仅备份最近一次完全备份或增量备份&#xff08;如果存在增量&#xff09;以来变化的数据&#xff0c;备份较快&#xff0c;还原复杂。 差异备份&#xff1a;对比前一次备…

2024 年第十四届亚太数学建模竞赛(中文赛项)浅析

需要完整B题资料&#xff0c;请关注&#xff1a;“小何数模”&#xff01; 本次亚太(中文赛)数学建模的赛题已正式出炉&#xff0c;无论是赛题难度还是认可度&#xff0c;该比赛都是仅次于数模国赛的独一档&#xff0c;可以用于国赛前的练手训练。考虑到大家解题实属不易&…

离线安装arm架构Firefox

离线安装Firefox浏览器及其插件在ARM架构的设备上&#xff08;如树莓派、部分Android设备或其他采用ARM处理器的Linux系统&#xff09;可能需要一些特殊步骤&#xff0c;因为默认情况下&#xff0c;大多数浏览器和插件都是为x86架构设计的。对于ARM架构&#xff0c;你需要找到特…

深圳航空顶象验证码逆向,和百度验证码训练思路

声明(lianxi a15018601872) 本文章中所有内容仅供学习交流使用&#xff0c;不用于其他任何目的&#xff0c;抓包内容、敏感网址、数据接口等均已做脱敏处理&#xff0c;严禁用于商业用途和非法用途&#xff0c;否则由此产生的一切后果均与作者无关&#xff01; 前言(lianxi a…

Java项目:基于SSM框架实现的校园快递代取管理系统【ssm+B/S架构+源码+数据库+毕业论文】

一、项目简介 本项目是一套基于SSM框架实现的校园快递代取管理系统 包含&#xff1a;项目源码、数据库脚本等&#xff0c;该项目附带全部源码可作为毕设使用。 项目都经过严格调试&#xff0c;eclipse或者idea 确保可以运行&#xff01; 该系统功能完善、界面美观、操作简单、…

Windows系统下载安装ngnix

一 nginx下载安装 nginx是HTTP服务器和反向代理服务器&#xff0c;功能非常丰富&#xff0c;在nginx官网首页&#xff0c;点击download 在download页面下&#xff0c;可以选择Stable version稳定版本&#xff0c;点击下载 将下载完成的zip解压即可&#xff0c;然乎在nginx所在…

Spring Boot 中的监视器是什么?有什么作用?

前言&#xff1a; 监听器相信熟悉 Spring、Spring Boot 的都知道&#xff0c;但是监视器又是什么&#xff1f;估计很多人一脸懵的状态&#xff0c;本篇分享一下 Spring Boot 的监视器。 Spring Boot 系列文章传送门 Spring Boot 启动流程源码分析&#xff08;2&#xff09; …

《数字图像处理-OpenCV/Python》第17章:图像的特征描述

《数字图像处理-OpenCV/Python》第17章&#xff1a;图像的特征描述 本书京东 优惠购书链接 https://item.jd.com/14098452.html 本书CSDN 独家连载专栏 https://blog.csdn.net/youcans/category_12418787.html 第17章&#xff1a;图像的特征描述 特征检测与匹配是计算机视觉的…

opencv概念以及安装方法

#opencv相关概念介绍 Open Source Computer Vision Library 缩写 opencv 翻译&#xff1a;开源的计算机视觉库 &#xff0c;英特尔公司发起并开发&#xff0c;支持多种编程语言&#xff08;如C、Python、Java等&#xff09;&#xff0c;支持计算机视觉和机器学习等众多算法&a…

【C++】开源:nlohmann/json数据解析库配置使用

&#x1f60f;★,:.☆(&#xffe3;▽&#xffe3;)/$:.★ &#x1f60f; 这篇文章主要介绍nlohmann/json数据解析库配置使用。 无专精则不能成&#xff0c;无涉猎则不能通。——梁启超 欢迎来到我的博客&#xff0c;一起学习&#xff0c;共同进步。 喜欢的朋友可以关注一下&am…

conda环境变量+常用操作+配置镜像源

、1、conda环境变量配置 根据我的上篇文章&#xff0c;应该都已经安装了conda和pycharm&#xff0c;然后可能会出现conda的没有配置到系统的环境变量上&#xff0c;这里首先教大家如何配置系统的环境变量&#xff0c;在进行后续操作&#xff0c;如果环境变量已经配置完毕可以自…

【C语言】指针(1)--入门理解

目录 一、内存和地址 二、指针变量和地址 三、指针变量类型的意义 一、内存和地址 只要讲指针就离不开内存 因为指针就是访问内存的 计算上CPU&#xff08;中央处理器&#xff09;在处理数据的时候&#xff0c;需要的数据是在内存中读取的&#xff0c;处理后的数 据也会放…

一款强大且免费开源的多连接数据库管理工具

大家好&#xff0c;今天给大家分享一款免费开源的跨平台数据库管理工具DbGate。 DbGate是一款免费开源的跨平台数据库管理工具&#xff0c;支持多种数据库&#xff0c;包括MySQL、PostgreSQL、SQL Server、MongoDB、SQLite等。它可以在Windows、Linux、Mac操作系统上运行&#…

亚信安全:《2024云安全技术发展白皮书》

标签 云计算 安全威胁 云安全技术 网络攻击 数据保护 一句话总结 《云安全技术发展白皮书》全面分析了云计算安全威胁的演进&#xff0c;探讨了云安全技术的发展历程、当前应用和未来趋势&#xff0c;强调了构建全面云安全防护体系的重要性。 摘要 云安全威胁演进&#xff…

刷题之合并两个有序数组(leetcode)

因为换了手机号码&#xff0c;之前leetcode的账号登不上去了&#xff0c;正好太久不刷题&#xff0c;很多思路都没了&#xff0c;所以重新开始刷leetcode&#xff01; 这道题很简单&#xff0c;指针模拟一下&#xff0c;从后往前考虑&#xff0c;先看最大值。 class Solution…

昇思25天学习打卡营第13天|linchenfengxue

Diffusion扩散模型 关于扩散模型&#xff08;Diffusion Models&#xff09;有很多种理解&#xff0c;本文的介绍是基于denoising diffusion probabilistic model &#xff08;DDPM&#xff09;&#xff0c;DDPM已经在&#xff08;无&#xff09;条件图像/音频/视频生成领域取得…

Qt json和xml操作

学习目标&#xff1a; 认识json和xml读写操作 前置环境 运行环境:qt creator 4.12 学习内容 XML XML&#xff08;Extensible Markup Language&#xff09;是一种标记语言,是一种用于描述数据结构的语言。它非常适合用于存储和传输数据。 XML 的主要特点如下: 可扩展性:XM…