输入与字符串
输入与字符串
一、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 | |
2. fgets(buf, 10, stdin) 的内存:
1 | |
3. gets_s(buf, 10) 的内存:
1 | |
💡 核心建议
- **忘记
gets**:就像忘记前任一样,永远不要在代码里写gets。 - **慎用
gets_s**:虽然它好用(不用手动删回车),但如果你把代码发给用 Linux/Mac 的同学,他们可能编译不过。 - **拥抱
fgets**:这是最通用的解法。如果你讨厌那个讨厌的\n,可以用一行代码把它去掉:
1 | |
三、输入带空格的字符串
方案一:最推荐、最标准的方法 (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 | |
- 现实含义:你在内存里只租了一个很小的单间(1个字节)。
- 后果:当你试图往里面塞进 “student”(7个字母+1个结束符)时,第1个字母 ‘s’ 住进了单间,剩下的 ‘t’, ‘u’, ‘d’… 就会挤爆墙壁,住到隔壁邻居(其他变量)的家里去。
- 结局:程序崩溃(Stack Corruption)。
2. 为什么不能写 char *str; ?
如果你写:
1 | |
- 现实含义:
char *str只是一个门牌号(指针)。- 当你定义
char *str;但不给它赋值时,这个门牌号是乱写的(野指针),它可能指向大海、外太空或者别人的保险柜。
- 当你定义
- 后果:
scanf试图把 “student” 这个字符串送到str指向的地方。因为那个地方根本不属于你,或者根本不存在,操作系统会立刻把你的程序杀掉。 - 结局:段错误(Segmentation Fault)。
3. 为什么要写 char str[100]; ?
如果你写:
1 | |
- 现实含义:你向系统申请(预订)了一个拥有 100 个房间的公寓。
- 后果:编译器在内存里划出了一块固定的区域专门给你。现在你把 “student” 存进去,完全够住,不会影响别人。
唯一可以“偷懒”的情况:初始化
如果你是在定义的同时直接赋值,你可以不写长度,让编译器帮你数。
1 | |
但是!这也仅限于这一行。 如果你是想先定义,后面再用 scanf 输入,你就必须把长度写死:
1 | |
总结
C 语言非常“笨”,它不会自动扩容。
- 想存字符串,必须先给够地盘。
- **
char str[100]**:意思是“给我留 100 个字节的空地,我要用来存字”。这是最稳妥的写法。
所以,记住了:定义字符串变量用来输入时,中括号里的数字(长度)是绝对不能省的!