ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

C语言字符串输入输出全解析:从scanf到fgets的避坑指南

2026/10/5 8:44:33 拓冰建站 浏览量
C语言字符串输入输出全解析:从scanf到fgets的避坑指南 入坑C语言的人十有八九都被字符串的输入输出这堂课劝退过一次。在Python里一个 input() 搞定的事情在C语言里要跟字符数组、\0、缓冲区、换行符打交道稍不留神程序不是崩溃就是输出一串乱码。我在带新人、帮人改代码的这些年里发现绝大多数字符串相关的bug根子都出在最基础的输入输出环节。所以这篇文章专门把字符串的输入输出这件事拆开揉碎讲清楚先搞懂底层存储再搞懂 printf、scanf、fgets、puts 各自的脾气接着演示缓冲区残留换行符这个最经典的翻车现场最后补上逆序、比较、排序等紧接着要用的基本功。内容全部从零讲起适合刚学完变量、分支和循环的初学者也适合那些scanf 用了一年但说不清为什么有时会出bug的朋友。1. 先把地基打好C语言的字符串到底是个什么东西1.1 字符串不是基本类型而是一条字符数组 结束标记的约定写代码前先把这个观念建立起来C语言里不存在像 int、double 那样的字符串类型。你看到的字符串本质是一个 char 数组加上一个约定——数组里必须有一个值为0的字符 \0转义后的ASCII码0放在有效字符的末尾用来标记字符串结束。我习惯用一个生活化的比喻\0 就是快递盒上的到此为止封条。printf、strlen 这类函数根本不知道你定义的数组有多大它们的完成依赖一个共识从头开始处理字符遇到 \0 就停下来。没有这张封条它们就会一路走下去直到读取到内存中随机出现的某个0字节才停这就是经典的越界读和野指针式乱码来源之一。1.2 两种写法看似相同一个能改一个改了会死初学者最容易在这上面翻车char s1[] hello; // 数组版本 char *s2 hello; // 指针版本char s1[] hello;会在栈上分配一个长度为6的字符数组5个字母 1个\0里面的内容可以随便改s1[0] H完全合法。char *s2 hello;则是指针指向内存中只读区域里的字符串字面量。你执行s2[0] H在不少环境下会直接段错误崩溃。这是C语言的经典陷阱凡是看到char *指向一个字符串字面量默认就不要去修改它凡是明确要改内容的就用数组。判断一句代码是否安全先看它写在数组里还是指针里这个习惯值得从第一天养成。1.3 sizeof 和 strlen一组几乎人人混淆的概念string.h 里的 strlen 和关键字 sizeof 经常出现在同一道题里但含义完全相反char s1[] hello; // sizeof 结果是 6strlen 结果是 5 char s2[100] hi; // sizeof 结果是 100strlen 结果是 2sizeof(s1)问的是这块内存盒子总共多大包含\0甚至包含你没用到的部分。strlen(s1)问的是在\0之前装了多少个有效字符不看盒子大小只数实际内容。如果考试或面试里出现sizeof(hello)答案是 6 而不是 5这就是最经典的扣分点。把盒子大小和内容长度分开记后面学 strcpy、memcpy 缓冲区问题时你才能避免 50% 以上的复制越界。2. 输出字符串printf 顺手但 puts 和格式化细节值得单独学2.1 printf(%s) 的工作逻辑一路打印到\0为止printf(%s, str);是大多数人认识的第一个字符串输出方式。它的执行逻辑是从 str 指向的首地址开始一个字符一个字符地送到标准输出直到读到一个 \0 才停下。操作系统的 printf 实现还会把数据先写入 stdout 的缓冲区在遇到换行或缓冲满时刷新这一点第四章我们还会深入讲。2.2 puts 和 fputs换行符到底谁来加很多人只知道 printf其实还有两个专门输出字符串的函数puts(str); // 输出字符串后自动补一个 \n相当于 printf(%s\n, str) fputs(str, stdout); // 只输出字符串不自动换行puts适合偷懒比如打印提示信息fputs适合需要严格控制换行位置的场合。两者都只接受字符串参数不能像 printf 那样拼格式所以日常主力还是 printf。但我一直建议初学者把 puts 练熟因为刷题时很多输出格式要求逐行输出puts 恰好能少写一个转义字符。2.3 printf 的宽度与精度格式化输出比你想的更强大printf(%20s\n, str); // 右对齐总宽度至少20个字符 printf(%-20s\n, str); // 左对齐总宽度至少20个字符 printf(%.5s\n, str); // 最多输出前5个字符后面的不打印 printf(%20.5s\n, str); // 组合使用宽度20、最多5个字符这在输出对齐表格、限定长度显示时非常实用。特别是%.5s它能在不截断原数组的情况下只输出前缀原理就是 printf 的内部循环在数到第5个字符时手动停下外界根本看不出来你处理过数据。2.4 缺少 \0 的翻车现场看下面的代码char s[5] {h, e, l, l, o}; printf(%s\n, s);这个数组根本没有 \0printf 不知道在哪里停就会继续打印旁边内存里的内容直到碰上一个随机的0字节才停结果就是乱码甚至崩溃。解决办法很简单要么多放一个元素存 \0要么手动补上。所有字符串的越界和乱码一半以上是因为忘了给\0留位置。记住这句话能帮你省下很多排查时间。3. 输入字符串scanf 的短板、gets 的消失以及值得信任的 fgets3.1 scanf(%s) 最大的问题一到空格就罢工char buf[100]; scanf(%s, buf);当输入hello world时buf 只装下helloworld 留在缓冲区下一次读取会直接拿到它。因为%s的读取规则是跳过前导空白然后一直读到下一个空白字符为止。换句话说它天生就不支持含空格的字符串。扛不住空格只是小问题更要命的是它默认不做边界检查。只要你输入超过数组容量scanf 会一路写下去把相邻内存踩烂轻则变量被改、重则程序崩溃。你在刷题网站看到的安全写法一般都会带宽度限制scanf(%19s, buf); // 最多读19个字符留1个给\0数组声明为char buf[100];宽度就写99永远不要把宽度填满因为 scanf 会自动在末尾补 \0。3.2 gets()为什么它从 C11 开始被废弃、C17 被直接移除很久以前的教材会让你用 gets(buf) 读带空格的整行。它虽然方便但有个致命的缺陷它不知道缓冲区有多大也没有任何参数可以限制长度。用户输入多长它就往内存里塞多长。当年著名的缓冲区溢出攻击大量就是通过 gets 这类函数做的。C11 标准把它标记为废弃obsolescentC17 干脆从标准库里删掉了。现在的编译环境里使用 gets要么编译报错要么弹出一堆 warning。我的建议是不管编译器允许不允许永远不要在正式代码里用 gets。它的合法替代品就是我们下面讲的 fgets。3.3 fgets既安全又会留着换行符fgets 是实际工程里读取字符串的首选char buf[100]; fgets(buf, sizeof(buf), stdin);它的行为有三点要记住最多读取size - 1个字符然后自动补 \0不会越界。如果在一行内读到了 \n会把\n也存进来之后再加 \0。如果输入超过缓冲区它只保留前size - 1个字符多余部分留在缓冲区里。第二点正是初学者最容易困惑的地方用 fgets 读到的字符串结尾往往带着一个换行符。如果你要做字符串比较或者逆序输出这个 \n 会捣乱。清理的办法很多最顺手的是buf[strcspn(buf, \n)] \0; // 把第一个换行符换成结束符strcspn返回 buf 中第一个出现 \n 的位置下标没有就返回整个字符串长度。所以这一句能兼容有换行和没换行两种情况是我个人最推荐的一种清理手法。3.4 四种输入方式对比方式支持空格自动补\0是否保留\n边界安全目前态度scanf(%s, buf)否是否不安全只能读单词scanf(%19s, buf)否是否相对安全读单词可用gets(buf)是是丢弃非常危险已废弃/移除fgets(buf, size, stdin)是是保留安全推荐首选一句话总结选型能读单词的场合用scanf(%19s, ...)能读整行的场合用fgets别去碰 gets。4. 经典翻车实录scanf 留下的幽灵换行符怎么排查与解决4.1 问题现场数字读完之后字符串怎么读都是空的这是我在帮新人改代码时遇到最多的组合错误int n; char name[100]; scanf(%d, n); // 输入 123 后按回车 fgets(name, sizeof(name), stdin); // 你以为能读名字实际上什么都没读到 printf(name[%s]\n, name);运行结果 name[] 或者只输出一个换行。原因不是 fgets 坏了而是 scanf 读完数字123后你按下的那个回车键\n还留在输入缓冲区里。fgets 一上来就把这个 \n 当成了一行读完了于是直接返回一行真正的文字根本没机会读进去。4.2 排查链路从看到空字符串到锁定残留换行当年我排查这类问题顺序基本固定你也照着做就行在读取后立刻打印printf([%s]\n, name);用方括号把内容括起来空字符串和只有换行立刻现形。检查 fgets 的返回值。fgets 返回指针成功读到内容返回 buf读到文件尾或出错返回 NULL。如果第一次调用就返回 NULL说明缓冲区里没能构成一行有效输入。打印缓冲区的每个字符例如printf(%d , name[0]);如果第一个字符值恰好是 10\n 的 ASCII 码就实锤了残留换行问题。第3步是最直观的证据。很多新人卡在明明调用了函数为什么什么都没干的困惑里一旦看到 name[0] 等于 10马上就能理解整个机制。4.3 清理缓冲区的常见三招清理残留的空格、换行、或者一行没读完的残留数据可以这样// 方法1一个个吃掉字符直到换行或文件结束 int c; while ((c getchar()) ! \n c ! EOF) ; // 方法2用 fgets 把残留的一行读走 char temp[256]; fgets(temp, sizeof(temp), stdin); // 方法3干脆别用 scanf数字也改用 fgets sscanf char line[256]; fgets(line, sizeof(line), stdin); sscanf(line, %d, n);方法1是最干净的通用做法注意c必须声明为 int 而不是 char因为 EOF 通常是一个负数用 char 可能把它截断方法2在某些题目里会被误解为多读一行需要小心方法3是我现在最推荐的正统方案后面单独讲。4.4 统一方案数字和字符串都用 fgets 读再各自解析既然 scanf 和 fgets 混用会踩坑不如统一都走 fgets 路线char line[256]; int n; char name[100]; fgets(line, sizeof(line), stdin); // 读整行 sscanf(line, %d, n); // 从行里解析数字 fgets(name, sizeof(name), stdin); // 再读一行不会受残留影响 name[strcspn(name, \n)] \0; // 去掉结尾换行这样做的核心思想是每一次输入都按整行为单位消费读完一行就是一行scanf 那种只消费一部分、留一部分的问题自然就不存在了。代价是稍微多写两行代码但对于初学者来说稳定的心智模型远比少打字重要。我的建议是从学会 fgets 那天起把所有 scanf(%d) 都替换成 fgets sscanf 的组合习惯之后你会感谢这个决定。5. 输入输出之后紧接着要掌握的基本功逆序、比较、排序5.1 字符串长度与逆序输出的黄金搭档学了 strlen 以后逆序输出一个字符串几乎是所有教材和OJ题的标配char s[128]; fgets(s, sizeof(s), stdin); s[strcspn(s, \n)] \0; // 先去掉换行否则逆序时会多出一个 \n int len strlen(s); for (int i len - 1; i 0; i--) { putchar(s[i]); } putchar(\n);注意先去 \n 再算 strlen否则逆序结果末尾会带着一个换行符。至于为什么很多人的循环写成for (i len; i 0; i--)那是笔误正确的下标范围是len - 1到0因为第 len 个位置是 \0。5.2 字符串比较为什么 abc abc 是错到离谱的写法初学者特别容易写出if (s abc)这种代码而且编译还能通过。问题是这里的s和abc都是地址比较的是两个地址是否相等而不是内容是否相等。两个不同数组即使内容完全相同地址也必然不同。正确做法是使用 strcmp#include string.h if (strcmp(s, abc) 0) { // 字符串内容完全相等 } else if (strcmp(s, abc) 0) { // s 在字典序上小于 abc } else { // s 在字典序上大于 abc }strcmp 按照字符的 ASCII 码逐个比较返回值是负数、0 或正数分别对应小于等于大于。注意判断相等一定要写 0很多人随手写if (strcmp(s, abc))那判断的是不相等语义正好反了。5.3 字符串排序本质就是 strcmp 交换有了 strcmp排序字符串数组就很简单了拿最直观的选择排序举例char words[5][20] {banana, apple, cherry, date, elderberry}; for (int i 0; i 4; i) { for (int j i 1; j 5; j) { if (strcmp(words[i], words[j]) 0) { char tmp[20]; strcpy(tmp, words[i]); strcpy(words[i], words[j]); strcpy(words[j], tmp); } } }二维字符数组words[5][20]本身就有隐患每一行只能存19个字符超了会越界。更灵活的方案是用char *words[5]的指针数组交换时只交换指针不做字符串拷贝效率更高但问题是字符串字面量不可修改。实际项目里更常用的是qsort不过对刚入门的人先把 strcmp 交换这个逻辑吃透面试问手写字符串排序时才能从容写出来。5.4 中文处理与多字节编码的提醒严格来说C语言的标准字符串操作都是按字节进行的不关心字符编码。在 UTF-8 环境下一个汉字占3个字节char s[] 你好; printf(字节数: %zu\n, strlen(s)); // 输出 6而不是 2 printf(%s\n, s); // 能正常显示你好strlen、strcpy 这类函数能正确处理中文只是因为它们不区分字符和字节按字节搬运罢了。真正的问题是逆序、截断这类按字符操作你不能靠s[0]捞出一个你来因为它是3个字节拼出来的。初学者阶段你只需要记住中文没问题但做逆序、比较、取中间字符时要先确认编码和字节数否则结果大概率是乱的。等到后面学到宽字符 wchar_t 和字符编码转换再回头解决中文按字符处理的完整方案也不迟。6. 记一些让我少踩坑的实操习惯到最后一个章节想分享几条这些年总结出来的个人习惯写代码时照着做能省去大量低级调试时间。字符串一律分配足够的空间并主动写 \0。凡是定义字符数组养成多留一字节的肌肉记忆。数组长度是 100字符串最大有效长度就按 99 算。凡是手动填字符末尾一定记得补 \0别让程序去赌运气。输入优先 fgets解析辅助 sscanf。我几乎不再用 scanf 直接读数字因为混用带来的换行符残留问题太折磨人。统一用 fgets 读一行、再 sscanf 解析逻辑简单还顺手把输入校验做了。调试时打印能看到边界的格式。printf([%s]\n, name);和printf(%d\n, name[0]);这两招能快速区分空字符串换行符残留乱码三种几乎一样的外在表现。我见过太多人对着乱码猜半天其实打印一下第一个字节的值立刻就有答案。把练习题的坑记在笔记本上。牛客、PTA、浙大翁恺的练习题里有大量字符串题比如逆序输出、删除指定字符、统计单词数、字符串排序它们本质上都在考本章讲的几个函数和 \0 意识。每踩一个新坑就把现象 - 原因 - 解决方案三行写下来半年后回头看你会发现自己调错的速度快了一倍不止。我个人在带新人的时候经常说字符串的输入输出是C语言的第一道分水岭跨过去之后指针、动态内存、结构体都会顺很多。以上这些技巧不复杂但都是我在实际调试里一条条试出来的希望能让你的入门路少几个晚上熬夜查bug的记忆。