最近项目中遇到了诡异的内存泄露,最开始用肉眼看,看malloc realloc和free是否对应上了,new和delete是否对应上了,无果。 再怀疑是tcmalloc搞的鬼,用heap check追踪了一阵,无果,主要是项目太大(跑起来占几十GB内存、70多线程),启动起来都要半分钟, 所以调试起来非常慢,尽管heap check比valgrind快多了,但还是不爽。 接着,编译时就把tcmalloc干掉了,结果还是泄露,看样子tcmalloc是无辜的,再怀疑是-O3的问题, 把-O3去掉,还是泄露,最终还是得从自己的代码里找问题,结果发现更诡异的现象:有一个析构函数,写在头文件里,内存就泄露, 写在cpp文件里,内存就不泄露。
最终在大量的编译时产生的warning信息中找到了答案:对于非完整类型(incomplete type),不能使用delete来析构,具体见下面的举例说明。
首先举例有如下几个简单的代码文件:
//文件b.h
#ifndef __B_H__
#define __B_H__
#include <iostream>
class B
{
public:
B()
{
std::cout << "b constructor" << std::endl;
}
~B()
{
std::cout << "b destructor" << std::endl;
}
};
#endif
//文件a.h
#ifndef __A_H__
#define __A_H__
#include <iostream>
//类声明
class B;
class A
{
public:
A();
~A()
{
if (_one) {
delete _one;
_one = NULL;
}
}
private:
B *_one;
};
#endif
//文件a.cpp
#include "a.h"
#include "b.h"
A::A()
{
_one = new B;
}
//文件main.cpp
#include <iostream>
#include "a.h"
using namespace std;
int main()
{
A a;
return 0;
}
编译
g++ a.cpp main.cpp -o run -I./
时会报如下warning:
In file included from a.cpp:1:
a.h: In destructor ‘A::~A()’:
a.h:17: warning: possible problem detected in invocation of delete operator:
a.h:17: warning: invalid use of incomplete type ‘struct B’
a.h:6: warning: forward declaration of ‘struct B’
a.h:17: note: neither the destructor nor the class-specific operator delete will be called, even if they are declared when the class is defined.
在a.h文件中,类型B就是所谓的非完整(incomplete type)类型,只是用class B;
进行了声明, 而并没有include "b.h"
包含具体头文件,这种做法是值得推荐的,因为a.h中只有B*
指针类型, 只声明一下class B;
可以加快编译,避免b.h被编译器反复读取。
在此种情形下,delete非完整类型就不被支持,正如编译器的warning所言:类的析构函数和操作符不会被调用, 换句话说,就什么都不会做。但这只是一个warning,并不是error,所以程序还是被正常编译生成。
一个大模块往往由很多人共同开发,各种风格代码,各种接口定义习惯,关键是编译出现的warning谁也不愿意管,warning一多,有用的warning就被忽略了。如此就出现了上面诡异的现象:写在cpp文件中不泄露,而写在头文件中就泄露。
编译报的warning一定得解决,一来编译时心情舒畅,二来便于扼杀问题于摇篮。 除内联和模板等情况,尽量在cpp中实现代码,保持头文件的简洁和轻巧。