记一次 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对象的 size 和 capacity 成员
经过数次尝试,我们发现,并不是每次游戏结束都会抛出异常,但抛出异常时, capacity 成员都瞬间变成了一个不正常的大数
这也确认了异常就是由该string对象的异常析构引起
而析构的异常与 capacity 的异常值有着密切的关系
那么接下来只要确定在 游戏结束瞬间 ,capacity 突变时
到底是哪些语句造成了这灾难性的后果即可
经过筛查
发现这句代码非常可疑
map[Y][X] = WALL;
map是用来储存俄罗斯方块每个网格的状态
capacity 的值在这句代码执行后发生了跳变
而且这也是唯一一句可以非法修改内存数据的代码(下标越界)
真相
事实也印证了这点
游戏结束的条件是有方块超出上方边界
即下标为负
此时仍执行赋值
map[Y][X] = WALL;
即对非法内存进行修改
而这块内存恰好 就是这string对象的所属范围
string对象的定义语句恰好在map上方
导致了在内存中string对象的 内存地址 相邻且小于map
何其巧合,越界后的下标恰好落在 capacity 的四个字节之内 导致了大数的产生
进而导致析构异常
害
这个bug花了我N个小时,查遍了内外网,谁又能想到string的析构异常与一个char数组有关
其实只要在访问前 确保下标合法即可规避此类问题
呜呜 奈何当事人神经大条