c++ 什么是非法访问内存位置访问无效 csdn博客

s2: 软件Develop进阶(755)
s2: 实战Unix/Linux编程(98)
s2: 实战Unix/Linux杂项(322)
s2: 后台开发(214)
& & & &本文继续介绍 valgind的使用, 看程序:
#include &stdio.h&
int main()
int a[100];
a[10000] = 0;
}& & & &用valgrind分析:
[root@xxx ~/valgrind-3.8.1/bin]# ./valgrind --tool=memcheck --leak-check=yes --show-reachable=yes ./a.out
==27955== Memcheck, a memory error detector
==27955== Copyright (C) , and GNU GPL'd, by Julian Seward et al.
==27955== Using Valgrind-3.8.1 and LibVEX; rerun with -h for copyright info
==27955== Command: ./a.out
==27955== Invalid write of size 4
at 0x40055F: main (test.cpp:6)
Address 0x7ff009f90 is not stack'd, malloc'd or (recently) free'd
==27955== Process terminating with default action of signal 11 (SIGSEGV): dumping core
Access not within mapped region at address 0x7FF009F90
at 0x40055F: main (test.cpp:6)
If you believe this happened as a result of a stack
overflow in your program's main thread (unlikely but
possible), you can try to increase the size of the
main thread stack using the --main-stacksize= flag.
The main thread stack size used in this run was 8388608.
==27955== HEAP SUMMARY:
in use at exit: 0 bytes in 0 blocks
total heap usage: 0 allocs, 0 frees, 0 bytes allocated
==27955== All heap blocks were freed -- no leaks are possible
==27955== For counts of detected and suppressed errors, rerun with: -v
==27955== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 6 from 6)
Segmentation fault
[root@xxx ~/valgrind-3.8.1/bin]# & & & &可以看到, &第6行有内存非法访问, 程序core dump了。 当然, 除了用valgrind, 我们还可以用其他方法来查出这个内存异常访问导致的core dump问题, 之前介绍过很多次了, 故不再赘述。
& & & &要注意的是, 如果写成a[100] = 0; 实际发现, 并没有core dump, 但这也是非常危险的操作, 应该避免。 至于是否真的core dump, 那就要看你的运气了。 总之, 不要依赖于此类undefined behavior.
& & & 类似的非法内存访问还有:
#include &stdio.h&
int main()
}& & &valgrind分析结果如下:
[root@xxx ~/valgrind-3.8.1/bin]# g++ -g test.cpp
[root@xxx ~/valgrind-3.8.1/bin]#
[root@xxx ~/valgrind-3.8.1/bin]# ./valgrind --tool=memcheck --leak-check=yes --show-reachable=yes ./a.out
==31774== Memcheck, a memory error detector
==31774== Copyright (C) , and GNU GPL'd, by Julian Seward et al.
==31774== Using Valgrind-3.8.1 and LibVEX; rerun with -h for copyright info
==31774== Command: ./a.out
==31774== Use of uninitialised value of size 8
at 0x40055C: main (test.cpp:6)
==31774== Invalid write of size 1
at 0x40055C: main (test.cpp:6)
Address 0x0 is not stack'd, malloc'd or (recently) free'd
==31774== Process terminating with default action of signal 11 (SIGSEGV): dumping core
Access not within mapped region at address 0x0
at 0x40055C: main (test.cpp:6)
If you believe this happened as a result of a stack
overflow in your program's main thread (unlikely but
possible), you can try to increase the size of the
main thread stack using the --main-stacksize= flag.
The main thread stack size used in this run was 8388608.
==31774== HEAP SUMMARY:
in use at exit: 0 bytes in 0 blocks
total heap usage: 0 allocs, 0 frees, 0 bytes allocated
==31774== All heap blocks were freed -- no leaks are possible
==31774== For counts of detected and suppressed errors, rerun with: -v
==31774== Use --track-origins=yes to see where uninitialised values come from
==31774== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 6 from 6)
Segmentation fault
[root@xxx ~/valgrind-3.8.1/bin]# &
& & & 再比如:
#include &stdio.h&
#include &stdlib.h&
#include &memory.h&
int main()
char *p = (char *)malloc(30);
p[31] = 'a';
& & & &valgrind分析结果如下:
[root@xxx ~/valgrind-3.8.1/bin]# ./valgrind --tool=memcheck --leak-check=yes --show-reachable=yes ./a.out
==32673== Memcheck, a memory error detector
==32673== Copyright (C) , and GNU GPL'd, by Julian Seward et al.
==32673== Using Valgrind-3.8.1 and LibVEX; rerun with -h for copyright info
==32673== Command: ./a.out
==32673== Invalid write of size 1
at 0x4005C2: main (test.cpp:8)
Address 0x5d8405f is 1 bytes after a block of size 30 alloc'd
at 0x4C278FE: malloc (vg_replace_malloc.c:270)
by 0x4005B5: main (test.cpp:7)
==32673== HEAP SUMMARY:
in use at exit: 30 bytes in 1 blocks
total heap usage: 1 allocs, 0 frees, 30 bytes allocated
==32673== 30 bytes in 1 blocks are definitely lost in loss record 1 of 1
at 0x4C278FE: malloc (vg_replace_malloc.c:270)
by 0x4005B5: main (test.cpp:7)
==32673== LEAK SUMMARY:
definitely lost: 30 bytes in 1 blocks
indirectly lost: 0 bytes in 0 blocks
possibly lost: 0 bytes in 0 blocks
still reachable: 0 bytes in 0 blocks
suppressed: 0 bytes in 0 blocks
==32673== For counts of detected and suppressed errors, rerun with: -v
==32673== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 6 from 6)
[root@xxx ~/valgrind-3.8.1/bin]#
& & & OK, &先说这么多。
&&相关文章推荐
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:3171386次
积分:44982
积分:44982
排名:第71名
原创:1653篇
转载:138篇
评论:1755条
(16)(61)(14)(32)(52)(8)(10)(28)(56)(32)(26)(15)(42)(63)(38)(39)(75)(43)(4)(20)(33)(17)(11)(7)(48)(53)(51)(52)(35)(20)(53)(41)(35)(13)(32)(36)(7)(1)(47)(23)(26)(4)(13)(4)(19)(72)(13)(2)(14)(45)(32)(7)(3)(28)(53)(106)(68)12108人阅读
原文地址:/debugging/valgrind.html
翻译难免会因个人水平原因而有不准确的地方,请大家多批评指正,上面是原文链接,大家也可以直接去看看。
valgrind是Linux平台一个多用途的代码审查和内存调试工具。它可以在valgrind自己的环境中运行你的程序,监控malloc/free,(new/delete for C++)等内存调用。如果你用了未初始化的内存,数组越界写入,或者忘了free一个指针,valgrind会检测到它们。由于这些都是一些日常最普通的问题,这篇文章就主要介绍如何用valgrind来发现这类简单的内存问题,虽然valgrind可以做的更多。
对windows使用者来说,如果你没有对linux机器的访问权限,或者你想开发windows程序,那么你可能对IBM的Purity软件更有兴趣,Purity在检测内存问题方面与valgrind功能类似,你可以自己去下载它。
获取Valgrind
如果你正在运行linux,而没有安装valgrind,那么你可以从下载。
安装很简单,只需要解压就可以了。(XYZ在下面的例子中是版本号的意思)
bzip2 -d valgrind-XYZ.tar.bz2
tar -xf valgrind-XYZ.tar
上面的操作会创建一个目录,名字为valgrind-XYZ,切换到这个目录,运行下面的命令
./configure
make install
现在你已经安装了valgrind,让我们看下怎么使用它
使用Valgrind查找内存泄漏
内存泄漏是最难检测的bug之一,因为直到你用完了内存而有一个malloc失败,它不会表现出任何外在的问题。实际上,当我们使用C/C++这类没有垃圾回收机制的语言来工作的时候,几乎一半的时间会花费在正确处理free内存的问题上。如果你的程序运行足够长的时间并且运行到了那个代码分支,即使一个错误也是代价巨大的。
当你用valgrind运行你的代码的时候,你需要指定使用valgrind的什么工具;简单地运行一下valgrind你就会得到当前的列表。在这篇文章中,我们主要使用memcheck工具,memcheck工具可以保证我们正确的内存使用。不加其他参数,valgrind会打印出调用call和malloc的一个概括信息(注意18490是我系统上的process id;在不同的运行时,它是不同的)
% valgrind --tool=memcheck program_name
=18515== malloc/free: in use at exit: 0 bytes in 0 blocks.
==18515== malloc/free: 1 allocs, 1 frees, 10 bytes allocated.
==18515== For a detailed leak analysis,
rerun with: --leak-check=yes
如果你有一处内存泄漏,那么alloc和free的数目就会不同(你不能用free去释放一块属于多个alloc的内存)。我们稍后会回来看这个概况,但是现在,注意一些errors被制止了,因为它们是来自标准库的,而不是来自你的代码。
如果alloc和free的数目不同,你需要用选项--leak-check来重新运行程序。这会向你展示所有的没有相匹配的free的malloc/new等调用。
为了说明,我用一个简单的程序,我编译成可执行文件&example1&
#include &stdlib.h&
int main()
char *x = malloc(100); /* or, in C++, &char *x = new char[100] */
% valgrind --tool=memcheck --leak-check=yes example1
这会产生关于上面展示的程序的一些信息,生成一份列表,调用malloc但是没有相应的free。
==2116== 100 bytes in 1 blocks are definitely lost in loss record 1 of 1
at 0x1B900DD0: malloc (vg_replace_malloc.c:131)
by 0x804840F: main (in /home/cprogram/example1)
这并没有按照我们希望的那样告诉我们太多,尽管我们知道这个内存泄漏是发现在main中对malloc的调用,但是我们不知道行号。这个问题是因为我们编译的时候没有用gcc的-g选项,这个选项会添加调试符号。因此我们重新编译,得到下面更有用的信息。
==2330== 100 bytes in 1 blocks are definitely lost in loss record 1 of 1
at 0x1B900DD0: malloc (vg_replace_malloc.c:131)
by 0x804840F: main (example1.c:5)
现在我们知道确切的行号,在哪里这块泄漏的内存被分配。尽管想要跟踪下去到free的时候,还是个问题,但是至少我们知道从哪开始查起。并且,既然对每一处malloc或new的调用,你都有计划如何处理这块内存,知道内存在哪里泄漏会让你知道从哪里开始查找问题。
有时,选项--leak-check=yes不会向你展示所有的内存泄漏。要找到所有的不成对的对free和new的调用,你需要使用选项--show-reachable=yes。它的输出基本是相同的,但是它会向你展示更多unfreed的内存。
使用Valgrind发现非法指针使用
Valgrind通过memcheck工具发现非法堆内存的使用。例如,如果你用malloc或new分配一个数组,然后尝试访问数组的边界外位置:
char *x = malloc(10);
x[10] = 'a';
Valgrind会检测到。例如,用valgrind运行下面的程序example2
#include &stdlib.h&
int main()
char *x = malloc(10);
x[10] = 'a';
运行方法:
valgrind --tool=memcheck --leak-check=yes example2
Invalid write of size 1
at 0x804841E: main (example2.c:6)
Address 0x1BA3607A is 0 bytes after a block of size 10 alloc'd
at 0x1B900DD0: malloc (vg_replace_malloc.c:131)
by 0x804840F: main (example2.c:5)
上面的结果告诉我们:我们正在越界使用一个被分配了10bytes空间的指针,结果有一个‘Invalid write’。如果我们尝试从那块内存读数据,我们会被将被警告'Invalid read of size X',此处的X是我们尝试读取的内存大小(对char类型来说,它是1,对int就是2或者4).通常,Valgrind会打印出函数调用的堆栈信息,我们就会确切地知道错误在哪发生。
使用Valgrind检测未初始化的变量使用
Valgrind还有一个用途,它可以检测到在条件语句中使用未初始化的值。尽管你应该习惯在创建一个变量的时候进行初始化,但是Valgrind会帮助你发现那些你忘记的地方。例如,运行下面example3的代码:
#include &stdio.h&
int main()
if(x == 0)
printf(&X is zero&); /* replace with cout and include
iostream for C++ */
==17943== Conditional jump or move depends on uninitialised value(s)
at 0x804840A: main (example3.c:6)
Valgrind会足够聪明,知道一个变量是否被用一个未初始化的变量赋值,例如,下面的代码:
#include &stdio.h&
int foo(int x)
if(x & 10)
printf(&x is less than 10\n&);
int main()
结果如下:
==4827== Conditional jump or move depends on uninitialised value(s)
at 0x8048366: foo (example4.c:5)
by 0x8048394: main (example4.c:14)
你可能会认为问题出在foo函数中,堆栈信息也不重要。但是,由于main传了一个未初始化的值给foo函数(你从来没有给y赋值),那么我们查找问题并跟踪变量的赋值堆栈,直到我们发现了一个未初始化的变量。
这只会在你的程序走到那个分支的时候帮助你,尤其是条件语句。所以确保在测试的时候能够让程序走过所有的分支。
Valgrind能够发现的其他问题
Valgrind会检测到其他的一些不恰当的内存使用:如果你free一个指针两次,Valgrind会为你检测到;你会得到下面错误:
Invalid free()
并跟随一些相关的堆栈
Valgrind也会检测到释放内存时选择了错误的方法。例如,在C++中,有3中基本的释放动态内存的方法:free,delete,delete[]。free函数只跟malloc匹配,delete只跟new匹配,delete[]只跟new[]匹配。(尽管有些编译器会为你处理此类用错delete的情况,但是不能保证所有的编译器都能,它不是变准要求的)。
如果你触发了这类问题,你会得到错误:
Mismatched free() / delete / delete []
这是必须要修复的,即使你的程序碰巧可以工作。
Valgrind不能发现的东西
Valgrind不能检查静态数组的边界(在栈上分配的空间)。因此,如果你在你的函数中声明一个数组
int main()
char x[10];
x[11] = 'a';
那么,Valgrind不会警告你的!一个可能用于测试目的的解决方案是在你需要做边界检测的地方转换你的静态数组为从堆上动态分配内存,但是这会产生很多unfreed的内存。
其他更过的警告信息
Valgrind的缺点是什么呢?它会消耗更多的内存--最大两倍于你源程序需要的内存。如果你在检测一个很大的内存问题,那这可能会导致一些问题。它会需要更长的时间去运行你的程序。这通常不应该有什么问题,并且也只是在你测试的时候有影响而已。但是如果你正在运行一个本来已经很慢的程序,那么这也可能会是个问题。
Valgrind是一个对x86和AMD64结构的一个工具,运行在Linux环境下。它允许程序员在它的环境下运行程序,因此可以检测不成对的malloc和其他使用非法内存(例如未初始化的内存)的问题或者非法内存操作(例如重复free同一块内存,调用错误的析构函数)。Valgrind不能检测静态内存问题。
水平有限,如果有朋友发现错误,欢迎留言交流。
转载请保留本文链接,如果觉得我的文章能帮到您,请顶一下。,谢谢。
&&相关文章推荐
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:584561次
积分:4765
积分:4765
排名:第5740名
原创:79篇
评论:107条
阅读:1425
文章:14篇
阅读:89366
阅读:5429
文章:14篇
阅读:203752
(13)(1)(1)(3)(1)(14)(4)(3)(2)(1)(1)(1)(2)(4)(6)(6)(1)(1)(4)(5)(6)(2)(2)(1)c/c++/vc(113)
2004 年 3 月 01 日
内存泄漏对于C/C++程序员来说也可以算作是个永恒的话题了吧。在Windows下,MFC的一个很有用的功能就是能在程序运行结束时报告是否发生了内存泄漏。在Linux下,相对来说就没有那么容易使用的解决方案了:像mpatrol之类的现有工具,易用性、附加开销和性能都不是很理想。本文实现一个极易于使用、跨平台的C++内存泄漏检测器。并对相关的技术问题作一下探讨。
对于下面这样的一个简单程序test.cpp:
int main()
char* p2 = new char[10];
我们的基本需求当然是对于该程序报告存在两处内存泄漏。要做到这点的话,非常简单,只要把debug_new.cpp也编译、链接进去就可以了。在Linux下,我们使用:
g++ test.cpp debug_new.cpp -o test
输出结果如下所示:
Leaked object at 0x805e438 (size 10, &Unknown&:0)
Leaked object at 0x805e410 (size 4, &Unknown&:0)
如果我们需要更清晰的报告,也很简单,在test.cpp开头加一行
#include "debug_new.h"
即可。添加该行后的输出如下:
Leaked object at 0x805e438 (size 10, test.cpp:5)
Leaked object at 0x805e410 (size 4, test.cpp:4)
非常简单!
在new/delete操作中,C++为用户产生了对operator new和operator delete的调用。这是用户不能改变的。operator new和operator delete的原型如下所示:
void *operator new(size_t) throw(std::bad_alloc);
void *operator new[](size_t) throw(std::bad_alloc);
void operator delete(void*) throw();
void operator delete[](void*) throw();
对于"new int",编译器会产生一个调用"operator new(sizeof(int))",而对于"new char[10]",编译器会产生"operator new[](sizeof(char) * 10)"(如果new后面跟的是一个类名的话,当然还要调用该类的构造函数)。类似地,对于"delete ptr"和"delete[] ptr",编译器会产生"operator delete(ptr)"调用和"operator delete[](ptr)"调用(如果ptr的类型是指向对象的指针的话,那在operator delete之前还要调用对象的析构函数)。当用户没有提供这些操作符时,编译系统自动提供其定义;而当用户自己提供了这些操作符时,就覆盖了编译系统提供的版本,从而可获得对动态内存分配操作的精确跟踪和控制。
同时,我们还可以使用placement new操作符来调整operator new的行为。所谓placement new,是指带有附加参数的new操作符,比如,当我们提供了一个原型为
void* operator new(size_t size, const char* file, int line);
的操作符时,我们就可以使用"new("hello", 123) int"来产生一个调用"operator new(sizeof(int), "hello", 123)"。这可以是相当灵活的。又如,C++标准要求编译器提供的一个placement new操作符是
void* operator new(size_t size, const std::nothrow_t&);
其中,nothrow_t通常是一个空结构(定义为"struct nothrow_t {};"),其唯一目的是提供编译器一个可根据重载规则识别具体调用的类型。用户一般简单地使用"new(std::nothrow) 类型"(nothrow是一个nothrow_t类型的常量)来调用这个placement new操作符。它与标准new的区别是,new在分配内存失败时会抛出异常,而"new(std::nothrow)"在分配内存失败时会返回一个空指针。
要注意的是,没有对应的"delete(std::nothrow) ptr"的语法;不过后文会提到另一个相关问题。
要进一步了解以上关于C++语言特性的信息,请参阅[Stroustrup1997],特别是6.2.6、10.4.11、15.6、19.4.5和B.3.4节。这些C++语言特性是理解本实现的关键。
和其它一些内存泄漏检测的方式类似,debug_new中提供了operator new重载,并使用了宏在用户程序中进行替换。debug_new.h中的相关部分如下:
void* operator new(size_t size, const char* file, int line);
void* operator new[](size_t size, const char* file, int line);
#define new DEBUG_NEW
#define DEBUG_NEW new(__FILE__, __LINE__)
拿上面加入debug_new.h包含后的test.cpp来说,"new char[10]"在预处理后会变成"new("test.cpp", 4) char[10]",编译器会据此产生一个"operator new[](sizeof(char) * 10, "test.cpp", 4)"调用。通过在debug_new.cpp中自定义"operator new(size_t, const char*, int)"和"operator delete(void*)"(以及"operator new[]&"和"operator delete[]&";为避免行文累赘,以下不特别指出,说到operator new和operator delete均同时包含数组版本),我可以跟踪所有的内存分配调用,并在指定的检查点上对不匹配的new和delete操作进行报警。实现可以相当简单,用map记录所有分配的内存指针就可以了:new时往map里加一个指针及其对应的信息,delete时删除指针及对应的信息;delete时如果map里不存在该指针为错误删除;程序退出时如果map里还存在未删除的指针则说明有内存泄漏。
不过,如果不包含debug_new.h,这种方法就起不了作用了。不仅如此,部分文件包含debug_new.h,部分不包含debug_new.h都是不可行的。因为虽然我们使用了两种不同的operator new --"operator new(size_t, const char*, int)"和"operator new(size_t)"-- 但可用的"operator delete"还是只有一种!使用我们自定义的"operator delete",当我们删除由"operator new(size_t)"分配的指针时,程序将认为被删除的是一个非法指针!我们处于一个两难境地:要么对这种情况产生误报,要么对重复删除同一指针两次不予报警:都不是可接受的良好行为。
看来,自定义全局"operator new(size_t)"也是不可避免的了。在debug_new中,我是这样做的:
void* operator new(size_t size)
return operator new(size, "&Unknown&", 0);
但前面描述的方式去实现内存泄漏检测器,在某些C++的实现中(如GCC 2.95.3中带的SGI STL)工作正常,但在另外一些实现中会莫名其妙地崩溃。原因也不复杂,SGI STL使用了内存池,一次分配一大片内存,因而使利用map成为可能;但在其他的实现可能没这样做,在map中添加数据会调用operator new,而operator new会在map中添加数据,从而构成一个死循环,导致内存溢出,应用程序立即崩溃。因此,我们不得不停止使用方便的STL模板,而使用手工构建的数据结构:
struct new_ptr_list_t
new_ptr_list_t*
const char*
我最初的实现方法就是每次在使用new分配内存时,调用malloc多分配 sizeof(new_ptr_list_t) 个字节,把分配的内存全部串成一个一个链表(利用next字段),把文件名、行号、对象大小信息分别存入file、line和size字段中,然后返回(malloc返回的指针 + sizeof(new_ptr_list_t))。在delete时,则在链表中搜索,如果找到的话((char*)链表指针 + sizeof(new_ptr_list_t) == 待释放的指针),则调整链表、释放内存,找不到的话报告删除非法指针并abort。
至于自动检测内存泄漏,我的做法是生成一个静态全局对象(根据C++的对象生命期,在程序初始化时会调用该对象的构造函数,在其退出时会调用该对象的析构函数),在其析构函数中调用检测内存泄漏的函数。用户手工调用内存泄漏检测函数当然也是可以的。
基本实现大体就是如此。
上述方案最初工作得相当好,直到我开始创建大量的对象为止。由于每次delete时需要在链表中进行搜索,平均搜索次数为(链表长度/2),程序很快就慢得像乌龟爬。虽说只是用于调试,速度太慢也是不能接受的。因此,我做了一个小更改,把指向链表头部的new_ptr_list改成了一个数组,一个对象指针放在哪一个链表中则由它的哈希值决定。--用户可以更改宏DEBUG_NEW_HASH和DEBUG_NEW_HASHTABLESIZE的定义来调整debug_new的行为。他们的当前值是我测试下来比较满意的定义。
使用中我们发现,在某些特殊情况下(请直接参看debug_new.cpp中关于DEBUG_NEW_FILENAME_LEN部分的注释),文件名指针会失效。因此,目前的debug_new的缺省行为会复制文件名的头20个字符,而不只是存储文件名的指针。另外,请注意原先new_ptr_list_t的长度为16字节,现在是32字节,都能保证在通常情况下内存对齐。
此外,为了允许程序能和 new(std::nothrow) 一起工作,我也重载了operator new(size_t, const std::nothrow_t&) throw();不然的话,debug_new会认为对应于 new(nothrow) 的delete调用删除的是一个非法指针。由于debug_new不抛出异常(内存不足时程序直接报警退出),所以这一重载的操作只不过是调用 operator new(size_t) 而已。这就不用多说了。
前面已经提到,要得到精确的内存泄漏检测报告,可以在文件开头包含"debug_new.h"。我的惯常做法可以用作参考:
#ifdef _DEBUG
#include "debug_new.h"
包含的位置应当尽可能早,除非跟系统的头文件(典型情况是STL的头文件)发生了冲突。在某些情况下,可能会不希望debug_new重定义new,这时可以在包含debug_new.h之前定义DEBUG_NEW_NO_NEW_REDEFINITION,这样的话,在用户应用程序中应使用debug_new来代替new(顺便提一句,没有定义DEBUG_NEW_NO_NEW_REDEFINITION时也可以使用debug_new代替new)。在源文件中也许就该这样写:
#ifdef _DEBUG
#define DEBUG_NEW_NO_NEW_REDEFINITION
#include "debug_new.h"
#define debug_new new
并在需要追踪内存分配的时候全部使用debug_new(考虑使用全局替换)。
用户可以选择定义DEBUG_NEW_EMULATE_MALLOC,这样debug_new.h会使用debug_new和delete来模拟malloc和free操作,使得用户程序中的malloc和free操作也可以被跟踪。在使用某些编译器的时候(如Digital Mars C++ Compiler 8.29和Borland C++ Compiler 5.5.1),用户必须定义NO_PLACEMENT_DELETE,否则编译无法通过。用户还可以使用两个全局布尔量来调整debug_new的行为:new_verbose_flag,缺省为false,定义为true时能在每次new/delete时向标准错误输出显示跟踪信息;new_autocheck_flag,缺省为true,即在程序退出时自动调用check_leaks检查内存泄漏,改为false的话用户必须手工调用check_leaks来检查内存泄漏。
需要注意的一点是,由于自动调用check_leaks是在debug_new.cpp中的静态对象析构时,因此不能保证用户的全局对象的析构操作发生在check_leaks调用之前。对于Windows上的MSVC,我使用了"#pragma init_seg(lib)"来调整对象分配释放的顺序,但很遗憾,我不知道在其他的一些编译器中(特别是,我没能成功地在GCC中解决这一问题)怎么做到这一点。为了减少误报警,我采取的方式是在自动调用了check_leaks之后设new_verbose_flag为true;这样,就算误报告了内存泄漏,随后的delete操作还是会被打印显示出来。只要泄漏报告和delete报告的内容一致,我们仍可以判断出没有发生内存泄漏。
Debug_new也能检测对同一指针重复调用delete(或delete无效指针)的错误。程序将显示错误的指针值,并强制调用abort退出。
还有一个问题是异常处理。这值得用专门的一节来进行说明。
我们看一下以下的简单程序示例:
#include &stdexcept&
#include &stdio.h&
void* operator new(size_t size, int line)
printf("Allocate %u bytes on line %d//n", size, line);
return operator new(size);
class Obj {
Obj(int n);
Obj::Obj(int n) : _n(n)
if (n == 0) {
throw std::runtime_error("0 not allowed");
int main()
Obj* p = new(__LINE__) Obj(0);
} catch (const std::runtime_error& e) {
printf("Exception: %s//n", e.what());
看出代码中有什么问题了吗?实际上,如果我们用MSVC编译的话,编译器的警告信息已经告诉我们发生了什么:
test.cpp(27) : warning C4291: 'void *__cdecl operator new(unsigned int,int)' :
no matching o memory will not be freed if initialization throws an exception
好,把debug_new.cpp链接进去。运行结果如下:
Allocate 4 bytes on line 27 Exception: 0 not allowed Leaked object at 00342BE8 (size 4, &Unknown&:0)
啊哦,内存泄漏了不是!
当然,这种情况并非很常见。可是,随着对象越来越复杂,谁能够保证一个对象的子对象的构造函数或者一个对象在构造函数中调用的所有函数都不会抛出异常?并且,解决该问题的方法并不复杂,只是需要编译器对 C++ 标准有较好支持,允许用户定义 placement delete 算符([C++1998],5.3.4节;网上可以找到1996年的标准草案,比如下面的网址 )。在我测试的编译器中,GCC(2.95.3或更高版本,Linux/Windows)和MSVC(6.0或更高版本)没有问题,而Borland C++ Compiler 5.5.1和Digital Mars C++ Compiler(到v8.38为止的所有版本)则不支持该项特性。在上面的例子中,如果编译器支持的话,我们就需要声明并实现 operator delete(void*, int) 来回收new分配的内存。编译器不支持的话,需要使用宏让编译器忽略相关的声明和实现。如果要让debug_new在Borland C++ Compiler 5.5.1或Digital Mars C++ Compiler下编译的话,用户必须定义宏NO_PLACEMENT_DELETE;当然,用户得自己注意小心构造函数中抛出异常这个问题了。
IBM developerWorks上刊载了洪琨先生设计实现的一个Linux上的内存泄漏检测方法([洪琨2003])。我的方案与其相比,主要区别如下:
跨平台:只使用标准函数,并且在GCC 2.95.3/3.2(Linux/Windows)、MSVC 6、Digital Mars C++ 8.29、Borland C++ 5.5.1等多个编译器下调试通过。(虽然Linux是我的主要开发平台,但我发现,有时候能在Windows下编译运行代码还是非常方便的。)
易用性:由于重载了operator new(size_t)--洪琨先生只重载了operator new(size_t, const char*, int)--即使不包含我的头文件也能检测内存泄漏;程序退出时能自动检测内存泄漏;可以检测用户程序(不包括系统/库文件)中malloc/free产生的内存泄漏。
灵活性:有多个灵活的可配置项,可使用宏定义进行编译时选择。
可重入性:不使用全局变量,没有嵌套delete问题。
异常安全性:在编译器支持的情况下,能够处理构造函数中抛出的异常而不发生内存泄漏。
单线程模型:跨平台的多线程实现较为麻烦,根据项目的实际需要,也为了代码清晰简单起见,我的方案不是线程安全的;换句话说,如果多个线程中同时进行new或delete操作的话,后果未定义。
未实现运行中内存泄漏检测报告机制:没有遇到这个需求J;不过,如果要手工调用check_leaks函数实现的话也不困难,只是跨平台性就有点问题了。
不能检测带 [] 算符和不带 [] 算符混用的不匹配:主要也是需求问题(如果要修改实现的话并不困难)。
不能在错误的delete调用时显示文件名和行号:应该不是大问题;由于我重载了operator new(size_t),可以保证delete出错时程序必然有问题,因而我不只是显示警告信息,而且会强制程序abort,可以通过跟踪程序、检查abort时程序的调用栈知道问题出在哪儿。
另外,现在已存在不少商业和Open Source的内存泄漏检测器,本文不打算一一再做比较。Debug_new与它们相比,功能上总的来说仍较弱,但是,其良好的易用性和跨平台性、低廉的附加开销还是具有很大优势的。
以上段落基本上已经说明了debug_new的主要特点。下面做一个小小的总结。
重载的算符:
operator new(size_t, const char*, int)
operator new[](size_t, const char*, int)
operator new(size_t)
operator new[](size_t)
operator new(size_t, const std::nothrow_t&)
operator new[](size_t, const std::nothrow_t&)
operator delete(void*)
operator delete[](void*)
operator delete(void*, const char*, int)
operator delete[](void*, const char*, int)
operator delete(void*, const std::nothrow_t&)
operator delete[](void*, const std::nothrow_t&)
提供的函数:
check_leaks() 检查是否发生内存泄漏
提供的全局变量
new_verbose_flag 是否在new和delete时"罗嗦"地显示信息
new_autocheck_flag 是否在程序退出是自动检测一次内存泄漏
可重定义的宏:
NO_PLACEMENT_DELETE 假设编译器不支持placement delete(全局有效)
DEBUG_NEW_NO_NEW_REDEFINITION 不重定义new,假设用户会自己使用debug_new(包含debug_new.h时有效)
DEBUG_NEW_EMULATE_MALLOC 重定义malloc/free,使用new/delete进行模拟(包含debug_new.h时有效)
DEBUG_NEW_HASH 改变内存块链表哈希值的算法(编译debug_new.cpp时有效)
DEBUG_NEW_HASHTABLE_SIZE 改变内存块链表哈希桶的大小(编译debug_new.cpp时有效)
DEBUG_NEW_FILENAME_LEN 如果在分配内存时复制文件名的话,保留的文件名长度;为0时则自动定义DEBUG_NEW_NO_FILENAME_COPY(编译debug_new.cpp时有效;参见文件中的注释)
DEBUG_NEW_NO_FILENAME_COPY 分配内存时不进行文件名复制,而只是保存其指针;效率较高(编译debug_new.cpp时有效;参见文件中的注释)
我本人认为,debug_new目前的一个主要缺陷是不支持多线程。对于某一特定平台,要加入多线程支持并不困难,难就难在通用上(当然,条件编译是一个办法,虽然不够优雅)。等到C++标准中包含线程模型时,这个问题也许能比较完美地解决吧。另一个办法是使用像boost这样的程序库中的线程封装类,不过,这又会增加对其它库的依赖性--毕竟boost并不是C++标准的一部分。如果项目本身并不用boost,单为了这一个目的使用另外一个程序库似乎并不值得。因此,我自己暂时就不做这进一步的改进了。
另外一个可能的修改是保留标准operator new的异常行为,使其在内存不足的情况下抛出异常(普通情况)或是返回NULL(nothrow情况),而不是像现在一样终止程序运行(参见debug_new.cpp的源代码)。这一做法的难度主要在于后者:我没想出什么方法,可以保留 new(nothrow) 的语法,同时能够报告文件名和行号,并且还能够使用普通的new。不过,如果不使用标准语法,一律使用debug_new和debug_new_nothrow的话,那还是非常容易实现的。
如果大家有改进意见或其它想法的话,欢迎来信讨论。
debug_new 的源代码目前可以在 处下载。
在这篇文章的写完之后,我终于还是实现了一个线程安全的版本。该版本使用了一个轻量级的跨平台互斥体类fast_mutex(目前支持Win32和POSIX线程,在使用GCC(Linux/MinGW)、MSVC时能通过命令行参数自动检测线程类型)。有兴趣的话可在 下载。
[C++1998] ISO/IEC 14882. Programming Languages-C++, 1st Edition. International Standardization Organization, International Electrotechnical Commission, American National Standards Institute, and Information Technology Industry Council, 1998
[Stroustrup1997] Bjarne Stroustrup. The C++ Programming Language, 3rd Edition. Addison-Wesley, 1997
[洪琨2003] 洪琨。 ,IBM developerWorks 中国网站。
吴咏炜,目前在Linux上从事高性能入侵检测系统的研发。对于开发跨平台、高性能、可重用的C++代码有着浓厚的兴趣。 可以跟他联系。
&&相关文章推荐
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:526772次
积分:6939
积分:6939
排名:第3097名
原创:110篇
转载:268篇
评论:116条
(2)(1)(1)(1)(1)(9)(12)(20)(1)(21)(20)(6)(11)(35)(16)(46)(4)(4)(31)(37)(20)(41)(6)(27)(4)(2)

我要回帖

更多关于 刷csdn博客访问量 的文章

 

随机推荐