编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

    • 入门教程

      • README
      • 基础语法
      • 数据类型
      • 运算符
      • 循环和选择
      • 输入输出
        • 5.1 输入输出库
          • 5.1.1 综合案例与思考
        • 5.2 格式化输出
          • 5.2.1 printf介绍
          • 5.2.2 占位符
          • 5.2.3 输出多个常量
          • 5.2.4 综合案例与思考
        • 5.3 格式化输入
          • 5.3.1 综合案例与思考
        • 5.4 字符输入输出
          • 5.4.1 读取一个字符
          • 5.4.2 写入一个字符
          • 5.4.3 综合案例与思考
        • 5.5 字符串输入输出
          • 5.5.1 字符串输入
          • 5.5.2 字符串输出
          • 5.5.3 综合案例与思考
      • 函数
      • 指针
      • 数组和容器
      • 类和内存
      • 流与文件
      • 结构体
      • 线程和锁
      • 预处理器
      • 高级数据
    • 综合案例

    • 专栏博客

    • 标准集库

  • Cpp入门到精通

  • Java入门精通

  • Go入门到精通

  • JavaScript入门

  • CodeX
  • C语言入门精通
  • 入门教程
杨充
2025-07-21
目录

输入输出

# 05.输入输出

# 目录介绍

  • 5.1 输入输出库
    • 5.1.1 综合案例与思考
  • 5.2 格式化输出
    • 5.2.1 printf介绍
    • 5.2.2 占位符
    • 5.2.3 输出多个常量
    • 5.2.4 综合案例与思考
  • 5.3 格式化输入
    • 5.3.1 综合案例与思考
  • 5.4 字符输入输出
    • 5.4.1 读取一个字符
    • 5.4.2 写入一个字符
    • 5.4.3 综合案例与思考
  • 5.5 字符串输入输出
    • 5.5.1 字符串输入
    • 5.5.2 字符串输出
    • 5.5.3 综合案例与思考

# 5.1 输入输出库

在 C 语言中,输入和输出(I/O) 是通过标准库函数实现的。C 语言提供了丰富的函数来处理输入和输出操作,主要包括格式化输入输出、字符输入输出和文件输入输出。

C 语言的输入输出函数定义在 <stdio.h> 头文件中,使用前需要包含该头文件。

#include <stdio.h>
1

原理说明: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;
}
1
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() 可以强制刷新缓冲区。

思考题:

  1. 为什么 stderr 被设计为无缓冲?如果 stderr 也使用缓冲,在程序崩溃时会发生什么?
  2. 在终端执行 ./program > output.txt 时,printf 的输出和 fprintf(stderr, ...) 的输出分别去了哪里?
  3. fflush(stdin) 能清空输入缓冲区吗?这样做是标准行为还是未定义行为?

# 5.2 格式化输出

# 5.2.1 printf介绍

printf 函数用于将格式化的数据输出到标准输出设备(通常是屏幕)。

语法

int printf(const char *format, ...);
1

示例

int age = 25;
printf("My age is %d\n", age);
1
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;
}
1
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;
}
1
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 无法进行类型检查。

思考题:

  1. printf("%d", 3.14) 会输出什么?为什么?这涉及到什么底层原理?
  2. printf 的返回值是什么?在什么场景下需要检查这个返回值?
  3. printf 和 sprintf 有什么区别?snprintf 相比 sprintf 的优势是什么?

# 5.3 格式化输入

scanf 函数用于从标准输入设备(通常是键盘)读取格式化数据。

语法

int scanf(const char *format, ...);
1

示例

int age;
printf("Enter your age: ");
scanf("%d", &age);
1
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;
}
1
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。

思考题:

  1. scanf("%d", &x) 中如果用户输入了 "abc",程序会怎样?如何正确处理这种情况?
  2. 为什么 scanf("%s", str) 中的 str 不需要加 &,而 scanf("%d", &x) 中的 x 必须加 &?
  3. scanf 存在缓冲区溢出风险,有哪些更安全的替代方案?

# 5.4 字符输入输出

# 5.4.1 读取一个字符

getchar 从标准输入读取一个字符。

int getchar(void);
1

示例

char c = getchar();
1

# 5.4.2 写入一个字符

putchar 向标准输出写入一个字符。

int putchar(int c);
1

示例

putchar('A');
1

# 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;
}
1
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)使用查表法实现,效率很高。

思考题:

  1. getchar() 的返回类型为什么是 int 而不是 char?如果定义 char c = getchar(),在什么情况下会出问题?
  2. putchar(c) 和 printf("%c", c) 功能相同,但性能有区别吗?为什么?
  3. 如何用 getchar 和 putchar 实现一个简单的字符过滤器,只保留字母和数字?

# 5.5 字符串输入输出

# 5.5.1 字符串输入

gets 和 fgets

  • gets 从标准输入读取一行字符串(不推荐使用,容易导致缓冲区溢出)。
  • fgets 从文件或标准输入读取一行字符串。
char *fgets(char *str, int n, FILE *stream);
1

示例

char str[100];
fgets(str, 100, stdin); // 从标准输入读取一行
1
2

# 5.5.2 字符串输出

puts 向标准输出写入字符串。

int puts(const char *str);
1

示例

puts("Hello, World!");
1

# 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;
}
1
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 的安全版本,它接受缓冲区大小参数,防止写入越界。

思考题:

  1. fgets 读取的字符串末尾会包含 \n,有哪些方法可以去掉这个换行符?哪种方式最安全?
  2. gets 为什么会被从C11标准中移除?它导致了哪些著名的安全漏洞?
  3. sprintf 和 snprintf 的区别是什么?在实际项目中应该优先使用哪个?为什么?
上次更新: 2026/06/10, 11:13:41
循环和选择
函数

← 循环和选择 函数→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式