输入输出 — 让程序与用户对话
C语言学习教程
输入输出 — 让程序与用户对话
🌟 课前故事:从”广播”到”对话”
到目前为止,你的程序像是一个”广播电台”——它不停地向世界说话(用 printf 输出信息),但从不聆听回应。这样的程序固然有用,但它每次运行的结果都是一样的,没有”参与感”。
真正的交互式程序像一个聊天机器人——它问你问题,等你的回答,再根据你的回答继续对话。
广播模式(只有输出) 对话模式(输入+输出)
┌───────────┐ ┌───────────┐
│ 程序 │ │ 程序 │
│ │ │ │
│ printf──→│ 屏幕 │ printf──→│ 屏幕
│ │ │ ↑ │
│ │ │ scanf │
│ │ │ │ │
│ │ │ 键盘──┘ │
└───────────┘ └───────────┘
单向输出 双向交互
今天,我们给程序”装上耳朵”——用 scanf 读取用户的输入。从此你的程序不再自言自语,而是可以和用户真正地”对话”了。
📖 概念讲解:为什么需要输入?
什么是输入?
输入(Input) 是程序从外部世界获取数据的过程。最常见的外部来源是键盘,也可以是文件、网络、传感器等。
为什么需要输入?
没有输入的程序是”死”的——它每次运行都做同样的事。有了输入,程序可以:
- 个性化:不同用户输入不同数据,得到不同结果
- 交互性:程序根据用户的选择做出不同的响应
- 实用性:计算器需要用户输入数字,登录需要用户输入密码
- 灵活性:同一个程序可以处理各种不同的数据
什么时候用到输入输出?
几乎所有有用的程序都涉及输入输出:
- 计算器:输入数字和运算符,输出结果
- 游戏:输入操作指令,输出画面和声音
- 聊天软件:输入文字,输出对方的回复
- 数据分析:输入原始数据,输出分析报告
📊 输出部分:printf 深度掌握
printf 基础回顾
#include <stdio.h>
int main() {
// 最基础:输出固定文字
printf("Hello, World!\n");
// 带变量:用占位符输出变量
int age = 25;
printf("我今年 %d 岁\n", age);
// 多个变量:占位符和参数一一对应
int a = 10, b = 20;
printf("%d + %d = %d\n", a, b, a + b);
return 0;
}
printf 格式化占位符参考表
| 占位符 | 数据类型 | 输出格式 | 示例 |
|---|---|---|---|
%d 或 %i | int | 有符号十进制整数 | printf("%d", 42) → 42 |
%u | unsigned int | 无符号十进制整数 | printf("%u", 42) → 42 |
%f | float / double | 十进制浮点数(默认6位) | printf("%f", 3.14) → 3.140000 |
%lf | double(scanf用) | 与 %f 用法略有不同 | scanf("%lf", &d) |
%c | char | 单个字符 | printf("%c", 'A') → A |
%s | char[](字符串) | 字符串 | printf("%s", "Hello") → Hello |
%x 或 %X | int | 十六进制整数 | printf("%x", 255) → ff |
%o | int | 八进制整数 | printf("%o", 8) → 10 |
%p | 指针 | 指针地址 | printf("%p", &x) → 0x7ffe... |
%% | (无) | 输出百分号本身 | printf("%%") → % |
printf 格式化控制详解
printf 的强大之处在于格式化控制——你可以精确控制数字的宽度、对齐方式、小数位数等。
格式说明符的完整结构:
%[标志][宽度][.精度][长度]类型
例如:%08.2f
│││ │└── f:浮点类型
│││ └──── .2:保留2位小数(精度)
││└────── 8:总宽度8个字符
│└─────── 0:用0填充(标志)
└──────── %:起始符号
#include <stdio.h>
int main() {
int n = 42;
float f = 3.14159f;
// === 整数宽度控制 ===
printf("默认: |%d|\n", n); // |42|
printf("宽度5右对齐: |%5d|\n", n); // | 42|
printf("宽度5左对齐: |%-5d|\n", n); // |42 |
printf("宽度5补零: |%05d|\n", n); // |00042|
// === 浮点数精度控制 ===
printf("默认: |%f|\n", f); // |3.141590|
printf("保留2位: |%.2f|\n", f); // |3.14|
printf("保留4位: |%.4f|\n", f); // |3.1416|(四舍五入)
printf("宽8精2: |%8.2f|\n", f); // | 3.14|
printf("左对齐宽8精2:|%-8.2f|\n", f); // |3.14 |
printf("补零宽8精2: |%08.2f|\n", f); // |00003.14|
// === 组合使用 ===
printf("\n%-15s %5s %8s\n", "商品", "数量", "单价");
printf("%-15s %5d %8.2f\n", "苹果", 3, 5.50);
printf("%-15s %5d %8.2f\n", "香蕉", 5, 3.20);
printf("%-15s %5d %8.2f\n", "橙子", 2, 6.80);
return 0;
}
输出效果:
默认: |42|
宽度5右对齐: | 42|
宽度5左对齐: |42 |
宽度5补零: |00042|
默认: |3.141590|
保留2位: |3.14|
保留4位: |3.1416|
宽8精2: | 3.14|
左对齐宽8精2:|3.14 |
补零宽8精2: |00003.14|
商品 数量 单价
苹果 3 5.50
香蕉 5 3.20
橙子 2 6.80
printf 返回值(你可能不知道的知识)
printf 不仅输出文字,还会返回实际输出了多少个字符。这在某些场景下很有用:
#include <stdio.h>
int main() {
int count;
count = printf("Hello\n"); // 6 个字符(H,e,l,l,o,\n)
printf("输出了 %d 个字符\n", count);
count = printf(""); // 输出空字符串
printf("输出了 %d 个字符\n", count); // 0
return 0;
}
📊 输入部分:scanf 详细教程
scanf 是 printf 的”反向伙伴”——printf 把数据从程序送到屏幕,scanf 把数据从键盘送到程序。
scanf 基本语法
scanf("格式字符串", &变量1, &变量2, ...);
↑ ↑
格式说明符 变量的地址(后面填到哪个变量里)
🔴 关键:为什么要加 &(取地址符号)?
int age;
scanf("%d", &age);
// ↑
// & 是取地址运算符
// 意思是:把用户输入的数字,存到 age 变量所在的内存位置
通俗比喻:
- 不加
&:你告诉快递员”送到张三”,但没说地址,快递员找不到 - 加上
&:你告诉快递员”送到张三的地址(北京市朝阳区…)”,快递员就找到了
scanf 需要知道数据该放到哪里,而 &变量名 就是”这个变量的内存地址”。
int age;
// 变量 age 在内存中占了一块空间(4字节)
// &age 就是这块空间的"门牌号"——地址
// scanf 根据这个地址,把数据写进正确的位置
scanf("%d", &age); // ✅ scanf 知道往哪里放
// scanf("%d", age); // ❌ scanf 不知道往哪里放,程序崩溃!
读取不同类型的数据
#include <stdio.h>
int main() {
int age;
float height;
double weight;
char grade;
// —— 读取整数 ——
printf("请输入你的年龄:");
scanf("%d", &age); // 用 %d 读整数
// —— 读取 float ——
printf("请输入你的身高(米):");
scanf("%f", &height); // 用 %f 读 float
// —— 读取 double ——
printf("请输入你的体重(公斤):");
scanf("%lf", &weight); // 用 %lf 读 double(注意是 lf!)
// —— 读取字符 ——
printf("请输入你的等级(A/B/C):");
scanf(" %c", &grade); // 用 %c 读字符(注意前面的空格!)
// —— 输出确认 ——
printf("\n===== 你的信息 =====\n");
printf("年龄:%d 岁\n", age);
printf("身高:%.2f 米\n", height);
printf("体重:%.1f 公斤\n", weight);
printf("等级:%c\n", grade);
return 0;
}
scanf 格式占位符对照表
| 占位符 | 读取类型 | 变量类型 | 示例 |
|---|---|---|---|
%d | 十进制整数 | int | scanf("%d", &x) |
%f | 浮点数 | float | scanf("%f", &f) |
%lf | 双精度浮点数 | double | scanf("%lf", &d) |
%c | 单个字符 | char | scanf(" %c", &ch) |
%s | 字符串(无空格) | char[] | scanf("%s", str) |
%x | 十六进制整数 | int | scanf("%x", &x) |
⚠️ printf 用 %f 输出 double,但 scanf 用 %lf 读 double
// ✅ printf 输出 double
double d = 3.14159;
printf("%f\n", d); // printf 中 float 和 double 都用 %f
// ✅ scanf 读取 float
float f;
scanf("%f", &f); // scanf 读取 float 用 %f
// ✅ scanf 读取 double
double d2;
scanf("%lf", &d2); // scanf 读取 double 用 %lf(注意!)
为什么? 这是C语言的一个历史遗留问题。记住这条规则就好:printf 输出用 %f,scanf 读取 double 用 %lf。
💻 完整交互示例:一个简单的计算器
#include <stdio.h>
int main() {
int num1, num2;
printf("╔══════════════════════╗\n");
printf("║ 简易加法计算器 ║\n");
printf("╚══════════════════════╝\n\n");
// 读取第一个数字
printf("请输入第一个数字:");
scanf("%d", &num1);
// 读取第二个数字
printf("请输入第二个数字:");
scanf("%d", &num2);
// 计算并输出结果
printf("\n--- 计算结果 ---\n");
printf("%d + %d = %d\n", num1, num2, num1 + num2);
printf("%d - %d = %d\n", num1, num2, num1 - num2);
printf("%d * %d = %d\n", num1, num2, num1 * num2);
printf("%d / %d = %d\n", num1, num2, num1 / num2);
printf("%d %% %d = %d\n", num1, num2, num1 % num2);
return 0;
}
运行效果:
╔══════════════════════╗
║ 简易加法计算器 ║
╚══════════════════════╝
请输入第一个数字:15
请输入第二个数字:4
--- 计算结果 ---
15 + 4 = 19
15 - 4 = 11
15 * 4 = 60
15 / 4 = 3
15 % 4 = 3
⚠️ scanf 的三大经典”坑”
坑1:%c 会”吃”到上一次遗留的换行符
这是 scanf 最著名的坑,几乎所有初学者都会遇到。
// ❌ 问题代码
#include <stdio.h>
int main() {
int num;
char ch;
printf("输入一个数字:");
scanf("%d", &num); // 用户输入:42[回车]
// scanf 读取了 42,但 [回车] 还在输入缓冲区里!
printf("输入一个字符:");
scanf("%c", &ch); // ch 读到了上次遗留的 '\n'(回车符)!
// 程序不等待用户输入,直接跳过了!
printf("数字:%d, 字符:[%c]\n", num, ch);
return 0;
}
实际运行效果(注意第二行根本没有等你输入):
输入一个数字:42
输入一个字符:数字:42, 字符:[
]
为什么会这样?
当你输入 42 然后按回车时,实际输入的是四个字符:4 2 \r \n。
输入缓冲区:
┌───┬───┬────┬────┐
│ 4 │ 2 │ \r │ \n │ ← 回车产生了 \r 和 \n
└───┴───┴────┴────┘
↑ ↑
%d 读取 42 \n 还留在缓冲区里!
下一次 %c 就读到了它
解决方案:
// ✅ 方案1:在 %c 前加一个空格(最常用)
scanf(" %c", &ch); // 前面的空格告诉 scanf:"跳过所有空白字符"
// 空白字符包括:空格、\t、\n 等
// ✅ 方案2:手动清空缓冲区
scanf("%d", &num);
getchar(); // 吃掉 \n
scanf("%c", &ch);
// ✅ 方案3:使用 fflush(不推荐,跨平台有问题)
scanf("%d", &num);
fflush(stdin); // Windows 上行,Linux/Mac 上不行
scanf("%c", &ch);
推荐用方案1——在 %c 前面加空格:" %c"。简单又跨平台。
坑2:%s 遇到空格就停止
// ❌ 问题代码
char name[50];
printf("请输入你的全名:");
scanf("%s", name); // 用户输入:张三丰
// name 里只有"张"!空格后面的丢了
printf("你好,%s\n", name); // 输出:你好,张
原因: %s 以空白字符(空格、Tab、换行)作为分隔符,遇到就停止读取。
解决方案:
// ✅ 方案1:用 fgets 读取整行(推荐)
char name[50];
printf("请输入你的全名:");
fgets(name, sizeof(name), stdin); // 读取整行,包括空格
// 注意:fgets 会保留末尾的 \n,需要手动去掉
name[strcspn(name, "\n")] = '\0'; // 去掉末尾的换行符
printf("你好,%s\n", name);
// ✅ 方案2:用 scanf 的字符集(只读取非换行字符)
char name[50];
printf("请输入你的全名:");
scanf("%[^\n]", name); // 读取直到遇到 \n 为止
getchar(); // 吃掉 \n
printf("你好,%s\n", name);
// ✅ 方案3:用 gets(不推荐,不安全,已被C标准废弃)
// gets(name); // 不要用!
注意: fgets 和 %[^\n] 后面课程的”字符串”部分会详细讲,这里知道有这个问题和解决方案就行。
坑3:scanf 的返回值——检查输入是否成功
// ❌ 脆弱代码:假设用户一定会输入数字
int age;
printf("请输入年龄:");
scanf("%d", &age); // 如果用户输入 "abc" 会怎样?
// scanf 读不到数字,age 的值不变(还是垃圾值)
// ✅ 健壮代码:检查 scanf 的返回值
int age;
printf("请输入年龄:");
if (scanf("%d", &age) == 1) {
// scanf 返回值 = 成功读取的变量个数
printf("你的年龄是:%d\n", age);
} else {
printf("输入无效!请输入一个数字。\n");
}
scanf 的返回值含义:
- 返回成功读取的变量个数
- 如果遇到文件结尾(用户按了 Ctrl+D 或 Ctrl+Z),返回
EOF(通常是-1) - 如果读取失败(比如该读数字时输入了字母),返回
0
// 多个变量的返回值检查
int a, b;
int result = scanf("%d %d", &a, &b);
if (result == 2) {
printf("成功读取两个数:%d 和 %d\n", a, b);
} else if (result == 1) {
printf("只成功读取了一个数\n");
} else {
printf("一个都没读到\n");
}
💻 综合示例:学生信息注册系统
#include <stdio.h>
int main() {
char name[50];
int studentId;
int age;
float chinese, math, english;
float average;
printf("╔══════════════════════════════╗\n");
printf("║ 学生信息注册系统 ║\n");
printf("╚══════════════════════════════╝\n\n");
// —— 基本信息 ——
printf("请输入姓名:");
scanf("%s", name); // 名字不含空格用 %s
printf("请输入学号:");
scanf("%d", &studentId);
printf("请输入年龄:");
scanf("%d", &age);
// —— 成绩信息 ——
printf("\n请输入各科成绩(0-100):\n");
printf(" 语文:");
scanf("%f", &chinese);
printf(" 数学:");
scanf("%f", &math);
printf(" 英语:");
scanf("%f", &english);
// —— 计算统计 ——
float total = chinese + math + english;
average = total / 3.0f;
// —— 输出结果 ——
printf("\n╔══════════════════════════════╗\n");
printf("║ 注册结果 ║\n");
printf("╚══════════════════════════════╝\n\n");
printf(" 姓名:%s\n", name);
printf(" 学号:%d\n", studentId);
printf(" 年龄:%d 岁\n", age);
printf(" ─────────────────────\n");
printf(" 语文:%6.1f 分\n", chinese);
printf(" 数学:%6.1f 分\n", math);
printf(" 英语:%6.1f 分\n", english);
printf(" ─────────────────────\n");
printf(" 总分:%6.1f 分\n", total);
printf(" 平均:%6.1f 分\n", average);
if (average >= 60.0f) {
printf(" 状态:✅ 及格\n");
} else {
printf(" 状态:❌ 不及格\n");
}
return 0;
}
📊 输入输出流程图
┌─────────────────────────────────────────────┐
│ 程序运行流程 │
│ │
│ ① printf 输出提示 ──→ 屏幕显示"请输入..." │
│ ↓ │
│ ② 程序暂停,等待用户输入 │
│ ↓ │
│ ③ 用户敲键盘,输入数据 │
│ ↓ │
│ ④ 用户按回车,数据进入"输入缓冲区" │
│ ↓ │
│ ⑤ scanf 从缓冲区读取数据 │
│ ↓ │
│ ⑥ scanf 把数据写入变量(需要 &地址) │
│ ↓ │
│ ⑦ 程序继续执行,处理变量 │
│ ↓ │
│ ⑧ printf 输出结果 ──→ 屏幕显示结果 │
└─────────────────────────────────────────────┘
关键理解:输入缓冲区
你在键盘上敲的字符不会立刻被程序收到。它们先存在一个叫”输入缓冲区”的地方,当你按下回车键后,scanf 才去缓冲区里取数据。这也是”坑1”(%c 读到遗留的 \n)的根本原因。
⚠️ 常见错误(5个经典”坑”)
错误1:scanf 忘记 &
// ❌ 错误代码
int age;
scanf("%d", age); // 忘记 &!
// 程序行为:崩溃(segmentation fault)或 age 的值不变
// ✅ 正确代码
int age;
scanf("%d", &age); // 必须用 & 取地址
// 例外:字符串数组不加 &
char name[50];
scanf("%s", name); // name 本身就是地址,不需要 &name
记住: 普通变量(int, float, char, double)用 &,数组/字符串不用 &。为什么?后面数组课程会详细解释。
错误2:double 用 %f 而不是 %lf
// ❌ 错误代码
double pi;
scanf("%f", &pi); // double 应该用 %lf!
// ✅ 正确代码
double pi;
scanf("%lf", &pi); // scanf 读取 double 必须用 %lf
// printf 则不同——输出 double 用 %f 就行
printf("%f\n", pi); // ✅ printf 中 double 和 float 都用 %f
错误3:%c 读到换行符
// ❌ 错误代码
int num;
char ch;
scanf("%d", &num); // 输入 42[回车]
scanf("%c", &ch); // ch 变成了 '\n'!
// ✅ 正确代码
int num;
char ch;
scanf("%d", &num);
scanf(" %c", &ch); // %c 前加空格,跳过空白字符
错误4:scanf 输入格式和用户实际输入不匹配
// ❌ 假设代码这样写
int year, month, day;
scanf("%d-%d-%d", &year, &month, &day);
// 用户必须输入:2024-01-15
// 如果用户输入:2024 01 15(空格分隔)——读取失败!
// ✅ 更宽松的写法
int year, month, day;
scanf("%d %d %d", &year, &month, &day);
// 用户输入:2024 1 15(空格或换行分隔都可以)
// 🏆 最佳做法:一次读一个,每个都有提示
printf("年:"); scanf("%d", &year);
printf("月:"); scanf("%d", &month);
printf("日:"); scanf("%d", &day);
错误5:gets 的缓冲区溢出风险(安全警告!)
// ❌ 危险代码——永远不要用 gets!
char buffer[10];
gets(buffer); // 如果用户输入 50 个字符……
// gets 不管三七二十一全写进去,覆盖了不该覆盖的内存!
// 这就是著名的"缓冲区溢出"漏洞
// ✅ 安全替代
char buffer[10];
fgets(buffer, sizeof(buffer), stdin); // fgets 最多读 sizeof(buffer)-1 个字符
💡 练习建议
初级练习
- 自我介绍:让用户输入姓名和年龄,然后程序打招呼
- 简单计算器:读取两个整数,输出和、差、积、商、余数
- 温度转换:读取摄氏度,转换为华氏度 (F = C * 9/5 + 32)
中级练习
- 圆的计算:读取半径,计算并输出圆的周长和面积
- 成绩统计:读取语文、数学、英语三科成绩,输出总分和平均分
- 字符探索:读取一个字符,输出它和它的ASCII码
进阶思考
- 输入验证:修改计算器程序,当用户输入无效数字时给提示而不是崩溃
- 格式化收据:读取商品名、数量、单价,输出一张格式整齐的购物小票
- 缓冲区实验:故意在
%d后接%c,体验”换行符问题”,然后修复它
📝 要点速查清单
-
printf输出数据到屏幕——用占位符%d%f%c%s嵌入变量值 -
scanf读取用户输入——需要&变量名指定数据存放位置 -
scanf的占位符必须和变量类型匹配:%d→int,%f→float,%lf→double,%c→char -
printf输出double和float都用%f;scanf读取double必须用%lf -
%c前面加空格变成" %c",可以跳过缓冲区中遗留的换行符 -
%s读取字符串遇到空格就停止——要读含空格的字符串用fgets -
scanf的返回值 = 成功读取的变量个数,可以用来检查输入是否有效 - 格式化控制:
%5d(宽度)、%-5d(左对齐)、%05d(补零)、%.2f(精度) - 字符串变量(
char[])在scanf中不加& - 永远不要用
gets(),用fgets()代替
🔗 相关主题
- 上一课:运算符 — 加减乘除与比较 — 用运算符处理输入的数据
- 继续学习:条件判断 if-else — 根据输入决定程序走向
- 继续学习:字符串操作 — fgets、gets 的替代方案详解
- 继续学习:文件操作 — 从文件读取输入,向文件写入输出
- 继续学习:新手避坑指南 — scanf 的坑只是冰山一角
- 参考:printf 完整文档
- 参考:scanf 完整文档