第04章 输入输出
4.1 格式化输出 printf
4.1.1 printf基本用法
#include <stdio.h>
int printf(const char *format, ...);
返回值: 成功时返回输出的字符数,失败返回负数。
基本示例:
#include <stdio.h>
int main(void)
{
int a = 10;
float f = 3.14;
char c = 'A';
printf("整数: %d\n", a);
printf("浮点数: %f\n", f);
printf("字符: %c\n", c);
return 0;
}
输出:
整数: 10
浮点数: 3.140000
字符: A
4.1.2 格式控制符
| 格式符 | 说明 | 示例 |
|---|---|---|
| %d / %i | 有符号十进制整数 | printf("%d", 123); |
| %u | 无符号十进制整数 | printf("%u", 123); |
| %o | 八进制整数 | printf("%o", 123); // 173 |
| %x / %X | 十六进制整数(小写/大写) | printf("%x", 255); // ff |
| %f | 浮点数 | printf("%f", 3.14); |
| %e / %E | 科学计数法 | printf("%e", 1234.5); |
| %g / %G | 自动选择%f或%e | printf("%g", 0.0001); |
| %c | 字符 | printf("%c", 'A'); |
| %s | 字符串 | printf("%s", "hello"); |
| %p | 指针地址 | printf("%p", ptr); |
| %% | 输出百分号 | printf("%%"); |
4.1.3 输出宽度控制
右对齐(默认):
#include <stdio.h>
int main(void)
{
int a = 123;
printf("[%d]\n", a); // [123]
printf("[%5d]\n", a); // [ 123] 右对齐,宽度5
printf("[%10d]\n", a); // [ 123] 右对齐,宽度10
return 0;
}
左对齐:
int main(void)
{
int a = 123;
printf("[%-5d]\n", a); // [123 ] 左对齐,宽度5
printf("[%-10d]\n", a); // [123 ] 左对齐,宽度10
return 0;
}
补零:
int main(void)
{
int a = 123;
printf("[%05d]\n", a); // [00123] 宽度5,前面补0
printf("[%010d]\n", a); // [0000000123] 宽度10,前面补0
return 0;
}
4.1.4 浮点数精度控制
#include <stdio.h>
int main(void)
{
double pi = 3.141592653589793;
printf("%f\n", pi); // 3.141593 (默认6位小数)
printf("%.2f\n", pi); // 3.14 (2位小数)
printf("%.10f\n", pi); // 3.1415926536 (10位小数)
printf("%10.2f\n", pi); // [ 3.14] 总宽度10,2位小数
printf("%-10.2f\n", pi); // [3.14 ] 左对齐
return 0;
}
4.1.5 特殊格式
输出正号:
int main(void)
{
int a = 10;
int b = -10;
printf("%+d\n", a); // +10
printf("%+d\n", b); // -10
return 0;
}
显示进制前缀:
int main(void)
{
int a = 255;
printf("%#o\n", a); // 0377 (八进制前缀0)
printf("%#x\n", a); // 0xff (十六进制前缀0x)
printf("%#X\n", a); // 0XFF
return 0;
}
4.2 格式化输入 scanf
4.2.1 scanf基本用法
#include <stdio.h>
int scanf(const char *format, ...);
返回值: 成功赋值的输入项个数,遇到文件结束符返回EOF(-1)。
基本示例:
#include <stdio.h>
int main(void)
{
int a;
float f;
char c;
printf("请输入整数: ");
scanf("%d", &a);
printf("请输入浮点数: ");
scanf("%f", &f);
printf("请输入字符: ");
scanf(" %c", &c); // 注意空格
printf("a=%d, f=%.2f, c=%c\n", a, f, c);
return 0;
}
⚠️ 重要: scanf的参数必须是变量的地址(使用&运算符)!
4.2.2 scanf常见格式符
| 格式符 | 说明 | 示例 |
|---|---|---|
| %d | 读取十进制整数 | scanf("%d", &a); |
| %u | 读取无符号整数 | scanf("%u", &a); |
| %f | 读取float | scanf("%f", &f); |
| %lf | 读取double | scanf("%lf", &d); |
| %c | 读取单个字符 | scanf("%c", &c); |
| %s | 读取字符串(遇空格结束) | scanf("%s", str); |
| %[^\n] | 读取一行(包含空格) | scanf("%[^\n]", str); |
4.2.3 scanf的陷阱
陷阱1: 缓冲区残留换行符
#include <stdio.h>
int main(void)
{
int a;
char c;
printf("输入整数: ");
scanf("%d", &a); // 输入:10回车 → 缓冲区残留'\n'
printf("输入字符: ");
scanf("%c", &c); // 读取到残留的'\n'!!!
printf("a=%d, c=%d\n", a, c); // c的值是'\n'的ASCII码10
return 0;
}
解决方法:
方法1: 格式符前加空格
scanf(" %c", &c); // 空格会跳过所有空白符(空格、制表符、换行符)
方法2: 清空缓冲区
while (getchar() != '\n'); // 读取直到换行符
scanf("%c", &c);
陷阱2: 字符串读取遇空格停止
#include <stdio.h>
int main(void)
{
char name[50];
printf("输入姓名: ");
scanf("%s", name); // 输入"Zhang San",只读到"Zhang"
printf("name = %s\n", name); // 输出: Zhang
return 0;
}
解决方法: 使用%[^\n]读取整行
scanf("%[^\n]", name); // 读取直到遇到换行符
陷阱3: 未检查返回值
#include <stdio.h>
int main(void)
{
int a;
int ret;
printf("输入整数: ");
ret = scanf("%d", &a);
if (ret != 1)
{
fprintf(stderr, "输入错误!\n");
return 1;
}
printf("a = %d\n", a);
return 0;
}
4.2.4 scanf的输入宽度限制
#include <stdio.h>
int main(void)
{
char str[10];
// 最多读取9个字符(留一个给'\0')
scanf("%9s", str);
printf("str = %s\n", str);
return 0;
}
⚠️ 重要: 使用scanf读取字符串时,务必限制输入长度,防止缓冲区溢出!
4.3 字符输入输出
4.3.1 getchar与putchar
getchar: 从标准输入读取一个字符
#include <stdio.h>
int getchar(void);
putchar: 向标准输出写入一个字符
#include <stdio.h>
int putchar(int c);
示例: 字符转换
#include <stdio.h>
int main(void)
{
int c;
printf("输入小写字母: ");
c = getchar();
if (c >= 'a' && c <= 'z')
{
c = c - 32; // 转换为大写
printf("大写字母: ");
putchar(c);
putchar('\n');
}
return 0;
}
4.3.2 getchar读取一行
#include <stdio.h>
int main(void)
{
int c;
printf("输入一行文本:\n");
while ((c = getchar()) != '\n')
{
putchar(c);
}
putchar('\n');
return 0;
}
4.3.3 getchar与EOF
EOF(End Of File)是一个宏,值为-1,表示文件结束。
#include <stdio.h>
int main(void)
{
int c;
printf("输入内容(Ctrl+D结束):\n");
while ((c = getchar()) != EOF)
{
putchar(c);
}
return 0;
}
⚠️ 注意:
- Linux/Mac: Ctrl+D表示EOF
- Windows: Ctrl+Z表示EOF
4.4 字符串输入函数
4.4.1 gets函数(已废弃)
char *gets(char *s); // 危险!不检查缓冲区大小
⚠️ 危险: gets不检查缓冲区大小,容易造成缓冲区溢出,已被C11标准废弃!
// 危险示例
char buf[10];
gets(buf); // 如果输入超过9个字符,会造成缓冲区溢出!
4.4.2 fgets函数(推荐)
#include <stdio.h>
char *fgets(char *s, int size, FILE *stream);
特点:
- 安全,会限制读取长度
- 读取整行,包括换行符'\n'
- 遇到换行符或读满size-1个字符时停止
示例:
#include <stdio.h>
int main(void)
{
char name[50];
printf("输入姓名: ");
if (fgets(name, sizeof(name), stdin) != NULL)
{
printf("name = %s", name); // 包含换行符
// 去除换行符
size_t len = strlen(name);
if (len > 0 && name[len-1] == '\n')
{
name[len-1] = '\0';
}
printf("处理后: %s\n", name);
}
return 0;
}
4.4.3 getline函数(GNU扩展)
#include <stdio.h>
ssize_t getline(char **lineptr, size_t *n, FILE *stream);
特点:
- 自动分配内存
- 可以读取任意长度的行
- 使用后需要free释放内存
示例:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
char *line = NULL;
size_t len = 0;
ssize_t nread;
printf("输入一行文本: ");
nread = getline(&line, &len, stdin);
if (nread != -1)
{
printf("读取了 %zd 字符\n", nread);
printf("内容: %s", line);
free(line); // 释放内存
}
return 0;
}
4.4.4 三种函数对比
| 函数 | 安全性 | 内存管理 | 是否包含换行符 | 推荐度 |
|---|---|---|---|---|
| gets | 不安全 | 手动 | 否 | 已废弃 |
| fgets | 安全 | 手动 | 是 | 推荐 |
| getline | 安全 | 自动 | 是 | 推荐 |
4.5 输入输出缓冲区
4.5.1 缓冲区类型
C语言的I/O系统使用缓冲区来提高效率:
- 全缓冲: 缓冲区满时才刷新(文件I/O)
- 行缓冲: 遇到换行符时刷新(标准输出stdout)
- 无缓冲: 立即输出(标准错误stderr)
4.5.2 缓冲区刷新
示例: 观察缓冲区
#include <stdio.h>
#include <unistd.h>
int main(void)
{
printf("Hello"); // 没有换行符,数据在缓冲区
sleep(3); // 等待3秒
printf(" World\n"); // 遇到换行符,刷新缓冲区
return 0;
}
输出: 等待3秒后一次性输出"Hello World"
手动刷新缓冲区:
#include <stdio.h>
int main(void)
{
printf("Hello");
fflush(stdout); // 强制刷新标准输出缓冲区
sleep(3);
printf(" World\n");
return 0;
}
输出: 立即输出"Hello",3秒后输出" World"
4.5.3 清空输入缓冲区
错误的方法:
fflush(stdin); // 未定义行为!
正确的方法:
// 方法1: 读取到换行符
while (getchar() != '\n');
// 方法2: 读取到EOF或换行符
int c;
while ((c = getchar()) != '\n' && c != EOF);
示例: 清空缓冲区
#include <stdio.h>
void clear_input_buffer(void)
{
int c;
while ((c = getchar()) != '\n' && c != EOF);
}
int main(void)
{
int a;
char c;
printf("输入整数: ");
scanf("%d", &a);
clear_input_buffer(); // 清空缓冲区
printf("输入字符: ");
scanf("%c", &c);
printf("a=%d, c=%c\n", a, c);
return 0;
}
4.6 文件定位符
4.6.1 stdin、stdout、stderr
#include <stdio.h>
extern FILE *stdin; // 标准输入
extern FILE *stdout; // 标准输出
extern FILE *stderr; // 标准错误
示例: 使用stderr输出错误信息
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int a, b;
printf("输入两个整数: ");
if (scanf("%d%d", &a, &b) != 2)
{
fprintf(stderr, "错误: 输入格式不正确!\n");
exit(1);
}
printf("%d + %d = %d\n", a, b, a + b);
return 0;
}
stdout与stderr的区别:
# stdout和stderr都输出到屏幕
$ ./test
# 只重定向stdout到文件,stderr仍输出到屏幕
$ ./test > output.txt
# 同时重定向stdout和stderr
$ ./test > output.txt 2>&1
4.7 实践练习
练习1: 格式化输出表格
编写程序输出格式化的成绩表:
#include <stdio.h>
int main(void)
{
printf("%-10s %8s %8s %8s\n", "姓名", "语文", "数学", "英语");
printf("%-10s %8.1f %8.1f %8.1f\n", "张三", 89.5, 92.0, 88.5);
printf("%-10s %8.1f %8.1f %8.1f\n", "李四", 85.0, 90.5, 91.0);
printf("%-10s %8.1f %8.1f %8.1f\n", "王五", 92.5, 88.0, 89.5);
return 0;
}
练习2: 安全的字符串输入
编写程序安全地读取用户姓名:
#include <stdio.h>
#include <string.h>
int main(void)
{
char name[50];
printf("请输入姓名: ");
if (fgets(name, sizeof(name), stdin) != NULL)
{
// 去除换行符
size_t len = strlen(name);
if (len > 0 && name[len-1] == '\n')
{
name[len-1] = '\0';
}
printf("您好, %s!\n", name);
}
return 0;
}
练习3: 输入验证
编写程序,要求用户输入1-10之间的整数,直到输入正确为止:
#include <stdio.h>
int main(void)
{
int num;
int ret;
do
{
printf("请输入1-10之间的整数: ");
ret = scanf("%d", &num);
// 清空缓冲区
int c;
while ((c = getchar()) != '\n' && c != EOF);
if (ret != 1)
{
printf("错误: 输入格式不正确!\n");
continue;
}
if (num < 1 || num > 10)
{
printf("错误: 数字必须在1-10之间!\n");
}
} while (num < 1 || num > 10);
printf("输入正确: %d\n", num);
return 0;
}
4.8 常见错误
错误1: scanf忘记取地址符&
int a;
scanf("%d", a); // 错误!应该是 &a
错误2: 字符串使用&
char str[50];
scanf("%s", &str); // 错误!数组名本身就是地址
scanf("%s", str); // 正确
错误3: 混用不同类型
int a;
scanf("%f", &a); // 错误!int应该用%d
错误4: 缓冲区溢出
char buf[10];
scanf("%s", buf); // 危险!未限制输入长度
scanf("%9s", buf); // 正确!限制最多读取9个字符
错误5: 不检查scanf返回值
int a;
scanf("%d", &a); // 如果输入非数字,a的值未定义
// 正确做法
if (scanf("%d", &a) != 1)
{
fprintf(stderr, "输入错误!\n");
return 1;
}
4.9 小结
本章学习了:
printf格式化输出(格式符、宽度、精度控制) scanf格式化输入及常见陷阱 getchar/putchar字符I/O gets/fgets/getline字符串输入函数对比 输入输出缓冲区机制 标准I/O流(stdin、stdout、stderr)
重点提示:
- 禁用gets,使用fgets替代
- scanf读取字符串时务必限制长度
- 总是检查scanf的返回值
- 注意清空输入缓冲区,避免读取到残留字符
- 使用fprintf(stderr, ...)输出错误信息