gets() 函数的危险性 - 以及合理的替代
起因是在 G++17 中图个省事,输入字符串的时候直接写了 gets()
,但怎么也没法编译,提示没有这个函数。我之前了解过 gets()
函数有危险而且不建议使用,但好像也没说不能用吧……? (不建议用就别用)
于是我去查了以下 gets()
函数,发现事情绝对没有我想象的那么简单……
什么是 gets()
函数
C 库函数 char *gets(char *str)
从标准输入 stdin 读取一行,并把它存储在 str 所指向的字符串中。当读取到换行符时,或者到达文件末尾时,它会停止,具体视情况而定。
我们来看看它的原理:
gets()
函数的形参只有一个指针。它会从标准输入流中读字符到一块连续的内存地址空间中。这块地址空间的开始位置就是指针str
指向的位置。当在输入流中遇到文件结束符 (EOF
) 或者换行符 (\n
) 时,读取操作结束。当读入换行符 (\n
) 时,该字符不会被放入那块连续的地址空间中。在读取结束时,gets()
会自动在内存空间的末尾追加一个NULL
字符。经过上述这些操作,对于程序员来说,这个函数得到的就是从标准输入进来的,以NULL
字符结尾的 C 字符串。如果读入的字符流是一整行的话,行尾的换行符将会被舍去。
这看起来挺好:我不用提前考虑输入字符串的长度,只需要用空格和换行区分就可以了。
但是真的是这样吗?
问题
gets
的问题其实就在这个连续输入上。当使用 gets()
时,它会读取用户输入的字符,直到遇到换行符为止,并将这些字符存储到一个字符数组(缓冲区)中,然后返回缓冲区里的字符数据。
然而十行代码九行补漏洞,也没法阻止奇葩的用户需求。 你不知道用户会输入多长的字符串,但是缓冲区的大小是有限的。如果用户输入的字符数量超过了缓冲区的大小,gets()
函数就会继续将字符写入缓冲区,导致缓冲区溢出。
缓冲区溢出发生时,超出缓冲区边界的字符会覆盖其他内存区域,这可能导致程序崩溃或者被攻击者利用。攻击者可以利用缓冲区溢出漏洞来修改程序的控制流,执行恶意代码或者获取敏感信息。
关于缓冲区溢出,可以看下这篇文章,我觉得讲得还可以的。
正因为如此,按理来说使用 gets()
函数时必须特别小心,确保输入字符数量不会超出缓冲区的大小。但是,你怎么知道用户要输入多长呢?
所以,与其不如想办法限制输入长度以防止缓冲区溢出,不如不用 gets()
函数。
也正因为如此,gets()
函数在 C 标准中被弃用,并于 C11 标准中正式移除。在 C11 标准中,gets() 函数被标记为 “obsolescent”(过时),并建议使用更安全的替代方案,如 fgets()
函数或者更推荐的 fgets()
的 C++ 等价函数 std::cin.getline()
。由于 gets()
存在严重的安全漏洞,因此在更早的标准和编译器版本中,就已经开始警告开发者避免使用这个函数。例如,一些编译器在 C99 标准中就开始发出警告,而在 C11 标准中正式将其移除。
那为什么最初的 C 标准还要引入 gets()
呢?
以下是我的个人理解。
当初设计 gets()
函数并将其纳入 C 标准的主要原因是为了简化输入字符串的操作,使得程序员能够更方便地从标准输入中获取字符串数据。C 语言早期的时候,开发者还没考虑到那么多的安全性问题,而更注重的是编程语言的简洁性和易用性,而且安全性也并不是当时主要考虑的地方。因此,gets()
函数被设计为一种快速而方便的方法来读取用户输入的字符串。
然而,随着时间的推移和计算机科学的发展,人们开始意识到缓冲区溢出等安全漏洞的严重性。人们逐渐意识到,这个看起来很方便的函数,所引发的问题可能造成非常严重的安全后果。而这个缓冲区也不好再修修补补,况且经过这么多年发展,gets()
也不再是唯一一种读取字符串的方法,与其修这些问题,不如砍掉一了了之。
替代 / 解决方案
其实要想实现 gets()
函数同样的效果也并不难,有很多方法都可以替代 gets()
。这其中有的是通过限制输入长度实现的,也有的可以直接读取。
fgets()
函数fgets()
可以从标准输入中读取一行字符串,并将其存储到指定大小的缓冲区中。与gets()
不同,fgets()
允许指定要读取的字符数目,从而避免了缓冲区溢出的问题。fgets()
也是普遍认可的一个gets()
的替代。1
2char a[1000]
fgets(a, sizeof(a), stdin);将
fgets()
的写入流改为stdin
即可向对应的字符串写入。std::cin.getline()
(仅限 C++)在 C++ 中,可以使用
std::cin.getline()
函数来替代gets()
。它可以从标准输入中读取一行字符串,并将其存储到指定大小的缓冲区中。与gets()
类似,std::cin.getline()
允许指定要读取的字符数目,从而避免了缓冲区溢出的问题。1
2
3
4#include<iostream>
using namespace std;
char buffer[100];
cin.getline(buffer, sizeof(buffer));std::getline()
在 C++ 中,也可以使用
std::getline()
函数来读取一行字符串,它更加灵活,可以读取任意长度的字符串,并将其存储到std::string
对象中。1
2
3
4
5#include<iostream>
#include<string>
using namespace std;
string line;
getline(cin, line);scanf()
+"%s"
可以在
scanf()
函数中使用格式化字符串来指定要读取的字符数量,并且建议使用%s
格式,并指定字段宽度。这样可以确保scanf()
函数不会读取超过指定长度的字符,从而避免了缓冲区溢出的风险。下面是一个使用
scanf()
函数读取字符串的示例,其中指定了字段宽度为 99:1
2char a[100];
scanf("%99s", a);scanf()
+"%[^\n]%*c"
这种方法是我在做一道题的时候用的,我觉得这个好处是可以在 C 中非常轻松地实现。
%[^\n]
格式表示读取除换行符以外的所有字符,直到遇到换行符为止。这样可以确保scanf()
函数在读取完整行字符串时停止,而不会受到换行符的影响。然后,
%*c
表示读取一个字符并丢弃它,也就是丢弃最后输入的换行符,这样可以清除输入缓冲区中的换行符,以防止它对后续输入产生干扰。1
2char a[10005];
scanf("%[^\n]%*c", a);
总结
一句话就是:gets()
别用。
实际编写代码的时候,对于编译器的警告,一定不能忽略,最好仔细看一下能不能有更好的替代。