目录
一、内存分配未成功就使用
二、内存分配成功了,但是尚未初始化就用
三、内存分配成功了,也初始化了,但是发生了越界使用
四、忘记了释放内存,造成了内存泄漏
五、释放内存后仍然继续使用
指针是C语言最强的特性之一,同时也是最危险的特性之一。
误用指针导致的错误通常难以定位且后果严重。常见的内存异常错误有两类,
1、非法内存访问错误,即代码访问了不该访问的内存地址。
2、因持续的内存泄漏导致系统内存不足。
编译器往往不易发现这类错误,在程序运行时才能捕捉到,且因征兆时隐时现,增加了排错的难度。
具体如下分类:
一、内存分配未成功就使用
造成这类错误的原因是他们没有意识到内存分配会不成功。避免这类错误的方法就是,在使用内存之前检查一下指向它的指针是否为空指针NULL即可。
例如,如果指针变量p指向的内存是用动态内存分配函数申请的内存,则可以用下述方式进行检测:
if(p = = NULL) 或 if(p ! = NULL)
二、内存分配成功了,但是尚未初始化就用
此类错误的起因主要有两个:
1、没有建立“指针必须先初始化后才能使用”的观念;
2、误以为内存的默认初值全为零。
尽管有时内存的默认初值是零(例如静态数组),但是为了避免使用未被初始化的内存导致的引用初值错误,解决这个问题的最简单的方法就是不要嫌麻烦。也就是说,无论数组是以何种方式创建的,都不要忘记给它赋初值,即使是赋零值也不要省略。
对用malloc()和calloc()动态分配的内存,最好用函数memset()进行清零操作。对于指针变量,即使后面右对其进行赋值的语句,也最好是在定义时就将其初始化为NULL。
三、内存分配成功了,也初始化了,但是发生了越界使用
在使用数组时常发生这类错误,特别是在循环语句中遍历数组元素时,循环次数很容易搞错,是的下标“多1”或者“少1”,从而导致数组操作越界。
四、忘记了释放内存,造成了内存泄漏
像系统申请的动态内存是不会自动释放掉的,因此,一定不要忘记释放不再使用的内存,否则会造成内存泄漏“Memory Leak” ,这好比借东西不还一样。
对于包含这类错误的函数,只要它被调用依次,就会丢失一块内存。有时未释放的内存垃圾并不足以导致系统因内存不足而崩溃,在不同情况下,内存垃圾给系统带来的影响是不同的。内存泄漏的严重程度取决于每次遗留内存垃圾的多少以及代码被调用的次数。调用次数越多,丢失的内存越多。因此这类错误比较隐蔽,刚开始时,系统内存也许是充足的,看不出来错误的征兆,当系统运行一段时间后,随着丢失内存数量的增多,程序就会因出现“内存耗尽”而突然死掉。
对释放内存不能草率了之,不要以为少量的内存未释放没什么关系,程序“临终”前,系统会将所有的内存一一回收。然而一旦将这段代码复制粘贴到需长期稳定运行的安全关键的软件代码中,那么偷懒的代价是惨重的。需长期稳定运行的服务程序和安全关键的软件系统对内存泄漏最敏感。降低内存泄漏错误发生概率的一般方法如下:
(1)、仅在需要时才使用malloc(),并尽量减少malloc()调用的次数,能用自动变量解决的问题,就不要用malloc()来解决。
(2)、配套使用malloc()和free(),并尽量让malloc()和与之配套的free()集中在一个函数内,尽量把malloc()放在函数的入口处,free()放在函数的出口处。
(3)、如果malloc()和free()无法集中在一个函数中,那么就要分别单独编写申请内存和释放内存的函数,然后使其配对使用。
(4)、重复利用malloc()申请到的内存,有助于减少内存泄漏发生的概率。
注意,以上只是基本原则,并不能完全杜绝内存泄漏错误的发生。
例题:读走试分析下列程序的错误:
void Init(void)
{char *pszmyname = NULL,*pszhername = NULL,*pszshisname = NULL;pszmyname = (char *)malloc(256);if(pszmyname == NULL) return;pszhername = (char *)malloc(256);if(pszhername ==NULL) return;pszhisname = (char *)malloc(256);if(pszhisname ==NULL) return;…… //正常处理的代码free(pszmyname);free(pszhername);free(pszhisname);return;
}
虽然程序中的malloc()和free()是配套使用的,但当前面的malloc()调用成功但后面的调用不成功时,直接退出函数将导致前面已分配的内存未被释放。因此程序可进行如下修改:
void Init(void)
{char *pszmyname = NULL, *pszhername = NULL, *pszhisname = NULL;pszmyname = (char *)malloc(256);if(pszmyname == NULL) return;pszhername = (char *)malloc(256);if(pszhername == NULL){free(pszmyname);return;}pszhisname = (char *)malloc(256);if(pszhisname ==NULL){free(pszmyname);free(pszhername):return;}…free(pszmyname);free(pszhername);free(pszhisname);return;
}
这个程序的问题是:有大量重复的语句,且如果在增加其他malloc函数调用语句,相应的free函数调用语句也要增多。进行第2次修改:
void Init(void)
{char *pszmyname = NULL,*pszhername = NULL, *pszhisname = NULL;pszmyname = (char *) malloc(256);if(pszmyname ==NULL) goto Exit;pszhername = (char *)malloc(256);if(pszhername == NULL) goto Exit:pszhisname = (char *)malloc(256);if(pszhisname == NULL) goto Exit;… //正常处理的代码Exit:if (pszmyname != NULL) free(pszmyname);if (pszhername != NULL) free(pszhername);if (pszhisname != NULL) free(pszhisname);return;
}
这个程序中,使用goto语句重用了第12~14行这段“重用率很高、但很难写成单一的函数”的代码,他使流程变得清晰,且代码集中,所有错误最后的指向Exit标号后的语句来处理。可见,goto语句并非罪大恶极,在对异常情况进行统一错误处理时还是很有用的。
五、释放内存后仍然继续使用
非法内存操作的一个共同特征就是代码访问了不该访问的内存地址。例如,使用未分配成功的内存、引用未初始化的内存、越界访问内存,以及释放了内存却继续使用它。其中,释放了内存但却仍然继续使用它,将导致产生“野指针”。
例题:分析下面的程序能否实现“输入一个不带空格的字符串并显示到屏幕上”的功能。
#include <stdio.h>char *getstr(void);int main(void)
{char *ptr = NULL;ptr = getstr();puts(ptr);return 0;
}char *getstr(void)
{char s[80];scanf("%s",s);return s;
}
[Warning] address of local variable 's' returned [-Wreturn-local-addr]
这个警告的含义是“返回了局部变量的地址”。虽然并不影响程序运行,但是运算结果是乱码。这是因为程序在第14行试图从函数返回值指向局部变量的地址,导致了野指针的错误。动态全局变量都是在栈上创建内存的,在函数调用结束后就会被自动释放了,释放后的内存中的数据将变为随机数,因此此时输出其中的数据必然为乱码。课在上面程序中增加几行打印语句来严重分析上述分析结果。
#include <stdio.h>
char *getstr(void);
int main(void)
{char *ptr = NULL;printf("ptr=%p\n",ptr); //打印初始化为NULL后的指针变量的值printf("Input a string:");ptr = getstr();printf("ptr = %p\n",ptr); //打印函数返回后在栈上创建的内存的首地址puts(ptr); //试图使用野指针,将导致程序输出乱码return 0;
}char *getstr(void)
{char s[80]; //定义动态存储类型的数组scanf("%s",s);printf("s = %p\n",s); //打印函数返回前在栈上创建的内存的首地址return s; //试图返回动态局部变量的地址
}
当指针指向的栈内存被释放以后,指向它的指针并未消亡。内存被释放后,指针的值(即栈内存的首地址)其实并没有改变,它仍然指向这块内存,只不过内存中存储的数据,使该内存存储的内容变成了垃圾。指向垃圾内存的指针,就被称为野指针。
内存释放后,指向它的指针不会自动变成空指针,野指针也不是空指针,空指针很容易检查,使用if语句判断指针值是否为NULL即可,但野指针却很危险 ,因为我们无法预知指针的值究竟是多少。
修改后的程序:
#include <stdio.h>
void getstr(char *);
int main(void)
{char s[80];char *ptr = s;getstr(ptr);puts(ptr);return 0;} void getstr(char *s)
{scanf("%s",s);
}
但是如果修改成如下程序呢?
错误修改法1:
#include <stdio.h>
void getstr(char *);
int main(void)
{char *ptr = NULL;getstr(ptr);puts(ptr);return 0;
}
void getstr(char *s)
{scanf("%s",s);
}
那么程序将会因为第12行和第7行试图使用空指针而异常终止,和引用没有初始化的指针变量的效果是一样的。
错误修改法2:(ptr仍为空指针)
#include <stdio.h>
#include <stdlib.h>
void getstr(char *);
int main(void)
{char *ptr = NULL;getstr(ptr);puts(ptr);return 0;
}
void getstr(char *s)
{s = (char *)malloc(80);scanf("%s",s);
}
可实现功能的修改方法:
#include <stdio.h>
#include <stdlib.h>
char *getstr(void);
int main(void)
{char *ptr = NULL;ptr = getstr(ptr);puts(ptr);free(ptr);return 0;
}
char *getstr(char *s)
{s = (char *)malloc(80);scanf("%s",s);return s;
}
//方法2
#include <stdio.h>
#include <stdlib.h>
char *getstr(void);
int main(void)
{char *ptr= NULL;ptr = getstr();puts(ptr);free(ptr);return 0;
}
char *getstr()
{char *s=NULL;s = (char *)malloc(80);scanf("%s",s);return s;
}
综上所述,野指针的形成主要有以下几种情况:
(1)指针操作超越了变量的作用范围,如用return语句返回动态局部变量的地址;
(2)指针变量未被初始化,指针混乱往往使得结果变得难以预料和莫名其妙;
(3)指针变量所指向得动态内存被free后位置为NULL,让人误以为它仍是合法的。
针对以上几种情形得解决对策是:
(1)不要把局部变量得地址(即指向“栈内存”得指针)作为函数得返回值返回,因为局部变量分配的内存在退出函数时将会被自动释放;
(2)在定义指针变量的同时对其初始化,要么置为NULL,要么使其指向合法地址。
(3)尽量把malloc()集中在函数的入口处,free()集中在函数的出口处,避免内存被释放后继续使用。如果free()不能放在函数的出口处,则在调用free()后,应立即将指向这段内存的指针设置为NULL,这样在使用指针之前检查其是否为NULL才有效。