ATEngin开发记录_5_C++日志打印引发的崩溃?一次虚函数调用引发的内存错误排查记录

发布于:2025-04-20 ⋅ 阅读:(47) ⋅ 点赞:(0)

该系列只做记录 不做教程 所以文章简洁直接 会列出碰到的问题和解决方案 只适合C++萌新

在使用 C++ 进行事件系统开发时,我遇到了一次由于调用虚函数 GetName() 输出日志而引发的崩溃问题。通过逐步排查、使用防御性编程和类型检查,最终定位到了隐藏的生命周期/类型问题,并找到了更安全的日志打印方式。本文记录了排查过程和背后原理,供自己和他人参考。

程序编译非常正常,就是在运行的时候总是弹出内存问题,查了很久才找到问题点!


成功编译运行之后发生崩溃

在这里插入图片描述

经过层层查询是这段代码出现了错误:

void OnEvent(at_engine::Event& event) override
{
	HZ_CLIENT_TRACE("{0}", event.ToString());
}

如果这个宏调用内部触发了格式化模板逻辑,而 event.GetName() 返回的是 const char*,但该指针是非法的(未初始化、返回了悬空的字符串等),那就可能崩了。


解决方案: 防御性编程!

void OnEvent(at_engine::Event& event) override
{
	try
	{
		auto type = event.GetEventType(); 
		auto name = event.GetName(); 
		HZ_CLIENT_TRACE("Event actual type: {}", typeid(event).name());
		HZ_CLIENT_TRACE("Event ptr: {}", (void*)&event);

		HZ_CLIENT_TRACE("EventType: {}", (int)type);
		HZ_CLIENT_TRACE("EventName: {}", name ? name : "null");
	} catch (const std::exception& e)
	{
		HZ_CLIENT_ERROR("Exception in OnEvent: {}", e.what());
	}
}

复盘一下:

  1. 这次的程序崩溃排查了很久,因为编译正常,没有实际指出是哪里错误。之前在窗口回调鼠标事件的时候出现过解析不了事件类型的情况,于是在回调鼠标事件之前加了判断,解决了当时的问题,但是不能确定是不是由此引发了新问题。后来经过排查GLFW 初始化本身应该不会影响 layer 的调用,除非在 glfwCreateWindow 后有一些后续的状态或资源没有正确初始化。例如,如果在窗口创建后尝试在没有有效上下文的情况下渲染到某个图层,这可能会导致崩溃。
glfwSetWindowSizeCallback(m_Window, [](GLFWwindow* window, int width, int height)
						  {
							  WindowData& data = *(WindowData*)glfwGetWindowUserPointer(window);
							  data.Width = width;
							  data.Height = height;

							  WindowResizeEvent event(width, height);
							  if (data.EventCallback)//这里就是新增的判断
								  data.EventCallback(event);
						  });

2.1 没有思路,准备一点点注释排查,然后新发现一旦运行m_LayerInsert_ = m_Layers_.insert(m_LayerInsert_, layer); 操作 就会报内存操作 。那么既然是内存泄露,那要不将Layer*替换成std::unique_ptr,使用智能指针来替代原始指针管理资源?

	void LayerStack::PushLayer(Layer* layer)
	{
		m_LayerInsert_ = m_Layers_.emplace(m_LayerInsert_, layer);
	}

我们先来看一下区别:std::unique_ptr vs Layer*:(AI搜索)
在这里插入图片描述
2.2 针对智能指针和原始指针看一下区别:
使用 Layer*:

Layer* layer = new Layer();
layerStack.PushLayer(layer);
// ...
delete layer; // 要记得手动释放,否则内存泄露

使用 std::unique_ptr:

auto layer = std::make_unique<Layer>();
layerStack.PushLayer(std::move(layer));
// 不需要 delete,作用域结束或 vector 清理时会自动释放

2.3当然了 修改智能指针并不会只修改这一点点,我更改完所有相关的指针之后,发现崩溃依旧,那就不是这里的问题。


3.1 又经过一系列的排查,发现这段代码注释了就不崩溃了:

class ExampleLayer : public at_engine::Layer
{
public:
   ExampleLayer()
   	: Layer("Example")
   {

   }

   void OnUpdate() override
   {
   	 HZ_CLIENT_INFO("ExampleLayer::Update");
   }

   void OnEvent(at_engine::Event& event) override
   {
   	 HZ_CLIENT_INFO("Event Type: {}", event.GetName()); //就是这句话 这里的event.GetName()
   }

};

3.2 分析一下:最可能的原因:对象类型不匹配或指针已失效。

  1. 虚函数调用崩溃 => 对象不是你以为的类型,或者 vtable 坏了
  2. 引用或指针的生命周期越界了
  3. 是否可能有线程间访问 event 对象,但没有加锁?如下这种会导致未定义行为甚至崩溃:
    在这里插入图片描述

3.3添加更多的Log进行测试:

   try
   {
   	auto type = event.GetEventType(); 
   	auto name = event.GetName(); 
   	HZ_CLIENT_TRACE("Event actual type: {}", typeid(event).name());
   	HZ_CLIENT_TRACE("Event ptr: {}", (void*)&event);

   	HZ_CLIENT_TRACE("EventType: {}", (int)type);
   	HZ_CLIENT_TRACE("EventName: {}", name ? name : "null");
   } catch (const std::exception& e)
   {
   	HZ_CLIENT_ERROR("Exception in OnEvent: {}", e.what());
   }


  1. Log说明:
    1. APP: Event actual type: class at_engine::MouseMovedEvent事件对象确实是 MouseMovedEvent 类型,虚函数机制没有失效,也没有被对象切片!
    2. APP: Event ptr: 0xbd0e12de48 说明:对象地址是有效的,不是空指针或野指针。
    3. APP: EventType: 14 和 APP: EventName: MouseMoved 说明:GetEventType() 和 GetName() 都正常调用成功,字符串返回值也没问题。
    4. ATEngine: MouseMovedEvent: 1255, 28 说明对 MouseMovedEvent 的 ToString() 重载输出,说明整个事件对象生命周期和成员访问都正常。
      在这里插入图片描述

  1. 如果这个宏调用内部触发了格式化模板逻辑,而 event.GetName() 返回的是 const char*,但该指针是非法的(未初始化、返回了悬空的字符串等),那就可能崩了。
HZ_CLIENT_TRACE("{}", event.GetName());

总结一下:

1.先看原先会报错写法:

HZ_CLIENT_INFO("Event Type: {}", event.GetName());

2.再看不会报错写法:

auto type = event.GetEventType(); 
auto name = event.GetName(); 
HZ_CLIENT_TRACE("Event actual type: {}", typeid(event).name());
  1. 区别在于:情况一:没有对象切片,一切正常
    如果 event 是一个有效的派生类对象引用,比如 MouseMovedEvent&,那么两种写法本质上是等价的 —— 都调用了虚函数 GetName(),从派生类中获取返回值。

  2. 区别在于:情况二:对象切片 或 引用悬空

Event e = MouseMovedEvent(...);  // 对象切片:派生类内容被"砍掉"
OnEvent(e);                      // 传的是 base Event 类型

event.GetName() 实际上不会调用到 MouseMovedEvent::GetName(),而是 Event::GetName()(如果不是纯虚函数)
如果 GetName() 是 virtual 并强行向下访问派生内容,会访问无效内存或崩溃

5.区别在于:event 引用或指针指向了一个已释放对象 比如一个临时对象或栈上变量已经离开作用域。
6.区别在于:日志系统在展开 event.GetName() 的时候发生副作用如果 GetName() 返回的是 const char*,而它背后用到了临时字符串或者 dangling 指针,就容易出问题。

7.为什么分开写就没问题?

auto name = event.GetName();  // 这里立即将结果保存下来

8.举个例子:
GetName() 是这样实现的:

virtual const char* GetName() const override {
    std::string result = fmt::format("MouseMovedEvent: {}, {}", m_X, m_Y);
    return result.c_str(); // ❌ 返回了临时对象的 c_str
}

9.所以最终安全写法:

auto name = std::string(event.GetName()); // 复制一份内容
HZ_CLIENT_INFO("Event Type: {}", name);
}

10.总结一下
在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到