
在 Linux 系统编程领域,文件操作是开发者必须掌握的核心技能。C 语言标准库提供了一套完善的文件操作函数,这些函数在底层系统调用的基础上进行了封装,既简化了编程复杂度,又保证了良好的跨平台兼容性。本文将系统讲解 Linux 环境下使用 C 库函数进行文件操作的全流程,涵盖文件的创建、打开、读写和关闭等关键环节。
在深入函数细节之前,有必要先理解 Linux 文件系统的基本特性和 C 库文件操作的设计思想。这些基础概念将帮助我们更好地理解后续函数的工作原理。
Linux 系统遵循 "一切皆文件" 的设计哲学,无论是普通文本文件、二进制文件,还是设备、管道、网络套接字等,都通过统一的文件接口进行操作。这种抽象使得文件操作具有极高的一致性,也为 C 库函数的封装提供了便利。
C 库文件操作的核心是文件流(FILE) 结构体,它是对底层文件描述符的封装,包含了缓冲区状态、文件位置指针、错误标志等关键信息。程序员通过文件指针(FILE*) 与文件流交互,无需直接操作底层细节。这种封装带来两个显著优势:一是通过用户空间缓冲区减少系统调用次数,大幅提升 IO 效率;二是隐藏了不同系统的底层差异,保证了代码的可移植性。
文件操作的基本流程遵循 "打开 - 操作 - 关闭" 的经典模式。在使用文件前必须先通过函数获取文件指针,所有操作都通过该指针完成,操作结束后必须关闭文件以释放资源。特别需要注意的是,Linux 系统对进程可打开的文件数量存在限制(默认通常为 1024),未正确关闭的文件会导致文件描述符泄露,最终引发 "too many open files" 错误。
创建和打开文件是所有文件操作的起点。C 库中最常用的函数是fopen(),它承担了文件创建、打开和初始化缓冲区的全部工作。
函数原型:
FILE *fopen(const char *path, const char *mode);参数解析:
模式 | 含义 | 文本 / 二进制 | 若文件不存在 | 写入行为 |
|---|---|---|---|---|
"r" | 只读 | 文本 | 打开失败 | 不允许 |
"w" | 只写 | 文本 | 创建新文件 | 覆盖原有内容 |
"a" | 追加 | 文本 | 创建新文件 | 写入到文件末尾 |
"r+" | 读写 | 文本 | 打开失败 | 从当前位置写入 |
"w+" | 读写 | 文本 | 创建新文件 | 覆盖原有内容 |
"a+" | 读写 | 文本 | 创建新文件 | 写入到文件末尾 |
"rb" | 只读 | 二进制 | 打开失败 | 不允许 |
"wb" | 只写 | 二进制 | 创建新文件 | 覆盖原有内容 |
"ab" | 追加 | 二进制 | 创建新文件 | 写入到文件末尾 |
模式字符串中的 "b" 表示二进制模式,这在 Windows 系统中会影响换行符的处理(文本模式会自动转换\n与\r\n),在 Linux 系统中虽无实质差异,但添加 "b" 可明确操作类型,提高代码可读性和可移植性。
返回值处理:
错误处理示例:
#include <stdio.h>
#include <errno.h>
#include <string.h>
int main() {
FILE *fp = fopen("secret.dat", "r");
if (fp == NULL) {
// 使用strerror()将errno转换为可读字符串
fprintf(stderr, "打开文件失败: %s\n", strerror(errno));
// 常见错误类型:ENOENT(文件不存在)、EACCES(权限不足)、EISDIR(路径指向目录)
return 1;
}
// 文件操作...
fclose(fp);
return 0;
}fopen()函数创建文件时使用默认权限(通常是 0666,受 umask 影响),如果需要精确控制文件权限,可结合系统调用open()与fdopen()函数:
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
int main() {
// 创建权限为0600的文件(仅所有者可读写)
int fd = open("private.txt", O_WRONLY | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR);
if (fd == -1) {
perror("open failed");
return 1;
}
// 将文件描述符转换为文件指针
FILE *fp = fdopen(fd, "w");
if (fp == NULL) {
perror("fdopen failed");
close(fd); // 转换失败时需手动关闭文件描述符
return 1;
}
// 使用文件指针操作...
fputs("敏感数据", fp);
fclose(fp); // fclose会自动关闭对应的文件描述符
return 0;
}O_EXCL标志确保文件不存在时才创建,避免意外覆盖已有文件,这在实现日志轮转、临时文件等场景中非常有用。fdopen()函数的作用是将底层文件描述符(由open()返回)转换为 C 库的文件指针,从而兼顾权限控制和缓冲 IO 的优势。
文件打开后,核心操作就是数据的读取和写入。C 库提供了多套读写函数,分别适用于不同场景:字符级、行级和块级操作,开发者可根据实际需求选择。
fgetc () 函数:从文件流中读取单个字符
int fgetc(FILE *stream);fputc () 函数:向文件流写入单个字符
int fputc(int c, FILE *stream);示例:实现文件复制功能
void copy_file(const char *src_path, const char *dest_path) {
FILE *src = fopen(src_path, "r");
FILE *dest = fopen(dest_path, "w");
if (src == NULL || dest == NULL) {
perror("文件打开失败");
// 避免内存泄漏:关闭已成功打开的文件
if (src) fclose(src);
if (dest) fclose(dest);
return;
}
int c;
// 循环读取并写入,直到遇到EOF
while ((c = fgetc(src)) != EOF) {
if (fputc(c, dest) == EOF) {
perror("写入失败");
break;
}
}
// 检查读取过程是否出现错误
if (ferror(src)) {
perror("读取失败");
}
// 关闭文件
fclose(src);
fclose(dest);
}字符级函数适合处理文本文件中的逐个字符,但效率较低,不适合大文件操作。
fgets () 函数:从文件流读取一行数据
char *fgets(char *s, int size, FILE *stream);参数解析:
工作机制:读取到换行符 '\n' 或 size-1 个字符时停止,自动在末尾添加 '\0'
返回值:成功返回缓冲区地址 s,失败或到达文件尾返回 NULL
注意事项:
fputs () 函数:向文件流写入字符串
int fputs(const char *s, FILE *stream);示例:处理配置文件
void parse_config(const char *filename) {
FILE *fp = fopen(filename, "r");
if (!fp) {
perror("打开配置文件失败");
return;
}
char line[1024];
int line_num = 0;
while (fgets(line, sizeof(line), fp) != NULL) {
line_num++;
// 去除行尾的换行符和回车符
size_t len = strlen(line);
if (len > 0 && line[len-1] == '\n') {
line[len-1] = '\0';
}
// 跳过空行和注释行
if (line[0] == '#' || line[0] == '\0') {
continue;
}
// 处理配置项
printf("第%d行配置: %s\n", line_num, line);
}
// 判断循环结束原因
if (ferror(fp)) {
perror("读取配置文件出错");
} else if (feof(fp)) {
printf("配置文件处理完毕,共%d行\n", line_num);
}
fclose(fp);
}行级函数适合处理文本文件、配置文件、日志文件等按行组织的数据,在脚本解析、日志分析等场景中应用广泛。
对于二进制文件或需要高效处理大量数据的场景,块级读写函数是更好的选择。
fread () 函数:从文件流读取数据块
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);参数解析:
返回值:成功读取的完整数据块数量,可能小于 nmemb(到达文件尾或出错)
fwrite () 函数:向文件流写入数据块
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);返回值:成功写入的数据块数量,若小于 nmemb 则表示出错
示例:处理二进制数据(结构体数组)
#include <stdio.h>
#include <stdlib.h>
// 定义用户数据结构体
typedef struct {
int id;
char name[32];
float salary;
} Employee;
// 写入员工数据到文件
void save_employees(const char *filename, Employee *emps, int count) {
FILE *fp = fopen(filename, "wb");
if (!fp) {
perror("打开文件失败");
return;
}
// 写入整个结构体数组
size_t written = fwrite(emps, sizeof(Employee), count, fp);
if (written != count) {
fprintf(stderr, "写入不完整:预期%d条,实际%d条\n", count, written);
} else {
printf("成功写入%d条员工数据\n", count);
}
fclose(fp);
}
// 从文件读取员工数据
Employee* load_employees(const char *filename, int *count) {
FILE *fp = fopen(filename, "rb");
if (!fp) {
perror("打开文件失败");
return NULL;
}
// 获取文件大小
fseek(fp, 0, SEEK_END);
long file_size = ftell(fp);
rewind(fp); // 重置到文件开头
// 计算员工数量
*count = file_size / sizeof(Employee);
if (file_size % sizeof(Employee) != 0) {
fprintf(stderr, "文件格式异常,大小不是结构体整数倍\n");
fclose(fp);
return NULL;
}
// 分配内存并读取数据
Employee *emps = malloc(*count * sizeof(Employee));
if (!emps) {
perror("内存分配失败");
fclose(fp);
return NULL;
}
size_t read = fread(emps, sizeof(Employee), *count, fp);
if (read != *count) {
fprintf(stderr, "读取不完整:预期%d条,实际%d条\n", *count, read);
free(emps);
emps = NULL;
*count = 0;
}
fclose(fp);
return emps;
}块级读写的优势在于效率 —— 通过一次函数调用处理多个数据单元,大幅减少函数调用开销。在处理图像、音频、数据库文件等二进制数据时是首选方式。需要注意的是,二进制数据具有平台相关性(如字节序、结构体对齐),跨平台传输时需进行格式转换。
格式化读写函数允许我们按照指定的格式读写数据,非常适合处理具有固定格式的文本数据。
fprintf () 函数:按照指定格式向文件流写入数据
int fprintf(FILE *stream, const char *format, ...);参数解析:
返回值:成功时返回写入的字符总数;失败时返回负数。
fscanf () 函数:按照指定格式从文件流读取数据
int fscanf(FILE *stream, const char *format, ...);参数解析:
返回值:成功时返回成功读取并赋值的变量个数;到达文件末尾时返回 EOF;失败时返回小于 EOF 的值。
示例:使用格式化函数读写学生信息
#include <stdio.h>
#include <stdlib.h>
typedef struct {
int id;
char name[20];
float score;
} Student;
// 写入学生信息
void write_students(const char *filename) {
FILE *fp = fopen(filename, "w");
if (fp == NULL) {
perror("打开文件失败");
return;
}
Student students[] = {
{1, "张三", 90.5},
{2, "李四", 85.0},
{3, "王五", 92.5}
};
int count = sizeof(students) / sizeof(Student);
for (int i = 0; i < count; i++) {
// 按照"id,name,score"的格式写入
fprintf(fp, "%d,%s,%.1f\n", students[i].id, students[i].name, students[i].score);
}
fclose(fp);
}
// 读取学生信息
void read_students(const char *filename) {
FILE *fp = fopen(filename, "r");
if (fp == NULL) {
perror("打开文件失败");
return;
}
Student s;
int count = 0;
// 按照格式读取数据
while (fscanf(fp, "%d,%[^,],%f\n", &s.id, s.name, &s.score) == 3) {
count++;
printf("ID: %d, 姓名: %s, 成绩: %.1f\n", s.id, s.name, s.score);
}
printf("共读取%d条学生信息\n", count);
fclose(fp);
}
int main() {
write_students("students.txt");
read_students("students.txt");
return 0;
}fscanf()函数中的%[^,]格式表示读取除逗号之外的所有字符,直到遇到逗号为止,非常适合读取 CSV 格式的数据。格式化读写函数在处理具有固定格式的配置文件、日志文件等场景中非常方便,但需要注意格式字符串与数据格式的匹配,否则可能导致读取错误。
文件操作完成后,必须及时关闭文件以释放资源。fclose()函数负责这一关键工作,它不仅关闭文件描述符,还会刷新缓冲区确保数据完整性。
函数原型:
int fclose(FILE *stream);功能:
返回值:成功返回 0,失败返回 EOF(通常因刷新缓冲区时出错)
示例:安全关闭文件
FILE *fp = fopen("data.txt", "w");
if (!fp) {
perror("打开失败");
return 1;
}
// 执行写入操作...
fputs("重要数据", fp);
// 关闭文件并检查错误
if (fclose(fp) == EOF) {
perror("关闭文件失败");
// 这里需要特别注意:数据可能未完全写入
return 1;
}1. 资源释放:
2. 数据完整性:
// 手动刷新缓冲区
if (fflush(fp) == EOF) {
perror("刷新缓冲区失败");
}3. 异常处理策略:
void process_files() {
FILE *f1 = fopen("a.txt", "r");
FILE *f2 = fopen("b.txt", "w");
if (!f1 || !f2) {
perror("打开文件失败");
goto cleanup; // 跳转到清理代码
}
// 执行文件操作...
if (some_error) {
goto cleanup;
}
cleanup:
if (f1) fclose(f1);
if (f2) fclose(f2);
}fclose(),会导致未定义行为(可能崩溃或损坏数据),因此应确保每个文件只被关闭一次。
fclose()可能无法被调用,导致数据丢失。此时可使用fflush()函数手动刷新缓冲区,或使用atexit()注册清理函数,确保程序退出前关闭文件。
// 使用atexit()注册文件关闭函数
FILE *fp;
void close_file() {
if (fp != NULL) {
fclose(fp);
fp = NULL;
}
}
int main() {
fp = fopen("log.txt", "a");
if (fp == NULL) {
perror("打开日志文件失败");
return 1;
}
atexit(close_file); // 注册程序退出时的清理函数
// 程序逻辑...
return 0;
}兼容Windows/Linux的路径:
#ifdef _WIN32
#define PATH_SEPARATOR '\\'
#else
#define PATH_SEPARATOR '/'
#endif
void create_path(const char *dir, const char *file) {
char path[1024];
snprintf(path, sizeof(path), "%s%c%s", dir, PATH_SEPARATOR, file);
// 使用path进行文件操作...
}使用O_EXCL创建唯一文件(需结合open系统调用):
#include <fcntl.h>
#include <unistd.h>
int create_unique_file(const char *path) {
int fd = open(path, O_WRONLY | O_CREAT | O_EXCL, 0644);
if (fd == -1) {
perror("文件已存在或创建失败");
return -1;
}
// 使用fd进行操作...
close(fd);
return 0;
}C库替代方案:对于简单场景,可使用fopen+检查文件是否存在(非原子操作)。
虽然不属于标准C库,但值得了解的替代方案:
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
void map_file(const char *path) {
int fd = open(path, O_RDONLY);
struct stat sb;
fstat(fd, &sb);
char *addr = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
if (addr == MAP_FAILED) {
perror("内存映射失败");
close(fd);
return;
}
// 直接访问文件内容如同内存
printf("文件内容: %s\n", addr);
munmap(addr, sb.st_size);
close(fd);
}适用场景:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_NAME_LEN 50
#define DB_FILE "students.db"
typedef struct {
int id;
char name[MAX_NAME_LEN];
float gpa;
} Student;
void save_student(const Student *s) {
FILE *fp = fopen(DB_FILE, "ab"); // 追加二进制模式
if (fp == NULL) {
perror("数据库打开失败");
return;
}
fwrite(s, sizeof(Student), 1, fp);
fclose(fp);
}
void list_students() {
FILE *fp = fopen(DB_FILE, "rb");
if (fp == NULL) {
printf("数据库为空或无法打开\n");
return;
}
Student s;
printf("%-10s %-20s %-10s\n", "ID", "姓名", "GPA");
while (fread(&s, sizeof(Student), 1, fp) == 1) {
printf("%-10d %-20s %-10.2f\n", s.id, s.name, s.gpa);
}
fclose(fp);
}
int main() {
int choice;
Student s;
while (1) {
printf("\n学生信息管理系统\n");
printf("1. 添加学生\n2. 显示所有\n3. 退出\n选择: ");
scanf("%d", &choice);
switch (choice) {
case 1:
printf("输入学号: ");
scanf("%d", &s.id);
printf("输入姓名: ");
scanf("%s", s.name);
printf("输入GPA: ");
scanf("%f", &s.gpa);
save_student(&s);
break;
case 2:
list_students();
break;
case 3:
exit(0);
default:
printf("无效选择\n");
}
}
return 0;
}