C语言编程中如何(安全地)用getline函数读取用户输入的信息

419 阅读5分钟

Business woman on laptop sitting in front of window

图片来源: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 函数正是这样做的。这个函数从一个输入流(如键盘或文件)中读取输入,并将数据存储在一个字符串变量中。但与fgetsgets 不同的是,getlinerealloc 调整字符串的大小,以确保有足够的内存来存储完整的输入。

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 提供了一种更灵活的方法,在不破坏系统的情况下将用户数据读入你的程序。