文件操作
文件操作是程序与外部世界交互的重要方式。C 语言提供了丰富的文件操作函数,可以读写文本文件和二进制文件。
文件的概念
文件类型
C 语言将文件分为两类:
- 文本文件:存储可读的字符数据,以行为单位
- 二进制文件:存储原始字节数据,保持数据的内部表示
文件指针
C 语言使用 FILE* 指针来操作文件:
FILE* fp; // 文件指针
FILE 是一个结构体类型,包含文件的各种信息,如缓冲区位置、文件状态等。
文件的打开与关闭
fopen
fopen 打开文件,返回文件指针:
FILE* fopen(const char* filename, const char* mode);
打开模式:
| 模式 | 说明 |
|---|---|
"r" | 只读,文件必须存在 |
"w" | 只写,文件不存在则创建,存在则清空 |
"a" | 追加,文件不存在则创建 |
"r+" | 读写,文件必须存在 |
"w+" | 读写,文件不存在则创建,存在则清空 |
"a+" | 读写追加,文件不存在则创建 |
二进制模式添加 b:
| 模式 | 说明 |
|---|---|
"rb" | 二进制只读 |
"wb" | 二进制只写 |
"ab" | 二进制追加 |
"rb+" | 二进制读写 |
"wb+" | 二进制读写(清空) |
"ab+" | 二进制读写追加 |
fclose
fclose 关闭文件,释放资源:
int fclose(FILE* stream);
成功返回 0,失败返回 EOF。
基本示例
#include <stdio.h>
#include <stdlib.h>
int main(void) {
FILE* fp = fopen("example.txt", "w");
if (fp == NULL) {
perror("打开文件失败");
return -1;
}
fprintf(fp, "Hello, World!\n");
fprintf(fp, "这是第二行\n");
fclose(fp);
printf("文件写入完成\n");
return 0;
}
错误处理
FILE* fp = fopen("nonexistent.txt", "r");
if (fp == NULL) {
perror("fopen"); // 输出错误信息
// 或使用 strerror
printf("错误: %s\n", strerror(errno));
return -1;
}
字符输入输出
fgetc 和 fputc
int fgetc(FILE* stream); // 读取一个字符
int fputc(int ch, FILE* stream); // 写入一个字符
复制文件示例:
int copy_file(const char* src, const char* dst) {
FILE* in = fopen(src, "rb");
if (in == NULL) {
perror("打开源文件失败");
return -1;
}
FILE* out = fopen(dst, "wb");
if (out == NULL) {
perror("打开目标文件失败");
fclose(in);
return -1;
}
int ch;
while ((ch = fgetc(in)) != EOF) {
fputc(ch, out);
}
fclose(in);
fclose(out);
return 0;
}
getc 和 putc
getc 和 putc 是宏,效率更高:
int getc(FILE* stream);
int putc(int ch, FILE* stream);
getchar 和 putchar
从标准输入读取和向标准输出写入:
int getchar(void); // 等价于 getc(stdin)
int putchar(int ch); // 等价于 putc(ch, stdout)
行输入输出
fgets
char* fgets(char* str, int n, FILE* stream);
读取一行(最多 n-1 个字符),保留换行符:
char line[256];
FILE* fp = fopen("example.txt", "r");
if (fp != NULL) {
while (fgets(line, sizeof(line), fp) != NULL) {
printf("%s", line); // line 包含换行符
}
fclose(fp);
}
fputs
int fputs(const char* str, FILE* stream);
写入字符串(不添加换行符):
FILE* fp = fopen("output.txt", "w");
if (fp != NULL) {
fputs("第一行\n", fp);
fputs("第二行\n", fp);
fclose(fp);
}
gets(不安全,已弃用)
gets 不检查缓冲区大小,容易导致缓冲区溢出:
char line[100];
gets(line); // 危险!不要使用
使用 fgets 替代:
char line[100];
if (fgets(line, sizeof(line), stdin) != NULL) {
line[strcspn(line, "\n")] = '\0'; // 去除换行符
}
格式化输入输出
fprintf 和 fscanf
int fprintf(FILE* stream, const char* format, ...);
int fscanf(FILE* stream, const char* format, ...);
写入格式化数据:
FILE* fp = fopen("data.txt", "w");
if (fp != NULL) {
fprintf(fp, "姓名: %s\n", "张三");
fprintf(fp, "年龄: %d\n", 25);
fprintf(fp, "分数: %.2f\n", 95.5);
fclose(fp);
}
读取格式化数据:
FILE* fp = fopen("data.txt", "r");
if (fp != NULL) {
char name[50];
int age;
float score;
fscanf(fp, "姓名: %s\n", name);
fscanf(fp, "年龄: %d\n", &age);
fscanf(fp, "分数: %f\n", &score);
printf("姓名: %s, 年龄: %d, 分数: %.2f\n", name, age, score);
fclose(fp);
}
sprintf 和 sscanf
写入和读取字符串:
char buffer[100];
// 写入字符串
sprintf(buffer, "值: %d, 名字: %s", 42, "test");
printf("%s\n", buffer);
// 从字符串读取
int value;
char name[20];
sscanf(buffer, "值: %d, 名字: %s", &value, name);
printf("value=%d, name=%s\n", value, name);
snprintf
安全的格式化写入字符串:
char buffer[10];
snprintf(buffer, sizeof(buffer), "很长的字符串会被截断");
printf("%s\n", buffer); // 输出截断后的字符串
二进制输入输出
fwrite
size_t fwrite(const void* ptr, size_t size, size_t count, FILE* stream);
写入二进制数据:
typedef struct {
int id;
char name[50];
float score;
} Student;
int main(void) {
Student students[] = {
{1, "张三", 85.5f},
{2, "李四", 92.0f},
{3, "王五", 78.5f}
};
FILE* fp = fopen("students.dat", "wb");
if (fp != NULL) {
size_t written = fwrite(students, sizeof(Student), 3, fp);
printf("写入了 %zu 条记录\n", written);
fclose(fp);
}
return 0;
}
fread
size_t fread(void* ptr, size_t size, size_t count, FILE* stream);
读取二进制数据:
Student students[10];
FILE* fp = fopen("students.dat", "rb");
if (fp != NULL) {
size_t count = fread(students, sizeof(Student), 10, fp);
for (size_t i = 0; i < count; i++) {
printf("ID: %d, 姓名: %s, 分数: %.1f\n",
students[i].id, students[i].name, students[i].score);
}
fclose(fp);
}
二进制文件的优势
- 存储效率高
- 读写速度快
- 保持数据的原始表示
注意:二进制文件可能不兼容不同平台(字节序、对齐等)。
文件定位
fseek
int fseek(FILE* stream, long offset, int origin);
移动文件位置指针:
| origin | 说明 |
|---|---|
SEEK_SET | 文件开头 |
SEEK_CUR | 当前位置 |
SEEK_END | 文件末尾 |
FILE* fp = fopen("data.bin", "rb");
if (fp != NULL) {
fseek(fp, 0, SEEK_END); // 移动到末尾
long size = ftell(fp); // 获取文件大小
printf("文件大小: %ld 字节\n", size);
fseek(fp, 10, SEEK_SET); // 移动到第 10 字节
int value;
fread(&value, sizeof(int), 1, fp);
fclose(fp);
}
ftell
long ftell(FILE* stream);
返回当前文件位置:
long pos = ftell(fp);
rewind
void rewind(FILE* stream);
将文件指针重置到开头:
rewind(fp); // 等价于 fseek(fp, 0, SEEK_SET)
fgetpos 和 fsetpos
用于大文件(超过 2GB):
int fgetpos(FILE* stream, fpos_t* pos);
int fsetpos(FILE* stream, const fpos_t* pos);
fpos_t pos;
fgetpos(fp, &pos); // 保存位置
// ... 其他操作
fsetpos(fp, &pos); // 恢复位置
错误处理
feof
检查是否到达文件末尾:
while (!feof(fp)) {
// 读取数据
}
注意:feof 在读取操作失败后才返回真,不应作为循环条件:
// 正确的读取方式
int ch;
while ((ch = fgetc(fp)) != EOF) {
putchar(ch);
}
// 或者
char line[256];
while (fgets(line, sizeof(line), fp) != NULL) {
printf("%s", line);
}
ferror
检查是否发生错误:
if (ferror(fp)) {
printf("读取文件时发生错误\n");
clearerr(fp); // 清除错误标志
}
clearerr
清除错误和 EOF 标志:
clearerr(fp);
标准流
C 语言预定义了三个标准流:
| 流 | 说明 | 文件描述符 |
|---|---|---|
stdin | 标准输入 | 0 |
stdout | 标准输出 | 1 |
stderr | 标准错误 | 2 |
fprintf(stdout, "正常输出\n");
fprintf(stderr, "错误信息\n"); // 即使输出被重定向,错误信息仍显示
文件缓冲
缓冲模式
| 模式 | 说明 |
|---|---|
_IOFBF | 全缓冲 |
_IOLBF | 行缓冲 |
_IONBF | 无缓冲 |
setvbuf
设置缓冲模式:
char buffer[BUFSIZ];
setvbuf(fp, buffer, _IOFBF, BUFSIZ); // 全缓冲
setvbuf(fp, NULL, _IONBF, 0); // 无缓冲
setbuf
简化版本:
char buffer[BUFSIZ];
setbuf(fp, buffer); // 全缓冲
setbuf(fp, NULL); // 无缓冲
fflush
刷新缓冲区:
fflush(fp); // 刷新指定流
fflush(stdout); // 立即输出
fflush(NULL); // 刷新所有输出流
临时文件
tmpfile
创建临时文件,关闭时自动删除:
FILE* tmp = tmpfile();
if (tmp != NULL) {
fprintf(tmp, "临时数据\n");
rewind(tmp);
char line[100];
if (fgets(line, sizeof(line), tmp) != NULL) {
printf("%s", line);
}
fclose(tmp); // 文件自动删除
}
tmpnam
生成临时文件名:
char filename[L_tmpnam];
tmpnam(filename);
printf("临时文件名: %s\n", filename);
注意:tmpnam 存在安全问题,推荐使用 tmpfile 或 mkstemp。
目录操作
C 标准库不提供目录操作,但可以使用平台相关函数:
POSIX(Linux/macOS)
#include <sys/stat.h>
#include <dirent.h>
#include <unistd.h>
int create_directory(const char* path) {
return mkdir(path, 0755);
}
int remove_directory(const char* path) {
return rmdir(path);
}
void list_directory(const char* path) {
DIR* dir = opendir(path);
if (dir == NULL) {
perror("opendir");
return;
}
struct dirent* entry;
while ((entry = readdir(dir)) != NULL) {
printf("%s\n", entry->d_name);
}
closedir(dir);
}
Windows
#include <windows.h>
int create_directory(const char* path) {
return CreateDirectory(path, NULL) ? 0 : -1;
}
int remove_directory(const char* path) {
return RemoveDirectory(path) ? 0 : -1;
}
文件属性
stat(POSIX)
获取文件信息:
#include <sys/stat.h>
void print_file_info(const char* path) {
struct stat st;
if (stat(path, &st) == 0) {
printf("文件大小: %ld 字节\n", st.st_size);
printf("最后修改: %ld\n", st.st_mtime);
printf("是否目录: %s\n", S_ISDIR(st.st_mode) ? "是" : "否");
printf("权限: %o\n", st.st_mode & 0777);
}
}
跨平台文件存在检查
#include <stdio.h>
int file_exists(const char* path) {
FILE* fp = fopen(path, "r");
if (fp != NULL) {
fclose(fp);
return 1;
}
return 0;
}
实用示例
读取配置文件
typedef struct {
char key[50];
char value[200];
} ConfigEntry;
int read_config(const char* filename, ConfigEntry* entries, int max_entries) {
FILE* fp = fopen(filename, "r");
if (fp == NULL) return -1;
char line[256];
int count = 0;
while (fgets(line, sizeof(line), fp) != NULL && count < max_entries) {
// 跳过注释和空行
if (line[0] == '#' || line[0] == '\n') continue;
// 解析 key=value
char* eq = strchr(line, '=');
if (eq != NULL) {
*eq = '\0';
strncpy(entries[count].key, line, sizeof(entries[count].key) - 1);
char* value = eq + 1;
// 去除换行符
value[strcspn(value, "\n")] = '\0';
strncpy(entries[count].value, value, sizeof(entries[count].value) - 1);
count++;
}
}
fclose(fp);
return count;
}
日志文件
#include <time.h>
void log_message(FILE* log_file, const char* level, const char* message) {
time_t now = time(NULL);
char* time_str = ctime(&now);
time_str[strlen(time_str) - 1] = '\0'; // 去除换行符
fprintf(log_file, "[%s] %s: %s\n", time_str, level, message);
fflush(log_file);
}
int main(void) {
FILE* log = fopen("app.log", "a");
if (log != NULL) {
log_message(log, "INFO", "程序启动");
log_message(log, "WARNING", "配置文件未找到,使用默认配置");
log_message(log, "ERROR", "数据库连接失败");
fclose(log);
}
return 0;
}
文件复制(高效版本)
int copy_file_fast(const char* src, const char* dst) {
FILE* in = fopen(src, "rb");
if (in == NULL) return -1;
FILE* out = fopen(dst, "wb");
if (out == NULL) {
fclose(in);
return -1;
}
char buffer[8192];
size_t bytes;
while ((bytes = fread(buffer, 1, sizeof(buffer), in)) > 0) {
fwrite(buffer, 1, bytes, out);
}
fclose(in);
fclose(out);
return 0;
}
小结
本章介绍了 C 语言的文件操作:
- 文件打开与关闭:fopen、fclose
- 字符输入输出:fgetc、fputc、getchar、putchar
- 行输入输出:fgets、fputs
- 格式化输入输出:fprintf、fscanf、sprintf、sscanf
- 二进制输入输出:fread、fwrite
- 文件定位:fseek、ftell、rewind
- 错误处理:feof、ferror、clearerr
- 标准流:stdin、stdout、stderr
- 文件缓冲:setvbuf、fflush
- 临时文件:tmpfile
下一章将学习 预处理器,了解宏定义和条件编译。