🤡博客主页:醉竺
🥰本文专栏:《C语言深度解剖》
😻欢迎关注:感谢大家的点赞评论+关注,祝您学有所成!
✨✨💜💛想要学习更多C语言深度解剖点击专栏链接查看💛💜✨✨
本篇文章将学习动态内存管理的诸多易错点和避坑指南,学习完本篇文章,你会对内存管理的理解更加透彻,你会了解内存管理中常见的和隐蔽性很强的错误,从而汲取开发经验!对动态内存管理还不是很熟悉的可以先看下面一篇文章~,下面一篇文章中讲解了内存管理的一些库函数的使用,以及一些常见的错误!
动态内存管理https://blog.csdn.net/weixin_43382136/article/details/138870524?spm=1001.2014.3001.5501
目录
1. 什么是野指针?
2. 栈、堆和静态区
3. 常见的内存错误及对策
3.1 指针没有指向一块合法的内存
3.1.1 结构体成员指针未初始化
3.1.2 没有为结构体指针分配足够的内存
3.1.3 函数的入口校验
3.2 为指针分配的内存太小
3.3 内存分配成功,但并未初始化
3.4 内存越界
3.5 内存泄漏
3.5.1 告老还乡求田
3.5.2 如何使用 malloc 函数
3.5.3 用 malloc 申请 0 字节内存
3.5.4 内存释放
3.5.5 内存释放之后
3.5.6 内存已经被释放了,但是继续通过指针来使用
1. 什么是野指针?
那到底什么是野指针呢?怎么去理解这个“野”呢?我们先看别的两个关于“野”的词:
野孩子:没人要,没人管的孩子;行为动作不守规矩,调皮捣蛋的孩子。
野狗:没有主人的狗,没有链子锁着的狗,喜欢四处咬人。
对付野孩子的最好办法是给他定一套规矩,好好管教,一旦发现没有按规矩办事就好好收拾他。对付野狗最好的办法就是拿条狗链锁着它,不让它四处乱跑。
对付野指针恐怕比对付野孩子或野狗更困难。我们需要把对付野孩子和野狗的办法都用上。既需要规矩,也需要链子。
前面我们把内存比作尺子,很轻松地理解了内存。尺子上的0 mm 处就是内存的 0 地址处,也就是 NULL 地址处。这条栓“野指针”的链子就是这个“NULL”。定义指针变量的同时最好初始化为NULL,用完指针之后也将指针变量的值设置为NULL。也就是说除了在使用时,别的时间都把指针“栓”到 0 地址处,这样它就老实了。
2. 栈、堆和静态区
对于程序员,一般来说,我们可以简单地理解为内存分为 3 个部分:堆、栈和静态区。
很多书没有把堆和栈解释清楚,导致初学者总是分不清楚。其实堆栈就是栈,而不是堆。堆的英文是 heap;栈的英文是 stack,也翻译为堆栈。堆和栈都有自已的特性,这里先不做讨论。
再打个比方:一层教学楼,可能有外语教室,允许外语系学生和老师进人;还可能有数学教师,允许数学系学生和老师进人;还可能有校长办公室,允许校长进人。同样,内存也是这样,内存的3个部分,不是所有的东西都能存进去的。
堆:由 malloc 系列函数或 new 操作符分配的内存。其生命周期由 free 或 delete 决定。在没有释放之前一直存在,直到程序结束。其特点是使用灵活,空间比较大,但容易出错。
栈:保存局部变量。栈上的内容只在函数的范围内存在,当函数运行结束,这些内容也会自动被销毁。其特点是效率高,但空间大小有限。
静态区:保存自动全局变量和 static 变量(包括 static 全局和局部变量)。静态区的内容在整个程序的生命周期内都存在,由编译器在编译的时候分配。
3. 常见的内存错误及对策
3.1 指针没有指向一块合法的内存
定义了指针变量,但是没有为指针分配内存,即指针没有指向一块合法的内存。浅显的例子就不举了,这里举几个比较隐蔽的例子。
3.1.1 结构体成员指针未初始化
- 很多初学者犯了这个错误还不知道是怎么回事。
- 这里定义了结构体变量 stu,但是他没想到这个结构体内部 char *name,该成员在定义结构体变量 stu时,只是给 name这个指针变量本身分配了4字节;
- name 指针并没有指向一个合法的地址,这时候其内部存的只是一些乱码。所以在调用 strcpy 函数时,会将字符串 “Tom" 往乱码所指的内存上复制,而这块内存 name 指针根本就无权访问,导致出错。
- 解决的办法是为 name 指针 malloc 一块空间。
正确示例:
同样,也有人犯如下错误:
为指针变量 pstu 分配了内存,但是同样没有给 name 指针分配内存。错误与上面第 1 种情况一样,解决的办法也一样。这里用了一个 malloc 给人一种错觉,以为也给 name 指针分配了内存。
3.1.2 没有为结构体指针分配足够的内存
为 pstu 分配内存的时候,分配的内存大小不合适。这里把 sizeof(struct student)误写为sizeof(struct student*)。当然name指针同样没有被分配内存。解决办法同上。
3.1.3 函数的入口校验
不管什么时候,我们使用指针之前一定要确保指针是有效的。
一般在函数人口处使用 assert(NULL!=p)对参数进行校验。在非参数的地方使用 if(NULL!= p) 来校验。但这都有一个要求,即 p 在定义的同时被初始化为 NUL L。
比如上面的例子,使用 if (NULL!=p) 校验也起不了作用,因为 name 指针并没有被初始化为NULL,其内部是一个非 NULL 的乱码。
assert 是一个宏,而不是函数,包含在 assert.h 头文件中。如果其后面括号里的值为假,则程序终止运行,并提示出错;如果后面括号里的值为真,则继续运行后面的代码。这个宏只在Debug版本上起作用,而在 Release 版本中被编译器完全优化掉,这样就不会影响代码的性能。
有人也许会问,既然在 Release 版本中被编译器完全优化掉,那 Release 版本是不是就完全没有这个参数人口校验了呢?这样的话那不就跟不使用它效果一样吗?
是的,使用 assert 宏的地方在 Release 版本里面确实没有这些校验。但是我们要知道,assert 宏只是帮助我们调试代码用的,它的一切作用就是让我们尽可能地在调试函数的时候把错误排除掉,而不是等到 Release之后。它本身并没有除错功能。再有一点就是,参数出现错误并非本函数有问题,而是调用者传过来的实参有问题。assert 宏可以帮助我们定位错误,而不是排除错误。
3.2 为指针分配的内存太小
为指针分配了内存,但是内存大小不够,导致出现越界错误。
char* p1 = "abcdefg";
char* p2 = (char*)malloc(sizeof(char)*strlen(p1));
strcpy(p2, p1);
p1 是字符串常量,其长度为 7 个字符,但其所占内存大小为 8 字节。初学者往往忘了字符串常量的结束标志 “\0”,这样的话将导致 p1 字符串中最后一个空字符 “\0" 没有被复制到 p2 中。解决的办法是加上这个字符串结束标志符:
char* p2 = (char*)malloc(sizeof(char)*strlen(p1) + sizeof(char)*1);
另外,不要因为 char 类型大小为 1字节就省略 sizof(char) 这种写法,这样只会使你的代码可移植性下降。
这里需要注意的是,只有字符串常量才有结束标志符,比如下面这种写法就没有结束标志符了:
char a[7] = {'a', 'b', 'c', 'd', 'e', 'f', 'g'}
3.3 内存分配成功,但并未初始化
犯这个错误往往是由于没有初始化的概念或者是以为内存分配好之后其值自然为 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 函数有 3 个参数:第 1 个参数是要被设置的内存起始地址;第 2 个参数是要被设置的值;第 3 个参数是要被设置的内存大小,单位为字节。这里并不想过多地讨论 memset 函数的用法,如果想了解更多,请参考相关资料。
3.4 内存越界
内存分配成功,且已经初始化,但是操作越过了内存的边界。这种错误经常是由于操作数组或指针时出现“多1"或“少1"而出现的,比如:
int a[10] = { 0 };
for(i = 0; i <= 10; i++)
{a[i] = i;
}
所以,for循环的循环变量一定要使用半开半闭的区间,而且如果不是特殊情况,循环变量尽量从0开始。
3.5 内存泄漏
内存泄漏几乎是很难避免的,不管是老手还是新手,都存在这个问题。甚至包括Windows、Linux 这类软件,都或多或少有内存泄漏。也许对于一般的应用软件来说,这个问题似乎不是那么突出,重启一下也不会造成太大损失。但是如果你开发的是嵌人式系统软件,比如汽车制动系统、心脏起搏器等对安全要求非常高的系统,你总不能让心脏起搏器重启吧,人家阎王老爷是非常好客的。
会产生泄漏的内存就是堆上的内存(这里不讨论资源、句柄等泄漏情况),也就是说由 malloc 系列函数或 new 操作符分配的内存。如果用完之后没有及时 free 或 delete,这块内存就无法释放,直到整个程序终止。
3.5.1 告老还乡求田
这里看一下小故事:
3.5.2 如何使用 malloc 函数
3.5.3 用 malloc 申请 0 字节内存
3.5.4 内存释放
既然有分配,那就必须有释放。不然的话,有限的内存总会用光,而没有释放的内存却在空闲。与 malloc 对应的就是free 函数了。free 函数只有一个参数,就是所要释放的内存块的首地址,接上例则为:
free(p);
3.5.5 内存释放之后
既然使用 free 函数之后指针变量 p 本身保存的地址并没有改变,那我们就需要重新把p的值变为NULL:
p = NULL;
这个 NULL 就是我们前面所说的“栓野狗的链子”,如果你不栓起来迟早会出问题的。比如:在 free(p)之后,你用 if(NULL!= p) 这样的校验语句还能起作用吗?
释放完块内存之后,没有把指针置NULL,这个指针就成为了“野指针”,这是很危险的,而且也是经常出错的地方。所以一定要记住一条:free完之后,一定要给指针置NULL。
3.5.6 内存已经被释放了,但是继续通过指针来使用
完结撒花!~ 欢迎订阅本专栏!