内存问题


内存泄露:

内存泄漏解释

  简单的说就是申请了一块内存空间,使用完毕后没有释放掉。它的一般表现方式是程序运行时间越长,占用内存越多,最终用尽全部内存,整个系统崩溃。由程序申请的一块内存,且没有任何一个指针指向它,那么这块内存就泄露了。

泄漏的分类

  以发生的方式来分类,内存泄漏可以分为4类:

(1). 常发性内存泄漏。

  发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。

(2). 偶发性内存泄漏。

  发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。

(3). 一次性内存泄漏。

  发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次。

(4). 隐式内存泄漏。

  程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。

危害

  从用户使用程序的角度来看,内存泄漏本身不会产生什么危害,作为一般的用户,根本感觉不到内存泄漏的存在。真正有危害的是内存泄漏的堆积,这会最终消耗尽系统所有的内存。从这个角度来说,一次性内存泄漏并没有什么危害,因为它不会堆积,而隐式内存泄漏危害性则非常大,因为较之于常发性和偶发性内存泄漏它更难被检测到。

缓冲区溢出:

缓冲区溢出是指当计算机程序缓冲区内填充的数据位数超过了缓冲区本身的容量。溢出的数据覆盖在合法数据上。理想情况是,程序检查数据长度并且不允许输入超过缓冲区长度的字符串。但是绝大多数程序都会假设数据长度总是与所分配的存储空间相匹配,这就为缓冲区溢出埋下隐患。操作系统所使用的缓冲区又被称为堆栈,在各个操作进程之间,指令被临时存储在堆栈当中,堆栈也会出现缓冲区溢出。 

当一个超长的数据进入到缓冲区时,超出部分就会被写入其他缓冲区,其他缓冲区存放的可能是数据、下一条指令的指针,或者是其他程序的输出内容,这些内容都被覆盖或者破坏掉。可见一小部分数据或者一套指令的溢出就可能导致一个程序或者操作系统崩溃。

     
 
 

总结段错误(Segmentation fault)

1)往受到系统保护的内存地址写数据 有些内存是内核占用的或者是其他程序正在使用,为了保证系统正常工作,所以会受到系统的保护,而不能任意访问.

1#include <stdio.h>
2int
3main()
4{
5int i = 0;
6scanf ("%d", i); /* should have used &i */
7printf ("%d\n", i);
8return 0;
9}

 

 

编译和执行一下, 咋一看,好像没有问题哦,不就是读取一个数据然后给输出来吗? falcon@falcon:~/temp$ gcc -g -o segerr segerr.c –加-g选项查看调试信息 falcon@falcon:~/temp$ gdb ./segerr GNU gdb 6.4-debian Copyright 2005 Free Software Foundation, Inc. GDB is free software, covered by the GNU General Public License, and you are welcome to change it and/or distribute copies of it under certain conditions. Type “show copying” to see the conditions. There is absolutely no warranty for GDB. Type “show warranty” for details. This GDB was configured as “i486-linux-gnu”…Using host libthread_db library “/ lib/tls/i686/cmov/libthread_db.so.1″. (gdb) l –用l(list)显示我们的源代码   

01#include <stdio.h>
02  
03 int
04 main()
05 {
06 int i = 0;
07  
08 scanf (”%d”, i); /* should have used &i */
09 printf (”%d\n”, i);
10 return 0;

 

(gdb) b 8 –用b(break)设置断点 Breakpoint 1 at 0×80483b7: file segerr.c, line 8. (gdb) p i –用p(print)打印变量i的值[看到没,这里i的值是0哦] $1 = 0 (gdb) r –用r(run)运行,直到断点处 Starting program: /home/falcon/temp/segerr Breakpoint 1, main () at segerr.c:8 8 scanf (”%d”, i); /* should have used &i */ –[试图往地址0处写进一个值] (gdb) n –用n(next)执行下一步 10 Program received signal SIGSEGV, Segmentation fault. 0xb7e9a1ca in _IO_vfscanf () from /lib/tls/i686/cmov/libc.so.6 (gdb) c –在上面我们接收到了SIGSEGV,然后用c(continue)继续执行 Continuing. Program terminated with signal SIGSEGV, Segmentation fault. The program no longer exists. (gdb) quit –退出gdb 果然 我们“不小心”把&i写成了i 而我们刚开始初始化了i为0,这样我们不是试图向内存地址0存放一个值吗? [补充: 可以通过man 7 signal查看SIGSEGV的信息。 falcon@falcon:~/temp$ man 7 signal | grep SEGV Reformatting signal(7), please wait… SIGSEGV 11 Core Invalid memory reference 例子2:

01#include <stdio.h>
02int
03main()
04{
05char *p;
06p = NULL;
07*p = ‘x’;
08printf(”%c”, *p);
09return 0;
10}

 

 

很容易发现,这个例子也是试图往内存地址0处写东西。 这里我们通过gdb来查看段错误所在的行 falcon@falcon:~/temp$ gcc -g -o segerr segerr.c falcon@falcon:~/temp$ gdb ./segerr GNU gdb 6.4-debian Copyright 2005 Free Software Foundation, Inc. GDB is free software, covered by the GNU General Public License, and you are welcome to change it and/or distribute copies of it under certain conditions. Type “show copying” to see the conditions. There is absolutely no warranty for GDB. Type “show warranty” for details. This GDB was configured as “i486-linux-gnu”…Using host libthread_db library “/lib/tls/i686/cmov/libthread_db.so.1″. (gdb) r –直接运行,我们看到抛出段错误以后,自动显示出了出现段错误的行,这就是一个找出段错误的方法 Starting program: /home/falcon/temp/segerr Program received signal SIGSEGV, Segmentation fault. 0×08048516 in main () at segerr.c:10 10 *p = ‘x’; (gdb) 2)内存越界(数组越界,变量类型不一致等)

1#include <stdio.h>
2int
3main()
4{
5char test[1];
6printf(”%c”, test[1000000000]);
7return 0;
8}

 

 

这里是比较极端的例子,但是有时候可能是会出现的,是个明显的数组越界的问题 或者是这个地址是根本就不存在的 例子4:

1#include <stdio.h>
2int
3main()
4{
5int b = 10;
6printf(”%s\n”, b);
7return 0;
8}

 

 

我们试图把一个整数按照字符串的方式输出出去,这是什么问题呢? 由于还不熟悉调试动态链接库,所以 我只是找到了printf的源代码的这里 声明部分:

01int pos =0 ,cnt_printed_chars =0 ,i ;
02unsigned char *chptr ;
03va_list ap ;
04/* %s格式控制部分:*/
05case 's':
06chptr =va_arg (ap ,unsigned char *);
07i =0 ;
08while (chptr [i ])
09{...
10cnt_printed_chars ++;
11putchar (chptr [i ++]);
12}

 

 

由于我没有仔细分析代码,大致的原因也可能是地址越界的原因?不过我可不确定哦。 如果大家知道怎么调试printf函数,麻烦帮忙找出越界的真正原因吧,这个段错误也可能是 处在va_start和va_arg等函数里头?或者直接看看这个这里的printf源代码的分析,看看是否 可以找出出错的地方: http://www.wangchao.net.cn/bbsdetail_47325.html 类似的,还有诸如:sprintf等的格式控制问题 比如,试图把char型或者是int的按照%s输出或存放起来,如:

01#include <stdio.h>
02#include <string.h>
03char c=’c';
04int i=10;
05char buf[100];
06printf(”%s”, c); //试图把char型按照字符串格式输出
07printf(”%s”, i); //试图把int型按照字符串输出
08memset(buf, 0, 100);
09sprintf(buf, “%s”, c); //试图把char型按照字符串格式转换
10memset(buf, 0, 100);
11sprintf(buf, “%s”, i); //试图把int型按照字符串转换

 

 

3)其他 其实大概的原因都是一样的,就是段错误的定义。 但是更多的容易出错的地方就要自己不断积累,不段发现,或者吸纳前人已经积累的经验,并且注意避免再次发生。 例如: <1>定义了指针后记得初始化,在使用的时候记得判断是否为NULL <2>在使用数组的时候是否被初始化,数组下标是否越界,数组元素是否存在等 <3>在变量处理的时候变量的格式控制是否合理等 一个比较不错的例子: 我在进行一个多线程编程的例子里头,定义了一个线程数组 #define THREAD_MAX_NUM pthread_t thread[THREAD_MAX_NUM]; 用pthread_create创建了各个线程,然后用pthread_join来等待线程的结束 刚开始 我就直接等待,在创建线程都成功的时候,pthread_join能够顺利等待各个线程结束 但是一旦创建线程失败,那用pthread_join来等待那个本不存在的线程时自然会存在访问不存在的内存的情况,从而导致段错误的发生 后来 通过不断调试和思考,并且得到网络上资料的帮助,找到了上面的出错原因和解决办法 解决办法是: 在创建线程之前,先初始化我们的线程数组 在等待线程的结束的时候,判断线程是否为我们的初始值 如果是的话,说明我们的线程并没有创建成功,所以就不能等拉。 上面给出了很常见的几种出现段错误的地方,这样在遇到它们的时候就容易避免拉。 但是人有时候肯定也会有疏忽的,甚至可能还是会经常出现上面的问题或者其他常见的问题 所以对于一些大型一点的程序,如何跟踪并找到程序中的段错误位置就是需要掌握的一门技巧拉。 4。如何发现程序中的段错误? 有个网友对这个做了比较全面的总结,除了感谢他外,我把地址弄了过来。 文章名字叫《段错误bug的调试》 地址是:http://www.cublog.cn/u/5251/showart.php?id=173718 应该说是很全面的。 而我常用的调试方法有: 1)在程序内部的关键部位输出(printf)信息,那样可以跟踪 段错误 在代码中可能的位置 为了方便使用这种调试方法,可以用条件编译指令#ifdef DEBUG和#endif把printf函数给包含起来,编译的时候加上-DDEBUG参数就可以查看调试信息。反之,不加上该参数进行调试就可以。 2)用gdb来调试,在运行到段错误的地方,会自动停下来并显示出错的行和行号 这个应该是很常用的,如果需要用gdb调试,记得在编译的时候加上-g参数,用来显示调试信息 对于这个,网友在《段错误bug的调试》文章里创造性的使用这样的方法,使得我们在执行程序的时候就可以动态扑获段错误可能出现的位置: 通过扑获SIGSEGV信号来触发系统调用gdb来输出调试信息。 如果加上上面提到的条件编译,那我们就可以非常方便的进行段错误的调试拉。

浅谈指针使用中注意事项:

相信大家对指针的用法已经很熟了,这里也不多说些定义性的东西了,只说一下指针使用中的注意事项吧。  一.在定义指针的时候注意连续声明多个指针时容易犯的错误,例如int * a,b;这种声明是声明了一个指向int类型变量的指针a和一个int型的变量b,这时候要清醒的记着,而不要混淆成是声明了两个int型指针。

  二.要避免使用未初始化的指针。很多运行时错误都是由未初始化的指针导致的,而且这种错误又不能被编译器检查所以很难被发现。这时的解决办法就是尽量在使用指针的时候定义它,如果早定义的化一定要记得初始化,当然初始化时可以直接使用cstdlib中定义的NULL也可以直接赋值为0,这是很好的编程习惯。
  三.指针赋值时一定要保证类型匹配,由于指针类型确定指针所指向对象的类型,因此初始化或赋值时必须保证类型匹配,这样才能在指针上执行相应的操作。
  四.void * 类型的指针,其实这种形式只是记录了一个地址罢了,如上所说,由于不知道所指向的数据类型是什么所以不能进行相应的操作。其实void * 指针仅仅支持几种有限的操作:1.与另外的指针进行比较,因为void *类型里面就是存的一个地址,所以这点很好理解;2.向函数传递void *指针或从函数返回void *指针,举个例子吧,我们平时常用的库函数qsort中的比较函数cmp(个人习惯于用这个名字)中传递的两个参数就是const void *类型的,用过的应该很熟了;3.给另一个void * 类型的指针赋值。还是强调一下不能使用void * 指针操纵它所指向的对象。
  五. 不要将两个指针变量指向同一块动态内存。这个容易引起很严重的问题。如果将两个指针变量指向同一块动态内存,而其中一个生命期结束释放了该动态内存,这个时候就会出现问题,另一个指针所指向的地址虽然被释放了但该指针并不等于NULL,这就是所谓的悬垂指针错误,这种错误很难被察觉,而且非常严重,因为这时该指针的值是随机的,可能指向一个系统内存而导致程序崩溃。但也就是因为值是随机的,所以运行程序时有时正常有时崩溃,这一点要特别注意。
  六.在动态delete释放一个指针所指向的内存后注意将该指针置空。
  七.在为一个指针再次分配内存之前一定要保证它原先没有指向其他内存,防止出现内存泄漏。解决的办法是我们必须判断该指针是否为空,这时候就显示出第六条的优势,因为如果释放某内存后相应指针不置空的话就不能为其分配新内存了。所以第六条很有必要。
  八.虽然程序在退出main函数时会释放所有内存空间,但对于大型程序最好还是某块内存不用了立刻释放,而不要指望系统最后的回收,因为内存泄漏会慢慢消耗系统资源直到内存不足而程序死掉。
  九.在用new动态分配完内存之后一定要判断是否分配成功,分配成功后才能使用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值