欢迎光临
我们一直在努力

0基础学嵌入式--全网最详细Linux开发指南:一篇文章带你学懂I/O编程

        Linux哲学的核心是“一切皆文件”,而掌握C语言的文件I/O操作是系统编程的基石。今天,通过本文带你彻底搞懂文件操作的所有细节。

一、Linux哲学:一切皆文件

1.1 文件类型全解析

在Linux系统中,所有东西都是文件。这不仅仅是哲学概念,而是实实在在的系统设计:

// Linux通过文件描述符统一管理所有资源
int fd = open("/dev/tty", O_RDWR); // 字符设备
int fd2 = open("/dev/sda1", O_RDONLY); // 块设备
int socket_fd = socket(AF_INET, SOCK_STREAM, 0); // 套接字

7种文件类型详解:

类型标识

英文名称

中文含义

典型示例

Regular File

普通文件

.txt, .c, .jpg

d

Directory

目录文件

/home, /usr/bin

l

Link

链接文件

符号链接、硬链接

b

Block Device

块设备文件

/dev/sda(硬盘)

c

Character Device

字符设备文件

/dev/tty(终端)

s

Socket

套接字文件

网络通信接口

p

Pipe

管道文件

进程间通信

查看文件类型:

# 使用ls -l查看文件类型
ls -l /dev/tty
# 输出:crw-rw-rw- 1 root tty 5, 0 Jan 1 00:00 /dev/tty
# 第一个字符'c'表示字符设备文件

1.2 I/O接口分类

根据操作对象不同,I/O接口分为三类:

// 1. 标准I/O(有缓存) – 操作普通文件
#include <stdio.h>
FILE *fp = fopen("data.txt", "r"); // 标准I/O

// 2. 文件I/O(无缓存) – 操作设备和通信文件
#include <fcntl.h>
int fd = open("/dev/device", O_RDWR); // 文件I/O

// 3. 目录I/O – 操作目录文件
#include <dirent.h>
DIR *dir = opendir("/home");

二、标准I/O核心:流与缓存机制

2.1 标准I/O头文件

所有标准I/O操作都基于stdio.h头文件:

#include <stdio.h> // 必须包含的头文件

// 常用接口分类:
// 1. 打开关闭:fopen, fclose
// 2. 字符I/O:fgetc, fputc, getchar, putchar
// 3. 字符串I/O:fgets, fputs, gets, puts
// 4. 格式化I/O:fscanf, fprintf, scanf, printf
// 5. 二进制I/O:fread, fwrite
// 6. 文件定位:fseek, rewind, ftell

2.2 文件指针与默认流

文件指针FILE*是标准I/O的核心概念:

// 三个默认打开的流(无需手动打开)
extern FILE *stdin; // 标准输入(键盘) – 文件描述符0
extern FILE *stdout; // 标准输出(屏幕) – 文件描述符1
extern FILE *stderr; // 标准错误(屏幕) – 文件描述符2

// 使用示例
char ch = fgetc(stdin); // 从键盘读取字符
fprintf(stdout, "Hello\\n"); // 输出到屏幕
fprintf(stderr, "Error\\n"); // 输出错误信息

2.3 缓存机制深度解析

为什么需要缓存?

  • 减少系统调用次数

  • 提高I/O效率

  • 批量处理数据

三种缓存类型:

// 1. 全缓存(4096字节/4K)
// 适用于:普通文件
// 刷新条件:缓存区满、程序结束、fclose、fflush
setvbuf(fp, NULL, _IOFBF, 4096); // 设置全缓存

// 2. 行缓存(1024字节/1K)
// 适用于:终端设备(stdin, stdout)
// 刷新条件:遇到\\n、缓存区满、程序结束、fflush
setvbuf(stdout, NULL, _IOLBF, 1024); // 设置行缓存

// 3. 不缓存(0字节)
// 适用于:stderr(错误信息需要立即显示)
setvbuf(stderr, NULL, _IONBF, 0); // 不缓存

缓存对比表:

缓存类型

大小

适用场景

刷新条件

全缓存

4KB

普通文件

满、结束、fclose、fflush

行缓存

1KB

终端设备

\\n、满、结束、fflush

不缓存

0KB

stderr

立即刷新

三、文件操作三部曲

3.1 打开文件:fopen详解

FILE *fopen(const char *pathname, const char *mode);

打开模式完全指南:

模式

含义

文件不存在

文件存在

备注

"r"

只读

错误返回NULL

打开文件

最基本

"r+"

读写

错误返回NULL

打开文件

可读可写

"w"

只写

创建新文件

清空文件

小心数据丢失

"w+"

写读

创建新文件

清空文件

先写后读

"a"

追加写

创建新文件

追加到末尾

不会覆盖

"a+"

追加读写

创建新文件

追加到末尾

读从开头,写从末尾

示例代码:

// 安全打开文件
FILE *safe_fopen(const char *filename, const char *mode) {
FILE *fp = fopen(filename, mode);
if (fp == NULL) {
perror("fopen failed"); // 打印错误信息
exit(EXIT_FAILURE);
}
return fp;
}

// 使用示例
FILE *fp_read = safe_fopen("data.txt", "r");
FILE *fp_write = safe_fopen("output.txt", "w");
FILE *fp_append = safe_fopen("log.txt", "a");

3.2 关闭文件:fclose

int fclose(FILE *stream);

重要:fclose会自动刷新缓存区!

// 安全关闭文件
int safe_fclose(FILE **fp) {
if (*fp != NULL) {
if (fclose(*fp) == EOF) {
perror("fclose failed");
*fp = NULL;
return -1;
}
*fp = NULL; // 避免野指针
}
return 0;
}

// 使用示例
FILE *fp = fopen("test.txt", "w");
// … 文件操作 …
safe_fclose(&fp); // 安全关闭

四、字符与字符串I/O操作

4.1 字符I/O:fgetc和fputc

// 从流中读取一个字符
int fgetc(FILE *stream);
// 成功:返回读取字符的ASCII码(0-255)
// 失败/EOF:返回EOF(-1)

// 向流中写入一个字符
int fputc(int c, FILE *stream);
// 成功:返回写入字符的ASCII码
// 失败:返回EOF

实用技巧:

// 1. 复制文件(字符版)
void copy_file_char(const char *src, const char *dst) {
FILE *fp_src = fopen(src, "r");
FILE *fp_dst = fopen(dst, "w");
int ch;

while ((ch = fgetc(fp_src)) != EOF) {
fputc(ch, fp_dst);
}

fclose(fp_src);
fclose(fp_dst);
}

// 2. 与getchar/putchar的关系
// getchar() 等价于 fgetc(stdin)
// putchar(c) 等价于 fputc(c, stdout)

4.2 字符串I/O:fgets和fputs

// 从流中读取字符串
char *fgets(char *s, int size, FILE *stream);
// 参数:s-缓冲区,size-最大读取长度,stream-文件流
// 返回值:成功返回s,失败/EOF返回NULL

// 向流中写入字符串
int fputs(const char *s, FILE *stream);
// 返回值:成功返回非负数,失败返回EOF

重要区别:

// fgets vs gets
char buffer[100];

// gets – 危险!不检查缓冲区大小
gets(buffer); // 已废弃,可能缓冲区溢出

// fgets – 安全!指定最大长度
fgets(buffer, sizeof(buffer), stdin);
// fgets会读取换行符,gets不会

// fputs vs puts
char str[] = "Hello\\n";

fputs(str, stdout); // 不会自动添加换行符
puts(str); // 会自动添加换行符

示例:逐行处理文件

void process_file_line_by_line(const char *filename) {
FILE *fp = fopen(filename, "r");
char line[256];
int line_num = 0;

while (fgets(line, sizeof(line), fp) != NULL) {
line_num++;
// 移除末尾的换行符
line[strcspn(line, "\\n")] = '\\0';
printf("Line %d: %s\\n", line_num, line);
}

fclose(fp);
}

五、格式化I/O:强大的fprintf和fscanf

5.1 格式化输出:fprintf

int fprintf(FILE *stream, const char *format, …);
// 功能:向流中写入格式化字符串
// 返回值:成功返回打印字符数,失败返回负数

应用场景:

// 1. 写入格式化数据到文件
FILE *fp = fopen("data.txt", "w");
fprintf(fp, "Name: %s, Age: %d, Score: %.2f\\n",
"Alice", 25, 95.5);

// 2. 创建日志文件
void write_log(FILE *log_fp, const char *level,
const char *format, …) {
time_t now = time(NULL);
fprintf(log_fp, "[%s] %s",
ctime(&now), level);

va_list args;
va_start(args, format);
vfprintf(log_fp, format, args);
va_end(args);

fprintf(log_fp, "\\n");
fflush(log_fp); // 立即写入日志
}

5.2 格式化输入:fscanf

int fscanf(FILE *stream, const char *format, …);
// 功能:从流中读取格式化数据
// 返回值:成功返回匹配项数,失败/EOF返回EOF

注意事项:

// 1. 基本使用
FILE *fp = fopen("input.txt", "r");
int age;
char name[50];
float score;

// 读取格式化数据
int count = fscanf(fp, "%s %d %f", name, &age, &score);
// count表示成功匹配的参数个数

// 2. 安全读取 – 检查返回值
if (fscanf(fp, "%49s %d %f", name, &age, &score) == 3) {
printf("读取成功: %s %d %.2f\\n", name, age, score);
} else {
printf("读取失败或格式错误\\n");
}

// 3. 与scanf的关系
// scanf(format, …) 等价于 fscanf(stdin, format, …)

六、文件定位与二进制I/O

6.1 文件定位函数

// 移动文件位置指针
int fseek(FILE *stream, long offset, int whence);
// whence: SEEK_SET(开头), SEEK_CUR(当前), SEEK_END(末尾)

// 回到文件开头
void rewind(FILE *stream); // 等价于 fseek(fp, 0, SEEK_SET)

// 获取当前位置
long ftell(FILE *stream); // 返回当前位置(字节偏移)

示例:

// 获取文件大小
long get_file_size(const char *filename) {
FILE *fp = fopen(filename, "r");
fseek(fp, 0, SEEK_END); // 移动到文件末尾
long size = ftell(fp); // 获取当前位置(即文件大小)
rewind(fp); // 回到文件开头
fclose(fp);
return size;
}

// 随机访问文件
FILE *fp = fopen("data.bin", "rb");
fseek(fp, 100, SEEK_SET); // 移动到第100字节处
// 读取数据…

6.2 二进制I/O:fread和fwrite

// 二进制读取
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);

// 二进制写入
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);

结构体读写示例:

typedef struct {
int id;
char name[50];
float score;
} Student;

// 写入结构体数组
Student students[3] = {
{1, "Alice", 95.5},
{2, "Bob", 88.0},
{3, "Charlie", 92.5}
};

FILE *fp = fopen("students.dat", "wb");
fwrite(students, sizeof(Student), 3, fp);
fclose(fp);

// 读取结构体数组
Student read_students[3];
fp = fopen("students.dat", "rb");
fread(read_students, sizeof(Student), 3, fp);
fclose(fp);

七、实战:完整的文件操作示例

7.1 文本文件处理工具

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 统计文件行数、单词数、字符数
void wc_file(const char *filename) {
FILE *fp = fopen(filename, "r");
if (fp == NULL) {
perror("无法打开文件");
return;
}

int lines = 0, words = 0, chars = 0;
int in_word = 0; // 标记是否在单词中
int ch;

while ((ch = fgetc(fp)) != EOF) {
chars++;

if (ch == '\\n') {
lines++;
}

if (ch == ' ' || ch == '\\n' || ch == '\\t') {
in_word = 0;
} else if (!in_word) {
in_word = 1;
words++;
}
}

fclose(fp);

printf("文件: %s\\n", filename);
printf("行数: %d\\n", lines);
printf("单词数: %d\\n", words);
printf("字符数: %d\\n", chars);
}

7.2 简单的文本编辑器

// 简单的文本查看器
void text_viewer(const char *filename) {
FILE *fp = fopen(filename, "r");
if (fp == NULL) {
printf("无法打开文件: %s\\n", filename);
return;
}

char line[256];
int line_num = 1;

printf("=== 文件内容: %s ===\\n", filename);
while (fgets(line, sizeof(line), fp) != NULL) {
// 显示行号和内容
printf("%4d: %s", line_num, line);
line_num++;
}

fclose(fp);
}

// 文本搜索功能
void search_in_file(const char *filename, const char *keyword) {
FILE *fp = fopen(filename, "r");
char line[256];
int line_num = 1;
int found = 0;

printf("在文件 %s 中搜索: %s\\n", filename, keyword);
printf("================================\\n");

while (fgets(line, sizeof(line), fp) != NULL) {
if (strstr(line, keyword) != NULL) {
printf("第%d行: %s", line_num, line);
found++;
}
line_num++;
}

fclose(fp);
printf("找到 %d 处匹配\\n", found);
}

八、常见错误与调试技巧

8.1 常见错误处理

// 1. 忘记检查fopen返回值
FILE *fp = fopen("nonexistent.txt", "r");
// 错误:如果文件不存在,fp为NULL,后续操作会崩溃

// 正确做法
FILE *fp = fopen("data.txt", "r");
if (fp == NULL) {
perror("fopen failed"); // 自动添加错误信息
// 或者:printf("Error: %s\\n", strerror(errno));
return;
}

// 2. 文件打开模式错误
FILE *fp = fopen("readonly.txt", "w"); // 会清空文件!
// 先检查文件是否存在,再决定打开模式

// 3. 忘记关闭文件
// 错误:文件描述符泄漏
fopen("file.txt", "r");
// 程序结束前没有fclose

// 正确:确保文件关闭
FILE *fp = NULL;
fp = fopen("file.txt", "r");
// … 操作文件 …
if (fp != NULL) {
fclose(fp);
fp = NULL; // 避免野指针
}

8.2 调试技巧

// 1. 使用perror输出错误信息
FILE *fp = fopen("data.txt", "r");
if (fp == NULL) {
perror("fopen");
// 输出: fopen: No such file or directory
}

// 2. 检查文件位置
printf("当前位置: %ld\\n", ftell(fp));
fseek(fp, 0, SEEK_END);
printf("文件大小: %ld\\n", ftell(fp));
rewind(fp);

// 3. 查看文件状态
#include <sys/stat.h>
struct stat file_stat;
stat("data.txt", &file_stat);
printf("文件大小: %lld字节\\n", file_stat.st_size);
printf("最后修改: %s", ctime(&file_stat.st_mtime));

九、性能优化建议

9.1 减少I/O操作次数

// 不好:每次写入一个字符
for (int i = 0; i < 1000; i++) {
fputc('A', fp); // 1000次系统调用
}

// 好:批量写入
char buffer[1001];
memset(buffer, 'A', 1000);
buffer[1000] = '\\0';
fputs(buffer, fp); // 1次系统调用

// 更好:使用合适大小的缓存
setvbuf(fp, buffer, _IOFBF, BUFSIZ); // 使用标准缓存大小

9.2 选择合适的缓存类型

// 根据使用场景选择缓存
FILE *fp = fopen("log.txt", "a");

// 场景1:频繁写入小数据(日志)
setvbuf(fp, NULL, _IOLBF, BUFSIZ); // 行缓存,及时看到日志

// 场景2:大量数据写入(数据库文件)
setvbuf(fp, NULL, _IOFBF, 8192); // 8KB全缓存,提高效率

// 场景3:实时数据(传感器)
setvbuf(fp, NULL, _IONBF, 0); // 不缓存,立即写入

十、总结与学习建议

10.1 核心要点回顾

  • 一切皆文件:Linux将所有资源抽象为文件

  • 标准I/O vs 文件I/O:有缓存 vs 无缓存

  • 三种缓存:全缓存、行缓存、不缓存

  • 文件操作三部曲:打开 → 操作 → 关闭

  • 安全检查:始终检查返回值

  • 10.2 学习路线建议

  • 基础阶段:掌握fopen、fclose、fgetc、fputc

  • 进阶阶段:学习fgets、fputs、fprintf、fscanf

  • 高级阶段:掌握fread、fwrite、fseek、缓存控制

  • 实战阶段:实现文件工具(复制、统计、搜索)

  • 10.3 面试常见问题

  • 标准I/O和文件I/O的区别?

  • 什么是文件缓存?有什么作用?

  • fopen的"r"、"w"、"a"模式有什么区别?

  • 如何安全地读取一行文本?

  • 如何获取文件大小?


  • 立即动手!

    不要只看不练,现在开始:

  • 实现一个文件复制工具

  • 编写一个文本文件统计工具

  • 创建简单的日志系统

  • 尝试二进制文件读写

  • 记住:文件I/O是系统编程的基础,掌握它才能编写出健壮、高效的程序。


    互动问题:

    你在文件操作中遇到过什么问题?

    有没有特别实用的文件处理技巧?

    在评论区分享你的经验,我们一起进步!

    赞(0)
    未经允许不得转载:171主机测评 » 0基础学嵌入式--全网最详细Linux开发指南:一篇文章带你学懂I/O编程
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址