【C语言】输入与字符串
输入与字符串
一、getchar和scanf
1. getchar() 的行为
它是最原始的字符读取函数。它的逻辑是:输入缓冲区里有什么,我就拿什么,绝不挑食。
2. scanf() 的行为
scanf 通常比较“聪明”,比如 %d 或 %s 会自动跳过空格、回车和制表符。一旦遇到这些空白字符,它就会立刻停止读取 , 但是! %c 是个特例。当使用 %c 时,它的行为和 getchar() 几乎一模一样:不跳过任何空白字符 。
二、gets 、 fgets 和 gets_s
1. gets() —— 亡命徒 (The Outlaw)
状态 :极度危险 ,已被废弃(C11 标准已将其从库中移除)。
- 安全性 :0 分 。
- 它不接受数组大小作为参数。
- 原理 :你给它一个 10 字节的杯子,如果用户倒入 100 升水,它会照单全收,溢出的水会覆盖掉杯子后面的内存(造成缓冲区溢出攻击)。
- 回车处理 :“吃掉”回车 。
- 读取到
\n为止,然后把\n扔掉,换成\0。
- 读取到
- 内存样子:输入 abc + 回车
[‘a’, ‘b’, ‘c’, ‘\0’]
2. fgets() —— 严谨的守卫 (The Strict Guard)
状态 :行业标准 ,最推荐使用。
- 安全性 :100 分 。
- 语法 :
fgets(str, size, stdin); - 它强制你告诉它杯子有多大(
size)。如果用户输入太长,它读满size-1个字符后就强制停止,并在最后补上\0。剩下的字符留在缓冲区供下次读取。
- 语法 :
- 回车处理 :“保留”回车 (这是它最特殊的点)。
- 读取到
\n为止,它会把\n当作普通字符存进数组,然后在\n后面补\0。 - 注意:如果输入的字符串太长超过了 size,它可能读不到回车。
- 读取到
- 内存样子:输入 abc + 回车
[‘a’, ‘b’, ‘c’, ‘\n’, ‘\0’]
3. gets_s() —— 特工 (The Special Agent)
状态 :C11 标准引入的可选 安全版本(注意:并非所有编译器都支持,VC++ 支持,但 GCC/Clang 默认不一定支持)。
- 安全性 :100 分 。
- 语法 :
gets_s(str, size); - 同样强制要求传入大小
size。 - 特殊机制 :如果用户输入的内容超过了
size,它不只是截断,它通常会报错(触发运行时约束处理),清空字符串或终止程序,视具体实现而定。
- 语法 :
- 回车处理 :“吃掉”回车 (这点模仿了
gets)。- 读取到
\n,扔掉它,换成\0。 - 目的 :为了让你从
gets迁移过来时,不用改处理回车的逻辑。
- 读取到
- 内存样子:输入 abc + 回车
[‘a’, ‘b’, ‘c’, ‘\0’]
📊 终极对比表
| 特性 | gets(str) | fgets(str, n, stdin) | gets_s(str, n) |
|---|---|---|---|
| 安全性 | ❌ 极差 (内存溢出) | ✅ 安全 (截断) | ✅ 安全 (报错/截断) |
| 是否需要数组大小 | 不需要 (瞎读) | 需要 | 需要 |
回车符\n |
扔掉 ,换成 \0 |
保留 ,后面补 \0 |
扔掉 ,换成 \0 |
| 结果字符串 | "abc" |
"abc\n" |
"abc" |
| 兼容性 | 现代编译器报错/移除 | 全平台通用 | 只有部分编译器支持 (如 VS) |
🔍 内存与回车处理图解
假设你的数组大小是 10 (char buf[10]),你输入了 abc 然后按了回车。
1.gets(buf) 的内存:
1 | | a | b | c | \0 | ? | ? | ... |
2.fgets(buf, 10, stdin) 的内存:
1 | | a | b | c | \n | \0 | ? | ... |
3.gets_s(buf, 10) 的内存:
1 | | a | b | c | \0 | ? | ? | ... |
💡 核心建议
- 忘记
gets:就像忘记前任一样,永远不要在代码里写gets。 - 慎用
gets_s:虽然它好用(不用手动删回车),但如果你把代码发给用 Linux/Mac 的同学,他们可能编译不过。 - 拥抱
fgets:这是最通用的解法。如果你讨厌那个讨厌的\n,可以用一行代码把它去掉:
1 | fgets(str, 100, stdin); |
三、输入带空格的字符串
方案一:最推荐、最标准的方法 (fgets)
这是目前 C 语言中读取带空格字符串的行业标准 写法。
1 |
|
- 输入 :
i am a student(按回车) - 内存结果 :
i am a student\n\0(它会把回车也存进去)
方案二:不存回车的方法 (scanf 高级用法)
如果你不想处理 fgets 带来的那个回车符,可以用 scanf 的“正则表达式”写法。
1 |
|
- 输入 :
i am a student(按回车) - 内存结果 :
i am a student\0(它不存回车)
选哪个?
- 选方案一 (
fgets):如果你怕内存溢出,或者不在意那个回车符。 - 选方案二 (
scanf):如果你想要一个干净的、没有回车的字符串。
四、字符串的定义
如果你是想接收输入 (比如用 scanf 或 fgets),你必须写成 str[长度]。不能只写 str。
我用一个**“租房”** 的例子来解释为什么。
1. 为什么不能写 char str; ?
如果你写:
1 | char str; |
- 现实含义 :你在内存里只租了一个 很小的单间(1个字节)。
- 后果 :当你试图往里面塞进 “student”(7个字母+1个结束符)时,第1个字母 ‘s’ 住进了单间,剩下的 ‘t’, ‘u’, ‘d’… 就会挤爆墙壁 ,住到隔壁邻居(其他变量)的家里去。
- 结局 :程序崩溃(Stack Corruption)。
2. 为什么不能写 char *str; ?
如果你写:
1 | char *str; |
- 现实含义 :
char *str只是一个门牌号 (指针)。- 当你定义
char *str;但不给它赋值时,这个门牌号是乱写的(野指针),它可能指向大海、外太空或者别人的保险柜。
- 当你定义
- 后果 :
scanf试图把 “student” 这个字符串送到str指向的地方。因为那个地方根本不属于你,或者根本不存在,操作系统会立刻把你的程序杀掉。 - 结局 :段错误(Segmentation Fault)。
3. 为什么要写 char str[100]; ?
如果你写:
1 | char str[100]; |
- 现实含义 :你向系统申请(预订)了一个拥有 100 个房间的公寓 。
- 后果 :编译器在内存里划出了一块固定的区域专门给你。现在你把 “student” 存进去,完全够住,不会影响别人。
唯一可以“偷懒”的情况:初始化
如果你是在定义的同时直接赋值 ,你可以不写长度,让编译器帮你数。
1 | // ✅ 编译器会自动数:i, a, m, \0 = 4个字节 |
但是!这也仅限于这一行 。 如果你是想先定义,后面再用scanf 输入,你就必须把长度写死:
1 | | a | b | c | \n | \0 | ? | ... |
0
总结
C 语言非常“笨”,它不会自动扩容。
- 想存字符串,必须先给够地盘 。
char str[100]:意思是“给我留 100 个字节的空地,我要用来存字”。这是最稳妥的写法。
所以,记住了:定义字符串变量用来输入时,中括号里的数字(长度)是绝对不能省的!
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 王总的博客!




