在 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
提供了一种更灵活的方式来将用户数据读取到您的程序中,而不会破坏系统。
评论已关闭。