C++中执行delete this后访问栈局部变量是否安全?
问题答复
先直接回答你的核心疑问
delete this之后访问当前函数栈上的局部变量,在你当前的编译运行环境下大概率不会触发崩溃,ASAN也不会报错,但这属于未定义行为范畴内的巧合可用,绝对不能在生产代码里依赖这种特性。
为什么会出现"看起来安全"的现象
- 内存布局上,栈帧和堆内存是完全独立的区域:你代码里的
local变量是分配在执行delete操作的线程当前调用栈上的,栈内存的生命周期绑定当前函数的执行周期——只要函数还没return,这个栈帧就不会被回收,上面存的局部变量始终可以正常读写。而delete this只会释放对象占用的堆内存块,根本不会触碰栈区域,自然不会破坏栈上的local变量。 - ASAN的检测逻辑是跟踪堆、栈、全局变量的内存访问合法性,你访问的栈变量本身是处于合法生命周期内的栈内存,ASAN当然不会报错;而访问被delete的类成员属于访问已释放的堆内存,ASAN会精准捕获这个错误。
这种写法的致命风险
- 只要执行完
delete this,this指针就成了悬空指针,此时你不能访问任何类成员、不能调用类的非静态成员函数、甚至不能隐式触碰this指针——这些都是C++标准明确规定的未定义行为。哪怕你代码里写的逻辑是删完就直接return,不同编译器、不同编译选项、不同ABI下生成的代码都可能隐式访问this(比如异常栈展开、虚表校验、安全插桩等逻辑),随时可能崩溃。 - 你当前的测试代码没崩完全是巧合:非删除分支的线程在对象被销毁时刚好在sleep,醒了之后也只访问提前拷贝到栈上的
local变量,没有触碰类成员;只要线程调度顺序变一下,比如非删除线程在对象被delete之后才访问counter_或者task_num_,立刻就会触发野指针访问错误。
规范实现方案
多线程场景下不要用delete this这种高风险的黑魔法,用生命周期托管机制保证对象存活到所有访问逻辑结束,下面两种是工业界通用的稳妥实现:
- 方案1:智能指针自动托管生命周期(最推荐)
用std::shared_ptr配合std::enable_shared_from_this,每个工作线程持有一份对象的强引用,只要还有线程在执行逻辑,对象的引用计数就不会归零,自然不会被提前释放,完全不需要手动delete:
#include <atomic> #include <chrono> #include <iostream> #include <memory> #include <mutex> #include <thread> #include <vector> std::mutex g_iostream_mutex; class TestDeleteBase : public std::enable_shared_from_this<TestDeleteBase> { int task_num_ = 0; std::atomic<int> counter_; public: virtual void Run() = 0; void Init(int task_num) { task_num_ = task_num; counter_.store(0, std::memory_order_relaxed); } void RunParallel() { int local = counter_.load(std::memory_order_relaxed); if (counter_.fetch_add(1, std::memory_order_acq_rel) == task_num_ - 1) { { std::lock_guard<std::mutex> guard(g_iostream_mutex); std::cout << std::this_thread::get_id() << " all tasks finished\n"; } return; } else { { std::lock_guard<std::mutex> guard(g_iostream_mutex); std::cout << std::this_thread::get_id() << " running\n"; } std::this_thread::sleep_for(std::chrono::seconds(1)); } { std::lock_guard<std::mutex> guard(g_iostream_mutex); std::cout << std::this_thread::get_id() << " access local val " << local << '\n'; } } virtual ~TestDeleteBase() = default; }; class TestDelete : public TestDeleteBase { void Run() override {} }; int main() { auto obj = std::make_shared<TestDelete>(); obj->Init(5); std::vector<std::thread> threads; for (int i = 0; i < 5; ++i) { // 每个线程持有独立的强引用,保证执行期间对象不被销毁 auto obj_ref = obj; threads.emplace_back([obj_ref]() { obj_ref->RunParallel(); }); } for (auto& t : threads) { t.join(); } // 所有线程执行完成后,引用计数归零,对象自动释放 }
- 方案2:主线程统一生命周期管理
完全去掉工作线程里的delete this逻辑,等所有工作线程join退出之后,再在主线程统一销毁对象,逻辑简单零心智负担:
// 仅需要修改RunParallel和main逻辑,去掉类内的delete this void RunParallel() { int local = counter_.load(std::memory_order_relaxed); if (counter_.fetch_add(1, std::memory_order_acq_rel) == task_num_ - 1) { { std::lock_guard<std::mutex> guard(g_iostream_mutex); std::cout << std::this_thread::get_id() << " all tasks finished\n"; } return; } else { { std::lock_guard<std::mutex> guard(g_iostream_mutex); std::cout << std::this_thread::get_id() << " running\n"; } std::this_thread::sleep_for(std::chrono::seconds(1)); } { std::lock_guard<std::mutex> guard(g_iostream_mutex); std::cout << std::this_thread::get_id() << " access local val " << local << '\n'; } } int main() { TestDeleteBase* obj = new TestDelete(); obj->Init(5); std::vector<std::thread> threads; for (int i = 0; i < 5; ++i) { threads.emplace_back(&TestDeleteBase::RunParallel, obj); } for (auto& t : threads) { t.join(); } delete obj; // 所有线程都已退出,此时释放100%安全 }
补充:
delete this本身不是语法错误,在单线程引用计数对象自销毁等严格受控场景下是可以用的,但多线程场景下要完全保证delete之后没有任何线程、任何逻辑会触碰对象内存的成本极高,完全没必要为了省一点代码冒线上崩溃的风险。
内容的提问来源于stack exchange,提问作者8.8.8.8
相关产品推荐
相关产品推荐

