关于C++内存管理这话题,永远都不过时。在我刚出道的时候,就已经在考虑怎么检测内存泄漏(https://www.cnblogs.com/coding-my-life/p/3985164.html)。想用一份简单的代码,并且不太影响执行效率去实现内存泄漏检测,是不太现实的。当时觉得重写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 = ; 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[]; return ;
}
$ g++ main.cpp$ ./a.out
new mem:
exit,mem counter =

上面简单地重写了operator new和operator delete,在程序退出时可以检测到还有一次内存没释放掉。但上面的代码存在很多问题。

1. 尽量重写所有函数

  C++的operator new和operator delete函数通常比你想像中的多。而且不同的版本会带来不同的函数,17、20版本都相应的增加了一些函数,参考https://en.cppreference.com/w/cpp/memory/new/operator_new。 如果你没有重写完,虽然能编译通过,但可能并不是你想要的结果。比如上面的代码,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 initialization order fiasco。当然我们有很多方法去处理它,比如把所有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( == pthread_mutex_init( &_mutex,NULL ) );
return &_mutex;
}
static pthread_mutex_t *_mem_mutex_ = counter_mutex(); int g_counter = ; 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 initialization order fiasco的问题,在这里变得更严重了。因为线程安全是用一个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是如何编译、链接一个程序的,尤其是对符号的管理。https://akkadia.org/drepper/dsohowto.pdf

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.
0000000000400d34 T operator new(unsigned long)
r operator new(unsigned long)::__PRETTY_FUNCTION__

T表示text,说明你已经重写了。U表示undefine,表示没有重写,程序运行时,要去库里查找这个符号。

  大部分人重写operator new和operator delete的初衷,无非就是检测内存泄漏,或者实现自己的内存管理。对于内存泄漏,通过重写operator new来实现的,可以看http://wyw.dcweb.cn/leakage.htm这里,现在还在维护的项目是https://github.com/adah1972/nvwa,我没用过,但看下逻辑应该还是不错的。而对于在代码中重写operator new来实现内存管理,我倒没见过。毕竟想写一个通用的内存管理不容易,写出来也是一个库了,比如jemalloc这种。

C++重写new和delete,比想像中困难的更多相关文章

  1. 【MYSQL】update/delete/select语句中的子查询

    update或delete语句里含有子查询时,子查询里的表不能在update或是delete语句中,如含有运行时会报错:但select语句里含有子查询时,子查询里的表可以在select语句中. 如:把 ...

  2. mysql中【update/Delete】update中无法用基于被更新表的子查询,You can't specify target table 'test1' for update in FROM clause.

    关键词:mysql update,mysql delete update中无法用基于被更新表的子查询,You can't specify target table 'test1' for update ...

  3. PHP MySQL Delete删除数据库中的数据

    PHP MySQL Delete DELETE 语句用于从数据库表中删除行. 删除数据库中的数据 DELETE FROM 语句用于从数据库表中删除记录. 语法 DELETE FROM table_na ...

  4. PyQt学习随笔:重写setData方法截获Model/View中视图数据项编辑的注意事项

    根据<PyQt学习随笔:Model/View中视图数据项编辑变动实时获取变动数据的方法>可以重写从PyQt的Model类继承的setData方法来实时截获View中对数据的更改,但需要注意 ...

  5. 编写一个Java应用程序,该程序包括3个类:Monkey类、People类和主类 E。要求: (1) Monkey类中有个构造方法:Monkey (String s),并且有个public void speak() 方法,在speak方法中输出“咿咿呀呀......”的信息。 (2)People类是Monkey类的子类,在People类中重写方法speak(),在speak方法 中输出“小样的,不

    package homework1; public class Monkey { //构造方法 Monkey(String s) { } //成员方法 public void speak() { Sy ...

  6. 将ecshop中的session机制重写,从DB移植到Memcache中去

    <?php if (!defined('IN_ECS')) { die('Hacking attempt'); } /*------------------------------------- ...

  7. Python3 tkinter基础 Canvas delete 删除画布中的所有图形

             Python : 3.7.0          OS : Ubuntu 18.04.1 LTS         IDE : PyCharm 2018.2.4       Conda ...

  8. 重写page的OnInit(学习中总结的)

    在写b/s框架的系统的时候,我们会发现,我们经常会在不同的网页中验证Session是否存在,,而我这里没有用Session,用的是MemCache技术,其实它就是键值对. 只不过将Memcache中的 ...

  9. Boost_1_33_1沒有想像中的恐怖 (李维)

    2006/11/16 下午 02:14:16原則上要先使用build.bat, 產生bjam.exe, 再編繹出library. 由於各編譯器不同, lib並無法共用! 編譯完成後:*.lib在 C: ...

随机推荐

  1. Linux下的常用文本编辑器

    Linux的文本编辑器 ed 在早期的unix平台上,ed可以说是唯一的编辑工具.它是一个很古老的行编辑器,vi这些编辑器都是从ed 演化而来.行编辑器使用起来很不方便,每次只能对一行进行操作,.而后 ...

  2. ffmpeg-4.1.1-win64-dev在vs2017的搭建

    没得话讲,先在官网下载对应的源码,下载dev/文件夹下的源码和静态链接库  ,下载/shared文件夹下的动态链接库 官网地址:https://ffmpeg.zeranoe.com/builds/wi ...

  3. neo4j通过LOAD CSV导入结点和关系

    1.neo4j默认的导入入口是:安装路径/import,所以要将csv文件放在import目录下,像下面这样: 2.导入后中文乱码: 因为neo4j是utf-8的,而CSV默认保存是ANSI的,需要用 ...

  4. goroutine 知识点

    goroutine: 协程是用户态的轻量级线程: 协程之间,通过消息传递进行通信(大多数语言通过共享内存进行通信) select 使用的场景: 正常处理业务的 routine 退出(超时.不满足某条件 ...

  5. 记一次node进程无法kill 问题

    起因 开发反馈测试环境某 node 进程使用 pm2 log 查看日志提示端口冲突,pm2 restart 重启进程问题依旧,但该服务可正常访问. 处理过程: 访问该服务 URL 业务正常,查看 Ng ...

  6. js赋值后,不影响源变量的方法。

    以前都没发现这个问题,特记录一下. var productListData={pages:001}; var data=productListData;//这样就会有问题. data=$.extend ...

  7. centos7安装nginx,以及使用node测试反向代理

    1.添加nginx的安装源 vi /etc/yum.repos.d/nginx.repo 2.输入下面内容,并保存退出 [nginx] name=nginx repo baseurl=http://n ...

  8. android 使用Canvas画箭头

    public class MyCanvas extends View{        private Canvas myCanvas;    private Paint myPaint=new Pai ...

  9. ubuntu 装机步骤表

    步骤 1. root 步骤 apt-get update ; apt-get upgrate apt-get install git zsh apt-get install -y make build ...

  10. mvc部分视图转换成html字符串

    public static class RenderViewTostring { /// <summary> ///将部分视图转成html 字符串方便我们扩展使用 /// </sum ...