一、IO 核心概念:理解 "一切皆文件"
在 Linux 中,IO 操作的本质是对 "文件" 的操作 ------ 这里的 "文件" 不仅包括我们日常接触的文本 / 二进制文件,还涵盖了设备(键盘、鼠标、磁盘)、通信对象(管道、套接字)等。所有这些 "文件" 都通过统一的文件描述符或流进行管理,实现了 "屏蔽底层差异,统一操作接口" 的目标。
1.1 常见文件类型分类
不同类型的 "文件" 对应不同的 IO 场景,其标识和用途如下表所示:
| 文件标识 | 类型名称 | 核心用途 | 典型示例 | |
|---|---|---|---|---|
| b | 块设备文件 | 按 "块" 读写,用于存储设备 | 硬盘、U 盘、分区 | |
| c | 字符设备文件 | 按 "字符" 读写,用于交互设备 | 键盘、鼠标、终端(tty) | |
| d | 目录文件 | 存储文件 / 子目录的索引信息 | /home、/etc 目录 | |
| - | 普通文件 | 存储用户数据 | 文本文件(.txt)、二进制文件(.exe) | |
| l | 链接文件 | 指向其他文件的 "快捷方式" | 软链接(ln -s 创建) | |
| s | 套接字文件 | 进程间网络通信 | 网络服务端 / 客户端通信 | |
| p | 管道文件 | 本地进程间通信 | 匿名管道( | )、命名管道 |
二、IO 接口分类:按需选择合适的操作方式
根据操作对象的不同,Linux IO 接口分为三类,核心差异在于是否有缓存 和适用场景:
| 接口类型 | 操作对象 | 缓存特性 | 核心用途 |
|---|---|---|---|
| 标准 IO | 普通文件(文本 / 二进制) | 有缓存(高效) | 日常文件读写(如配置文件、日志) |
| 文件 IO | 设备文件、通信文件(管道) | 无缓存(实时) | 硬件操作、实时数据交互 |
| 目录 IO | 目录文件 | 无缓存 | 目录创建、删除、遍历(如 ls 功能) |
三、标准 IO 详解:从基础到核心接口
标准 IO 由 C 标准库(<stdio.h>)提供,基于 "流(FILE*)" 实现对文件的操作,支持缓存管理、格式化读写等功能,适用于绝大多数普通文件场景。
3.1 标准 IO 的核心前提
1. 头文件
使用标准 IO 必须包含头文件:
#include <stdio.h>
2. 普通文件的两种形式
- ASCII 码文件 :内容由可显示的 ASCII 字符组成(如代码、文本),可通过
cat直接查看; - 二进制文件:内容是二进制数据(如图片、音视频、压缩包),直接查看会显示乱码;
- 注意:ASCII 码文件是特殊的二进制文件(仅包含 0~127 的 ASCII 值)。
3. 默认打开的 3 个流
程序启动时,系统会自动打开 3 个标准流,无需手动fopen:
stdin:标准输入流(对应键盘),行缓存;stdout:标准输出流(对应终端),行缓存;stderr:标准错误流(对应终端),无缓存(错误信息需实时输出)。
3.2 标准 IO 的缓存机制
缓存是标准 IO 的核心优化 ------ 通过在内存中开辟缓存区,批量读写数据,减少与磁盘 / 终端的直接交互次数。标准 IO 的缓存分为三类:
| 缓存类型 | 缓存大小 | 刷新条件(数据写入目标) | 适用场景 |
|---|---|---|---|
| 全缓存 | 4KB(默认) | 1. 缓存区满;2. 调用fclose/ 程序结束;3. 手动fflush |
普通文件(如.log) |
| 行缓存 | 1KB(默认) | 1. 缓存区满;2. 遇到\n;3. 调用fclose/ 程序结束;4. 手动fflush |
终端交互(stdin/stdout) |
| 不缓存 | 0KB | 无缓存,数据直接写入目标 | 错误输出(stderr) |
示例 :用printf("Hello")打印时,若未加\n,stdout(行缓存)不会立即输出;加上\n或调用fflush(stdout),数据才会显示到终端。
3.3 标准 IO 核心函数接口
标准 IO 提供了一套完整的文件操作函数,从 "打开 - 读写 - 关闭" 到 "定位 - 刷新",覆盖所有常见场景。以下是最常用的函数详解:
1. 文件打开:fopen
- 原型 :
FILE *fopen(const char *pathname, const char *mode); - 功能:打开指定路径的文件,并创建一个 "流(FILE*)" 用于后续操作;
- 参数 :
pathname:文件路径(如./test.txt、/home/user/data.bin);mode:打开模式(决定读写权限和文件不存在时的行为),常见模式如下:
| mode | 读写权限 | 文件不存在时 | 文件存在时 | 适用场景 |
|---|---|---|---|---|
| r | 只读 | 报错(NULL) | 打开文件 | 读取配置文件 |
| r+ | 读写 | 报错(NULL) | 打开文件(不清空) | 读写已存在的文件 |
| w | 只写 | 创建文件 | 清空文件内容 | 新建日志文件(覆盖旧内容) |
| w+ | 读写 | 创建文件 | 清空文件内容 | 新建并读写文件 |
| a | 追加只写 | 创建文件 | 指针定位到文件尾 | 追加日志(不覆盖旧内容) |
| a+ | 追加读写 | 创建文件 | 指针定位到文件尾 | 追加并读取日志 |
- 返回值 :成功返回
FILE*流指针;失败返回NULL(需用perror查看错误原因)。
2. 文件关闭:fclose
- 原型 :
int fclose(FILE *stream); - 功能 :关闭流,释放缓存和文件资源(必须调用,避免内存泄漏和数据丢失);
- 参数 :
stream:fopen返回的流指针; - 返回值 :成功返回 0;失败返回
EOF(-1)。
3. 字符读写:fgetc / fputc
适用于逐字符读写(如统计文件字符数)。
-
fgetc(读字符):- 原型:
int fgetc(FILE *stream); - 功能:从流中读取一个字符(返回 ASCII 码值);
- 返回值:成功返回字符 ASCII 码;失败 / 文件末尾返回
EOF(-1); - 等价:
getchar()≡fgetc(stdin)(从键盘读字符)。
- 原型:
-
fputc(写字符):- 原型:
int fputc(int c, FILE *stream); - 功能:将字符
c(ASCII 码)写入流; - 返回值:成功返回字符
c;失败返回EOF; - 等价:
putchar(c)≡fputc(c, stdout)(向终端写字符)。
- 原型:
4. 字符串读写:fgets / fputs
适用于逐行读写(如读取配置文件的行内容)。
-
fgets(读字符串):-
原型:
char *fgets(char *s, int size, FILE *stream); -
功能:从流中读取最多
size-1个字符(留 1 个位置存\0),遇到\n或EOF停止; -
关键特性:会保留输入中的
\n(若一行未读完,下次继续读); -
示例(模拟
gets功能,去掉\n):char buf[128]; fgets(buf, sizeof(buf), stdin); buf[strlen(buf)-1] = '\0'; // 替换末尾的\n为字符串结束符
-
-
fputs(写字符串):- 原型:
int fputs(const char *s, FILE *stream); - 功能:将字符串
s写入流(不自动添加\n); - 对比:
puts(s)会自动在末尾加\n,而fputs不会。
- 原型:
5. 格式化读写:fscanf / fprintf
适用于结构化数据读写(如读写 "姓名 + 年龄" 这类格式化信息)。
-
fprintf(格式化写):-
原型:
int fprintf(FILE *stream, const char *format, ...); -
功能:按
format格式将数据写入流(类似printf,但输出到文件而非终端); -
示例:向
test.txt写入用户信息:FILE *fp = fopen("test.txt", "w"); fprintf(fp, "Name: %s, Age: %d\n", "ZhangSan", 20); // 写入文件 fclose(fp);
-
-
fscanf(格式化读):-
原型:
int fscanf(FILE *stream, const char *format, ...); -
功能:按
format格式从流中读取数据(类似scanf,但输入来自文件而非键盘); -
返回值:成功返回匹配的参数个数;失败 / EOF 返回
EOF; -
示例:从
test.txt读取用户信息:char name[32]; int age; FILE *fp = fopen("test.txt", "r"); fscanf(fp, "Name: %s, Age: %d", name, &age); // 读取数据 printf("Read: %s, %d\n", name, age); // 输出:ZhangSan, 20 fclose(fp);
-
6. 文件定位:fseek / rewind / ftell
用于调整文件读写指针的位置(如 "跳转到文件第 100 字节处读写")。
-
fseek:设置指针位置,原型:int fseek(FILE *stream, long offset, int whence);offset:偏移量(正数向后移,负数向前移);whence:基准位置:SEEK_SET(文件开头)、SEEK_CUR(当前位置)、SEEK_END(文件末尾);- 示例:跳转到文件开头向后 10 字节处:
fseek(fp, 10, SEEK_SET);
-
rewind:将指针重置到文件开头,等价于fseek(fp, 0, SEEK_SET); -
ftell:获取当前指针相对于文件开头的偏移量(字节数),原型:long ftell(FILE *stream);
四、标准 IO 实践案例
案例 1:文件拷贝(将 A 文件内容复制到 B 文件)
需求:从终端接收两个文件路径(源文件 A、目标文件 B),将 A 的内容拷贝到 B。
#include <stdio.h>
#include <string.h>
int main() {
char src_path[128] = {0}; // 源文件路径
char dest_path[128] = {0}; // 目标文件路径
FILE *src_fp = NULL, *dest_fp = NULL;
int ch;
// 1. 从终端获取文件路径
printf("请输入源文件路径:");
fgets(src_path, sizeof(src_path), stdin);
src_path[strlen(src_path)-1] = '\0'; // 去掉fgets保留的\n
printf("请输入目标文件路径:");
fgets(dest_path, sizeof(dest_path), stdin);
dest_path[strlen(dest_path)-1] = '\0';
// 2. 打开源文件(只读)和目标文件(只写,不存在则创建)
src_fp = fopen(src_path, "r");
if (src_fp == NULL) {
perror("打开源文件失败");
return -1;
}
dest_fp = fopen(dest_path, "w");
if (dest_fp == NULL) {
perror("打开目标文件失败");
fclose(src_fp); // 避免内存泄漏
return -1;
}
// 3. 逐字符拷贝:读源文件→写目标文件
while ((ch = fgetc(src_fp)) != EOF) {
fputc(ch, dest_fp);
}
// 4. 关闭文件
fclose(src_fp);
fclose(dest_fp);
printf("文件拷贝完成!\n");
return 0;
}
案例 2:统计文件中出现次数最多的字符
需求:从终端接收文件路径,统计文件中所有 ASCII 字符的出现次数,并输出出现最多的字符及其次数。
#include <stdio.h>
#include <string.h>
int main() {
char file_path[128] = {0};
FILE *fp = NULL;
int ch;
int count[128] = {0}; // 索引0~127对应ASCII码,值为出现次数
int max_count = 0;
char max_char = 0;
int i;
// 1. 获取文件路径
printf("请输入文件路径:");
fgets(file_path, sizeof(file_path), stdin);
file_path[strlen(file_path)-1] = '\0';
// 2. 打开文件
fp = fopen(file_path, "r");
if (fp == NULL) {
perror("打开文件失败");
return -1;
}
// 3. 统计字符出现次数
while ((ch = fgetc(fp)) != EOF) {
if (ch >= 0 && ch < 128) { // 只统计标准ASCII字符
count[ch]++;
}
}
// 4. 找到出现次数最多的字符
for (i = 0; i < 128; i++) {
if (count[i] > max_count) {
max_count = count[i];
max_char = (char)i;
}
}
// 5. 输出结果
printf("出现次数最多的字符:'%c'(ASCII:%d)\n", max_char, (int)max_char);
printf("出现次数:%d\n", max_count);
// 6. 关闭文件
fclose(fp);
return 0;
}