输入与字符串

输入与字符串

一、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
| a | b | c | \0 | ? | ? | ...
^--- 哪怕你输入 100 个字,它也会往后写,覆盖 ? 区域

2. fgets(buf, 10, stdin) 的内存:

1
2
| a | b | c | \n | \0 | ? | ...
^--- 注意多出来的 \n,这导致字符串长度变成了 4

3. gets_s(buf, 10) 的内存:

1
2
| a | b | c | \0 | ? | ? | ...
^--- 只要不超过 10,效果和 gets 一样,但是安全

💡 核心建议

  1. **忘记 gets**:就像忘记前任一样,永远不要在代码里写 gets
  2. **慎用 gets_s**:虽然它好用(不用手动删回车),但如果你把代码发给用 Linux/Mac 的同学,他们可能编译不过。
  3. **拥抱 fgets**:这是最通用的解法。如果你讨厌那个讨厌的 \n,可以用一行代码把它去掉:
1
2
3
fgets(str, 100, stdin);
// 查找 \n 并把它变成 \0
str[strcspn(str, "\n")] = 0;

三、输入带空格的字符串

方案一:最推荐、最标准的方法 (fgets)

这是目前 C 语言中读取带空格字符串的行业标准写法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>

int main() {
char str[100]; // 准备足够大的数组

printf("请输入: ");

// 语法:fgets(数组名, 数组大小, stdin)
// stdin 代表从键盘输入
fgets(str, 100, stdin);

printf("你存入的是: %s", str);

return 0;
}
  • 输入i am a student (按回车)
  • 内存结果i am a student\n\0 (它会把回车也存进去)

方案二:不存回车的方法 (scanf 高级用法)

如果你不想处理 fgets 带来的那个回车符,可以用 scanf 的“正则表达式”写法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

int main() {
char str[100];

printf("请输入: ");

// %[^\n] 的意思是:读取所有字符,直到遇到回车(\n)才停
scanf("%[^\n]", str);

printf("你存入的是: %s\n", str);

return 0;
}
  • 输入i am a student (按回车)
  • 内存结果i am a student\0 (它不存回车)

选哪个?

  • **选方案一 (fgets)**:如果你怕内存溢出,或者不在意那个回车符。
  • **选方案二 (scanf)**:如果你想要一个干净的、没有回车的字符串。

四、字符串的定义

如果你是想接收输入(比如用 scanffgets),**你必须写成 str[长度]**。不能只写 str

我用一个“租房”的例子来解释为什么。


1. 为什么不能写 char str;

如果你写:

1
2
char str;
scanf("%s", &str); // ❌ 极其危险!
  • 现实含义:你在内存里只租了一个很小的单间(1个字节)。
  • 后果:当你试图往里面塞进 “student”(7个字母+1个结束符)时,第1个字母 ‘s’ 住进了单间,剩下的 ‘t’, ‘u’, ‘d’… 就会挤爆墙壁,住到隔壁邻居(其他变量)的家里去。
  • 结局:程序崩溃(Stack Corruption)。

2. 为什么不能写 char *str;

如果你写:

1
2
char *str;
scanf("%s", str); // ❌ 程序会直接炸掉(Segmentation Fault)
  • 现实含义char *str 只是一个门牌号(指针)。
    • 当你定义 char *str; 但不给它赋值时,这个门牌号是乱写的(野指针),它可能指向大海、外太空或者别人的保险柜。
  • 后果scanf 试图把 “student” 这个字符串送到 str 指向的地方。因为那个地方根本不属于你,或者根本不存在,操作系统会立刻把你的程序杀掉。
  • 结局:段错误(Segmentation Fault)。

3. 为什么要写 char str[100];

如果你写:

1
2
char str[100];
scanf("%s", str); // ✅ 安全
  • 现实含义:你向系统申请(预订)了一个拥有 100 个房间的公寓
  • 后果:编译器在内存里划出了一块固定的区域专门给你。现在你把 “student” 存进去,完全够住,不会影响别人。

唯一可以“偷懒”的情况:初始化

如果你是在定义的同时直接赋值,你可以不写长度,让编译器帮你数。

1
2
3
// ✅ 编译器会自动数:i, a, m, \0 = 4个字节
// 实际上它等同于 char str[4] = "iam";
char str[] = "iam";

但是!这也仅限于这一行。 如果你是想先定义,后面再用 scanf 输入,你就必须把长度写死:

1
2
char str[];       // ❌ 错误!编译器不知道该给多大空间
char str[] = ""; // ❌ 依然错误!这只申请了1个字节的空间(存\0),放不下后面的输入

总结

C 语言非常“笨”,它不会自动扩容。

  • 想存字符串,必须先给够地盘
  • **char str[100]**:意思是“给我留 100 个字节的空地,我要用来存字”。这是最稳妥的写法。

所以,记住了:定义字符串变量用来输入时,中括号里的数字(长度)是绝对不能省的!


输入与字符串
http://example.com/2025/12/13/输入与字符串/
作者
王柏森
发布于
2025年12月13日
许可协议