【转载】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) #endif static 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技术的解释.应该是这哥们发明的,这里只是使用. 首先列出涉及到的头文件:( ...
随机推荐
- emulator-arm.exe 已停止工作、 emulator-x86 已停止工作
问题描述: emulator-arm.exe 已停止工作. emulator-x86 已停止工作.AVD模拟器启动一直黑屏.AVD模拟器启动一直显示andorid界面 解决方法: 1. sdk的安 ...
- nginx在windwos中的使用
本文章参考了 nginx for windows的介绍:http://nginx.org/en/docs/windows.html 你从官网上下载到的是一个 zip 格式的压缩包,首先要把压缩包解压. ...
- ionic入门之AngularJS扩展基本布局
目录: 标题栏 : ion-header-bar 页脚栏 : ion-footer-bar header/footer : 样式及内容 内容区 : ion-content 滚动框 : ion-scro ...
- NOIP201405生活大爆炸版石头剪刀布
NOIP201405生活大爆炸版石头剪刀布 试题描述 石头剪刀布是常见的猜拳游戏:石头胜剪刀,剪刀胜布,布胜石头.如果两个人出拳一 样,则不分胜负.在<生活大爆炸>第二季第 8 集中出现了 ...
- C#操作CSV存取类
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.D ...
- Java简单数据类型转换
1. Integer<---String (1) Integer x = new Integer(Integer.parseInt(String)); 2. Integer<--- ...
- ASP+Access UTF-8 网页乱码问题解决办法
用ACCESS数据库和ASP做网站时用UTF-8编码有时会出现乱码,再者网页出错或者刷新页面后就是乱码,如果数据库取值乱码在开头加上<%@LANGUAGE="VBSCRIPT" ...
- Centos6.5和Centos7 php环境搭建如何实现呢
首先我们先查看下centos的版本信息 代码如下: #适用于所有的linux lsb_release -a#或者cat /etc/redhat-release#又或者rpm -q centos-rel ...
- 2、HTML
软件的结构: C/S(Client Server)结构的软件: 比如: QQ. 极品飞车. 飞信 . 迅雷 cs结构的软件的缺点:更新的时候需要用户下载更新包然后再安装,程序员则需要开发客户端与服务端 ...
- js instanceof运算符
a instanceof B; instanceof检测对象a的原型链中有没有构造函数B.prototype 如下: function In (a, B) { var p = a.__proto__; ...