【转载】C++内存分配
原文:C++内存分配
内存泄露相信对C++程序员来说都不陌生。解决内存泄露的方案多种多样,大部分方案以追踪检测为主,这种方法实现起来容易,使用方便,也比较安全。
首先我们要确定这个模块的主要功能:
- 能追踪内存的分配和释放过程。
- 要能显示内存分配的相关信息,比如内存块大小,代码所在文件所在行等。
- 在发现内存泄露时及时给出相关信息。
- 能正确处理一些异常情况,比如内存不足,对象初始化失败等等。
- 是线程安全的。[*这个还没有实现]
有了一些基本功能需求,我们需要考虑每种功能怎么去实现。首先,我们可以通过重载的方式来追踪new,delete.malloc和free,C++给我提供了这样的特性。因为本文主要针对C++,所以主要讲重载new,delete的方法,malloc和free的重载实现于此类似,最终版本的程序中也实现了malloc和free的重载。
1.重载new和delete
首先我们要了解一下new和delete是怎么工作的。C++中的操作符最终都会被转换成函数形式,例如"new int"会变成"opetaor new(sizeof(int))",而"new double[10]"会变成"operator new(sizeof(double)*10)",同样“delete p”就变成了"operator delete(p)"。另外一个需要特别注意的地方是,new对于用户定义的数据类型(即你的自定义类)会自动调用该类型的构造函数,如果构造函数没有抛出异常,则正确分配,否则会中断分配操作,将异常传递给用户。默认情况下,new可以对象构造异常进行捕获。另外一个版本的new就是不带捕获异常功能的的了,所以C++系统提供的new和delete有:
|
1
2
3
4
5
6
7
8
9
|
void* operator new(size_t size)throw(std::bad_alloc);void* operator new[](size_t size) throw(std::bad_alloc);void* operator new(size_t,std::nothrow_t&)throw();void* operator new[](size_t,std::nothrow_t&)throw();void operator delete(void* pointer);void operator delete[](void* pointer);void operator delete(void* pointer,std::nothrow_t&);void operator delete[](void* pointer,std::nothrow_t&);<br> |
其中,nothrow_t是一个空结构体“struct nothrow_t{}",它的一个实例就是nothrow,C++用它来区分可以捕获异常的new和不可捕获异常的new。我们不能直接修改内部函数的行为,但是我们可以重载它们。为了实现提供内存分配信息的功能,我们给重载的函数加上几个参数。得到以下函数:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
void* operator new(size_t size);void* operator new[](size_t size);void* operator new(size_t,std::nothrow_t&)throw();void* operator new[](size_t,std::nothrow_t&)throw();void* operator new(size_t size,const char* file,const char* func,const int line)throw(std::bad_alloc);void* operator new[](size_t size,const char* file,const char* func,const int line) throw(std::bad_alloc);void* operator delete(void* pointer);void* operator delete[](void* pointer);/*******Placement Delete********/void operator delete(void* pointer,const char* file,const char* func,const int line);void operator delete[](void* pointer,const char* file,const char* func,const int line);void operator delete(void* pointer,std::nothrow_t&);void operator delete[](void* pointer,std::nothrow_t&);/*******************************/ |
中间的几个函数,就是我们主要需要重载的函数,模块的大部分工作也都由着几个函数完成。这些函数参数中,file表示分配代码所在的文件名,func表示代码所在的函数名,line表示代码行号。这几个参数信息我们可以通过编译器预定义好的几个宏来获得:__FILE__,__FUNCTION__,__LINE__。也就是说可以将"new ..."替换成"new(__FILE__,__FUNCTION__,__LINE__) ...",最终成为"operator new(sizeof(...),__FILE__,__FUNCTION__,__LINE__)"的形式,也就达到了我们的目的。关于 placement delete将在下面详细解释。
2.空间分配
接下来我们要考虑内存分配信息的组织问题了。我们先来了解一下编译器是怎么组织的。在大部分编译器中,new所分配的空间都要大于实际申请的空间,大出来的部分就是编译器定义的内存块的信息,包括了内存块的大小还有一些其他信息。如下图所示:

我们把包含内存分配信息的部分叫做cookie数据。为了方便,我们把cookie数据放在分配的内存的起始位置,之后紧接有效数据区。我们还需要把返回给调用者的指针和new分配的数据区联系起来,原本想用性能比较好的STL的map数据结构来储存这些数据,但是map内部同样也使用new来分配内存,所以如果直接使用map来储存,就会陷入死循环中。所以这里我们必须自己现实一个数据结构。我们可以对返回给调用者的地址进行Hash,得到hash 表中的地址,具有相同Hash值的数据我们用一个单向链表连接起来。最终的数据结构如下图所示:

2.1.构造函数中的异常
另外一个必须要注意的一点是,new操作符会先分配空间然后调用用户自定义类型的构造函数,如果构造函数抛出异常,需要用户手动释放已分配的内存。问题在于释放这样的内存不能用一般的delete操作符,可以用一个例子来说明:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
#include <stdio.h>#include <stdexcept>void* operator new(size_t size, int line) { printf("Allocate %u bytes on line %d\\n", size, line); return operator new(size);}class UserClass {public: UserClass(int n)throw(){ if(n<=0){ throw std::runtime_error("n must be positive"); } }};int main(){ try{ UserClass* myobj=new(__LINE__) UserClass(-10); delete myobj; //doesn't work if placement was not defined } catch(const std::runtime_error& e) { fprintf(stderr,"Exception: %s\n",e.what()); } return 0;}<br> |
这里,虽然在new过后试图使用delete释放已经分配的内存,但是实际上不会释放。也许你的编译器会给出这样一条消息:
“no matching operator delete found”
为了正确处理这种情况,并给用户提供相关的信息,我们需要定义placement delete操作符。placement delete是C++98标准中才有的一个特性,所以对于某些老的编译器(大致可以认为是指那些98年以前编写的编译器)不支持这个特性。这需要在模块中添加宏定义让用户可以关闭placement delete的定义,以便模块能在较老的编译器上编译。以下就是需要定义的placement delete操作符:
|
1
2
3
4
|
void operator delete(void* pointer,const char* file,const char* func,const int line);void operator delete[](void* pointer,const char* file,const char* func,const int line);void operator delete(void* pointer,std::nothrow_t&);void operator delete[](void* pointer,std::nothrow_t&);<br> |
3.检查内存泄露
有了上面的实现,我们可以方便的手动检测内存泄露。通过一个函数来实现,它会检索整个hash表,如果表不为空则存在内存泄露。
为了达到在最后程序退出时检查内存泄露的目的,我们需要在所有对象调用析构函数后进行内存泄露检测,这是因为某些用户类型在构造函数里调用new而在析构函数里调用delete。这样做能大大的减小误报的概率。而且因为对象的析构函数的调用往往在主函数main()执行结束之后进行,所以我们也不能直接在主函数里进行内存泄露检测。这里我们利用一个全局对象来实现这种检测。首先我们定义一个类:
|
1
2
3
4
5
6
7
8
9
10
11
|
class MemCheck{ public: MemCheck(){ memset(pTable,0,sizeof(mc_block_node_t*) * MC_HASHTABLESIZE); } ~MemCheck(){ if(mc_checkmem()){ abort(); } }}; |
这里的构造函数初始化Hash表。析构函数检测内存泄露。然后定义一个MemCheck的全局静态对象,这样当程序运行之前会初始化hash表,程序退出时检测内存泄露。可是问题又来了,如果一个程序中有多个全局静态对象会怎样?不幸的是,对于全局静态对象的构造顺序和析构顺序是C++标准中的一个未定义问题,也就是说,这个顺序取决于编译器的具体实现。考虑,绝大多数平台使用VC和GCC编译器,我们可以针对这两种编译器来控制全局对象的构造和解析顺序。
|
1
2
3
4
5
6
7
8
9
|
#ifdef _MSC_VER#pragma init_seg(lib)#endifstatic MemCheck mc_autocheck_object#ifdef __GNUC____attribute__((init_priority (101)))#endif; |
这里的宏定义部分都是编译器的选项。
【转载】C++内存分配的更多相关文章
- [转载]Java 内存分配全面浅析
Java 内存分配全面浅析 2013-02-20 17:54:45 袭烽 阅读数 91353更多 分类专栏: java基础 本文将由浅入深详细介绍Java内存分配的原理,以帮助新手更轻松的学习Ja ...
- 转:内存区划分、内存分配、常量存储区、堆、栈、自由存储区、全局区[C++][内存管理][转载]
内存区划分.内存分配.常量存储区.堆.栈.自由存储区.全局区[C++][内存管理][转载] 一. 在c中分为这几个存储区1.栈 - 由编译器自动分配释放2.堆 - 一般由程序员分配释放,若程序员不释放 ...
- 【转载】Java 内存分配全面浅析
本文将由浅入深详细介绍Java内存分配的原理,以帮助新手更轻松的学习Java.这类文章网上有很多,但大多比较零碎.本文从认知过程角度出发,将带给读者一个系统的介绍. 本文转载自袭烽大神的博客,原文链接 ...
- Spark记录-Spark On YARN内存分配(转载)
Spark On YARN内存分配(转载) 说明 按照Spark应用程序中的driver分布方式不同,Spark on YARN有两种模式: yarn-client模式.yarn-cluster模式. ...
- [图解tensorflow源码] [转载] tensorflow设备内存分配算法解析 (BFC算法)
转载自 http://weibo.com/p/1001603980563068394770 @ICT_吴林阳 tensorflow设备内存管理模块实现了一个best-fit with coales ...
- (转载)图解Java多态内存分配以及多态中成员方法的特点
图解Java多态内存分配以及多态中成员方法的特点 图解Java多态内存分配以及多态中成员方法的特点 Person worker = new Worker(); 子类实例对象地址赋值给父类类型引 ...
- 转载: Java虚拟机:运行时内存数据区域、对象内存分配与访问
转载: https://blog.csdn.net/a745233700/article/details/80291694 (虽然大部分内容都其实是深入理解jvm虚拟机这本书里的,不过整理的很牛逼 ...
- 【转载】C内存分配
一.预备知识—程序的内存分配 一个由C/C++编译的程序占用的内存分为以下几个部分 1.栈区(stack)— 由编译器自动分配释放,存放函数的参数值,局部变量的值等.其操作方式类似于数据结构中的栈. ...
- 【转载】Ogre的内存分配策略
原文:Ogre的内存分配策略 读这个之前,强烈建议看一下Alexandrescu的modern c++的第一章关于policy技术的解释.应该是这哥们发明的,这里只是使用. 首先列出涉及到的头文件:( ...
随机推荐
- ionic之AngularJS扩展 移动开发(视图导航一)
目录: 内联模板 : script 路由机制 : 状态机 导航视图 : ion-nav-view 模板视图 : ion-view 导航栏 : ion-nav-bar 回退按钮 : ion-nav-ba ...
- 缓存方案之Redis
Redis简介 Redis是Remote Dictionary Server(Redis) 的缩写,或许光听名字你就能猜出它大概是做什么的.不错,它是一个由Salvatore Sanfilippo ...
- android 项目学习随笔十四(WebView)
1.布局文件 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:andro ...
- python随机数的使用记录
一.random模块简介 Python标准库中的random函数,可以生成随机浮点数.整数.字符串,甚至帮助你随机选择列表序列中的一个元素,打乱一组数据等. 二.random模块重要函数 1 ).ra ...
- iOS发布条款检查表
序号 分类 条款编号 条款 案例 1 功能 2.1 崩溃的程序将会被拒绝 2 2.2 有错误的程序将会被拒绝 点击版本升级无反应/点击版本升级,在线版本和当前版本都是2.0.3 3 2.3 跟开发者宣 ...
- 10 个迅速提升你 Git 水平的提示【转】
转自:https://www.oschina.net/translate/10-tips-git-next-level 最近我们推出了两个教程:熟悉Git的基本功能和让你在开发团队中熟练的使用Git ...
- restful 注解
@Path @Path 注释被用来描述根资源.子资源方法或子资源的位置.value 值可以包含文本字符.变量或具有定制正则表达式的变量. @GET.@POST.@PUT.@DELETE.@HEAD @ ...
- 织梦系统中出现DedeTag Engine Create File False提示原因及解决方法
今天更新网站时dedecms系统时,遇到一个问题:DedeTag Engine Create File False 出现这样的提示. 其实这也不算是什么错误,我个人觉得最重要的一点就是根目录下没有给 ...
- expect语法
沙河西ftp上传,使用了expect语言的脚本. 我们经常会遇到一些需要与服务器程序打交道的场景,比如,从登陆某个服务器,然后进行某项工作.这很平常,但是如果把这个工作自动化进行,你就需要一个程序能自 ...
- java面试每日一题13
题目:有一个分数数列2/1.3/2.5/3.8/5.13/8........求出这个数列的前20项之和.运行结果如下32.660263 public class Page80 { /** * * @p ...