当前位置: 首页 > 科技观察

C语言内存常见错误及对策

时间:2023-03-12 04:16:22 科技观察

1.指针没有指向合法内存定义了指针变量,但是没有为指针分配内存,即指针没有指向合法内存。我不会举明显的例子,但这里有一些更微妙的例子。1.结构体成员指针未初始化structstudent{char*name;intscore;}stu,*pstu;intmain(){strcpy(stu.name,"Jimy");stu.score=99;return0;}很多初学者使mistakes得到这个错误并且不知道发生了什么。这里定义了结构体变量stu,没想到结构体内部的成员char*name在定义结构体变量stu的时候只给指针变量本身分配了4个字节。名字指针没有指向合法地址,此时它的内存只是一些乱码。因此,在调用strcpy函数时,会将字符串“Jimy”复制到乱码指示的内存中,而这块内存的name指针根本无权访问,从而导致错误。解决方案是为名称指针分配一个空间。同样,有人会犯如下错误:intmain(){pstu=(structstudent*)malloc(sizeof(structstudent));strcpy(pstu->name,"Jimy");pstu->score=99;free(pstu);return0;}为指针变量pstu分配内存,但也不为名称指针分配内存。报错和上面第一种情况一样,解决方法一样。这里用了一个malloc,给人一种错觉,内存也分配给了name指针。2.没有为结构体指针分配足够的内存intmain(){pstu=(structstudent*)malloc(sizeof(structstudent*));strcpy(pstu->name,"Jimy");pstu->score=99;free(pstu);return0;}为pstu分配内存时,分配的内存大小不合适。这里sizeof(structstudent)被错误地写成了sizeof(structstudent*)。当然,名字指针也是没有分配内存的。解决办法同上。3.函数入口验证无论何时我们使用指针,我们都必须确保指针是有效的。一般使用assert(NULL!=p)来验证函数入口处的参数。使用if(NULL!=p)检查非参数。但是有一个要求,p在定义的时候就初始化为NULL。比如上面的例子,即使用(NULL!=p)来校验也是不行的,因为name指针还没有初始化为NULL,它的内部是非NULL的乱码。assert是一个宏,而不是函数,包含在assert.h头文件中。如果括号内的值为false,则程序终止,并提示错误;如果括号中的值为真,代码将继续运行。该宏只对Debug版本有效,在Release版本中完全由编译器优化,不会影响代码的性能。可能有人会问,既然Release版本是完全由编译器优化的,那么Release版本是不是根本就没有这个参数入口检查呢?这样的话,不就等于不用了吗?是的,使用assert宏的地方在Release版本中没有这些检查。但是我们要知道,assert宏只是用来帮助我们调试代码的,它的所有作用都是为了让我们在调试功能的同时尽可能的排除错误,而不是等到Release之后。它本身没有调试功能。还有一点,参数错误不是这个函数的问题,而是调用者传递的实参有问题。assert宏可以帮助我们定位错误,而不是消除错误。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)+1*sizeof(char));这里需要注意的是,只有字符串Constants才有结束标记。例如下面的写法没有结束符:chara[7]={'a','b','c','d','e','f','g'};另外,不要因为char类型的大小是1个字节,所以省略了sizof(char)的写法。这只会降低您的代码的可移植性。3、内存分配成功,但还没有初始化。这个错误往往是由于缺乏初始化的概念或者认为分配内存后内存的值自然为0。未初始化的指针变量可能看起来没有那么严重,但它确实是一个非常严重的问题,而且往往很难找到导致这种错误的原因。曾经有一个同学,在写windows程序的时候,想调用字体库中的某种字体。而调用这个字体需要填写一个结构。他很自然地定义了一个结构体变量,然后将自己想要的字体代码赋值给相关变量。然而,问题来了,不管他怎么调试,他需要的字体效果总是出不来。我查看了他的代码,没有发现问题,就单步调试。在观察这个结构体变量的内存时,发现有几个成员的值是乱码。是乱码惹祸之一!因为系统会根据这个结构体中某些特定成员的值,在字体库中寻找匹配的字体。当这些值匹配到字体库中某个字体的某些项目时,就会调用这个字体。但遗憾的是,正是因为这些乱码,才没有找到匹配的字体!因为系统无法区分哪些数据是乱码,哪些数据是有效数据。只要有数据,系统就理所当然地认为它是有效的。也许这种严重的问题很少见,但绝不能掉以轻心。所以在定义一个变量的时候,首先要做的就是初始化。您可以将其初始化为有效值,例如:inti=10;char*p=(char*)malloc(sizeof(char));但是往往这个时候我们并不确定这个变量的初始值,所以我们可以初始化为0或者NULL。inti=0;char*p=NULL;如果定义的是数组,可以这样初始化:inta[10]={0};或者使用memset函数初始化为0:memset(a,0,sizeof(a));memset函数有三个参数,第一个是要设置的内存的起始地址;第二个参数是要设置的值;第三个参数是要设置的内存大小,以字节为单位。这里不想过多讨论memset函数的用法,如果想了解更多请参考相关资料。至于指针变量,如果不初始化,if语句或者assert宏校验都会失败。这一点上面已经分析过了。4.内存越界内存分配成功,已经初始化,但是操作越界了内存。这种错误往往是由于在操作数组或指针时“多了1”或“少了1”。例如:inta[10]={0};for(i=0;i<=10;i++){a[i]=i;}所以for循环的循环变量必须使用半开和half-closedinterval,而且如果不是特殊情况,尽量从0开始循环变量。5.内存泄漏内存泄漏几乎是不可避免的,不管你是老手还是新手,这个问题都存在。即使是windows、linux等软件,也或多或少存在内存泄漏的情况。或许对于一般的应用软件来说,这个问题似乎并没有那么突出,重启也不会造成太大的损失。但是如果你开发嵌入式系统软件呢?例如,汽车制动系统、心脏起搏器等对安全要求非常高的系统。不能让心脏起搏器重启,冥王大人好客。会泄漏的内存是堆上的内存(我们这里不讨论资源或句柄等泄漏),也就是malloc系列函数或new操作符分配的内存。如果在使用后没有及时free或者delete,这块内存要等到整个程序终止才能释放。1、如何理解养老退老还乡求良田后内存分配和释放的过程?先看下面这段对话:万岁爷:艾情,你为我立下了汗马功劳,你想要什么奖赏?某英雄:万岁,金银为粪。臣年老,欲告老,归乡。我乞求万亩良田,为子孙后代遮荫,别无所求。万岁爷:爱卿,你辛苦了,想要的只是这么一点点报酬。今天我会实现你的愿望。户部刘侍郎,查一查湖广地区还有几千亩优质良田没有分批。刘世朗:长沙还有5万多亩优质良田没有授牌。万岁君:在长沙分出良田千亩,赏赐爱卿。爱卿,你要一千亩良田做什么?某英雄:万岁谢谢。长沙地区适合种水稻,我想用它来种水稻。种水稻,需要分一亩田,方便耕种。....2.不要对如何使用malloc函数感到困惑。其实上面的小对话就是使用malloc的过程。malloc是一个旨在从堆中分配内存的函数。使用malloc函数需要几个条件:内存分配给谁?这里是把良田分给一个英雄。分配了多少内存?这里是一千亩地的分配。还有足够的内存可以分配吗?这里有足够的内存来分配。内存将用于存储什么格式的数据,即内存将用于什么?这里是用来种水稻的,需要分一亩田。分配的内存在哪里?这是在长沙。如果确定了这五点,那么就可以分配内存了。我们先看一下malloc函数的原型:(void*)malloc(intsize)malloc函数的返回值是一个void类型的指针,参数是int类型的数据,即内存大小到分配,单位为字节。内存分配成功后,malloc函数返回内存首地址。你需要一个指针来接收这个地址。但是由于函数的返回值是void*类型,所以必须强制转换为你接收到的类型。也就是说,这块内存会用来存放什么类型的数据。例如:char*p=(char*)malloc(100);在堆上分配100字节的内存,返回这块内存的首地址,将地址转换成char*类型赋给一个char*类型的指针变量p。同时告诉我们,这块内存会用来存放char类型的数据。也就是说,你只能通过指针变量p来操作这块内存。这块内存本身没有名字,访问它是匿名的。以上就是使用malloc函数成功分配一块内存的过程。但是你能每次都分配成功吗?不必要。在上面的对话中,皇帝让户部尚书询问是否还有足够的好土地没有分配。使用malloc函数时还要注意一点:如果申请的内存块大于当前堆上剩余的内存块(整个块),内存分配就会失败,函数会返回NULL。注意,这里所说的“堆上剩余内存块”并不是所有剩余内存块的总和,因为malloc函数申请的是连续的一块内存。由于malloc函数可能申请内存失败,所以当我们使用指向这块内存的指针时,必须使用if(NULL!=p)语句来验证内存是否分配成功。3、使用malloc函数申请0字节内存还有一个问题:使用malloc函数申请0字节内存会返回NULL指针吗?大家可以测试一下,也可以搜索一下malloc函数的文档。申请0字节内存,函数不返回NULL,而是返回一个正常的内存地址。但是你不能用这个大小为0的内存,这是尺子上的一定刻度。秤本身是没有长度的,只能用一定的两把秤来量度长度。对此要小心,因为此时if(NULL!=p)语句验证将不起作用。4.既然有内存释放的分配,就必须释放。否则,有限的内存会一直被用完,未释放的内存会一直闲置。与malloc对应的是free函数。free函数只有一个参数,就是要释放的内存块的首地址。比如上面的例子:free(p);free函数看起来很狠,但它到底有什么作用呢?其实它做了一件事:切断指针变量和这块内存的联系。比如上面的例子,我们可以说malloc函数分配的内存块是属于p的,因为我们对这块内存的所有访问都需要通过p来完成。free函数切断了这块内存和p的所有关系。从此以后,p和那块内存就没有任何关系了。至于指针变量p本身保存的地址,它并没有改变,只是它不再拥有这个地址处的内存所有权。释放的内存中存储的值没有改变,但是没有办法再使用它了。这就是free函数的作用。根据上面的分析,如果连续两次以上对p使用free函数,肯定会出错。因为第一次使用free函数的时候,p所属的内存已经释放了,第二次使用的时候就没有内存可以释放了。关于这一点,我在课堂上提醒同学们的是:一定要一夫一妻制,否则肯定会出错。malloc两次只free一次会造成内存泄漏;malloconcefreetwice肯定会出错。也就是说,程序中使用malloc的次数必须和free相等,否则一定会出错。这种错误主要发生在循环使用malloc函数时,malloc和free的时间经常搞错。下面是一个练习:写两个函数,一个生成链表,一个释放链表。这两个函数都只接受一个头指针作为它们的参数。5、内存释放后,由于指针变量p本身保存的地址在使用free函数后并没有改变,所以我们需要再次将p的值改为NULL:p=NULL;这个NULL就是我们前面说的。chain”。如果你不把它绑起来迟早会出问题。例如:free(p)之后,您还可以使用if(NULL!=p)之类的检查语句吗?例如:char*p=(char*)malloc(100);strcpy(p,"hello");free(p);/*释放p指向的内存,但p指向的地址不变*/...if(NULL!=p){/*没有起到防止错误的作用*/strcpy(p,"world");/*错误*/}释放内存块后,如果指针没有设置为NULL,这个指针就变成了“野指针”,有的书上称它为“悬挂指针”。这是很危险的,也是经常出错的地方。所以我们必须记住一件事:free之后,一定要给指针置NULL。同时留个问题:连续多次freeNULL指针会报错吗?为什么?如果让你设计free函数,你会怎么处理这个问题?6.内存有被释放了,但是通过指针继续使用。两种情况:第一种:上面说到free(p)之后,继续通过p指针访问内存。解决方案是将p设置为NULL。第二种:函数返回栈内存。这是初学者最常犯的错误。例如,在函数内部定义了一个数组,但使用return语句返回指向数组的指针。解决办法就是弄清楚变量在栈上的生命周期。第三种:内存的使用过于复杂,分不清哪块内存释放,哪块不释放。解决办法是重新设计程序,完善对象间的调用关系。上面详细讨论了六个常见错误及其解决方案。希望读者仔细研究,尽量熟悉每一个错误的产生原因和预防措施。一定要多练习,多调试代码,同时多总结经验。