关于c++内存管理这话题,永远都不过时。在我刚出道的时候,就已经在考虑怎么检测内存泄漏()。想用一份简单的代码,并且不太影响执行效率去实现内存泄漏检测,是不太现实的。当时觉得重写new和delete是没有太大价值的,不过后来在自己的项目中还是重写了,加了个计数。在程序退出时检测下计数new的次数和delete次数是否对得上,对不上就是有问题了,再用valgrind之类的工具去检测。这种排除不了所有情况,但确实也解决了一些问题。毕竟每次写新功能时发现问题立马去解决,比你写了成千上万个功能,上线后出问题再查找容易得多。
在windows下则有另一种方案, c run-time library (crt) debug,_crtdumpmemoryleaks()函数,这也仅仅是发现泄漏,定位还得用另一个工具visual leak detector。最近在解决公司程序内存泄漏过程中,发现其实并没有内存泄漏,而是程序是在atexit里调用_crtdumpmemoryleaks()函数的,而static变量申请的内存,可能要在atexit回调之后释放。由此,我忽然想到我以前重写new和delete,有些地方写得并不对,在这里重新整理一下。
new、delete并不是一个函数,它在编译的时候会被解析成三个步骤:1.调用operator new分配内存;2.调用构造函数;3.把指针转换成对应的类型返回。能够重写的,是operator new函数。
#include <cstdlib> #include <iostream> int g_counter = 0; void *operator new(size_t size) { ++g_counter; std::cout << "new mem:" << g_counter << std::endl; return ::malloc(size); } void operator delete(void* ptr) { --g_counter; std::cout << "delete mem:" << g_counter << std::endl; ::free(ptr); } void on_exit() { std::cout << "exit,mem counter = " << g_counter << std::endl; } int main() { atexit(on_exit); char *ptr = new char[8]; return 0; }
$ g++ main.cpp$ ./a.out new mem:1 exit,mem counter = 1
上面简单地重写了operator new和operator delete,在程序退出时可以检测到还有一次内存没释放掉。但上面的代码存在很多问题。
1. 尽量重写所有函数
c++的operator new和operator delete函数通常比你想像中的多。而且不同的版本会带来不同的函数,17、20版本都相应的增加了一些函数,参考。 如果你没有重写完,虽然能编译通过,但可能并不是你想要的结果。比如上面的代码,new char[8]本应该调用operator new[]函数的,由于没有重写operator new[],默认调用了libstdcxx中的operator new[],默认函数又调用了operator new。虽然这不一定有什么问题,但在某些项目中,对内存分配做了特殊处理,或者一些特殊操作(比如一个内存池重写了operator new[]但没重写operator delete[],而他们的内存是回收到不同地方),这就会出问题。
2. 利用atexit统计内存并不准确
atexit是在程序退出时调用,对绝大多数变量来说都是ok的,但对static和global变量则不一定了。根据c++标准:https://isocpp.org/files/papers/n3690.pdf 3.6.3 termination,atexit注册之前就已经创建的变量,则在atexit之后释放,这意味着你的static和global变量如果new了内存必须在atexit之后创建。但这又引出c++的另一个问题:。当然我们有很多方法去处理它,比如把所有static和global放到一个cpp文件里,或者在程序退出时手动释放new的内存。另外,gcc链接的时候,放在最后的object文件里的global变量会优先初始化,或者用gcc的__attribute__ ((init_priority (n)))属性来指定初始化优先级,但这不是标准,不过这毕竟是值得注意的地方。
3. 线程安全
上面的代码没有加锁,所以是不能用在多线程中的。但现在有几个程序不用多线程的,所以还是得把锁加上,加锁的代码很简单。
static pthread_mutex_t *counter_mutex() { static pthread_mutex_t _mutex; assert( 0 == pthread_mutex_init( &_mutex,null ) ); return &_mutex; } static pthread_mutex_t *_mem_mutex_ = counter_mutex(); int g_counter = 0; void *operator new(size_t size) { assert( _mem_mutex_ ); pthread_mutex_lock( _mem_mutex_ ); ++g_counter; pthread_mutex_unlock( _mem_mutex_ ); std::cout << "new mem:" << g_counter << std::endl; return ::malloc(size); }
4. 线程安全带来初始化问题
在上面说atexit统计内存不准确的时候提到的问题,在这里变得更严重了。因为线程安全是用一个static pthread_mutex_t指针来实现的,那么在其他global变量创建时如果调用了new,那么它可能是没有被初始化的。当然如果你已按上面的方法解决了,那就不会有这个问题了。或者,根据c++标准,static initialization初始化必须在所有dynamic initialization之前,我们可以这样写:
/* static initialization */ static pthread_mutex_t *_mem_mutex_ = null; class global_static { public: global_static() { assert( !_mem_mutex_ ); _mem_mutex_ = counter_mutex(); } ~global_static() {_mem_mutex_ = null;} }; /* dynamic initialization */ const static global_static gs;
这样虽然不能解决问题,但是由于我们在new里校验了_mem_mutex_是否为null,至少能发现问题。
既然是c++,那么还可以construct on first use idiom:在使用_mem_mutex_时去检测是否已初始化,未初始化就初始化。而不是像上面那样全局一次初始化,以后都不用检测。
5. 能否统计到stl、boost、so、.a等外部代码中的new、delete是否会被重写
stl和boost这种很多时候是是模板,也就是源码,和你项目中的代码一样,当然也会被重写。对于so动态链接库,他和程序是分离的。当你的程序加载这个so文件时,它会优先在你的程序里查找他需要的符号,如果找到了,就会优先使用。这和ld_preload的机制是一样的,因此也是会被重写的。而.a这种静态链接库,在gcc链接时会按你传入的库顺序查找符号,一般来说你项目中的符号都是优先于libgcc这种标准库的,因此也是会被重写的。
要明白这些,要懂得gcc是如何编译、链接一个程序的,尤其是对符号的管理。
6. 可以用nm来判断是否重写
xzc@xzc-hp-probook-4446s:~/documents/code/test$ nm -c a.out | grep new 0000000000400f60 t test_static_new() u operator new[](unsigned long)@@glibcxx_3.4 0000000000400d34 t operator new(unsigned long) 0000000000401120 r operator new(unsigned long)::__pretty_function__
t表示text,说明你已经重写了。u表示undefine,表示没有重写,程序运行时,要去库里查找这个符号。
大部分人重写operator new和operator delete的初衷,无非就是检测内存泄漏,或者实现自己的内存管理。对于内存泄漏,通过重写operator new来实现的,可以看这里,现在还在维护的项目是,我没用过,但看下逻辑应该还是不错的。而对于在代码中重写operator new来实现内存管理,我倒没见过。毕竟想写一个通用的内存管理不容易,写出来也是一个库了,比如jemalloc这种。
本文来自网络收集,不代表计算机技术网立场,如涉及侵权请联系管理员删除。
ctvol管理联系方式QQ:251552304
本文章地址:https://www.ctvol.com/c-cdevelopment/605623.html