临时对象引起的内存泄漏

※ 问题

程序退出时,VC 开发环境提示有多处内存泄漏。

※ 调试

经过数次尝试之后,找到了重现的方法,以及问题出现的操作路径。

于是,使用 _CrtSetBreakAlloc() 来进一步地分析。这是一个 Win32 API,它的用处是在堆上第 { 指定 } 次内存分配时,产生一次异常,调试程序从而可以接管整个程序,直接定位到是哪里的内存分配。这个 API 的调用位置,通常是在程序初始化的时候。

添加调试代码、编译、运行,调试器很顺利地捕获到了这个断点,停留在了

{
    obj.SetArgs(&ArgsWrapper(&info));
    this->Do(&obj);
}

这段代码里,先构造一个 ArgsWrapper 的 C++ 对象,然后它又作为参数,传递给 SetArgs 函数。后续地,会调用 Do 函数来执行。

ArgsWrapper 是一个 C++ 的类,它内部里有多个子类对象,都会有堆内存的申请操作,在这些子类对象的析构函数里会进行释放。既然调试器提示了,就说明这些析构函数可能没有调用到,更进一步地,作为父类的 ArgsWrapper 的析构函数没有调用到。

在 ArgsWrapper 的构造函数及析构函数里添加了打印,再次运行后,发现构造函数和析构函数都有调用到,但在 obj.SetArgs 调用时即已经完成。这说明,析构在接口方法使用前已经被调用,之后调用的接口方法里申请到的内存,已经没有了释放的时机。看来是由于没有控制好这个临时 C++ 对象的生命周期,从而引起了内存泄漏的问题。

那另外一个问题是,为什么析构以后这个对象还是可以访问呢。这个问题应该是这样的,上述代码里定义了一个临时变量,它本身的位置在当前函数的调用栈上,这片内存会在这个函数调用结束后才会被释放,所以在此期间,虽然已经被构了,但内存还是有效的,可以正常操作,除了一点,那就是它的析构函数不会被再次调用。

※ 解决

找到了问题的原因,同时也定位了问题代码的位置,修改就容易了。将上述代码拆分成两句,即先定义一个变量,然后将变量传递给后续函数使用:

{
    ArgsWrapper wrapper(&info);
    obj.SetArgs(&wrapper);
    this->Do(&obj);
}

这样就可以避免临时对象生命周期短的问题了。

Read More: