10. 字符串输入输出:gets、puts、fgets、fputs 函数的安全使用

字符串的输入输出,是 C 语言里最基础也最容易踩坑的地方。我刚开始写 C 的时候,觉得不就是读个字符串嘛,能有多难?结果被 gets 坑过好几次,缓冲区溢出、程序崩溃、甚至安全漏洞……嗯,今天咱们就把这几个函数彻底聊透。

10.1 经典组合:gets 和 puts

getsputs 是一对老搭档。一个负责从标准输入读字符串,一个负责往标准输出写字符串。用起来确实方便,但问题也出在这个“方便”上。

gets 函数

原型长这样:

char *gets(char *str);

它会从 stdin 读取字符,直到遇到换行符 \n 或文件结束符 EOF。然后自动把换行符替换成 \0,存到 str 指向的缓冲区里。

听起来挺正常对吧?但问题来了——它不检查缓冲区大小。你想想看,如果用户输入了 100 个字符,而你的缓冲区只分配了 50 个字节,那多出来的 50 个字符就会直接写到缓冲区后面的内存里。这就是经典的缓冲区溢出。

⚠ 重要警告: C11 标准已经正式将 gets 标记为“废弃”函数。说白了,就是官方都建议你别用了。我在项目中见过有人为了省事用 gets,结果被安全审计直接打回。

puts 函数

原型:

int puts(const char *str);

puts 相对安全一些。它把 str 指向的字符串输出到 stdout,然后自动加一个换行符。返回值:成功返回非负数,失败返回 EOF

举个例子:

#include <stdio.h>

int main() {
    char name[20];
    printf("请输入你的名字:");
    gets(name);  // ❌ 危险!不检查缓冲区大小
    puts(name);  // ✅ 输出并自动换行
    return 0;
}

这段代码能跑,但千万别学。我曾经在一个嵌入式项目里看到类似的代码,用户输入了一个超长的名字,直接导致堆栈被破坏,系统重启了。

10.2 安全替代方案:fgets 和 fputs

既然 gets 不安全,那用什么?答案是 fgets。它俩的区别,说白了就是 fgets 多了一个参数——缓冲区大小。

fgets 函数

原型:

char *fgets(char *str, int n, FILE *stream);

参数说明:

  • str:存放字符串的缓冲区
  • n:最多读取 n-1 个字符(留一个给 \0
  • stream:文件流,从标准输入读就传 stdin

成功返回 str,失败或读到文件尾返回 NULL

这里有个细节要注意:fgets 会保留换行符。也就是说,如果你输入了 "hello\n",它读到的字符串是 "hello\n\0"。而 gets 会把换行符吃掉。这个差异经常让人困惑。

💡 我的习惯: 用 fgets 读完后,手动把末尾的换行符去掉。代码很简单:
if (str[strlen(str)-1] == '\n')
    str[strlen(str)-1] = '\0';
这样后续处理就统一了。

fputs 函数

原型:

int fputs(const char *str, FILE *stream);

puts 的区别在于:fputs 不会自动加换行符。你需要自己决定要不要加。成功返回非负数,失败返回 EOF

看个完整例子:

#include <stdio.h>
#include <string.h>

int main() {
    char buf[50];
    
    printf("请输入一行文字:");
    if (fgets(buf, sizeof(buf), stdin) != NULL) {
        // 去掉末尾的换行符
        size_t len = strlen(buf);
        if (len > 0 && buf[len-1] == '\n')
            buf[len-1] = '\0';
        
        printf("你输入的是:");
        fputs(buf, stdout);  // 不会自动换行
        putchar('\n');       // 手动加换行
    }
    return 0;
}

10.3 四个函数的对比

我把它们整理成了一张表,方便你对照:

函数 输入/输出 缓冲区安全 保留换行符 自动加换行 推荐使用
gets 输入 ❌ 不安全 ❌ 丢弃 ❌ 永不使用
puts 输出 ✅ 自动加 ✅ 简单场景可用
fgets 输入 ✅ 安全 ✅ 保留 ✅ 首选
fputs 输出 ❌ 不加 ✅ 灵活控制

10.4 避坑指南

我这些年踩过的坑,总结成几条:

  • 永远别用 gets。 我曾经在一个遗留代码里看到 gets,替换成 fgets 后,程序再也没崩溃过。
  • fgets 的第二个参数要传缓冲区实际大小。 别传 sizeof(指针),那只会得到指针本身的大小(通常是 4 或 8 字节)。
  • 处理 fgets 的换行符。 如果你不处理,后续的字符串比较、拼接都会出问题。
  • fputs 不会自动换行。 需要换行时记得自己加 \n

核心原则: 能用 fgets 就别用 gets,能用 fputs 就少用 puts。安全第一,方便第二。

10.5 知识体系图

下面这张图帮你理清这几个函数的关系和选择逻辑:

字符串输入输出函数选择逻辑 字符串 I/O 输入函数 输出函数 gets ❌ 废弃 缓冲区溢出风险 fgets ✅ 推荐 指定缓冲区大小 puts ⚠ 可用 自动加换行符 fputs ✅ 推荐 灵活控制换行 核心原则:优先使用 fgets + fputs,安全第一

10.6 总结

字符串输入输出看似简单,但安全无小事。gets 已经被历史淘汰了,别再用它。fgetsfputs 才是你日常开发中的好搭档。记住:缓冲区大小永远要检查,换行符处理要细心,输出时换行要明确。

嗯,这些经验都是我用实际 bug 换来的。希望你读完这一章,能少走一些弯路。


公众号:蓝海资料掘金营,微信 deep3321