即使是最优秀的程序员也可能创建编程错误。根据您的程序的功能,这些错误可能会引入安全漏洞、导致程序崩溃或产生意外行为。
C 编程语言有时会因为不像 Rust 等较新的编程语言那样具有内存安全性而声名狼藉。但是,通过添加少量额外的代码,您可以避免最常见和最严重的 C 编程错误。以下是五个可能破坏您的应用程序的错误以及如何避免它们的方法
1. 未初始化的变量
程序启动时,系统会为其分配一块内存,程序使用该内存来存储数据。这意味着您的变量将获得程序启动时内存中的任何随机值。
某些环境会在程序启动时有意“清零”内存,因此每个变量都以零值开头。并且在您的程序中假设所有变量都将从零开始可能很诱人。但是,C 编程规范指出系统不会初始化变量。
考虑一个使用少量变量和两个数组的示例程序
#include <stdio.h>
#include <stdlib.h>
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。在 numbers 数组中,大多数元素也恰好以零开头,除了第三个元素,它的初始值为 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 到 9,或者长度为一千个元素的数组从 0 到 999。
一些程序员有时会忘记这一点,并引入“差一”错误,即他们从一开始就引用数组。在长度为五个元素的数组中,程序员打算在数组元素“5”处找到的值实际上不是数组的第五个元素。相反,它是内存中的其他一些值,与数组完全无关。
这是一个很好地超出数组边界的示例。该程序从一个只有五个元素长的数组开始,但引用了该范围之外的数组元素
#include <stdio.h>
#include <stdlib.h>
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 <stdio.h>
#include <string.h>
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
<Raleigh> is length 7
var1 = 1; var2 = 2
Ok
威尔士小镇 Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch
拥有世界上最长的地名之一。这个字符串有 58 个字符,远远超出了 name
变量中保留的 10 个字符。因此,程序将值存储在内存的其他区域中,包括 var1
和 var2
的值
var1 = 1; var2 = 2
Where do you live?
Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch
<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 <stdio.h>
#include <stdlib.h>
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
函数放在同一个函数中。
例如,单人纸牌程序可能会在 main 函数中为一副牌分配内存,然后在其他函数中使用该副牌来玩游戏。在 main 函数中释放内存,而不是在其他函数中释放。将 malloc
和 free
语句放在一起有助于避免多次释放内存。
5. 使用无效的文件指针
文件是存储数据的便捷方式。例如,您可以将程序的配置数据存储在名为 config.dat
的文件中。Bash shell 从用户主目录中的 .bash_profile
读取其初始脚本。GNU Emacs 编辑器查找文件 .emacs
以获取其起始值。Zoom 会议客户端使用 zoomus.conf
文件来读取其程序配置。
因此,从文件中读取数据的能力对于几乎所有程序都很重要。但是,如果您要读取的文件不存在怎么办?
要在 C 语言中读取文件,您首先使用 fopen
函数打开文件,该函数返回指向文件的流指针。您可以将此指针与其他函数一起使用来读取数据,例如 fgetc
一次读取文件中的一个字符。
如果您要读取的文件不存在或程序无法读取,则 fopen
函数将返回 NULL
作为文件指针,这表示文件指针无效。但是,这是一个示例程序,它天真地不检查 fopen
是否返回 NULL
,并尝试读取文件
#include <stdio.h>
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 编程错误。预先添加几行代码来捕获这些错误可以为您节省以后数小时的调试时间。
2 条评论