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

news/2024/10/12 5:41:44/

背景

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

基本原则

按照构造顺序,逆序的析构;可以类比与栈的操作,先入栈的后出栈;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_ptrstd::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.ppmy.cn/news/1537774.html

相关文章

Bob_ 1.0.1靶机渗透

项目地址 plain https://download.vulnhub.com/bob/Bob_v1.0.1.ova 实验过程 开启靶机虚拟机 ![](https://img-blog.csdnimg.cn/img_convert/c4ede7d1724d430e1c446b8c49e3e42d.png) 使用nmap进行主机发现&#xff0c;获取靶机IP地址 plain nmap 192.168.47.1-254 根据对比…

GRU--详解

GRU&#xff08;Gated Recurrent Unit&#xff09;&#xff08;门控循环单元&#xff09;是RNN&#xff08;循环神经网络&#xff09;的一种变体。GRU的设计简化了另一种RNN变体——LSTM&#xff08;长短期记忆网络&#xff09;&#xff0c;与LSTM不同的是&#xff0c;GRU将输入…

Java_EE ( IO 流技术)

什么是IO输入(Input)指的是&#xff1a;可以让程序从外部系统获得数据&#xff08;核心含义是“读”&#xff0c;读取外部数据&#xff09;。输出(Output)指的是&#xff1a;程序输出数据给外部系统从而可以操作外部系统&#xff08;核心含义是“写”&#xff0c;将数据写出到外…

Linux Git

在.gitignore 文件中的后缀 上传到仓库时会忽略。 git仓库本质是.git目录中的内容&#xff0c;push到远端就是将.git内容传到仓库中。 process存储进度条程序 git add .将process存储在仓库临时存储部分 下图指令将process存储到仓库中 上传到远端GitHub中。用令牌登录。 仓库…

[已解决]DockerTarBuilder永久解决镜像docker拉取异常问题

前阵子发现阿里云的docker加速镜像失效了&#xff08;甚至连nginx都拉取不了&#xff09;&#xff0c;重新换了并且加多了网络上比较常用的dokcer加速源&#xff0c;可以解决一部分问题&#xff0c;但仍然有一些镜像的某个版本或一些比较冷的镜像就是拉取不了&#xff0c;原因未…

消峰限流有哪几种方式?

消峰限流的方式 业务视角 验证码回答问题环节 技术视角 消息队列异步化用户请求 限流&#xff0c;对流量进行层层过滤 nginx 层限流&#xff0c; 一是控制速率 limit_req 漏桶算法 limit_req_zone $binary_remote_addr zonemylimit:10m rate2r/s; server { location / { lim…

mac电脑如何删除应用程序?怎么删除苹果电脑里的软件

在使用Mac电脑的过程中&#xff0c;随着时间的推移&#xff0c;我们可能会安装大量的应用程序。然而&#xff0c;这些应用程序中有很多可能只是临时使用&#xff0c;或者已经不再需要了。这些无用的应用程序不仅占据了宝贵的硬盘空间&#xff0c;还可能拖慢Mac系统的运行速度。…

今日总结10.11

接口和抽象类共同点和区别 接口和抽象类共同点 不能直接实例化&#xff1a; 接口和抽象类都不能直接创建对象。它们必须通过子类&#xff08;实现接口的类或继承抽象类的类&#xff09;来实例化。定义行为&#xff1a; 接口和抽象类都可以定义类应该实现的方法或属性。提高代码…