在计算机系统中,文件是存储在外部介质(如硬盘、U盘、SSD等)上的数据集合,用于长期保存信息。与内存中的临时数据不同,文件具有持久化特性,即使程序关闭或系统断电,文件内容依然能够保留。这种特性使得文件成为程序与外部世界交互的重要媒介。
从使用角度来看,文件主要分为两大类:
程序文件:包含可执行代码或脚本,由编程语言编写,经过编译或解释后运行。常见的程序文件扩展名包括:
数据文件:存储程序运行时需要读写的信息内容,供程序读取或处理。数据文件可以结构化(如数据库文件)或非结构化(如文本日志)形式保存。在实际开发中,我们主要关注的就是这类文件的操作。
一个有效的文件标识通常包含三个部分:
code复制[文件路径] + [文件名主干] + [文件后缀]
例如:
code复制C:\Projects\data\records.txt
其中:
C:\Projects\data\ 是文件路径records 是文件名主干.txt 是文件后缀(扩展名)注意:不同操作系统对文件名的限制不同。Windows系统不区分大小写,而Linux/Unix系统严格区分。在跨平台开发时需要特别注意这一点。
数据文件又可分为两种存储形式:
文本文件:
二进制文件:
关键区别在于内存中的数据表示:
c复制int num = 12345;
流是计算机中处理数据序列的抽象概念,代表一种按顺序访问或传输数据的方式。可以将流想象为数据的管道,支持逐段读取或写入,无需一次性加载全部数据到内存。
在C语言中,流通过文件指针(FILE*)来操作。这种设计有两大优势:
每个C程序启动时,会自动打开三个标准流:
| 流名称 | 文件指针 | 默认设备 | 常用函数 |
|---|---|---|---|
| 标准输入 | stdin | 键盘 | scanf, getchar |
| 标准输出 | stdout | 显示器 | printf, puts |
| 标准错误 | stderr | 显示器 | perror, fprintf |
这些流的类型都是FILE*,因此可以使用所有文件操作函数来处理它们。例如:
c复制fprintf(stderr, "Error: invalid input\n");
文件指针是C语言文件操作的核心概念。每个打开的文件在内存中都有一个对应的FILE结构体,包含:
典型的文件操作流程:
c复制FILE *fp = fopen("data.txt", "r"); // 打开文件
if(fp == NULL) {
perror("Failed to open file");
return 1;
}
// 文件操作...
fclose(fp); // 关闭文件
fp = NULL; // 指针置空
重要提示:不同编译器实现的FILE结构可能不同,这是C标准故意留出的实现空间。因此绝对不要直接访问FILE结构的成员,必须通过标准库函数操作。
fopen函数是文件操作的起点,其原型为:
c复制FILE *fopen(const char *filename, const char *mode);
常用打开模式:
| 模式 | 说明 | 文件存在 | 文件不存在 |
|---|---|---|---|
| "r" | 只读 | 打开成功 | 打开失败 |
| "w" | 只写 | 清空内容 | 创建新文件 |
| "a" | 追加 | 追加写入 | 创建新文件 |
| "r+" | 读写 | 打开成功 | 打开失败 |
| "w+" | 读写 | 清空内容 | 创建新文件 |
| "a+" | 读写 | 追加写入 | 创建新文件 |
二进制版本只需在模式后加'b',如"rb"、"wb+"等。
路径问题:
推荐做法:
c复制FILE *fp = fopen("data/files/data.txt", "r"); // 相对路径
// 或
FILE *fp = fopen("/home/user/data.txt", "r"); // Linux绝对路径
权限问题:
错误处理最佳实践:
c复制FILE *fp = fopen("important.dat", "rb");
if(fp == NULL) {
perror("fopen failed");
// 更详细的错误信息
fprintf(stderr, "Failed to open important.dat in mode rb\n");
exit(EXIT_FAILURE);
}
fclose函数完成以下工作:
常见错误:
c复制FILE *fp = fopen("data.txt", "w");
fputs("Hello", fp);
// 忘记fclose!可能导致数据丢失
安全做法:
c复制FILE *fp = fopen("data.txt", "w");
if(fp) {
fputs("Hello", fp);
if(fclose(fp) != 0) {
perror("fclose failed");
}
fp = NULL; // 避免悬垂指针
}
fgetc从文件读取一个字符:
c复制int ch = fgetc(fp);
if(ch == EOF) {
if(feof(fp)) {
printf("End of file reached\n");
} else if(ferror(fp)) {
perror("Read error");
}
}
fputc向文件写入一个字符:
c复制int result = fputc('A', fp);
if(result == EOF) {
perror("Write failed");
}
注意:这些函数使用int而非char作为返回值,是为了能返回EOF(-1)表示错误/结束。
fgets读取一行(包含换行符):
c复制char buffer[256];
if(fgets(buffer, sizeof(buffer), fp)) {
printf("Read: %s", buffer);
} else {
if(feof(fp)) {
printf("End of file\n");
} else {
perror("fgets error");
}
}
fputs写入字符串(不自动添加换行):
c复制if(fputs("Hello World\n", fp) == EOF) {
perror("fputs failed");
}
fscanf从文件格式化读取:
c复制int id;
char name[50];
float score;
while(fscanf(fp, "%d %49s %f", &id, name, &score) == 3) {
printf("ID:%d, Name:%s, Score:%.2f\n", id, name, score);
}
fprintf向文件格式化写入:
c复制fprintf(fp, "%d %s %.2f\n", student.id, student.name, student.score);
fwrite写入二进制数据:
c复制struct Student {
int id;
char name[50];
float gpa;
} stu = {123, "Alice", 3.8};
size_t written = fwrite(&stu, sizeof(struct Student), 1, fp);
if(written != 1) {
perror("fwrite failed");
}
fread读取二进制数据:
c复制struct Student stu;
size_t read = fread(&stu, sizeof(struct Student), 1, fp);
if(read != 1) {
if(feof(fp)) {
printf("Unexpected end of file\n");
} else {
perror("fread error");
}
}
关键点:二进制I/O不进行任何数据转换,直接按内存映像写入/读取。特别适合结构体、数组等复杂数据的存储。
fseek改变文件位置:
c复制// 移动到文件开头后100字节处
if(fseek(fp, 100, SEEK_SET) != 0) {
perror("fseek failed");
}
// 从当前位置向前移动50字节
fseek(fp, 50, SEEK_CUR);
// 移动到文件末尾前20字节处
fseek(fp, -20, SEEK_END);
ftell获取当前位置:
c复制long pos = ftell(fp);
if(pos == -1L) {
perror("ftell error");
} else {
printf("Current position: %ld\n", pos);
}
rewind重置位置到开头:
c复制rewind(fp); // 等价于 fseek(fp, 0, SEEK_SET)
假设有一个学生记录文件,每个记录固定大小:
c复制struct Student {
int id;
char name[50];
float gpa;
};
// 读取第5条记录(索引从0开始)
int record_num = 4; // 第5条
if(fseek(fp, record_num * sizeof(struct Student), SEEK_SET) != 0) {
perror("fseek failed");
return;
}
struct Student stu;
if(fread(&stu, sizeof(struct Student), 1, fp) != 1) {
perror("fread failed");
return;
}
printf("Student %d: %s, GPA: %.2f\n", stu.id, stu.name, stu.gpa);
正确检测文件结束的方法:
c复制while(1) {
int ch = fgetc(fp);
if(ch == EOF) {
if(feof(fp)) {
break; // 正常结束
} else if(ferror(fp)) {
perror("Read error");
break;
}
}
// 处理字符...
}
常见误区:
c复制// 错误做法:直接使用feof判断
while(!feof(fp)) {
char buf[100];
fgets(buf, sizeof(buf), fp);
// 最后一次读取可能已经到达EOF,但feof还未置位
// 导致最后一次处理无效数据
}
C标准库对文件操作进行了缓冲,主要分为:
手动刷新缓冲区:
c复制fflush(fp); // 将缓冲区内容写入磁盘
重要提示:程序异常退出时,未刷新的缓冲区数据可能丢失。关键操作后应立即fflush或使用无缓冲模式。
创建临时文件的安全方法:
c复制char tmpname[L_tmpnam];
tmpnam(tmpname); // 不推荐,有安全风险
// 更安全的方法
FILE *tmp = tmpfile(); // 自动创建并打开临时文件
if(tmp) {
// 使用临时文件...
fclose(tmp); // 自动删除
}
问题1:文件内容未正确写入
问题2:读取到错误数据
问题3:性能问题
批量读写:使用大缓冲区减少I/O次数
c复制char buf[4096];
size_t n;
while((n = fread(buf, 1, sizeof(buf), fp)) > 0) {
// 处理buf中的数据
}
减少定位操作:顺序访问通常比随机访问快
选择合适的缓冲策略:
c复制// 设置自定义缓冲区
char my_buffer[8192];
setvbuf(fp, my_buffer, _IOFBF, sizeof(my_buffer));
异步I/O:在支持的系统上使用aio_*函数族
统计日志文件中错误出现的次数:
c复制int count_errors(const char *filename) {
FILE *fp = fopen(filename, "r");
if(!fp) return -1;
int count = 0;
char line[1024];
while(fgets(line, sizeof(line), fp)) {
if(strstr(line, "ERROR")) {
count++;
}
}
if(ferror(fp)) {
fclose(fp);
return -1;
}
fclose(fp);
return count;
}
完整的学生记录管理系统:
c复制struct Student {
int id;
char name[50];
float gpa;
};
void add_student(const char *filename, const struct Student *stu) {
FILE *fp = fopen(filename, "ab"); // 追加二进制模式
if(!fp) return;
if(fwrite(stu, sizeof(struct Student), 1, fp) != 1) {
perror("Write failed");
}
fclose(fp);
}
struct Student *find_student(const char *filename, int id) {
FILE *fp = fopen(filename, "rb");
if(!fp) return NULL;
static struct Student stu;
while(fread(&stu, sizeof(stu), 1, fp) == 1) {
if(stu.id == id) {
fclose(fp);
return &stu;
}
}
fclose(fp);
return NULL;
}
高效的文件复制程序:
c复制int copy_file(const char *src, const char *dst) {
FILE *in = fopen(src, "rb");
if(!in) return -1;
FILE *out = fopen(dst, "wb");
if(!out) {
fclose(in);
return -1;
}
char buffer[4096];
size_t n;
while((n = fread(buffer, 1, sizeof(buffer), in)) > 0) {
if(fwrite(buffer, 1, n, out) != n) {
fclose(in);
fclose(out);
return -1;
}
}
if(ferror(in)) {
fclose(in);
fclose(out);
return -1;
}
fclose(in);
fclose(out);
return 0;
}
在实际开发中,文件操作是C程序与外部世界交互的重要方式。掌握这些核心概念和技巧,能够处理从简单配置文件到复杂二进制数据存储的各种需求。记住始终检查错误、及时释放资源,并根据具体场景选择最合适的I/O方式。