最近在看侯捷的STL源码剖析,有很多新的C++知识以前不曾见过,找到了这个博文写的不错,转下,以便日后查看。
如果用户不定义析构函数,而是用系统自带的,则说明,析构函数基本没有什么用(但默认会被调用)我们称之为trivial destructor。反之,如果特定定义了析构函数,则说明需要在释放空间之前做一些事情,则这个析构函数称为non-trivial destructor。如果某个类中只有基本类型的话是没有必要调用析构函数的,delelte p的时候基本不会产生析构代码,
在C++的类中如果只有基本的数据类型,也就不需要写显式的析构函数,即用默认析构函数就够用了,但是如果类中有个指向其他类的指针,并且在构造时候分配了新的空间,则在析构函数中必须显式释放这块空间,否则会产生内存泄露,
在STL中空间配置时候destory()函数会判断要释放的迭代器的指向的对象有没有 trivial destructor(STL中有一个 has_trivial_destructor函数,很容易实现检测),如果有trivial destructor则什么都不做,如果没有即需要执行一些操作,则执行真正的destory函数。
下面是一个例子,不记得在哪里看到了,一直保存在电脑上,是转载的。
项目中有一些地方为了得到较快的速度,使用了无需释放的简易内存分配器:先一次分配一大块内存,然后每次需要内存的时候从这块内存里面直接递增分配合适大小的内存块。当使用完这些内存了以后,不做显式的释放,直到整个大任务结束了以后,才一次释放那整块内存。这样做优点在于
- 内存分配非常快速,仅仅递增指针即可
- 没有释放的开销(除了整块释放)
- 不会有碎片
这种做法是很多引擎的常见选择。但是也有一个很大的缺陷,就是每个分配对象的析构函数并没有被调用。当然我们在释放所有内存的时候也可以试图调用所有的对象的析构函数,不过出于效率考虑,很多引擎并没有在所有的模块里进行析构函数的调用,尤其是一些POD(plain old data),不调用析构函数也明显无害。当然,这样就留下了隐患。
根据墨菲定理,不想发生的事情,最终总会发生。最近项目就遇到了这样的情况。
先是大家发现有很快速的内存泄漏,不巧的是,正好引擎的内存监测系统年久失修,本来可以秒杀的问题,由于内存监测系统的罢工,导致无法被快速定位。
手工人力定位,最后发现,问题就出在我们省略的析构函数调用上。
原来,新来的同事扩展了原来的POD结构,在POD里面加上了资源分配,现在这就不是POD了,变成标准的类,顺理成章的,他也添加了析构函数,在里面释放新分配出来的资源。问题就在于,这个类型是用前述的快速分配系统创建出来的,析构函数从我们设计之初就没打算调用,这样分配的资源无法释放,造成了泄漏。
对于这样有隐患的系统,的确无法继续容忍,但我们也不打算为这样的例外牺牲效率,对所有快速分配系统调用析构函数。所以问题的核心,就变成了我们是否能够检测的这种情况了。
解决问题的想法是,我们能否检测到这个情况,如果能在编译时刻检测到这一情况,我们就可以报错,或者用模版来产生不同的代码,从而对POD沿用旧的机制,没有额外的开销,其他类则自动调用析构函数。
进一步简化问题,我们如果能确定一个类有non-trivial destructor,我们就要在释放的时候对这些对象调用析构函数。
google了一下,原来stl里面就有一个std::has_trivial_destructor,大喜,使用,果然有用,用这个模板类可以检测出一个类有没有non-trivial destructor。这样就很容易能实现编译时刻的类型检测,从而产生报警或是模版类处理析构情况。
由于项目中使用的是eastl,就原样替换成eastl的has_trivial_destructor,发现失败了。。。原来eastl只支持检测一个类型是否是pod,但是这个pod检测也不准。。。放了一个pod的struct,居然说不是pod。看了eastl代码,发现注视里面提到,考虑到跨平台的问题,说只能做到那样了。。。但是vc自带stl是可以工作的,再去看VC stl实现代码,vc的stl可读性太差了。。。都是各种宏替换。遂使用编译器预处理cpp文件,产生了一个.i文件,看了一下,里面检测non-trivial destructor的地方调用了一个__has_trivial_destructor。这个东西有意思,据我所知一般vc里面__开头的函数都是intrinsic函数。查了msdn,真是vc自己扩展的,还顺藤摸瓜,查到了其他一堆检测c++类型的intrinsic函数。
问题迎刃而解,只要用vc的扩展函数就可以搞定了。但是这东西牺牲了跨平台性,也算双刃剑。