记一次 C++ string 假性“内存泄露“ (_Big_allocation_sentinel)

710 阅读3分钟

记一次 C++ string 假性"内存泄露" (_Big_allocation_sentinel)

起因

那是一个没有星星的夜晚,阿珍爱上了阿... 啊,不是,我正在测试[俄罗斯方块],当最后一块方块落下,宣告游戏结束时,Per~

在这里插入图片描述

出现了 Runtime_Error 运行时错误

啊、这...

定位到 xmemory 的115行

	// If the following asserts, it likely means that we are performing
	// an aligned delete on memory coming from an unaligned allocation.
	
_STL_ASSERT(_Ptr_user[-2] == _Big_allocation_sentinel, "invalid argument");

	// Extra paranoia on aligned allocation/deallocation; ensure _Ptr_container is
	// in range [_Min_back_shift, _Non_user_size]

该行的 _STL_ASSERT 抛出异常

根据上下注释以及变量的字面意思可以大致了解到以下信息

该异常与 [释放大量内存] 有关 并且 并非直接由程序代码引起 而是与STL有关

接着单击[重试] 继续运行

抛出第二个异常 在这里插入图片描述

该异常发生在右花括号处,即作用域末尾 而该作用域仅有一个对象,也就是游戏核心Game类对象

由此判断,异常发生在Game类析构过程中

而Game类中并没有手动申请内存,因此不存在该类风险

但值得注意的是,第一处异常由_STL_ASSERT 抛出,那么应该是标准库设施出现问题

观察数据成员,仅有一个STL对象,即

const string savePath = "C:\\Users\\18134\\source\\repos\\Tetris v2.0\\Tetris-save.txt";

该string对象用以储存游戏存档路径

也就是说该string对象析构时抛出了异常,且与内存容量有关

可是一个 const 对象为什么会造成内存泄露呢?

Debug

修正程序错误的第一步是要重现这个错误。—— Tom Duff

为了找出原因,我们开始监控该string对象的 sizecapacity 成员 经过数次尝试,我们发现,并不是每次游戏结束都会抛出异常,但抛出异常时, capacity 成员都瞬间变成了一个不正常的大数 在这里插入图片描述 在这里插入图片描述

这也确认了异常就是由该string对象的异常析构引起 而析构的异常与 capacity 的异常值有着密切的关系

那么接下来只要确定在 游戏结束瞬间capacity 突变时 到底是哪些语句造成了这灾难性的后果即可

经过筛查 发现这句代码非常可疑

map[Y][X] = WALL;

map是用来储存俄罗斯方块每个网格的状态

capacity 的值在这句代码执行后发生了跳变

而且这也是唯一一句可以非法修改内存数据的代码(下标越界)

真相

事实也印证了这点 游戏结束的条件是有方块超出上方边界

即下标为 此时仍执行赋值

map[Y][X] = WALL;

即对非法内存进行修改

而这块内存恰好 就是这string对象的所属范围

string对象的定义语句恰好在map上方 导致了在内存中string对象的 内存地址 相邻且小于map

何其巧合,越界后的下标恰好落在 capacity 的四个字节之内 导致了大数的产生 进而导致析构异常

这个bug花了我N个小时,查遍了内外网,谁又能想到string的析构异常与一个char数组有关

其实只要在访问前 确保下标合法即可规避此类问题

呜呜 奈何当事人神经大条