即使是最好的程序员也会产生编程错误。根据你的程序所做的事情,这些错误可能会引入安全漏洞,导致程序崩溃,或产生意外的行为。
C编程语言有时会得到不好的名声,因为它不像最近的编程语言(包括Rust)那样具有内存安全性。但只要有一点额外的代码,你就可以避免最常见和最严重的C语言编程错误。这里有五个可能破坏你的应用程序的bug以及你如何避免它们。
1.未初始化的变量
当程序启动时,系统会给它分配一个内存块,程序用它来存储数据。这意味着你的变量将得到程序启动时内存中的任何随机值。
有些环境会在程序启动时故意将内存 "清零",因此每个变量都以零值开始。在你的程序中,假设所有的变量都从零开始是很诱人的。然而,C语言编程规范说,系统不对变量进行初始化。
考虑一个使用几个变量和两个数组的示例程序。
#include
#include
int
main()
{
int i, j, k;
int numbers[5];
int *array;
puts("These variables are not initialized:");
printf(" i = %d\n", i);
printf(" j = %d\n", j);
printf(" k = %d\n", k);
puts("This array is not initialized:");
for (i = 0; i < 5; i++) {
printf(" numbers[%d] = %d\n", i, numbers[i]);
}
puts("malloc an array ...");
array = malloc(sizeof(int) * 5);
if (array) {
puts("This malloc'ed array is not initialized:");
for (i = 0; i < 5; i++) {
printf(" array[%d] = %d\n", i, array[i]);
}
free(array);
}
/* done */
puts("Ok");
return 0;
}
该程序没有初始化这些变量,所以它们以系统当时在内存中的任何数值开始。在我的Linux系统上编译和运行这个程序,你会看到一些变量恰好有 "零 "值,但其他变量却没有。
These variables are not initialized:
i = 0
j = 0
k = 32766
This array is not initialized:
numbers[0] = 0
numbers[1] = 0
numbers[2] = 4199024
numbers[3] = 0
numbers[4] = 0
malloc an array ...
This malloc'ed array is not initialized:
array[0] = 0
array[1] = 0
array[2] = 0
array[3] = 0
array[4] = 0
Ok
幸运的是,i
和j
变量的起始值为零,但k
的起始值为 32766。在数字数组中,大多数元素也碰巧以零开始,除了第三个元素,它的初始值为4199024。
在不同的系统上编译同一程序,进一步显示了未初始化变量的危险。不要假设 "全世界都在运行Linux",因为有一天,你的程序可能会在不同的平台上运行。例如,这里是在FreeDOS上运行的同一个程序。
These variables are not initialized:
i = 0
j = 1074
k = 3120
This array is not initialized:
numbers[0] = 3106
numbers[1] = 1224
numbers[2] = 784
numbers[3] = 2926
numbers[4] = 1224
malloc an array ...
This malloc'ed array is not initialized:
array[0] = 3136
array[1] = 3136
array[2] = 14499
array[3] = -5886
array[4] = 219
Ok
始终初始化你的程序的变量。如果你认为一个变量将以零值开始,请添加额外的代码,将零值分配给该变量。这种额外的输入方式将为你以后的调试工作省去麻烦。
2.超出数组的界限
在C语言中,数组从数组索引0开始。这意味着一个有10个元素的数组从0到9,或者一个有1000个元素的数组从0到999。
一些程序员有时会忘记这一点,并引入 "偏离一 "的错误,他们引用数组从一开始。在一个有五个元素的数组中,程序员想在数组元素 "5 "处找到的值实际上不是数组的第五个元素。相反,它是内存中的一些其他值,与数组完全没有关联。
这里有一个例子,它远远超出了数组的范围。这个程序从一个只有5个元素的数组开始,但引用了这个范围之外的数组元素。
#include
#include
int
main()
{
int i;
int numbers[5];
int *array;
/* test 1 */
puts("This array has five elements (0 to 4)");
/* initalize the array */
for (i = 0; i < 5; i++) {
numbers[i] = i;
}
/* oops, this goes beyond the array bounds: */
for (i = 0; i < 10; i++) {
printf(" numbers[%d] = %d\n", i, numbers[i]);
}
/* test 2 */
puts("malloc an array ...");
array = malloc(sizeof(int) * 5);
if (array) {
puts("This malloc'ed array also has five elements (0 to 4)");
/* initalize the array */
for (i = 0; i < 5; i++) {
array[i] = i;
}
/* oops, this goes beyond the array bounds: */
for (i = 0; i < 10; i++) {
printf(" array[%d] = %d\n", i, array[i]);
}
free(array);
}
/* done */
puts("Ok");
return 0;
}
请注意,程序初始化了数组的所有数值,从0到4,但随后试图读取0到9,而不是0到4。前五个数值是正确的,但之后你不知道数值会是什么。
This array has five elements (0 to 4)
numbers[0] = 0
numbers[1] = 1
numbers[2] = 2
numbers[3] = 3
numbers[4] = 4
numbers[5] = 0
numbers[6] = 4198512
numbers[7] = 0
numbers[8] = 1326609712
numbers[9] = 32764
malloc an array ...
This malloc'ed array also has five elements (0 to 4)
array[0] = 0
array[1] = 1
array[2] = 2
array[3] = 3
array[4] = 4
array[5] = 0
array[6] = 133441
array[7] = 0
array[8] = 0
array[9] = 0
Ok
当引用数组时,要始终跟踪其大小。将其存储在一个变量中;不要硬编码一个数组的大小。否则,当你后来更新程序以使用不同的数组大小时,你的程序可能会偏离数组的边界,但你却忘记了改变硬编码的数组长度。
3.溢出字符串
字符串只是一个不同类型的数组。在C语言中,字符串是一个由char
值组成的数组,用一个零字符来表示字符串的结束。
因此,像数组一样,你需要避免超出字符串的范围。这有时被称为溢出字符串。
溢出字符串的一个简单方法是用gets
函数读取数据。gets
函数是非常危险的,因为它不知道它能在一个字符串中存储多少数据,而且它天真地从用户那里读取数据。如果你的用户输入的是短字符串,如foo
,这是很好的,但当用户输入的值对你的字符串值来说太长时,就会造成灾难性的后果。
下面是一个使用gets
函数读取一个城市名称的示例程序。在这个程序中,我还添加了一些未使用的变量,以显示字符串溢出会如何影响其他数据。
#include
#include
int
main()
{
char name[10]; /* Such as "Chicago" */
int var1 = 1, var2 = 2;
/* show initial values */
printf("var1 = %d; var2 = %d\n", var1, var2);
/* this is bad .. please don't use gets */
puts("Where do you live?");
gets(name);
/* show ending values */
printf("<%s> is length %d\n", name, strlen(name));
printf("var1 = %d; var2 = %d\n", var1, var2);
/* done */
puts("Ok");
return 0;
}
当你测试类似的短的城市名称时,那个程序可以正常工作,比如伊利诺伊州的Chicago
或北卡罗来纳州的Raleigh
。
var1 = 1; var2 = 2
Where do you live?
Raleigh
is length 7
var1 = 1; var2 = 2
Ok
威尔士的城镇Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch
,是世界上最长的名字之一。这个字符串有58个字符,远远超过了name
变量中保留的10个字符。因此,程序在内存的其他区域存储数值,包括var1
和var2
的数值。
var1 = 1; var2 = 2
Where do you live?
Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch
is length 58
var1 = 2036821625; var2 = 2003266668
Ok
Segmentation fault (core dumped)
在中止之前,程序用这个长字符串覆盖了内存的其他部分。注意,var1
和var2
不再有它们的起始值1
和2
。
避免使用gets
,并使用更安全的方法来读取用户数据。例如,getline
函数将分配足够的内存来存储用户的输入,因此用户不能通过输入一个长值来意外地溢出字符串。
4.两次释放内存
良好的C语言编程规则之一是,"如果你分配了内存,你应该释放它"。程序可以使用malloc
函数为数组和字符串分配内存,该函数保留一个内存块并返回一个指向内存起始地址的指针。之后,程序可以使用free
函数释放内存,该函数使用指针将内存标记为未使用。
然而,你应该只使用一次free
函数。第二次调用free
将导致意外的行为,可能会破坏你的程序。这里有一个简短的例子程序来说明这一点。它分配了内存,然后立即释放它。但是像一个健忘但有条理的程序员一样,我也在程序结束时释放了内存,结果是释放了两次相同的内存。
#include
#include
int
main()
{
int *array;
puts("malloc an array ...");
array = malloc(sizeof(int) * 5);
if (array) {
puts("malloc succeeded");
puts("Free the array...");
free(array);
}
puts("Free the array...");
free(array);
puts("Ok");
}
运行这个程序,在第二次使用free
函数时,导致了戏剧性的失败。
malloc an array ...
malloc succeeded
Free the array...
Free the array...
free(): double free detected in tcache 2
Aborted (core dumped)
避免在一个数组或字符串上调用free
一次以上。避免两次释放内存的方法之一是将malloc
和free
函数放在同一个函数中。
例如,一个接龙程序可能在主函数中为一副牌分配内存,然后在其他函数中使用这副牌来玩游戏。在主函数中释放内存,而不是其他函数。将malloc
和free
语句放在一起,有助于避免多次释放内存。
5.使用无效的文件指针
文件是存储数据的一种方便的方式。例如,你可以将你的程序的配置数据存储在一个叫做config.dat
的文件中。Bash shell从用户的主目录中的.bash_profile
读取其初始脚本。GNU Emacs编辑器寻找文件.emacs
,以获得其起始值。而Zoom会议客户端使用zoomus.conf
文件来读取其程序配置。
因此,从文件中读取数据的能力对几乎所有的程序都很重要。但是,如果你想读取的文件不在那里呢?
要在C语言中读取一个文件,你首先要用fopen
函数打开该文件,该函数返回一个指向该文件的流指针。你可以用这个指针和其他函数一起读取数据,比如fgetc
,一次读取一个字符的文件。
如果你想读取的文件不在那里,或者不能被你的程序读取,那么fopen
函数将返回NULL
作为文件指针,这表明文件指针是无效的。但这里有一个示例程序,它无辜地不检查fopen
是否返回NULL
,并试图读取该文件,不管怎样。
#include
int
main()
{
FILE *pfile;
int ch;
puts("Open the FILE.TXT file ...");
pfile = fopen("FILE.TXT", "r");
/* you should check if the file pointer is valid, but we skipped that */
puts("Now display the contents of FILE.TXT ...");
while ((ch = fgetc(pfile)) != EOF) {
printf("<%c>", ch);
}
fclose(pfile);
/* done */
puts("Ok");
return 0;
}
当你运行这个程序时,对fgetc
的第一次调用导致了惊人的失败,并且程序立即中止。
Open the FILE.TXT file ...
Now display the contents of FILE.TXT ...
Segmentation fault (core dumped)
始终检查文件指针以确保它是有效的。例如,在调用fopen
来打开一个文件后,用类似if (pfile != NULL)
的东西来检查指针的值,以确保该指针是你可以使用的。
我们都会犯错误,编程错误发生在最好的程序员身上。但是如果你遵循这些准则,并添加一点额外的代码来检查这五种类型的错误,你就可以避免最严重的C语言编程错误。前面的几行代码来捕捉这些错误,可能会节省你以后几个小时的调试时间。