常见的内存错误
-
定义了指针变量,但是没有为指针分配内存,即指针没有指向一块合法的内存。
结构体成员指针未初始化
- 很多初学者犯了这个错误还不知道是怎么回事。这里定义了结构体变量 stu,但是他没 想到这个结构体内部 char *name 这成员在定义结构体变量 stu 时,只是给 name 这个指针变 量本身分配了 4 个字节。name 指针并没有指向一个合法的地址,这时候其内部存的只是一 些乱码。所以在调用 strcpy 函数时,会将字串"Jimy"往乱码所指的内存上拷贝,而这块内存 name 指针根本就无权访问,导致出错。解决的办法是为 name 指针 malloc 一块空间。 同样,也有人犯如下错误:
- 为指针变量 pstu 分配了内存,但是同样没有给 name 指针分配内存。错误与上面第一种 情况一样,解决的办法也一样。这里用了一个 malloc 给人一种错觉,以为也给 name 指针分 配了内存。
- 不管什么时候,我们使用指针之前一定要确保指针是有效的。
- 一般在函数入口处使用 assert(NULL != p)对参数进行校验。在非参数的地方使用 if(NULL != p)来校验。但这都有一个要求,即 p 在定义的同时被初始化为 NULL 了。比 如上面的例子,即使用 if(NULL != p)校验也起不了作用,因为 name 指针并没有被初始 化为 NULL,其内部是一个非 NULL 的乱码。
- 使用指针之前需要对其赋值为NULL 使用完之后也需要将其赋值为 NULL
- assert 是一个宏,而不是函数,包含在 assert.h 头文件中。如果其后面括号里的值为假, 则程序终止运行,并提示出错;如果后面括号里的值为真,则继续运行后面的代码。这个 宏只在 Debug 版本上起作用,而在 Release 版本被编译器完全优化掉,这样就不会影响代码 的性能。
- 有人也许会问,既然在 Release 版本被编译器完全优化掉,那 Release 版本是不是就完 全没有这个参数入口校验了呢?这样的话那不就跟不使用它效果一样吗?
是的,使用 assert 宏的地方在 Release 版本里面确实没有了这些校验。但是我们要知道, assert 宏只是帮助我们调试代码用的,它的一切作用就是让我们尽可能的在调试函数的时候 把错误排除掉,而不是等到 Release 之后。它本身并没有除错功能。再有一点就是,参数出现错误并非本函数有问题,而是调用者传过来的实参有问题。assert 宏可以帮助我们定位错误,而不是排除错误。
为指针分配的内存太小
- char *p1 = “abcdefg”;
- char *p2 = (char *)malloc(sizeof(char)*strlen(p1));
- strcpy(p2,p1);
- p1 是字符串常量,其长度为 7 个字符,但其所占内存大小为 8 个 byte。初学者往往忘 了字符串常量的结束标志“\0”。这样的话将导致 p1 字符串中最后一个空字符“\0”没有被 拷贝到 p2 中。解决的办法是加上这个字符串结束标志符:
- char *p2 = (char *)malloc(sizeof(char)*strlen(p1)+1*sizeof(char));
- 这里需要注意的是,只有字符串常量才有结束标志符。比如下面这种写法就没有结束标志符 了:
- char a[7] = {‘a’,’b’,’c’,’d’,’e’,’f’,’g’};
- 另外,不要因为 char 类型大小为 1 个 byte 就省略 sizof(char)这种写法。这样只会使你的代码可移植性下降。
内存分配成功,但并未初始化
- 犯这个错误往往是由于没有初始化的概念或者是以为内存分配好之后其值自然为0。未初始化指针变量也许看起来不那么严重,但是它确确实实是个非常严重的问题,而且往往 出现这种错误很难找到原因。
- int i = 10; char *p = (char *)malloc(sizeof(char)); 但是往往这个时候我们还不确定这个变量的初值,这样的话可以初始化为 0 或 NULL。
- int i=0; char *p = NULL; 如果定义的是数组的话,可以这样初始化: int a[10] = {0}; 或者用 memset 函数来初始化为 0: memset(a,0,sizeof(a));
- memset 函数有三个参数,第一个是要被设置的内存起始地址;第二个参数是要被设置 的值;第三个参数是要被设置的内存大小,单位为 byte。
- 至于指针变量如果未被初始化,会导致 if 语句或 assert 宏校验失败。因为指针指向数据存放的是乱码,但是系统认为有数据是正确的,所以不对其存储的数据和默认的数值进行比较就无法判定数据的有效性
内存越界
- 内存分配成功,且已经初始化,但是操作越过了内存的边界。 这种错误经常是由于操作数组或指针时出现“多 1”或“少 1”,常发生于循环遍历
- 所以,for 循环的循环变量一定要使用半开半闭的区间,而且如果不是特殊情况,循环变 量尽量从 0 开始
内存泄露
- 会产生泄漏的内存就是堆上的内存(这里不讨论资源或句柄等泄漏情况),也就是说由 malloc 系列函数或 new 操作符分配的内存。如果用完之后没有及时 free 或 delete,这块内存 就无法释放,直到整个程序终止
- (void *)malloc(int size)
- malloc 函数的返回值是一个 void 类型的指针,参数为 int 类型数据,即申请分配的内存 大小,单位是 byte。内存分配成功之后,malloc 函数返回这块内存的首地址。你需要一个指 针来接收这个地址。但是由于函数的返回值是 void *类型的,所以必须强制转换成你所接收 的类型。也就是说,这块内存将要用来存储什么类型的数据。
- 比如:char *p = (char *)malloc(100);在堆上分配了 100 个字节内存,返回这块内存的首地址,把地址强制转换成 char *类型后赋 给 char *类型的指针变量 p。同时告诉我们这块内存将用来存储 char 类型的数据。也就是说 你只能通过指针变量 p 来操作这块内存。这块内存本身并没有名字,对它的访问是匿名访 问。
- 上面就是使用 malloc 函数成功分配一块内存的过程。但是,每次你都能分配成功吗? 不一定。使用 malloc 函数同样要注意这点:如果所申请的内存块大于目前堆上剩余内存块(整块),则内存分配 会失败,函数返回 NULL。注意这里说的“堆上剩余内存块”不是所有剩余内存块之和,因为 malloc 函数申请的是连续的一块内存。
既然 malloc 函数申请内存有不成功的可能,那我们在使用指向这块内存的指针时,必 须用 if(NULL != p)语句来验证内存确实分配成功了。
使用malloc函数申请 0 字节内存
- 另外还有一个问题:用 malloc 函数申请 0 字节内存会返回 NULL 指针吗?
- 可以测试一下,也可以去查找关于 malloc 函数的说明文档。申请 0 字节内存,函数并不返回 NULL,而是返回一个正常的内存地址。但是你却无法使用这块大小为 0 的内存。这 好尺子上的某个刻度,刻度本身并没有长度,只有某两个刻度一起才能量出长度。对于这 一点一定要小心,因为这时候 if(NULL != p)语句校验将不起作用。
内存释放
- 既然有分配,那就必须有释放。不然的话,有限的内存总会用光,而没有释放的内存 却在空闲。与 malloc 对应的就是 free 函数了。free 函数只有一个参数,就是所要释放的内 存块的首地址。
- 比如上例: free(p);
- free 函数看上去挺狠的,但它到底作了什么呢?其实它就做了一件事:斩断指针变量与这块内存的关系。比如上面的例子,我们可以说 malloc 函数分配的内存块是属于 p 的,因 为我们对这块内存的访问都需要通过 p 来进行。free 函数就是把这块内存和 p 之间的所有关系斩断。从此 p 和那块内存之间再无瓜葛。至于指针变量 p 本身保存的地址并没有改变, 但是它对这个地址处的那块内存却已经没有所有权了。那块被释放的内存里面保存的值也没有改变,只是再也没有办法使用了。
这就是 free 函数的功能。 - 按照上面的分析,如果对 p 连续两次以上使用 free 函数,肯 定会发生错误。因为第一使用 free 函数时,p 所属的内存已经被释放,第二次使用时已经无 内存可释放了。关于这点,我上课时让学生记住的是:一定要一夫一妻制,不然肯定出错。内存释放之后需要将指针重新设置为NULL
内存已经被释放了,但是继续通过指针来使用
- 这里一般有三种情况:
- 第一种:就是上面所说的,free(p)之后,继续通过 p 指针来访问内存。解决的办法 就是给 p 置 NULL。
- 第二种:函数返回栈内存。这是初学者最容易犯的错误。比如在函数内部定义了一个 数组,却用 return 语句返回指向该数组的指针。解决的办法就是弄明白栈上变量的生命周期。
- 第三种:内存使用太复杂,弄不清到底哪块内存被释放,哪块没有被释放。解决的办 法是重新设计程序,改善对象之间的调用关系。