图片来源:Mapbox Uncharted ERG,CC-BY 3.0 US
在C语言中读取字符串曾经是一件非常危险的事情。在读取用户的输入时,程序员可能会被诱惑使用C语言标准库中的gets
函数。gets
的用法很简单。
char *gets(char *string);
也就是说,gets
从标准输入中读取数据,并将结果存储在一个字符串变量中。使用gets
,会返回一个指向该字符串的指针,如果没有读取任何数据,则返回值NULL。
作为一个简单的例子,我们可以问用户一个问题,并将结果读入一个字符串。
#include <stdio.h>
#include <string.h>
int
main()
{
char city[10]; // Such as "Chicago"
// this is bad .. please don't use gets
puts("Where do you live?");
gets(city);
printf("<%s> is length %ld\n", city, strlen(city));
return 0;
}
用上述程序输入一个相对较短的数值,效果就很好。
Where do you live?
Chicago
<Chicago> is length 7
然而,gets
函数非常简单,它会天真地读取数据,直到它认为用户已经完成。但gets
,并不检查字符串是否足够长,以容纳用户的输入。输入一个非常长的值将导致gets
,存储的数据超过了字符串变量所能容纳的范围,导致覆盖了内存的其他部分。
Where do you live?
Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch
<Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch> is length 58
Segmentation fault (core dumped)
在最好的情况下,覆盖部分内存只是破坏了程序。在最坏的情况下,这引入了一个关键的安全漏洞,一个坏的用户可以通过你的程序向计算机的内存插入任意的数据。
这就是为什么在程序中使用gets
函数是危险的。使用gets
,你无法控制你的程序试图从用户那里读取多少数据。这往往会导致缓冲区溢出。
更安全的方法
fgets
函数历来是安全读取字符串的推荐方法。这个版本的gets
提供了一个安全检查,它只读取一定数量的字符,作为一个函数参数传递。
char *fgets(char *string, int size, FILE *stream);
fgets
函数从文件指针读取数据,并将数据存储到一个字符串变量中,但只读到size
所指示的长度。我们可以通过更新我们的示例程序,使用fgets
,而不是gets
,来测试这一点。
#include <stdio.h>
#include <string.h>
int
main()
{
char city[10]; // Such as “Chicago”
// fgets is better but not perfect
puts(“Where do you live?”);
fgets(city, 10, stdin);
printf("<%s> is length %ld\n", city, strlen(city));
return 0;
}
如果你编译并运行这个程序,你可以在提示符下输入一个任意长的城市名称。然而,该程序将只读取足够的数据,以适应size
=10的字符串变量。而由于C语言会在字符串的末尾添加一个空字符('\0'),这意味着fgets
,只能读到9个字符的字符串。
Where do you live?
Minneapolis
<Minneapol> is length 9
虽然这肯定比使用fgets
读取用户的输入更安全,但如果用户的输入太长,它的代价是 "切断 "了用户的输入。
新的安全方法
读取长数据的一个更灵活的解决方案是,如果用户输入的数据超过了变量可能容纳的数量,允许字符串读取函数为字符串分配更多的内存。通过在必要时调整字符串变量的大小,程序总是有足够的空间来存储用户的输入。
getline
函数正是这样做的。这个函数从一个输入流(如键盘或文件)中读取输入,并将数据存储在一个字符串变量中。但与fgets
和gets
不同的是,getline
用realloc
调整字符串的大小,以确保有足够的内存来存储完整的输入。
ssize_t getline(char **pstring, size_t *size, FILE *stream);
getline
实际上是对一个类似的函数的包装,该函数被称为getdelim
,读取数据到一个特殊的分隔符。在这种情况下,getline
使用换行符('\n')作为分隔符,因为当从键盘或文件中读取用户输入时,数据行之间是用换行符分隔的。
其结果是用一种更安全的方法来读取任意的数据,一次一行。要使用getline
,定义一个字符串指针并将其设置为NULL,以表示还没有预留内存。同时定义一个类型为size_t
的 "字符串大小 "变量并给它一个零值。当你调用getline
,你将使用指向字符串和字符串大小变量的指针,并指示在哪里读取数据。对于一个示例程序,我们可以从标准输入中读取。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int
main()
{
char *string = NULL;
size_t size = 0;
ssize_t chars_read;
// read a long string with getline
puts("Enter a really long string:");
chars_read = getline(&string, &size, stdin);
printf("getline returned %ld\n", chars_read);
// check for errors
if (chars_read < 0) {
puts("couldn't read the input");
free(string);
return 1;
}
// print the string
printf("<%s> is length %ld\n", string, strlen(string));
// free the memory used by string
free(string);
return 0;
}
当getline
读取数据时,它将根据需要自动为字符串变量重新分配更多的内存。当函数从一行读完所有数据后,它通过指针更新字符串的大小,并返回读到的字符数,包括定界符。
Enter a really long string:
Supercalifragilisticexpialidocious
getline returned 35
<Supercalifragilisticexpialidocious
> is length 35
请注意,该字符串包括定界符。对于getline
,定界符是换行符,这就是为什么输出中会有换行符。如果你不想要字符串中的定界符,你可以使用另一个函数将定界符改为字符串中的空字符。
有了getline
,程序员可以安全地避免C语言编程的一个常见陷阱。你永远无法知道你的用户可能会尝试输入什么数据,这就是为什么使用gets
是不安全的,而fgets
是尴尬的。相反,getline
提供了一种更灵活的方法,在不破坏系统的情况下将用户数据读入你的程序。