C++对象声明周期问题记录

背景

当程序中创建了多个对象并且是多个类型的对象时,需要理解这些对象的生命周期(构造和析构)是什么关系至关重要

基本原则

按照构造顺序,逆序的析构;可以类比与栈的操作,先入栈的后出栈;thread_local对象跟随所属线程第一次执行到构造,线程结束后析构
对象类型:全局对象,静态全局对象,局部对象,静态局部对象,类成员对象,thread_local对象

#include <thread>
#include <iostream>
#include <chrono>
#include <ctime>
#include <memory>
#include <vector>

class A {
    public:
    A() { std::cout << "A()" << std::endl;}
    ~A() { std::cout << "~A()" << std::endl;}
};

class B {
    public:
    static B& instance() {
        static B b; // 局部静态对象
        return b;
    }
    B() { std::cout << "B()" << std::endl;}
    ~B() { std::cout << "~B()" << std::endl;}
};
class C {
    public:
    C() {
        a_ = std::make_shared<A>();
        B::instance();
        std::cout << "C()" << std::endl;
    }
    ~C() { std::cout << "~C()" << std::endl;}
    std::shared_ptr<A> a_; // 类成员对象
};

class D {
    public:
    D() { std::cout << "D()" << std::endl;}
    ~D() { std::cout << "~D()" << std::endl;}
};

class E {
    public:
    E() { std::cout << "E()" << std::endl;}
    ~E() { std::cout << "~E()" << std::endl;}
};

class F {
    public:
    F() { std::cout << "F()" << this << std::endl;}
    ~F() { std::cout << "~F()" << this << std::endl;}
};


void f() {
    thread_local F f; // thread_local对象
}

D d; // 全局对象
static E e; //静态全局对象
int main() {
    std::cout << "main begin" << std::endl;
    C c; // 局部对象
    bool running1 = true;
    std::thread t1([&running1](){
        std::cout << "t1 is running" << std::endl;
        {
            f();
            f();
        }
        while(running1){}
        std::cout << "t1 is dead" << std::endl;
    });
    bool running2 = true;
    std::thread t2([&running2](){
        std::cout << "t2 is running" << std::endl;
        {
            f();
            f();
        }
        while(running2){}
        std::cout << "t2 is dead" << std::endl;
    });
    running1 = false;
    running2 = false;
    if(t1.joinable()) {
        t1.join();
    }
    if(t2.joinable()) {
        t2.join();
    }
    std::cout << "main end" << std::endl;
}

输出

D()
E()
main begin
A()
B()
C()
t2 is running
F()0x7215905fe630
t2 is dead
~F()0x7215905fe630
t1 is running
F()0x721590dff630
t1 is dead
~F()0x721590dff630
main end
~C()
~A()
~B()
~E()
~D()

案例(程序退出时崩溃)

现象

在一个类的成员方法中,将this通过lamda函数注册给一个异步回调;然后回调执行的时候this被析构

代码示例

比如如下这个代码

#include <iostream>
#include <memory>
#include <functional>
#include <thread>

bool block = true;

class Base{
    public:
    virtual void func() = 0;
};
class Derived : public Base {
    public:
    Derived() {
        p = new int(1);
    }
    ~Derived() { 
        std::cout << "~Derived" << std::endl;
        delete p;
        p = nullptr;
    }
    void func() {
        std::cout << "call func, *p:" << *p << std::endl; // 如果p是空指针那么就会crash
    }
    
    int* p;
};

class MyClass {
public:
    void doAsyncWork() {
        // 创建一个weak_ptr来捕获this指针

        // 注册一个lambda作为异步回调
        std::function<void()> callback = [this]() {
            while(block){}
            doWork();
        };

        // 模拟异步操作(比如另外一个线程执行回调)
        std::thread(callback).detach();
    }

    // 为了演示目的,定义一个成员函数
    void doWork() {
        std::cout << "Doing some work" << std::endl;
        d.func();
    }
    ~MyClass() { std::cout << "~MyClass" << std::endl;}
    Derived d;
};

int main() {
    // 创建一个MyClass实例的shared_ptr
    {
    std::shared_ptr<MyClass> instance = std::make_shared<MyClass>();

    // 调用doAsyncWork注册异步回调
    instance->doAsyncWork();
    }
    block = false; // 原来的这个对象析构后,异步回调再执行
    // 等待足够的时间让异步回调完成,避免main函数立刻退出
    std::this_thread::sleep_for(std::chrono::seconds(1));
    return 0;
}

我们MyClass的成员函数doAsyncWork中注册了一个异步回调callback,这个callback中当解除block时会调用MyClass成员对象d的func函数;
上述当MyClass析构时,然后异步回调再被执行,那么访问成员对象d已经析构,这里会产生未定义行为,为了凸显这未定义行为,我们在对象d的func函数中访问一个指针(析构时会设置为nullptr),这时将会直接crash。

原因

在异步回调中访问了已经析构的对象。

解决办法

异步回调中的lambda函数捕获的只是this指针,这个对象生命周期不可控了;需要想办法将这个对象的声明周期延长到这个异步回调里;
这个是时候大名鼎鼎的enable_shared_from_this就来了,在成员函数中通过shared_from_this()返回一个智能指针,用来判断和延长对象的生命周期

改动如下,让MyClass继承enable_shared_from_this,然后通过shared_from_this+弱指针进行判断和延长MyClass对象的生命周期

class MyClass : public std::enable_shared_from_this<MyClass> {
public:
    void doAsyncWork() {
        // 创建一个weak_ptr来捕获this指针
        std::weak_ptr<MyClass> weak_self = shared_from_this();

        // 注册一个lambda作为异步回调
        std::function<void()> callback = [weak_self]() {
          	while(block){}
            // 在callback中,尝试从weak_ptr升级到shared_ptr
            std::shared_ptr<MyClass> self = weak_self.lock();
            if (self) {
                // 如果self不为nullptr,说明原始对象还活着,可以安全调用
                self->doWork();
            } else {
                // 原始对象已经销毁
                std::cout << "Object was destroyed, cannot call member function." << std::endl;
            }
        };

        // 模拟异步操作(比如另外一个线程执行回调)
        std::thread(callback).detach();
    }

    // 为了演示目的,定义一个成员函数
    void doWork() {
        std::cout << "Doing some work" << std::endl;
        d.func();
    }
    ~MyClass() { std::cout << "~MyClass" << std::endl;}
    Derived d;
};

请注意,在实际应用中,根据异步操作的具体情况和库的选择,注册异步回调的方式可能有所不同,但基本思路是相同的——使用 std::weak_ptr 以防止循环引用并在回调中检查对象是否仍然存在。

一些coredump堆栈

#0  0x00007f0445877438 in __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:54
#1  0x00007f044587903a in __GI_abort () at abort.c:89
#2  0x00007f04461bb84d in __gnu_cxx::__verbose_terminate_handler() () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#3  0x00007f04461b96b6 in ?? () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#4  0x00007f04461b9701 in std::terminate() () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#5  0x00007f04461ba23f in __cxa_pure_virtual () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6

当看到__cxa_pure_virtual这个堆栈时,如果这个类的纯虚函数已经被实现了,那么大概率是这个对象已经损坏了(析构也算,它的虚函数表已经损坏了)。
或者直接访问了空指针

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

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

相关文章

【JavaEE初阶】深入理解不同锁的意义,synchronized的加锁过程理解以及CAS的原子性实现(面试经典题);

前言 &#x1f31f;&#x1f31f;本期讲解关于锁的相关知识了解&#xff0c;这里涉及到高频面试题哦~~~ &#x1f308;上期博客在这里&#xff1a;【JavaEE初阶】深入理解线程池的概念以及Java标准库提供的方法参数分析-CSDN博客 &#x1f308;感兴趣的小伙伴看一看小编主页&am…

【STM32单片机_(HAL库)】4-2-1【定时器TIM】定时器输出PWM实现呼吸灯实验

1.硬件 STM32单片机最小系统LED灯模块 2.软件 pwm驱动文件添加定时器HAL驱动层文件添加GPIO常用函数定时器输出PWM配置步骤main.c程序 #include "sys.h" #include "delay.h" #include "led.h" #include "pwm.h"int main(void) {HA…

【瑞萨RA8D1 CPK开发板】串口的使用和STDOUT输出重定向

串口 本次串口的使用关于时钟导致串口的波特率不对&#xff0c;坑了我很久的时间 使能时钟 串口发现一个问题就是&#xff0c;只能使用下边的时钟配置&#xff0c;修改时钟源和分频系数都会导致串口波特率不正常&#xff0c;这种问题出现在mdkrasc的使用场景之下&#xff1b…

adaptor lora基础

https://www.zhihu.com/question/508658141/answer/3340979311 adaptor和PEFT的区别&#xff1a;前者在模型子层后加一个小型的dense&#xff1b;后者直接稀疏化模型本身&#xff1b; Loading Pre-Trained Adapters — AdapterHub documentation CVPR 2024 | SD-DiT&#xff…

Python | Leetcode Python题解之第468题验证IP地址

题目&#xff1a; 题解&#xff1a; class Solution:def validIPAddress(self, queryIP: str) -> str:if queryIP.find(".") ! -1:# IPv4last -1for i in range(4):cur (len(queryIP) if i 3 else queryIP.find(".", last 1))if cur -1:return &q…

每日OJ题_牛客_小乐乐改数字_模拟_C++_Java

目录 牛客_小乐乐改数字_模拟 题目解析 C代码 Java代码 牛客_小乐乐改数字_模拟 小乐乐改数字_牛客题霸_牛客网 (nowcoder.com) 描述&#xff1a; 小乐乐喜欢数字&#xff0c;尤其喜欢0和1。他现在得到了一个数&#xff0c;想把每位的数变成0或1。如果某一位是奇数&#…

【网络安全】CVE-2024-46990: Directus环回IP过滤器绕过实现SSRF

未经许可,不得转载。 文章目录 背景漏洞详情受影响版本解决方案背景 Directus 是一款开源 CMS,提供强大的内容管理 API,使开发人员能够轻松创建自定义应用程序,凭借其灵活的数据模型和用户友好的界面备受欢迎。然而,Directus 存在一个漏洞,允许攻击者绕过默认的环回 IP …

大数据之——VWare、Ubuntu、CentOs、Hadoop安装配置

前言&#xff1a;这里很抱歉前几期考研专题以及PyTorch这些内容都没有更新&#xff0c;并不是没有在学了&#xff0c;而是事太鸡儿多了&#xff0c;前不久刚刚打完华为开发者比赛&#xff0c;然后有紧接着高数比赛、考研复习&#xff0c;因此这些后续文章都在草稿状态中&#x…

Allegro在PCB上开槽的三种方法操作指导

Allegro如何在PCB上开槽的三种方法操作指导 当PCB有特殊设计要求的时候&#xff0c;需要在PCB上开槽&#xff0c;Allegro支持在PCB上开槽操作&#xff0c;具体操作如下 以下图为例&#xff0c;需要在这个板框中间开槽 开方形槽 选择shape add rect命令 画在Board Geometry-o…

【技术详解】SpringMVC框架全面解析:从入门到精通(SpringMVC)

文章目录 【技术详解】SpringMVC框架全面解析&#xff1a;从入门到精通(SpringMVC)SpringMVC概述1. 三层架构与MVC架构区别1.1 三层架构1.2 MVC架构1.3前后端分离开发模式 2. SpringMVC环境搭建2.1 注解启动方式2.2 xml启动方式2.3 SpringMVC PostMan工具使用 3. SpringMVC 请求…

MySQL 实验1:Windows 环境下 MySQL5.5 安装与配置

MySQL 实验1&#xff1a;Windows 环境下 MySQL5.5 安装与配置 目录 MySQL 实验1&#xff1a;Windows 环境下 MySQL5.5 安装与配置一、MySQL 软件的下载二、安装 MySQL三、配置 MySQL1、配置环境变量2、安装并启动 MySQL 服务3、设置 MySQL 字符集4、为 root 用户设置登录密码 一…

【华为欧拉】国产OpenEuler服务器系统安装以及图形界面

openEuler下载 | openEuler ISO镜像 | openEuler社区官网 下载安装iso 本次选择4G的社区版本 安装&#xff0c;复制到光盘&#xff0c;光盘引导安装。虚拟机安装&#xff0c;准备好iso文件引用&#xff0c;指定好安装源&#xff0c;安装界面和centOS基本一样。选择最小安装就…

代码随想录算法训练营第三十天|491. 非递减子序列,46. 全排列,47. 全排列 II,332. 重新安排行程,51. N 皇后,37. 解数独

491. 非递减子序列&#xff0c;46. 全排列&#xff0c;47. 全排列 II&#xff0c;332. 重新安排行程&#xff0c;51. N 皇后&#xff0c;37. 解数独 491. 非递减子序列46. 全排列47. 全排列 II332. 重新安排行程51. N 皇后37. 解数独 491. 非递减子序列 给你一个整数数组 nums…

友思特方案 | FantoVision边缘计算:嵌入式视觉系统如何实现“更快 更高 更强”?

导读 便于集成的嵌入式视觉系统一直以来面临着带宽、内存、算力三个方面的挑战。友思特 FantoVision 边缘计算设备拥有更快的处理速度和更高的带宽选择&#xff0c;其开放式架构有效突破了上述三重阻碍。 嵌入式视觉 嵌入式视觉是传统机器视觉衍生出来的子集&#xff0c;嵌入…

k8s 中存储之 PV 持久卷 与 PVC 持久卷申请

目录 1 PV 与 PVC 介绍 1.1 PersistentVolume&#xff08;持久卷&#xff0c;简称PV&#xff09; 1.2 PersistentVolumeClaim&#xff08;持久卷声明&#xff0c;简称PVC&#xff09; 1.3 使用了PV和PVC之后&#xff0c;工作可以得到进一步的细分&#xff1a; 2 持久卷实验配置…

UE5运行时动态加载场景角色动画任意搭配-相机及运镜(二)

通过《MMD模型及动作一键完美导入UE5》系列文章,我们可以把外部场景、角色、动画资产导入UE5,接下来我们将实现运行时动态加载这些资产,并任意组合搭配。 1、运行时播放相机动画 1、创建1个BlueprintActor,通过这个蓝图动态创建1个LevelSequence,并Play 2、将这个Bluep…

Verdin AM62使用CODESYS

By Toradex 胡珊逢 简介 CODESYS 是基于 IEC 61131-3 的 PLC 开发工具&#xff0c;在工业控制、交通等领域中有着广泛的应用。文章将介绍如何在 Toradex 采用 TI AM62 SoC 的 Arm 计算机模块 Verdin AM62 使用评估版本的 CODESYS。 硬件介绍 Verdin AM62使用 TI AM623/AM625…

打卡第四天 P1081 [NOIP2012 提高组] 开车旅行

今天是我打卡第四天&#xff0c;做个省选/NOI−题吧(#^.^#) 原题链接&#xff1a;[NOIP2012 提高组] 开车旅行 - 洛谷 题目描述 输入格式 输出格式 输入输出样例 输入 #1 4 2 3 1 4 3 4 1 3 2 3 3 3 4 3 输出 #1 1 1 1 2 0 0 0 0 0 输入 #2 10 4 5 6 1 …

✨机器学习笔记(七)—— 交叉验证、偏差和方差、学习曲线、数据增强、迁移学习、精确率和召回率

机器学习笔记&#xff08;七&#xff09; 1️⃣评估模型&#x1f397;️使用测试集评估模型&#x1f397;️交叉验证集&#xff08;cross validation&#xff09; 2️⃣偏差和方差&#xff08;Bias / Variance&#xff09;3️⃣学习曲线&#xff08;Learning curves&#xff09…

获取时隔半个钟的三天

摘要&#xff1a; 今天遇到需求是配送时间&#xff0c;时隔半个钟的排线&#xff01;所以需要拼接时间&#xff01;例如2024-10-08 14&#xff1a;30&#xff0c;2024-10-08 15&#xff1a;00&#xff0c;2024-10-08 15&#xff1a;30 <el-form-item label"配送时间&a…