输入输出
# 05.输入输出
# 目录介绍
# 5.1 输入输出库
在 C 语言中,输入和输出(I/O) 是通过标准库函数实现的。C 语言提供了丰富的函数来处理输入和输出操作,主要包括格式化输入输出、字符输入输出和文件输入输出。
C 语言的输入输出函数定义在 <stdio.h> 头文件中,使用前需要包含该头文件。
#include <stdio.h>
原理说明:C语言本身不包含输入输出语句,所有I/O操作都是通过标准库函数实现的。<stdio.h> 中定义了 FILE 结构体、EOF 常量以及各种I/O函数的原型。C的I/O系统基于流(stream) 的概念,流是对数据源或数据目标的抽象。程序启动时,系统自动打开三个标准流:stdin(标准输入,通常是键盘)、stdout(标准输出,通常是屏幕)、stderr(标准错误输出)。所有I/O函数本质上都是对这些流进行操作。
# 5.1.1 综合案例与思考
综合案例:探索标准I/O流
#include <stdio.h>
int main() {
// 标准输出流
fprintf(stdout, "这是通过 stdout 输出的\n");
// 标准错误流(不受重定向影响)
fprintf(stderr, "这是通过 stderr 输出的\n");
// printf 本质上等价于 fprintf(stdout, ...)
printf("printf 等价于 fprintf(stdout, ...)\n");
// 查看流的缓冲模式
printf("\nstdout 是%s缓冲的\n",
stdout->_flags ? "行" : "全");
printf("stderr 是%s缓冲的\n",
stderr->_flags ? "无" : "有");
// 手动刷新缓冲区
printf("等待刷新...");
fflush(stdout); // 强制刷新stdout缓冲区
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
原理说明:C标准I/O库使用缓冲机制来提高效率。stdout 通常是行缓冲(遇到换行符\n时自动刷新),而 stderr 是无缓冲的(立即输出)。这就是为什么调试时用 fprintf(stderr, ...) 更可靠——即使程序崩溃,错误信息也已经输出了。fflush() 可以强制刷新缓冲区。
思考题:
- 为什么
stderr被设计为无缓冲?如果stderr也使用缓冲,在程序崩溃时会发生什么? - 在终端执行
./program > output.txt时,printf的输出和fprintf(stderr, ...)的输出分别去了哪里? fflush(stdin)能清空输入缓冲区吗?这样做是标准行为还是未定义行为?
# 5.2 格式化输出
# 5.2.1 printf介绍
printf 函数用于将格式化的数据输出到标准输出设备(通常是屏幕)。
语法
int printf(const char *format, ...);
示例
int age = 25;
printf("My age is %d\n", age);
2
# 5.2.2 占位符
在 C 语言中,占位符(格式说明符)是用于格式化输入/输出函数(如 printf 和 scanf)的特殊标记,用于指定变量类型和格式化规则。常用占位符表
| 占位符 | 适用数据类型 | 说明 | 示例 |
|---|---|---|---|
%d | int | 十进制有符号整数 | printf("%d", 42); |
%u | unsigned int | 十进制无符号整数 | printf("%u", 42); |
%ld | long | 长整型十进制整数 | printf("%ld", 123456L); |
%f | float / double | 十进制浮点数(默认保留6位小数) | printf("%f", 3.14); |
%.2f | float / double | 保留2位小数(可修改数字控制精度) | printf("%.2f", 3.1415); |
%e / %E | float / double | 科学计数法(小写e/大写E) | printf("%e", 1000.0); |
%c | char | 单个字符 | printf("%c", 'A'); |
%s | char[] / char* | 字符串(以 \0 结尾) | printf("%s", "Hello"); |
%p | 指针类型 | 打印指针地址(十六进制) | printf("%p", &x); |
%x / %X | int / unsigned | 十六进制无符号整数(小写/大写) | printf("%x", 255); |
%o | int / unsigned | 八进制无符号整数 | printf("%o", 8); |
%% | 无 | 输出一个 % 字符 | printf("%%"); |
# 5.2.3 输出多个常量
一个printf中可以同时输出多个数据,占位符和后面的数据要一一对应
练习:输出以下内容:
我亲亲女朋友的姓名是:小诗诗。性别:女。年龄:18岁。身高:1米82。体重:110斤。
要求:女朋友的姓名,性别,年龄,身高,体重等信息需要结合占位符的形式进行输出
#include <stdio.h>
int main() {
printf("我亲亲女朋友的姓名是:%s。性别:%s。年龄:%d岁。身高:%f。体重:%d斤", "小诗诗","女",18,1.82,110);
return 0;
}
2
3
4
5
# 5.2.4 综合案例与思考
综合案例:printf高级格式化技巧
#include <stdio.h>
int main() {
// 案例1:宽度与对齐
printf("=== 宽度与对齐 ===\n");
printf("[%10d]\n", 42); // 右对齐,宽度10
printf("[%-10d]\n", 42); // 左对齐,宽度10
printf("[%010d]\n", 42); // 用0填充
// 案例2:浮点数精度控制
printf("\n=== 浮点数精度 ===\n");
double pi = 3.141592653589793;
printf("默认精度: %f\n", pi); // 6位小数
printf("2位小数: %.2f\n", pi);
printf("10位小数: %.10f\n", pi);
printf("科学计数: %e\n", pi);
printf("自动选择: %g\n", pi); // 自动选择%f或%e
// 案例3:字符串截取
printf("\n=== 字符串截取 ===\n");
printf("[%.5s]\n", "HelloWorld"); // 只输出前5个字符
printf("[%10.5s]\n", "HelloWorld"); // 宽度10,取前5字符
// 案例4:打印特殊值
printf("\n=== 特殊值 ===\n");
printf("百分号: %%\n");
printf("十六进制: 0x%08X\n", 255); // 大写十六进制,8位补0
printf("八进制: %#o\n", 255); // 带前缀0的八进制
printf("指针地址: %p\n", (void *)&pi);
// 案例5:格式化表格输出
printf("\n=== 成绩表 ===\n");
printf("%-8s %-6s %6s\n", "姓名", "科目", "分数");
printf("%-8s %-6s %6.1f\n", "张三", "数学", 95.5);
printf("%-8s %-6s %6.1f\n", "李四", "语文", 88.0);
printf("%-8s %-6s %6.1f\n", "王五", "英语", 92.3);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
原理说明:printf 的格式字符串遵循 %[标志][宽度][.精度][长度修饰符]转换说明符 的完整格式。标志包括 -(左对齐)、+(显示正号)、0(零填充)、#(备用形式,如八进制前加0)。printf 内部使用可变参数列表(va_list),通过格式字符串中的占位符来确定参数类型和数量。如果占位符与实际参数类型不匹配,会导致未定义行为,因为 printf 无法进行类型检查。
思考题:
printf("%d", 3.14)会输出什么?为什么?这涉及到什么底层原理?printf的返回值是什么?在什么场景下需要检查这个返回值?printf和sprintf有什么区别?snprintf相比sprintf的优势是什么?
# 5.3 格式化输入
scanf 函数用于从标准输入设备(通常是键盘)读取格式化数据。
语法
int scanf(const char *format, ...);
示例
int age;
printf("Enter your age: ");
scanf("%d", &age);
2
3
注意事项
scanf 需要传递变量的地址(使用 & 运算符)。
如果输入的数据类型与格式说明符不匹配,会导致未定义行为。
原理说明:scanf 从输入流中读取字符并按照格式字符串进行匹配。& 运算符取变量地址是必需的,因为 scanf 需要知道将读取的值存储到哪个内存位置(C语言是值传递)。scanf 会跳过空白字符(空格、制表符、换行符),但 %c 是例外——它会读取包括空白在内的任何字符。scanf 的返回值是成功匹配并赋值的项数,应该检查此值以确保输入有效。
# 5.3.1 综合案例与思考
综合案例:scanf的安全使用与输入验证
#include <stdio.h>
int main() {
// 案例1:基本输入验证
int num;
printf("请输入一个整数: ");
// 模拟输入验证逻辑
num = 42; // 模拟输入
printf("读取到: %d\n", num);
// 案例2:scanf返回值检查
int a, b;
printf("\nscanf返回值演示:\n");
// scanf("%d %d", &a, &b) 返回成功读取的项数
// 如果输入 "10 abc",返回1(只成功读取了a)
a = 10; b = 20;
printf("成功读取2个值: a=%d, b=%d\n", a, b);
// 案例3:读取不同类型的数据
char name[50];
int age;
float height;
printf("\n综合输入演示:\n");
// scanf("%s %d %f", name, &age, &height);
// 注意:%s遇到空格就停止,name不需要&(数组名就是地址)
sprintf(name, "张三");
age = 25;
height = 1.75f;
printf("姓名: %s, 年龄: %d, 身高: %.2f\n", name, age, height);
// 案例4:防止缓冲区溢出
char buffer[10];
printf("\n安全输入演示:\n");
// 正确做法:限制读取长度
// scanf("%9s", buffer); // 最多读9个字符,留1个给\0
sprintf(buffer, "Hello");
printf("安全读取: %s\n", buffer);
// 案例5:清除输入缓冲区中的残留
printf("\n清除缓冲区演示:\n");
// 当scanf读取失败时,错误字符留在缓冲区
// 需要用循环清除: while(getchar() != '\n');
printf("缓冲区已清除\n");
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
原理说明:scanf 在内部维护一个输入缓冲区,键盘输入的字符先存入缓冲区,按下回车后 scanf 才开始解析。如果解析失败(如期望整数却输入了字母),未匹配的字符会留在缓冲区中,导致后续的 scanf 调用也失败。这是初学者最容易遇到的"无限循环"问题。解决方案是在 scanf 失败后用 while(getchar() != '\n'); 清空缓冲区。%s 不会读取空格,要读取含空格的字符串应使用 fgets。
思考题:
scanf("%d", &x)中如果用户输入了 "abc",程序会怎样?如何正确处理这种情况?- 为什么
scanf("%s", str)中的str不需要加&,而scanf("%d", &x)中的x必须加&? scanf存在缓冲区溢出风险,有哪些更安全的替代方案?
# 5.4 字符输入输出
# 5.4.1 读取一个字符
getchar 从标准输入读取一个字符。
int getchar(void);
示例
char c = getchar();
# 5.4.2 写入一个字符
putchar 向标准输出写入一个字符。
int putchar(int c);
示例
putchar('A');
# 5.4.3 综合案例与思考
综合案例:字符I/O的实际应用
#include <stdio.h>
#include <ctype.h>
int main() {
// 案例1:逐字符处理——大小写转换
char text[] = "Hello World 123!";
printf("原始字符串: %s\n", text);
printf("大写转换: ");
for (int i = 0; text[i] != '\0'; i++) {
putchar(toupper(text[i]));
}
printf("\n");
printf("小写转换: ");
for (int i = 0; text[i] != '\0'; i++) {
putchar(tolower(text[i]));
}
printf("\n");
// 案例2:字符统计
char sample[] = "C Programming is Fun! 2024.";
int letters = 0, digits = 0, spaces = 0, others = 0;
for (int i = 0; sample[i] != '\0'; i++) {
if (isalpha(sample[i])) letters++;
else if (isdigit(sample[i])) digits++;
else if (isspace(sample[i])) spaces++;
else others++;
}
printf("\n字符统计 \"%s\":\n", sample);
printf("字母: %d, 数字: %d, 空格: %d, 其他: %d\n",
letters, digits, spaces, others);
// 案例3:简单的凯撒密码加密
char message[] = "HELLO";
int shift = 3;
printf("\n凯撒密码加密 (偏移=%d):\n", shift);
printf("明文: %s\n", message);
printf("密文: ");
for (int i = 0; message[i] != '\0'; i++) {
if (isupper(message[i])) {
putchar((message[i] - 'A' + shift) % 26 + 'A');
} else {
putchar(message[i]);
}
}
printf("\n");
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
原理说明:getchar() 和 putchar() 是最基本的字符I/O函数,它们一次只处理一个字符。getchar() 的返回类型是 int 而非 char,这是为了能够返回 EOF(通常定义为 -1)来表示输入结束。如果返回类型是 char,在某些平台上 char 是无符号的,255和EOF(-1)无法区分。<ctype.h> 提供的字符分类函数(如 isalpha、isdigit)使用查表法实现,效率很高。
思考题:
getchar()的返回类型为什么是int而不是char?如果定义char c = getchar(),在什么情况下会出问题?putchar(c)和printf("%c", c)功能相同,但性能有区别吗?为什么?- 如何用
getchar和putchar实现一个简单的字符过滤器,只保留字母和数字?
# 5.5 字符串输入输出
# 5.5.1 字符串输入
gets 和 fgets
gets从标准输入读取一行字符串(不推荐使用,容易导致缓冲区溢出)。fgets从文件或标准输入读取一行字符串。
char *fgets(char *str, int n, FILE *stream);
示例
char str[100];
fgets(str, 100, stdin); // 从标准输入读取一行
2
# 5.5.2 字符串输出
puts 向标准输出写入字符串。
int puts(const char *str);
示例
puts("Hello, World!");
# 5.5.3 综合案例与思考
综合案例:字符串I/O的安全实践
#include <stdio.h>
#include <string.h>
int main() {
// 案例1:fgets vs gets 的安全性对比
char buffer[20];
// 危险写法(已在C11中移除):
// gets(buffer); // 不检查缓冲区大小,可能溢出!
// 安全写法:
// fgets(buffer, sizeof(buffer), stdin);
// fgets 会保留换行符,通常需要去掉:
strcpy(buffer, "Hello World!\n");
printf("fgets读取(含换行): [%s]", buffer);
// 去掉换行符
buffer[strcspn(buffer, "\n")] = '\0';
printf("去掉换行后: [%s]\n", buffer);
// 案例2:puts vs printf 的区别
printf("\nputs自动添加换行:\n");
puts("第一行"); // 自动追加\n
puts("第二行");
printf("\nprintf不自动添加换行:\n");
printf("第一行");
printf("第二行");
printf("\n");
// 案例3:多行字符串输入模拟
printf("\n模拟多行输入处理:\n");
char *lines[] = {
"第一行内容",
"第二行内容",
"END"
};
for (int i = 0; strcmp(lines[i], "END") != 0; i++) {
printf("读取到: %s\n", lines[i]);
}
// 案例4:字符串拼接输出
char first_name[] = "张";
char last_name[] = "三";
char full_name[100];
sprintf(full_name, "%s%s", first_name, last_name);
printf("\n拼接结果: %s\n", full_name);
// 案例5:安全的字符串格式化
char safe_buf[20];
int written = snprintf(safe_buf, sizeof(safe_buf),
"数值=%d,浮点=%.2f", 42, 3.14);
printf("snprintf写入: %s (需要%d字符)\n", safe_buf, written);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
原理说明:gets 函数因为不检查缓冲区边界,是C语言中最危险的函数之一,已在C11标准中被正式移除。著名的Morris蠕虫(1988年)就利用了 gets 的缓冲区溢出漏洞。fgets 通过参数 n 限制最大读取长度来防止溢出,但它会保留输入中的换行符 \n。puts 会自动在输出末尾追加换行符,而 fputs 不会。snprintf 是 sprintf 的安全版本,它接受缓冲区大小参数,防止写入越界。
思考题:
fgets读取的字符串末尾会包含\n,有哪些方法可以去掉这个换行符?哪种方式最安全?gets为什么会被从C11标准中移除?它导致了哪些著名的安全漏洞?sprintf和snprintf的区别是什么?在实际项目中应该优先使用哪个?为什么?