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

news2024/10/6 6:25:08

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/1895485.html

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

相关文章

明星代言6个提升企业形象的杀手锏-华媒舍

在当今竞争激烈的商业世界中&#xff0c;企业形象的塑造对于品牌的发展至关重要。而明星代言作为一种常见的营销手段&#xff0c;被广泛使用来提升企业形象和产品销售。本文将介绍明星代言的六个杀手锏&#xff0c;帮助您了解如何通过明星代言来提升企业形象。 1. 拥有广泛的影…

PCB设计时,信号走线要先过ESD/TVS管,这是为什么?

目录 为什么有上面这个问题&#xff1f; 问题的原因——走线电感 走线电感的阻抗 电感的影响 小结 都说接口处的信号要先过ESD/TVS管&#xff0c;然后拉到被保护器件&#xff0c;为什么不这样做效果就不好&#xff1f;那如果受板子实际情况限制&#xff0c;必须这样layout…

专题二:Spring源码编译

目录 下载源码 配置Gradle 配置环境变量 配置setting文件 配置Spring源码 配置文件调整 问题解决 完整配置 gradel.properties build.gradle settiings.gradel 在专题一&#xff1a; Spring生态初探中我们从整体模块对Spring有个整体的印象&#xff0c;现在正式从最…

经典卷积神经网络 LeNet

一、实例图片 #我们传入的是28*28&#xff0c;所以加了padding net nn.Sequential(nn.Conv2d(1, 6, kernel_size5, padding2), nn.Sigmoid(),nn.AvgPool2d(kernel_size2, stride2),nn.Conv2d(6, 16, kernel_size5), nn.Sigmoid(),nn.AvgPool2d(kernel_size2, stride2),nn.Flat…

Android EditText的属性与用法

EditText 是编辑框控件&#xff0c;可以接收用户输入&#xff0c;并在程序中对用户输入进行处理。EditText在App里随处可见&#xff0c;在进行搜索、聊天、拨号等需要输入信息的场合&#xff0c;都可以使用 EditText。 图1 编辑框示意图 EditText 是TextView的子类&#xff0c…

吴恩达深度学习笔记:机器学习策略(2)(ML Strategy (2)) 2.5-2.6

目录 第三门课 结构化机器学习项目&#xff08;Structuring Machine Learning Projects&#xff09;第二周&#xff1a;机器学习策略&#xff08;2&#xff09;(ML Strategy (2))2.5 数据分布不匹配时的偏差与方差的分析&#xff08;Bias and Variance with mismatched data di…

下载安装MySQL

1.软件的下载 打开官网下载mysql-installer-community-8.0.37.0.msi 2.软件的安装 mysql下载完成后&#xff0c;找到下载文件&#xff0c;双击安装 3.配置环境变量 4.自带客户端登录与退出

CSS filter(滤镜)属性,并实现页面置灰效果

目录 一、filter&#xff08;滤镜&#xff09;属性 二、准备工作 三、常用的filter属性值 1、blur(px) 2、brightness(%) 3、contrast(%) 4、grayscale(%) 5、opacity(%) 6、saturate(%) 7、sepia(%) 8、invert(%) 9、hue-rotate(deg) 10、drop-shadow(h-shadow v…

前端JS 插件实现下载【js-tool-big-box,下载大文件(fetch请求 + 下载功能版)

上一节&#xff0c;我们添加了下载大文件的纯功能版&#xff0c;意思就是需要开发者&#xff0c;在自己项目里发送请求&#xff0c;请求成功后&#xff0c;获取文件流的blob数据&#xff0c;然后 js-tool-big-box 帮助下载。 但考虑到&#xff0c;有些项目&#xff0c;可能比较…

装饰模式解析:基本概念和实例教程

目录 装饰模式装饰模式结构装饰模式应用场景装饰模式优缺点练手题目题目描述输入描述输出描述题解 装饰模式 装饰模式&#xff0c;又称装饰者模式、装饰器模式&#xff0c;是一种结构型设计模式&#xff0c;允许你通过将对象放入包含行为的特殊封装对象中来为原对象绑定新的行…

面试篇-Redis-1缓存三兄弟+数据一致性

文章目录 前言一、你们项目中使用Redis都做了什么&#xff1a;二、使用过程中遇到缓存穿透&#xff0c;缓存击穿&#xff0c;缓存雪崩你们如何处理&#xff1a;2.1 缓存穿透&#xff1a;2.1.1 通过缓存key值为null 进行处理&#xff1a;2.1.2 使用布隆过滤器&#xff1a;2.1.3 …

OpenCV基础(2)

目录 滤波处理 均值滤波 基本原理 函数用法 程序示例 高斯滤波 基本原理 函数用法 程序示例 中值滤波 基本原理 函数用法 程序示例 形态学 腐蚀 膨胀 通用形态学函数 前言&#xff1a;本部分是上一篇文章的延续&#xff0c;前面部分请查看&#xff1a;OpenCV…

深入理解如何撤销 Git 中不想提交的文件

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

图增强LLM + 可穿戴设备实时数据,生成个性化健康见解

图增强LLM 可穿戴设备实时数据&#xff0c;生成个性化健康见解 提出背景图增强LLM 子解法1&#xff08;使用层次图模型&#xff09; 子解法2&#xff08;动态数据整合&#xff09; 子解法3&#xff08;LLM引导评估&#xff09; 提出背景 论文&#xff1a;https://arxiv.or…

【js正则】去除文本中的a标签及其内容

场景&#xff1a;有时候服务端返回的文本中&#xff0c;包含a标签&#xff0c;前端不需要展示。 // 示例 const inputText 【提醒&#xff1a;XXXX】\nXXXXXX: 1\n\n<a href"https://export.shobserver.com/baijiahao/html/767805.html">详情</a>;JS正…

【营销策划模型大全】私域运营必备

营销策划模型大全&#xff1a;战略屋品牌屋、电商运营模型、营销战略、新媒体运营模型、品牌模型、私域运营模型…… 该文档是一份策划总监工作模型的汇总&#xff0c;包括战略屋/品牌屋模型、营销战略模型、品牌相关模型、电商运营模型、新媒体运营模型和私域运营模型等&…

JavaScript基础-函数(完整版)

文章目录 函数基本使用函数提升函数参数arguments对象&#xff08;了解&#xff09;剩余参数(重点)展开运算符(...) 逻辑中断函数参数-默认参数函数返回值-return作用域(scope)全局作用域局部作用域变量的访问原则垃圾回收机制闭包 匿名函数函数表达式立即执行函数 箭头函数箭头…

全自动内衣洗衣机什么牌子好?四大热门内衣洗衣机多角度测评

内衣洗衣机是近几年新兴的一种家用电器产品&#xff0c;正日益引起人们的重视。但是&#xff0c;面对市面上品牌繁多、款式繁多的内衣洗衣机&#xff0c;使得很多人都不知道该如何选择。身为一个数码家电博主&#xff0c;我知道这类产品在挑选方面有着比较深入的了解。为此&…

AIGC对设计师积极性的影响

随着科技的迅猛发展&#xff0c;生成式人工智能&#xff08;AIGC&#xff09;工具正逐渐深入设计的每个角落&#xff0c;对设计师的工作方式和思维模式产生了深远的影响。AIGC不仅极大提升了设计师的工作效率&#xff0c;更激发了他们的创新思维&#xff0c;为设计行业带来了翻…

好文阅读-日志篇

https://mp.weixin.qq.com/s/jABbG4MKvEiWXwdYwUk8SA 这里直接看最佳实践。 Maven 依赖 <dependencyManagement><dependencies><dependency><groupId>org.slf4j</groupId><artifactId>slf4j-api</artifactId><version>1.7.36…