C++ 智能指针使用不当导致内存泄漏问题

news2024/11/16 5:36:57

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.coloradmin.cn/o/1906315.html

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

相关文章

优选算法之技巧(一):双指针一:移位0与复写0

引用&#xff1a;我们之前学过快排&#xff0c;首先用三元取中&#xff0c;找(key)&#xff0c;然后就用到了双指针的方法来进行交换排序&#xff0c;那我们今天要讲的双指针其实大同小异&#xff0c;无非在数组中就变成了下标。 题一&#xff1a; 给定一个数组 nums&#xf…

云平台教程 | 手把手教你主成分分析PCA的可视化

爱基百客云平台小工具使用 1.1 爱基百客云平台之PCA分析 1.2 PCA分析原理 1.3 参数设置 1.4 任务查看 1.5 结果 1.5.1 PCA图 01 爱基百客云平台小工具使用 首先&#xff0c;打开爱基百客官网&#xff1a;http://www.igenebook.com&#xff1b;点击菜单栏最右侧“云平台”按钮…

藏汉翻译通工具推荐使用《藏文翻译词典》App:藏族文化的掌上宝典,帮助你了解学习藏语反义词近义词和藏文作文!

如果你正在学习藏语&#xff0c;遇到不同地区的发音不同时&#xff0c;卫藏语、安多语和康巴语&#xff0c;那么你需要一款好用的翻译和语音朗读工具&#xff0c;帮助你掌握藏语。 如果你正在用藏文写作文&#xff0c;发现一些词汇不会时&#xff0c;需要使用藏文词典&#xf…

盘点效率工具RunFlow那些容易被忽略的功能

本文我们将带您了解RunFlow有哪些容易被忽略、但是又非常实用的功能。 固定工作窗口 您还可以通过双击 Ctrl 键来切换窗口固定状态&#xff0c;您也可以在 热点事件 设置页面自定义该快捷键。 预览菜单内容 用浏览器打开剪贴板复制的URL 多行输入 按 CtrlEnter 或 ShiftEnter …

C语言 -- 深入理解指针(二)

C语言 -- 深入理解指针&#xff08;二&#xff09; 1. 数组名的理解2. 使用指针访问数组3. 一维数组传参的本质4. 冒泡排序5. 二级指针6. 指针数组7. 指针数组模拟二维数组8. 字符指针变量9. 数组指针变量2.1数组指针变量是什么&#xff1f;2.2 数组指针变量怎么初始化 10. 二维…

海外媒体发稿-全媒体百科

全球知名媒体机构 在全球范围内&#xff0c;有许多知名的新闻机构负责报道世界各地的新闻事件。以下是一些国外常见的媒体机构&#xff1a; AP&#xff08;美联社&#xff09;合众国际社&#xff08;UPI&#xff09;AFP(法新社)EFE&#xff08;埃菲通讯社&#xff09;Europa …

【vue】下载 打印 pdf (问题总结)- 持续更新ing

这里是目录标题 一、pdf1.查看 下载一、pdf 1.查看 下载 样式 Code<template><div><el-table :data="pdfList" style="width: 100%" border ><el-table-columnprop="index"label="序号"width="80"ali…

JVM:介绍

文章目录 一、什么是JVM二、JVM的功能1、解释和运行2、内存管理3、即时编译 三、常见的JVM四、Java虚拟机规范五、HotSpot发展历程 一、什么是JVM JVM的全称为Java Virtual Machine&#xff0c;Java虚拟机。本质上是一个运行在计算机上的程序&#xff0c;职责是运行Java字节码…

气膜体育馆作为临时赛场有哪些优势—轻空间

一、气膜体育馆简介 气膜体育馆是一种利用气膜技术建造的室内场馆&#xff0c;其结构主要由高强度膜材、充气系统和钢缆固定系统组成。通过向膜体内部充气&#xff0c;使其形成稳定的内部压力来支撑整个建筑结构。气膜体育馆因其快速建设、环保节能、灵活多样等优点&#xff0c…

tqdm进度条函数使用 python

1.作用&#xff1a; 通过使用 tqdm &#xff0c;可以让您在处理大量数据或长时间运行的循环时&#xff0c;更好地了解程序的执行进度&#xff0c;增强用户体验。 2.使用 ---可以使用 pip install tqdm 进行安装。 ---tqdm.tqdm(iterator可迭代对象&#xff0c;desc描述符&…

数据中台设计书及建设指南(中台及大数据解决技术方案)

1. 中台概念 2. 推动企业组织模式演进 3. 建设方法 4 .中台内容 5. 数据安全体系 中台内容围绕数据中台建设评估、整体框架、数据采集&#xff0c;结构化、半结构化、非结构化的数据采集&#xff0c;数据计算能力、存储计算引擎、数据架构、数据挖掘、各种不同数据层建设、模型…

7月8号直播预告 | 全国产EtherCAT运动控制器ZMC432HG及其EtherCAT驱动器与控制器常用回零模式介绍

EtherCAT运动控制边缘控制器是工业互联网的关键组件之一&#xff0c;结合丰富的运动控制功能、实时数据采集、处理和本地计算等&#xff0c;具备高度灵活的可编程性和出色的运动控制性能&#xff0c;为运动控制协同工业互联网应用带来巨大市场潜力&#xff0c;同时也使其成为企…

spark shuffle写操作——SortShuffleWriter

写入的简单流程&#xff1a; 1.生成ExternalSorter对象 2.将消息都是插入ExternalSorter对象中 3.获取到mapOutputWriter&#xff0c;将中间产生的临时文件合并到一个临时文件 4.生成最后的data文件和index文件 可以看到写入的重点类是ExternalSorter对象 ExternalSorter 基…

关于振动盘正反料下料逻辑编写

写在前文 借鉴某个程序的逻辑套路写的 1.就是第一个料是正方向&#xff0c;第二个料是反方向。 (* 基础逻辑应该都差不多&#xff0c;这个是一个振动盘&#xff0c;振动盘的末端是一个上下对射的感应器&#xff0c;这个感应器的作用是对射感应到物料的到位信号&#xff0c;末端…

网安加·百家讲坛 | 关昕健:新时代企业数据安全运营思路

作者简介&#xff1a;关昕健&#xff0c;某运营商安全专家&#xff0c;2015年获CISSP认证&#xff0c;长期负责企业安全运营工作&#xff0c;关注国内外数据安全动态与解决方案&#xff0c;持续开展数据安全运营实践。 近年来&#xff0c;随着《数据安全法》的出台和国家数据局…

怎么将视频翻译免费?这篇文章告诉你5个视频翻译的方法

在探索不同文化的经典影视剧时&#xff0c;我们常常被那些精彩绝伦的台词深深吸引。 然而&#xff0c;难以理解的外语符号让我们难以完全领略其魅力。不过&#xff0c;如果你认识免费视频翻译软件的话&#xff0c;那将这些经典台词从陌生的外语符号变成直观的母语表达&#xf…

Git注释规范

主打一个有用 代码的提交规范参考如下&#xff1a; init:初始化项目feat:新功能&#xff08;feature&#xff09;fix:修补bugdocs:文档&#xff08;documentation&#xff09;style:格式&#xff08;不影响代码运行的变动&#xff09;refactor:重构&#xff08;即不是新增功能…

【双出版加持!录用率高!见刊、检索更稳定!】第六届结构抗震与土木工程研究国际学术会议 (ICSSCER 2024,8月16-18)

随着社会的发展&#xff0c;城市规模的不断扩大&#xff0c;建筑形态也趋于多样化和复杂化&#xff0c;建筑结构形式逐渐由规则简单向高层、大跨甚至特殊复杂的方向发展。而房屋建筑是人们正常生活和生产活动的基本场所&#xff0c;房屋建筑结构的安全必须得到充分保障。但是&a…

提升曝光与转化率:速卖通、敦煌网店铺运营结合自养号测评

全球速卖通&#xff08;AliExpress&#xff09;是中国最大的跨境出口B2C平台之一&#xff0c;面向海外买家客户&#xff0c;是全球第三大英文在线购物网站。速卖通是阿里巴巴集团旗下电商业务之一&#xff0c;致力于服务全球中小创业者出海&#xff0c;让天下没有难做的跨境生意…

Maven Nexus3 私服搭建、配置、项目发布指南

maven nexus私服搭建 访问nexus3官方镜像库,选择需要的版本下载:Docker Nexus docker pull sonatype/nexus3:3.49.0 创建数据目录并赋权 sudo mkdir /nexus-data && sudo chown -R 200 /nexus-data 运行(数据目录选择硬盘大的卷进行挂载) docker run -d -p 808…