23、缓冲区溢出:gets函数的禁用与fgets的替代
缓冲区溢出,这词儿听着就让人头疼。
我做了十几年嵌入式开发,见过太多因为缓冲区溢出导致的诡异bug。系统跑着跑着就崩了,或者莫名其妙地执行了错误逻辑。最可怕的是,这种bug往往很难复现,排查起来简直要命。
而这一切的罪魁祸首,很多时候就是那个看似无害的gets()函数。
为什么gets()是定时炸弹?
gets()函数的设计,说白了就是「不负责任」。它从标准输入读取一行字符串,但完全不检查目标缓冲区的大小。
你想想看,如果你只准备了64字节的缓冲区,用户却输入了200个字符,会发生什么?
多余的字符会直接覆盖掉缓冲区后面的内存区域。那里可能存放着其他变量、函数返回地址,甚至是程序的控制流数据。
我记得有一次,一个同事调试了整整三天,发现某个全局变量总是被莫名其妙地修改。最后定位到,就是gets()读入的数据悄悄越界,把那个变量给覆盖了。嗯,从那以后,我们团队就立下了规矩:代码中绝对不允许出现gets()。
fgets():安全的替代方案
那用什么?fgets()。它才是正经干活儿的函数。
fgets()的原型是这样的:
char *fgets(char *s, int size, FILE *stream);
关键就在第二个参数size。它告诉函数:最多只能读 size-1 个字符,剩下的一个位置留给字符串结束符\0。这样,无论用户输入多长,都不会溢出缓冲区。
我个人习惯,在嵌入式开发中,fgets()的size参数直接填缓冲区的大小,比如:
char buf[64];
if (fgets(buf, sizeof(buf), stdin) != NULL) {
// 处理输入
}
这里有个细节要注意。fgets()会把换行符\n也读进来。如果你不需要它,得手动去掉:
// 去掉末尾的换行符
size_t len = strlen(buf);
if (len > 0 && buf[len-1] == '\n') {
buf[len-1] = '\0';
}
gets() vs fgets() 对比
| 特性 | gets() | fgets() |
|---|---|---|
| 缓冲区大小检查 | 无 | 有(通过size参数) |
| 读取换行符 | 丢弃 | 保留(包含在字符串中) |
| 安全性 | 极低(高危) | 高(推荐) |
| C11标准状态 | 已移除 | 保留 |
| 适用场景 | 永远不要用 | 所有需要读取字符串的场景 |
避坑指南:fgets()的常见陷阱
我曾经在项目中遇到过一个问题:用fgets()读取用户输入,结果程序总是多出一个空行。排查了半天,才发现是fgets()把换行符也读进来了,而后续处理逻辑没考虑到这一点。
所以,使用fgets()时,有几点要特别注意:
- 换行符处理:记得检查并移除末尾的
\n,除非你确实需要它。 - 缓冲区大小:size参数一定要填对。我见过有人填
sizeof(ptr),结果ptr是个指针,只读了4个字节。正确做法是填sizeof(buf)或者明确的数字。 - 返回值检查:
fgets()返回NULL表示读取失败(比如遇到EOF或错误)。一定要检查返回值,不要假设每次都能读到数据。 - 输入截断:如果用户输入超过了size-1个字符,
fgets()会截断输入。剩下的字符会留在输入流中,下次读取时会读到。如果你不希望这样,需要手动清空输入缓冲区。
while (getchar() != '\n'); 但注意,这只能在确定还有未读字符时使用,否则会阻塞等待新输入。
核心逻辑:缓冲区溢出防护
下面这张图,是我自己总结的缓冲区溢出防护的核心逻辑。说白了,就是「输入前检查,输入后清理」。
实际项目中的经验
我在做嵌入式项目时,遇到过一种情况:某个模块需要从串口接收不定长的命令。一开始有人用gets(),结果只要发送一个超长命令,系统就死机。
后来改成fgets(),配合一个循环读取的逻辑,问题就解决了。具体做法是:
- 用
fgets()每次读取固定长度的数据块。 - 检查是否读到换行符,如果没读到,说明数据还没完,继续读。
- 拼接所有数据块,直到收到完整的命令。
这种做法虽然代码量多了几行,但系统稳定性提升了一个档次。你想想看,一个稳定的系统,比什么都重要。
嗯,关于gets()和fgets(),就聊这么多。记住一句话:用fgets(),别用gets()。这不仅是编程规范,更是对自己代码负责的态度。