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

C编程中的五个常见错误及其解决方法

时间:2023-03-14 18:58:36 科技观察

即使是最优秀的程序员也无法完全避免错误。根据程序的运行方式,这些错误可能会引入安全漏洞、导致程序崩溃或产生意外行为。C有时名声不好,因为它不像最近的编程语言(如Rust)那样内存安全。但是使用额外的代码,可以避免一些最常见和最严重的C语言错误。以下是可能影响您的应用程序的五个错误以及如何避免它们:1.未初始化的变量当程序启动时,系统会分配一块内存来存储数据。这意味着当程序启动时,该变量将在内存中获得一个随机值。某些编程环境会在程序启动时故意将内存“置零”,以便每个变量的初始值为零。程序中的变量都以零值作为初值,听起来很不错。但是在C编程规范中,系统并没有初始化变量。看看这个使用多个变量和两个数组的示例程序:#include#includeintmain(){inti,j,k;intnumbers[5];int*array;puts("这些变量没有初始化:");printf("i=%d\n",i);printf("j=%d\n",j);printf("k=%d\n",k);puts("这个数组没有初始化:");for(i=0;i<5;i++){printf("numbers[%d]=%d\n",i,numbers[i]);}puts("mallocanarray...");array=malloc(sizeof(int)*5);if(array){puts("Thismalloc'edarrayisnotinitialized:");for(i=0;i<5;i++){printf("array[%d]=%d\n",i,array[i]);}free(array);}/*done*/puts("Ok");return0;}这个程序不会初始化变量,因此变量存储在系统内存中的一个随机值作为初始值。在我的Linux系统上编译和运行这个程序,我看到一些变量恰好有“零”值,但其他变量没有:Thesevariablesarenotinitialized:i=0j=0k=32766Thisarrayisnotinitialized:numbers[0]=0numbers[1]=0numbers[2]=4199024numbers[3]=0numbers[4]=0mallocanarray...Thismalloc'ed数组未初始化:array[0]=0array[1]=0array[2]=0array[3]=0array[4]=0Ok非常幸运,i和j变量从零开始,但是k从32766开始。在numbers数组中,大多数元素也恰好从零值开始,只有第三个元素的初始值为4199024。编译相同不同系统上的程序可以进一步证明未初始化变量的危险。不要误以为“全世界都在运行Linux”,你的程序可能有一天会在其他平台上运行。例如,这是在FreeDOS上运行相同程序的结果:这些变量未初始化:i=0j=1074k=3120此数组未初始化:numbers[0]=3106numbers[1]=1224numbers[2]=784numbers[3]=2926numbers[4]=1224mallocanarray。..Thismalloc'edarrayisnotinitialized:array[0]=3136array[1]=3136array[2]=14499array[3]=-5886array[4]=219Ok永远记得初始化程序的变量。如果要用零值初始化变量,请添加附加代码以将零分配给变量。预先编程这些额外的代码将有助于减少以后调试的麻烦。2.数组越界在C语言中,数组索引从零开始。这意味着对于长度为10的数组,索引是从0到9;对于长度为1000的数组,索引是从0到999。程序员有时会忘记这一点,他们引用从索引1开始的数组,从而产生“差一”错误。在长度为5的数组中,程序员在索引“5”处使用的值实际上并不是数组的第5个元素。相反,它是内存中与此数组完全无关的其他值。这是数组越界的示例程序。该程序使用一个只有5个元素的数组,但引用了该范围之外的数组元素:#include#includeintmain(){inti;intnumbers[5];int*array;/*test1*/puts("Thisarrayhasfiveelements(0to4)");/*初始化数组*/for(i=0;i<5;i++){numbers[i]=i;}/*oops,thisgoesbeyondthearraybounds:*/for(i=0;i<10;i++){printf("numbers[%d]=%d\n",i,numbers[i]);}/*test2*/puts("mallocanarray...");array=malloc(sizeof(int)*5);if(array){puts("Thismalloc'edarrayalsohasfiveelements(0to4)");/*initalizethearray*/for(i=0;i<5;i++){array[i]=i;}/*oops,thisgoesbeyondthearraybounds:*/for(i=0;i<10;i++){printf("array[%d]=%d\n",i,array[i]);}free(array);}/*done*/puts("Ok");return0;}可以看到,程序初始化数组的所有值(从索引0到4),然后从索引0开始读取,以索引9而不是索引4结束。前五个值是正确的,后面的值会让你迷惑:Thisarrayhasfiveelements(0to4)numbers[0]=0numbers[1]=1numbers[2]=2numbers[3]=3numbers[4]=4numbers[5]=0numbers[6]=4198512numbers[7]=0numbers[8]=1326609712numbers[9]=32764mallocanarray...这个malloc'edarrayalsohasfiveelements(0to4)array[0]=0array[1]=1array[2]=2array[3]=3array[4]=4array[5]=0array[6]=133441array[7]=0array[8]=0array[9]=0Ok引用数组时,请始终记住跟踪数组大小。将数组大小存储在变量中;不要硬编码数组大小。否则,如果稍后标识符指向另一个大小不同的数组,但您忘记更改硬编码的数组长度,程序可能会导致数组越界。3.字符串溢出字符串只是某种类型的数组。在C语言中,字符串是char类型的值数组,零字符表示字符串结束。因此,与数组一样,注意不要越界使用字符串。有时也称为字符串溢出。使用gets函数读取数据是一种容易出现字符串溢出的行为。gets函数非常危险,因为它不知道一个字符串中可以存储多少数据,它只是机械地从用户那里读取数据。如果用户输入像foo这样的短字符串,则不会发生任何意外;但是当用户输入一个超过字符串长度的值时,后果可能是灾难性的。下面是一个使用gets函数读取城市名称的示例程序。在这个程序中,我还添加了一些未使用的变量来显示字符串溢出对其他数据的影响:#include#includeintmain(){charname[10];/*Suchas"Chicago"*/intvar1=1,var2=2;/*showinitialvalues*/printf("var1=%d;var2=%d\n",var1,var2);/*这很糟糕..请不要使用gets*/puts("Wheredoyoulive");gets(name);/*showendingvalues*/printf("<%s>islength%d\n",name,strlen(name));printf("var1=%d;var2=%d\n",var1,var2);/*done*/puts("Ok");return0;}当您测试类似的城市简称时,例如芝加哥、伊利诺伊州或北卡罗来纳州的罗利,该程序运行良好:var1=1;var2=2Wheredoyoulive?Raleighislength7var1=1;var2=2Ok威尔士小镇Llanfairpwllgwyngyllgogerychwyrndrobwlllllantysiliogogogoch拥有世界上最长的名字之一。这个字符串有58个字符,远远超过了name变量中保留的10个字符。结果,程序将值存储在内存的其他区域,覆盖了var1和var2的值:var1=1;var2=2Wheredoyoulive?Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogochislength58var1=2036821625;var2=2003266668OkSegmentationfault(coredumped)在运行结束之前,程序会用长字符串会覆盖内存的其他部分。注意var1和var2的值不再是原来的1和2。避免使用gets函数,有利于更安全的读取用户数据的方法。例如,getline函数会分配足够的内存来存储用户输入,这样就不会因为输入长值而导致意外的字符串溢出。4、反复释放内存“分配的内存必须手动释放”是良好的C编程的原则之一。程序可以使用malloc函数为数组和字符串分配内存,它分配一块内存并返回一个指向内存中起始地址的指针。之后,程序可以使用free函数释放内存,该函数使用指针将内存标记为未使用。但是,您应该只使用一次免费功能。第二次调用free可能会产生意想不到的后果,可能会破坏您的程序。下面是一个用于此目的的简短示例程序。该程序分配内存并立即释放它。但是为了模仿一个健忘但有条不紊的程序员,我在程序结束时再次释放内存,导致相同的内存被释放两次:#include#includeintmain(){int*array;puts("mallocanarray...");array=malloc(sizeof(int)*5);if(array){puts("mallocsucceeded");puts("Freethearray...");free(array);}puts("Freethearray...");free(array);puts("Ok");}运行这个程序导致在第二次使用free函数时出现戏剧性的失败:mallocanarray...mallocsucceededFreethearray。..Freethearray...free():doublefreedetectedintcache2Aborted(coredumped)请记住避免在数组或字符串上多次调用free。将malloc和free函数放在同一个函数中是避免双重释放内存的一种方法。例如,扑克牌程序可能会在main函数中为一副纸牌分配内存,然后在其他函数中使用这副纸牌来玩游戏。请记住在main函数中释放内存,而不是在其他函数中。将malloc和free语句放在一起有助于避免多次释放内存。5.使用无效的文件指针文件是一种方便的数据存储方式。例如,您可以将程序的配置数据存储在config.dat文件中。Bashshell从用户主目录中的.bash_profile读取初始化脚本。GNUEmacs编辑器查找文件.emacs以确定起始值。相反,Zoom会议客户端使用zoomus.conf文件来读取其程序配置。因此,从文件中读取数据的能力对几乎所有的程序都很重要。但是如果要读取的文件不存在怎么办?要在C中读取文件,首先使用fopen函数打开文件,该函数返回指向文件的流指针。您可以使用此指针与其他函数结合读取数据,例如fgetc将逐字符读取文件。如果要读取的文件不存在或程序没有读取权限,则fopen函数返回NULL作为文件指针,即文件指针无效。但这是一个机械读取文件的示例程序,不检查fopen是否返回NULL:#includeintmain(){FILE*pfile;intch;puts("OpentheFILE.TXTfile...");pfile=fopen("FILE.TXT","r");/*你应该检查文件指针是否有效,但我们跳过了那个*/puts("NowdisplaythecontentsofFILE.TXT...");while((ch=fgetc(pfile))!=EOF){printf("<%c>",ch);}fclose(pfile);/*done*/puts("Ok");return0;}当你运行这个程序时,第一个调用tofgetc将失败,程序立即中止:OpentheFILE.TXTfile...NowdisplaythecontentsofFILE.TXT...Segmentationfault(coredumped)始终检查文件指针以确保其有效。例如,在调用fopen打开一个文件后,用if(pfile!=NULL)这样的语句检查指针以确保指针可用。人会犯错,即使是最优秀的程序员也会犯编程错误。但是,通过遵循这些准则并添加一些额外的代码来检查这五种类型的错误,您可以避免最严重的C编程错误。预先编写几行代码来捕获这些错误可能会节省您数小时的调试时间。