☰
Linux嵌入式学习——标准IO与文件IO
2026/10/29 2:07:33 网站建设 项目流程

一、标准 IO(C 库函数,带缓存)

1. 基本概念

输入输出是以内存为参考点。数据进入内存叫输入,离开内存叫输出。
标准 IO 是 C 库中关于文件操作的函数集合,通常自带缓存区,跨平台性好。

2. 核心函数

FILE *fopen(const char *path, const char *mode)
功能:打开文件并建立流,返回 FILE* 指针。
参数 mode 拓展解析:
r:只读模式。文件必须存在,否则报错。
r+:读写模式。文件必须存在,否则报错。
w:只写模式。文件不存在则创建;文件存在则清空(截断为0)。
w+:读写模式。文件不存在则创建;文件存在则清空。
a:追加只写模式。文件不存在则创建;文件存在则在末尾追加。
a+:追加读写模式。文件不存在则创建;文件存在则在末尾追加。读指针初始在开头,写操作永远在末尾。

int fputc(int c, FILE *stream)
功能:向流中写入一个字符。
参数 c:要写入的字符(以 int 形式传递 ASCII 码值)。

int fgetc(FILE *stream)
功能:从流中读取下一个字符。
返回值:成功返回字符 ASCII 码(转为 int),读到文件末尾或失败返回 EOF (-1)。

int fputs(const char *s, FILE *stream)
功能:将字符串 s 写入流中,不包含末尾的 '\0'。

char *fgets(char *s, int size, FILE *stream)
功能:从流中读取字符串。
参数 size 解析:最多读取 size - 1 个字符,或者遇到换行符 '\n' 停止。读取的内容会自动在末尾补 '\0'。

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream)
功能:按数据块读取。
参数 size 与 nmemb 解析:总共读取 nmemb 个数据块,每个数据块大小为 size 字节。总读取字节数为 size * nmemb。返回值为实际读取的块数(不是字节数)。

size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)
功能:按数据块写入。
参数 size 与 nmemb 解析:总共写入 nmemb 个数据块,每个数据块大小为 size 字节。返回值为实际写入的块数。

int fseek(FILE *stream, long offset, int whence)
功能:移动文件指针。
参数 whence 拓展解析:
SEEK_SET:从文件开头开始偏移。
SEEK_CUR:从当前文件指针位置开始偏移。
SEEK_END:从文件末尾开始偏移。

long ftell(FILE *stream)
功能:返回当前文件指针相对于文件开头的字节偏移量。

void rewind(FILE *stream)
功能:将文件指针复位到文件开头(等价于 fseek(stream, 0, SEEK_SET))。

3. 标准 IO 的缓存机制与类型

全缓存:普通文件,缓冲区满才写入磁盘(通常 4096 字节)。
行缓存:终端设备,遇到 '\n' 或者缓冲区满时写入(通常 1024 字节)。
无缓存:标准错误 stderr,直接输出。
int fflush(FILE *stream)
功能:强制刷新缓冲区,将缓冲区的数据立即写回内核。

int fileno(FILE *stream)
功能:获取标准 IO 流底层的文件描述符(fd)。
FILE *fdopen(int fd, const char *mode)
功能:将一个已有的文件描述符转换为标准 IO 流(FILE*)。

4. 综合示例

#include <stdio.h> #include <string.h> #include <stdlib.h> typedef struct { char name[64]; int age; char address[100]; char phone[20]; } PER; // ========================================== // 1. 字符与字符串读写 (fputc, fgetc, fputs, fgets) // ========================================== void test_char_string_io() { // [笔记] fopen mode "w": 文件不存在则创建,存在则清0只写。 FILE* fp = fopen("1.txt", "w"); if (NULL == fp) { perror("fopen error"); return; } fputc('o', fp); fputc('k', fp); fputs("how areyou\n", fp); // [注意] 一个汉字通常占3字节,这里将 utf-8 字符串写入文件 char buf[256] = "你好\n"; fputs(buf, fp); fclose(fp); // [笔记] fopen mode "r": 文件不存在报错,存在则只读。 fp = fopen("1.txt", "r"); if (NULL == fp) { perror("fopen error"); return; } printf("--- 单字符读取 fgetc 演示 ---\n"); while (1) { int c = fgetc(fp); if (EOF == c) break; // End of file printf("%c", c); } // [重点] 测试 fgets 读取,注意大小限制,并且会读取换行符 rewind(fp); // [笔记] rewind 把文件读写位置复位到开始的地方 printf("\n--- 字符串读取 fgets 演示 ---\n"); while (1) { char s[256] = {0}; // [笔记] fgets 从流中获取 size 大小字节,读到换行或末尾停止 char *ret = fgets(s, sizeof(s), fp); if (NULL == ret) break; printf("%s", s); } fclose(fp); } // ========================================== // 2. 数据块读写与定位 (fread, fwrite, fseek, ftell) // ========================================== void test_block_io() { PER per = {"张三", 20, "北京市", "13800000000"}; FILE* fp = fopen("1.bin", "w"); if (NULL == fp) return; // [笔记] fwrite: 从 ptr 取出 nmemb 个大小为 size 的数据块写入 stream fwrite(&per, sizeof(per), 1, fp); fclose(fp); fp = fopen("1.bin", "r"); if (NULL == fp) return; PER read_per; // [笔记] fread: 从 stream 获取 nmemb 个大小为 size 的数据块到 ptr fread(&read_per, sizeof(read_per), 1, fp); printf("name:%s age:%d addr:%s phone:%s\n", read_per.name, read_per.age, read_per.address, read_per.phone); // [笔记] fseek: 将 stream 文件指针从 whence 位置偏移 offset 字节 fseek(fp, 0, SEEK_END); // [笔记] ftell: 获取当前文件流指针的具体位置,一般用于算文件大小 long size = ftell(fp); printf("1.bin file size is %ld\n", size); fclose(fp); } // ========================================== // 3. 命令行参数与文本文件处理 (argc, argv) // ========================================== void test_cmd_args(int argc, char **argv) { // 模拟命令行:./a.out count_lite if (argc < 2) { printf("usage:./a.out [filename]\n"); return; } // [代码整合] 对应 08count_lite.c (统计小写字母个数) 和 09line_num.c (统计行数) FILE* fp = fopen(argv[1], "r"); if (NULL == fp) { perror("fopen error"); return; } int num[26] = {0}; int line_num = 0; while (1) { char buf[256] = {0}; if (NULL == fgets(buf, sizeof(buf), fp)) break; // 统计行数 if ('\n' == buf[strlen(buf) - 1]) line_num++; // 统计字母 for (int i = 0; buf[i] != '\0'; i++) { if (buf[i] >= 'a' && buf[i] <= 'z') { num[buf[i] - 'a']++; } } } fclose(fp); printf("line_num is %d\n", line_num); for (int i = 0; i < 26; i++) { printf("%c:%d\n", i + 'a', num[i]); } } int main(int argc, char **argv) { test_char_string_io(); test_block_io(); if (argc >= 2) { test_cmd_args(argc, argv); } return 0; }


二、文件 IO(系统调用,无缓存)

1. 基本概念

系统调用是 Linux 系统直接提供的接口。没有缓存区,适合操作设备文件。
操作对象是文件描述符(非负整数,通常为 0-1023)。系统默认打开 0(标准输入)、1(标准输出)、2(标准错误)。

2. 核心函数

int open(const char *pathname, int flags, mode_t mode)
功能:获得文件描述符。
参数 flags 拓展解析(必须包含以下三种之一):
O_RDONLY:只读打开。
O_WRONLY:只写打开。
O_RDWR:读写打开。
参数 flags 扩展标志(可通过 | 组合):
O_CREAT:若文件不存在则创建。此时必须提供第三个参数 mode(如 0666)。
O_TRUNC:若文件已存在且以写或读写方式打开,则将其长度清空为 0。
O_APPEND:每次写入操作前,文件指针自动移到文件末尾。
O_EXCL:与 O_CREAT 结合使用,若文件已存在则 open 失败(用于确保创建的是新文件)。

ssize_t read(int fd, void *buf, size_t count)
功能:从文件描述符读取数据到内存。
参数 count 解析:请求读取的最大字节数。实际读取的字节数可能小于 count(如遇到文件末尾)。
返回值:成功返回实际读到的字节数;文件末尾返回 0;失败返回 -1。

ssize_t write(int fd, const void *buf, size_t count)
功能:将内存数据写入文件描述符。
参数 count 解析:请求写入的字节数。
返回值:成功返回实际写入的字节数;失败返回 -1。

off_t lseek(int fd, off_t offset, int whence)
功能:定位文件读写指针。参数 whence 同 fseek。
返回值:成功返回当前指针相对于文件开头的偏移量;失败返回 -1。

int close(int fd)
功能:关闭文件描述符,释放系统资源。

3. errno 与常见错误码

机制:errno 是全局变量。系统调用失败(通常返回 -1)时,内核会自动修改它的值。成功的系统调用不会将其清零。
常见错误码:
ENOENT (2):No such file or directory(文件不存在)。
EACCES (13):Permission denied(权限不足)。
EEXIST (17):File exists(文件已存在,配合 O_EXCL 使用)。
EISDIR (21):Is a directory(试图对目录进行写操作)。
ENOTDIR (20):Not a directory(试图把文件当目录操作)。
EMFILE (24):Too many open files(当前进程打开的 fd 达到了上限 1024)。

4. 综合示例

#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> // unix standard #include <string.h> #include <stdlib.h> // [笔记] 标准IO 文件IO区别: // 1. 文件IO没有缓存区,适合操作设备文件 // 2. 操作对象不是 FILE* 而是文件描述符 (很小的非负整数 0-1023) // 0->STDIN 1->STDOUT 2->STDERR void test_basic_file_io() { // [笔记] open flags: O_WRONLY(只写) | O_CREAT(创建) | O_TRUNC(清空) // mode: 0666 权限 int fd = open("io_test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666); if (-1 == fd) { perror("open error"); return; } char buf[256] = "hello, are you ok?"; // [笔记] write: 通过文件描述符向文件中写数据 write(fd, buf, strlen(buf)); close(fd); fd = open("io_test.txt", O_RDONLY); if (-1 == fd) return; // [笔记] read: 返回值 成功返回读到数据的个数, 失败-1, 读到文件结尾返回0 while (1) { char read_buf[256] = {0}; int ret = read(fd, read_buf, sizeof(read_buf)); if (ret <= 0) break; printf("ret is %d, buf:[%s]\n", ret, read_buf); } // [笔记] lseek: 定位光标位置 (SEEK_SET, SEEK_CUR, SEEK_END) off_t off = lseek(fd, 0, SEEK_END); printf("file size by lseek is %ld\n", off); lseek(fd, 0, SEEK_SET); char c = 0; read(fd, &c, 1); printf("first char: %c\n", c); close(fd); } // [代码整合] 对应 05cp.c (基于文件IO实现复制) void test_cp() { int srcfd = open("io_test.txt", O_RDONLY); int dstfd = open("io_test_copy.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666); if (-1 == srcfd || -1 == dstfd) return; while (1) { char buf[4096] = {0}; int ret = read(srcfd, buf, sizeof(buf)); if (ret <= 0) break; write(dstfd, buf, ret); // 注意写入的字节数是实际读到的 ret } close(srcfd); close(dstfd); printf("File copied.\n"); } // [代码整合] 对应 06insert.c (中间插入数据) void test_insert() { int pos = 6; char insert_buf[10] = "ok"; // [笔记] O_RDWR 读写模式 int fd = open("io_test.txt", O_RDWR); if (-1 == fd) return; off_t size = lseek(fd, 0, SEEK_END); // 分配足够空间存放插入点之后的数据 char *data = malloc(size - pos); if (NULL == data) return; lseek(fd, pos, SEEK_SET); int ret = read(fd, data, size - pos); // 读出后半段 lseek(fd, pos, SEEK_SET); write(fd, insert_buf, strlen(insert_buf)); // 写入插入内容 write(fd, data, ret); // 把后半段追加回来 free(data); close(fd); } int main() { test_basic_file_io(); test_cp(); test_insert(); return 0; }

三、目录操作与文件属性

1. 目录遍历

DIR *opendir(const char *name)
功能:打开目录,返回目录流指针。

struct dirent *readdir(DIR *dirp)
功能:读取目录中的下一个目录项。
参数返回解析:返回的 dirent 结构体中,d_name 为文件名,d_type 为文件类型(DT_DIR 目录,DT_REG 普通文件,DT_BLK 块设备等)。

int closedir(DIR *dirp)
功能:关闭目录流。

2. 路径与目录管理

int chdir(const char *path)
功能:改变当前工作目录。

char *getcwd(char *buf, size_t size)
功能:获取当前绝对工作路径,存入 buf 中。

int mkdir(const char *pathname, mode_t mode)
功能:创建新目录。mode 参数如 0777。
目录权限 rwx 拓展解析:
r:可以读取目录内的文件列表(ls)。
w:可以在目录内创建、删除、重命名文件(touch、rm)。
x:可以进入该目录(cd),或者访问目录内的文件。

int rmdir(const char *pathname)
功能:删除目录。注意:被操作的目录必须为空目录。

int remove(const char *pathname)
功能:删除文件或空目录(比 rmdir 更通用)。

int rename(const char *oldpath, const char *newpath)
功能:重命名文件或目录,也可用于移动文件。

int access(const char *pathname, int mode)
功能:检查文件是否存在或具备某种权限。
参数 mode 解析:F_OK(存在性)、R_OK(读权限)、W_OK(写权限)、X_OK(执行权限)。

3. 属性获取

int stat(const char *path, struct stat *buf)
功能:获取文件属性。如果文件是软链接,会追踪到目标文件。
参数返回解析(struct stat 核心成员):
st_size:文件大小(字节)。
st_mode:文件类型和权限。可用宏 S_ISREG() 判断普通文件,S_ISDIR() 判断目录,S_ISLNK() 判断软链接等。权限位可用 S_IRUSR、S_IWUSR 等宏进行按位与(&)判断。
st_nlink:硬链接数。
st_mtim:最后修改时间(Modify)。
st_atim:最后访问时间(Access)。
st_ctim:状态改变时间(Change,如修改权限)。
int lstat(const char *path, struct stat *buf)
功能:与 stat 相同,但如果文件是软链接,获取的是软链接本身的属性,不追踪目标。

4. umask 与权限掩码

八进制法则:rwx 对应 4-2-1。0666 (rw-rw-rw-),0777 (rwxrwxrwx),0644 (rw-r--r--)。
umask 机制:系统默认掩码(通常 0022)。创建文件时,实际权限 = mode & ~umask。例如 open 传 0666,经 0022 掩码后,实际为 0644。


四、时间与错误处理

1. 时间相关

time_t time(time_t *t)
功能:获取从 1970-01-01 00:00:00 到现在的秒数(Unix 时间戳)。

struct tm *localtime(const time_t *timep)
功能:将秒数转化为本地日历时间。
参数返回解析:struct tm 中 tm_year 需加 1900 才是真实年份,tm_mon 需加 1 才是真实月份。

char *ctime(const time_t *timep)
功能:将秒数直接转化为可读的日期字符串。

2. 错误处理

perror(const char *s)
功能:打印 s 字符串加上 errno 对应的系统错误信息到标准错误输出(stderr)。

char *strerror(int errnum)
功能:传入错误码,返回对应的错误描述字符串。

3. 二进制文件解析与内存对齐

#pragma pack(1) 或 #pragma pack(push, 1)
功能:强制结构体按 1 字节对齐。
作用:在解析 BMP 等二进制文件头时,防止编译器默认的字节填充(Padding)导致读取的数据错位。

4. 综合示例(三、四)

#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <dirent.h> #include <time.h> #include <errno.h> #include <string.h> void test_dir_ops() { // [笔记] opendir: 打开目录获得目录流指针(DIR*),表示整个目录信息 DIR *dir = opendir("."); if (NULL == dir) { perror("opendir error"); return; } // [笔记] readdir: 从目录流中读取文件信息(dirent 结构体) while (1) { struct dirent *info = readdir(dir); if (NULL == info) break; // [笔记] d_type 判断文件类型 switch (info->d_type) { case DT_DIR: printf("目录文件:"); break; case DT_REG: printf("普通文件:"); break; case DT_BLK: printf("块设备:"); break; default: printf("其他类型:"); } printf("%s\n", info->d_name); } // [笔记] closedir: 关闭目录流 closedir(dir); } void test_path_ops() { char buf[512] = {0}; // [笔记] getcwd: 获得当前工作路径 getcwd(buf, sizeof(buf)); printf("before cd :%s\n", buf); // [笔记] chdir: 切换当前工作路径 int ret = chdir("../"); if (-1 == ret) { perror("chdir error"); } getcwd(buf, sizeof(buf)); printf("after cd :%s\n", buf); // [笔记] mkdir: 新建目录 (0777 权限) mkdir("test_mkdir_123", 0777); // [笔记] rmdir: 删除目录 (必须为空目录) rmdir("test_mkdir_123"); } void test_stat_time_err() { struct stat st; // [笔记] stat: 获取文件属性,最常用成员 st_size 表示大小(字节) int ret = stat("./01ls.c", &st); // 如果没有这个文件,会报错 if (-1 == ret) { // [笔记] perror: 打印 s 字符串和 errno 对应的错误信息到 stderr perror("stat"); // [笔记] strerror: 将错误码 errno 转为字符串 printf("errno:%d errmsg:%s\n", errno, strerror(errno)); } else { printf("size: %lu bytes\n", st.st_size); } // [笔记] time: 记录 Unix 时间戳 (从1970-1-1 0:0:0开始的秒数) time_t tm = time(NULL); printf("timestamp: %lu\n", tm); // [笔记] localtime: 将秒数转化为日历时间(struct tm) struct tm* tminfo = localtime(&tm); printf("%d-%02d-%02d %02d:%02d:%02d\n", tminfo->tm_year + 1900, tminfo->tm_mon + 1, tminfo->tm_mday, tminfo->tm_hour, tminfo->tm_min, tminfo->tm_sec); // [笔记] ctime: 将时间秒数直接转化为字符串 (类似 date 命令) char *data = ctime(&tm); printf("%s", data); } int main() { test_dir_ops(); test_path_ops(); test_stat_time_err(); return 0; }

五、易错点总结

1.fgets 会保留行末的换行符 '\n'(如果缓冲区没满且读到了换行),处理字符串时经常需要手动将 '\n' 替换为 '\0'。
2. read 系统调用不会自动在读取内容的末尾补 '\0'。如果读取的是文本,必须手动将 buf[ret] 置为 '\0' 才能用 printf 打印。
3. stat 解析文件权限时,绝对不能使用 == 比较(如 if(st.st_mode == S_IRUSR) 错误),必须使用按位与&(如 if(st.st_mode & S_IRUSR))。
4. 对结构体直接进行 fread/fwrite 时,必须注意内存对齐(padding)问题,必要时使用 #pragma pack(1) 取消对齐,否则跨平台或不同编译器读取会错位。
5. 使用 open 创建文件时(O_CREAT),必须提供第三个参数 mode(如 0666),否则系统会给一个随机或错误的权限。
6. 文件 IO 和标准 IO 不要混用。同一个文件同时用 FILE* 和 fd 操作,会导致缓存区和内核指针不同步。如果需要混用,应先用 fflush 刷新缓冲区,或通过 fileno/fdopen 进行转换。
7. 系统调用成功时不会自动把 errno 清零。排查错误时,不要依赖 errno 是否为 0 来判断上一次调用是否成功,而应直接判断函数的返回值。
8. umask 会影响最终创建文件的权限。如果发现 open 传入的 mode 和最终文件权限不符,检查当前终端的 umask 值(终端输入 umask 命令查看)。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询