C语言的文件操作
文件操作是C语言中用于持久化存储数据的重要机制。通过文件操作,程序可以将数据保存到硬盘中,或从硬盘中读取数据,实现数据的持久化存储和跨程序共享。C语言提供了一系列标准库函数,用于打开、关闭、读写文件,以及进行文件定位和错误处理。掌握文件操作的基本概念和使用方法,是编写实用且功能丰富的C程序的关键。
1 文件的打开与关闭
在C语言中,文件的打开与关闭是进行任何文件操作的前提。打开文件时,程序需要指定文件的路径和访问模式;关闭文件则释放与文件相关的资源,确保数据完整性和系统资源的有效利用。
文件的打开
函数原型:
FILE *fopen(const char *filename, const char *mode);
filename:要打开的文件的路径,可以是绝对路径或相对路径。mode:文件的打开模式,决定了文件的访问权限和操作方式。返回值:成功时返回指向FILE对象的指针,失败时返回NULL。
常见的打开模式:
模式描述"r"以只读方式打开文件,文件必须存在。"w"以写入方式打开文件,如果文件存在则清空,不存在则创建。"a"以追加方式打开文件,数据写入将追加到文件末尾。"r+"以读写方式打开文件,文件必须存在。"w+"以读写方式打开文件,文件存在则清空,不存在则创建。"a+"以读写追加方式打开文件,文件存在则保留内容,不存在则创建。"rb", "wb", "ab", "r+b", "w+b", "a+b"以上模式的二进制版本,用于处理二进制文件。示例与详细说明:
#include
int main() {
FILE *fp;
// 以只读方式打开一个文本文件
fp = fopen("example.txt", "r");
if(fp == NULL) {
perror("打开文件失败"); // 输出错误信息
return 1;
}
// 成功打开文件后,可以进行读操作
// ...
// 关闭文件
fclose(fp);
return 0;
}
详细解释:
使用fopen函数尝试以只读模式打开example.txt文件。如果文件不存在或无法以指定模式打开,fopen返回NULL,程序通过perror输出错误信息并终止。成功打开文件后,可以使用文件指针fp进行后续的读写操作。操作完成后,通过fclose函数关闭文件,释放资源。
文件的关闭
函数原型:
int fclose(FILE *stream);
stream:指向要关闭的FILE对象的指针。返回值:成功时返回0,失败时返回EOF(通常为-1)。
示例与详细说明:
#include
int main() {
FILE *fp;
// 以写入模式打开一个文本文件
fp = fopen("output.txt", "w");
if(fp == NULL) {
perror("打开文件失败");
return 1;
}
// 写入数据到文件
fprintf(fp, "Hello, World!\n");
// 关闭文件
if(fclose(fp) != 0) {
perror("关闭文件失败");
return 1;
}
return 0;
}
输出(文件output.txt内容):
Hello, World!
详细解释:
通过fopen函数以写入模式打开output.txt文件。使用fprintf函数将字符串写入文件。使用fclose函数关闭文件,并检查关闭操作是否成功。如果fclose失败,程序通过perror输出错误信息并终止。
注意事项
确保文件关闭:每次成功打开文件后,必须确保最终调用fclose关闭文件,以防止内存泄漏和数据丢失。错误检查:始终检查fopen和fclose的返回值,以便及时处理文件操作中的错误。文件指针有效性:在调用fclose之前,确保文件指针不为NULL,以避免未定义行为。
2 文件读写操作
文件读写操作是文件操作的核心,包括向文件写入数据和从文件读取数据。C语言提供了多种函数用于文本文件和二进制文件的读写操作。
2.1 文本文件操作
文本文件操作用于处理人类可读的文本数据,常见函数包括fgetc、fgets、fputc、fputs、fprintf、fscanf等。
写入文本文件
示例:使用fprintf和fputs写入文本文件。
#include
int main() {
FILE *fp;
// 以写入模式打开文件
fp = fopen("text_output.txt", "w");
if(fp == NULL) {
perror("打开文件失败");
return 1;
}
// 使用fprintf写入格式化数据
fprintf(fp, "姓名: %s\n", "李雷");
fprintf(fp, "年龄: %d\n", 21);
fprintf(fp, "成绩: %.2f\n", 88.5);
// 使用fputs写入字符串
fputs("这是一个测试文本。\n", fp);
// 关闭文件
fclose(fp);
return 0;
}
文件text_output.txt内容:
姓名: 李雷
年龄: 21
成绩: 88.50
这是一个测试文本。
详细解释:
使用fprintf函数以格式化方式写入姓名、年龄和成绩信息。使用fputs函数写入一行字符串。fclose函数关闭文件,确保数据被正确写入磁盘。
读取文本文件
示例:使用fgets和fscanf读取文本文件。
#include
int main() {
FILE *fp;
char buffer[100];
char name[50];
int age;
float score;
// 以读取模式打开文件
fp = fopen("text_output.txt", "r");
if(fp == NULL) {
perror("打开文件失败");
return 1;
}
// 使用fgets逐行读取文件内容
printf("使用fgets读取文件内容:\n");
while(fgets(buffer, sizeof(buffer), fp) != NULL) {
printf("%s", buffer);
}
// 重置文件指针到文件开头
rewind(fp);
// 使用fscanf读取格式化数据
printf("\n使用fscanf读取结构化数据:\n");
fscanf(fp, "姓名: %s\n", name);
fscanf(fp, "年龄: %d\n", &age);
fscanf(fp, "成绩: %f\n", &score);
printf("姓名: %s\n", name);
printf("年龄: %d\n", age);
printf("成绩: %.2f\n", score);
// 关闭文件
fclose(fp);
return 0;
}
输出:
使用fgets读取文件内容:
姓名: 李雷
年龄: 21
成绩: 88.50
这是一个测试文本。
使用fscanf读取结构化数据:
姓名: 李雷
年龄: 21
成绩: 88.50
详细解释:
使用fgets函数逐行读取文件内容,并打印到控制台。使用rewind函数将文件指针重置到文件开头,以便重新读取文件内容。使用fscanf函数按格式读取姓名、年龄和成绩信息。打印读取到的结构化数据。
逐字符读写
示例:使用fputc和fgetc逐字符写入和读取文件。
#include
int main() {
FILE *fp;
int ch;
// 写入文件
fp = fopen("char_output.txt", "w");
if(fp == NULL) {
perror("打开文件失败");
return 1;
}
// 使用fputc逐字符写入
fputc('H', fp);
fputc('e', fp);
fputc('l', fp);
fputc('l', fp);
fputc('o', fp);
fputc('\n', fp); // 换行符
fclose(fp);
// 读取文件
fp = fopen("char_output.txt", "r");
if(fp == NULL) {
perror("打开文件失败");
return 1;
}
printf("文件内容:\n");
// 使用fgetc逐字符读取
while((ch = fgetc(fp)) != EOF) {
putchar(ch);
}
fclose(fp);
return 0;
}
文件char_output.txt内容:
Hello
输出:
文件内容:
Hello
详细解释:
使用fputc函数逐字符写入"Hello\n"到文件中。关闭文件后,重新以读取模式打开文件。使用fgetc函数逐字符读取文件内容,并通过putchar函数打印到控制台。
总结
文本文件操作适用于处理人类可读的文本数据,提供了多种函数用于格式化和逐行、逐字符的读写。通过合理使用这些函数,可以实现高效的数据存储和读取。
2.2 二进制文件操作
二进制文件操作用于处理非文本数据,如图像、音频、视频或任何其他二进制格式的数据。二进制文件以二进制格式存储数据,读写操作不进行任何转换,适用于高效的数据存储和传输。
写入二进制文件
示例:使用fwrite写入结构体数据到二进制文件。
#include
#include
#include
// 定义一个结构体
typedef struct {
char name[50];
int age;
float gpa;
} Student;
int main() {
FILE *fp;
Student student1 = {"王五", 22, 3.9};
Student student2 = {"赵六", 23, 3.7};
// 以二进制写入模式打开文件
fp = fopen("binary_output.bin", "wb");
if(fp == NULL) {
perror("打开文件失败");
return 1;
}
// 使用fwrite写入结构体数据
fwrite(&student1, sizeof(Student), 1, fp);
fwrite(&student2, sizeof(Student), 1, fp);
// 关闭文件
fclose(fp);
printf("二进制数据已写入文件。\n");
return 0;
}
输出:
二进制数据已写入文件。
详细解释:
定义Student结构体,包含姓名、年龄和GPA。
创建两个Student实例student1和student2。
使用fopen函数以二进制写入模式"wb"打开binary_output.bin文件。
使用
fwrite
函数将结构体数据写入文件:
第一个参数是数据的地址(使用&运算符)。第二个参数是每个元素的字节大小(使用sizeof运算符)。第三个参数是要写入的元素数量。第四个参数是文件指针。
关闭文件后,二进制数据被写入到文件中。
读取二进制文件
示例:使用fread读取二进制文件中的结构体数据。
#include
#include
#include
// 定义一个结构体
typedef struct {
char name[50];
int age;
float gpa;
} Student;
int main() {
FILE *fp;
Student student;
int i = 0;
// 以二进制读取模式打开文件
fp = fopen("binary_output.bin", "rb");
if(fp == NULL) {
perror("打开文件失败");
return 1;
}
printf("读取的二进制数据:\n");
// 使用fread读取结构体数据
while(fread(&student, sizeof(Student), 1, fp) == 1) {
printf("学生%d:\n", ++i);
printf(" 姓名: %s\n", student.name);
printf(" 年龄: %d\n", student.age);
printf(" GPA: %.2f\n\n", student.gpa);
}
// 关闭文件
fclose(fp);
return 0;
}
输出:
读取的二进制数据:
学生1:
姓名: 王五
年龄: 22
GPA: 3.90
学生2:
姓名: 赵六
年龄: 23
GPA: 3.70
详细解释:
使用fopen函数以二进制读取模式"rb"打开binary_output.bin文件。
使用
fread
函数逐个读取结构体数据:
第一个参数是数据的存储地址。第二个参数是每个元素的字节大小。第三个参数是要读取的元素数量。第四个参数是文件指针。
通过循环读取所有结构体数据,并打印到控制台。
关闭文件后,所有二进制数据已成功读取并显示。
逐字节读写
示例:使用fwrite和fread逐字节写入和读取数据。
#include
#include
int main() {
FILE *fp;
char data[] = "Hello, Binary World!";
size_t dataSize = sizeof(data); // 包含终止符'\0'
// 写入二进制文件
fp = fopen("byte_output.bin", "wb");
if(fp == NULL) {
perror("打开文件失败");
return 1;
}
fwrite(data, sizeof(char), dataSize, fp);
fclose(fp);
printf("字节数据已写入文件。\n");
// 读取二进制文件
fp = fopen("byte_output.bin", "rb");
if(fp == NULL) {
perror("打开文件失败");
return 1;
}
char buffer[100];
fread(buffer, sizeof(char), dataSize, fp);
fclose(fp);
printf("读取的字节数据: %s\n", buffer);
return 0;
}
输出:
字节数据已写入文件。
读取的字节数据: Hello, Binary World!
详细解释:
定义一个字符串data,包含要写入的数据。使用fwrite函数以字节为单位写入数据到byte_output.bin文件。关闭文件后,使用fread函数以字节为单位读取数据到buffer数组。通过printf函数显示读取的数据。
注意事项
文本模式与二进制模式的区别
:
文本模式:数据以文本形式读写,处理换行符(如Windows上的\r\n)。二进制模式:数据按原始二进制形式读写,不进行任何转换。
跨平台兼容性:在不同操作系统之间传输文件时,注意文本模式下的换行符差异,建议使用二进制模式传输二进制文件。
数据对齐和字节顺序:在处理二进制文件时,注意不同系统的字节顺序(大端或小端),避免数据解释错误。
3 文件指针与文件定位
文件指针是指向文件中当前读取或写入位置的指针。通过文件定位,可以在文件中移动文件指针,进行随机访问操作。这对于需要访问文件中不同位置的数据或修改特定位置的数据非常有用。
文件指针的基本操作
ftell:获取当前文件指针的位置。fseek:移动文件指针到指定位置。rewind:将文件指针重置到文件开头。feof:检测是否到达文件末尾。ferror:检测文件操作中是否发生错误。
fseek 函数
函数原型:
int fseek(FILE *stream, long int offset, int origin);
stream:文件指针。
offset:相对于origin的字节偏移量。
origin
:参考位置,可以是:
SEEK_SET:文件开头。SEEK_CUR:当前位置。SEEK_END:文件末尾。
示例:在二进制文件中随机访问数据。
#include
#include
typedef struct {
int id;
char name[20];
float salary;
} Employee;
int main() {
FILE *fp;
Employee emp;
int i;
// 假设有一个二进制文件包含多个Employee记录
fp = fopen("employees.bin", "rb");
if(fp == NULL) {
perror("打开文件失败");
return 1;
}
// 获取文件大小
fseek(fp, 0, SEEK_END);
long fileSize = ftell(fp);
int numEmployees = fileSize / sizeof(Employee);
rewind(fp); // 重置文件指针到开头
printf("总共有 %d 名员工记录。\n", numEmployees);
// 随机访问第3个员工记录(索引为2)
int targetIndex = 2;
if(targetIndex < numEmployees) {
fseek(fp, targetIndex * sizeof(Employee), SEEK_SET);
fread(&emp, sizeof(Employee), 1, fp);
printf("第 %d 名员工信息:\n", targetIndex + 1);
printf(" ID: %d\n", emp.id);
printf(" 姓名: %s\n", emp.name);
printf(" 薪水: %.2f\n", emp.salary);
} else {
printf("目标索引超出范围。\n");
}
fclose(fp);
return 0;
}
详细解释:
使用fseek和ftell函数获取文件大小,计算员工记录的数量。使用fseek函数移动文件指针到第3个员工记录的位置。使用fread函数读取该记录,并打印员工信息。rewind函数用于将文件指针重置到文件开头,以便后续操作。
ftell 和 rewind 函数
示例:跟踪文件指针的位置。
#include
int main() {
FILE *fp;
char ch;
// 以文本读取模式打开文件
fp = fopen("example.txt", "r");
if(fp == NULL) {
perror("打开文件失败");
return 1;
}
// 读取前三个字符
for(int i = 0; i < 3; i++) {
ch = fgetc(fp);
if(ch == EOF) {
break;
}
putchar(ch);
}
// 获取当前文件指针位置
long pos = ftell(fp);
printf("\n当前文件指针位置: %ld\n", pos);
// 重置文件指针到开头
rewind(fp);
printf("文件指针已重置到开头。\n");
// 再次读取文件内容
printf("重新读取文件内容:\n");
while((ch = fgetc(fp)) != EOF) {
putchar(ch);
}
fclose(fp);
return 0;
}
文件example.txt内容:
Hello, World!
输出:
Hel
当前文件指针位置: 3
文件指针已重置到开头。
重新读取文件内容:
Hello, World!
详细解释:
使用fgetc函数读取文件中的前三个字符,并打印到控制台。使用ftell函数获取当前文件指针的位置(3)。使用rewind函数将文件指针重置到文件开头。再次使用fgetc函数读取整个文件内容,并打印到控制台。
注意事项
文件指针越界:使用fseek时,确保offset和origin的组合不会导致文件指针越出文件范围,否则可能导致未定义行为。文本模式下的fseek限制:在某些系统中,文本模式下的fseek可能不支持SEEK_END或其他偏移方式,尤其是在Windows系统上。文件关闭后操作:在调用fseek或其他文件定位函数之前,确保文件已经成功打开,并在关闭文件后避免对文件指针进行任何操作。
4 文件操作中的错误处理
在进行文件操作时,可能会遇到各种错误,如文件不存在、权限不足、磁盘空间不足等。有效的错误处理可以提高程序的健壮性和用户体验。
常见的错误处理函数
perror:打印最近一次标准库函数调用失败的错误信息。feof:检查是否到达文件末尾。ferror:检查文件操作中是否发生错误。
使用perror进行错误处理
示例:使用perror输出错误信息。
#include
int main() {
FILE *fp;
// 尝试以只读模式打开一个不存在的文件
fp = fopen("nonexistent.txt", "r");
if(fp == NULL) {
perror("打开文件失败"); // 输出: 打开文件失败: No such file or directory
return 1;
}
fclose(fp);
return 0;
}
输出:
打开文件失败: No such file or directory
详细解释:
尝试打开一个不存在的文件,fopen返回NULL。使用perror函数输出错误信息,提供更详细的错误描述。
使用feof和ferror进行错误检测
示例:检查文件是否到达末尾或是否发生错误。
#include
int main() {
FILE *fp;
int ch;
// 打开文件
fp = fopen("example.txt", "r");
if(fp == NULL) {
perror("打开文件失败");
return 1;
}
// 读取文件内容
while((ch = fgetc(fp)) != EOF) {
putchar(ch);
}
// 检查是否因为到达文件末尾而退出循环
if(feof(fp)) {
printf("\n已成功到达文件末尾。\n");
}
// 检查是否因为错误而退出循环
if(ferror(fp)) {
perror("读取文件时发生错误");
}
fclose(fp);
return 0;
}
输出(文件example.txt内容):
Hello, World!
已成功到达文件末尾。
详细解释:
使用fgetc函数逐字符读取文件内容,直到遇到EOF。使用feof函数检查是否到达文件末尾。使用ferror函数检查是否在读取过程中发生错误。关闭文件后,输出读取结果和错误检测信息。
处理读取和写入错误
示例:在读取和写入过程中处理错误。
#include
#include
int main() {
FILE *fp;
char buffer[100];
// 打开文件以读取
fp = fopen("example.txt", "r");
if(fp == NULL) {
perror("打开文件失败");
return 1;
}
// 读取文件内容
if(fgets(buffer, sizeof(buffer), fp) == NULL) {
if(feof(fp)) {
printf("文件为空或已到达文件末尾。\n");
} else if(ferror(fp)) {
perror("读取文件时发生错误");
}
fclose(fp);
return 1;
}
printf("读取的内容: %s\n", buffer);
// 关闭文件
if(fclose(fp) != 0) {
perror("关闭文件失败");
return 1;
}
return 0;
}
输出(文件example.txt内容):
读取的内容: Hello, World!
详细解释:
使用fgets函数读取文件内容,并检查返回值是否为NULL。如果fgets返回NULL,使用feof和ferror函数判断原因。根据错误类型输出相应的错误信息。
综合示例:读取和写入文件,进行错误处理
示例:将一个文件的内容复制到另一个文件,同时处理可能的错误。
#include
#include
int main() {
FILE *src, *dest;
int ch;
// 打开源文件以读取
src = fopen("source.txt", "r");
if(src == NULL) {
perror("无法打开源文件");
return 1;
}
// 打开目标文件以写入
dest = fopen("destination.txt", "w");
if(dest == NULL) {
perror("无法打开目标文件");
fclose(src); // 关闭已打开的源文件
return 1;
}
// 复制文件内容
while((ch = fgetc(src)) != EOF) {
if(fputc(ch, dest) == EOF) {
perror("写入目标文件时发生错误");
fclose(src);
fclose(dest);
return 1;
}
}
// 检查读取是否因为错误而结束
if(ferror(src)) {
perror("读取源文件时发生错误");
}
// 关闭文件
if(fclose(src) != 0) {
perror("关闭源文件失败");
}
if(fclose(dest) != 0) {
perror("关闭目标文件失败");
}
printf("文件复制完成。\n");
return 0;
}
输出:
文件复制完成。
详细解释:
打开源文件source.txt以读取,目标文件destination.txt以写入。使用fgetc和fputc函数逐字符复制文件内容。在每次写入操作后检查是否发生错误。读取结束后,使用ferror检查是否由于错误导致读取终止。关闭所有打开的文件,处理关闭操作中的可能错误。
注意事项
错误处理的及时性:在每次文件操作后,及时检查返回值,确保程序能够及时处理错误,避免数据损坏或程序崩溃。资源管理:在发生错误时,确保所有已打开的文件都被正确关闭,以避免资源泄漏。用户友好性:通过详细的错误信息输出,帮助用户理解和解决文件操作中遇到的问题。
总结
文件操作是C语言中用于数据持久化存储的重要机制,通过合理使用文件操作函数,程序可以高效地读写数据,管理复杂的数据结构。以下是本节的关键点:
文件的打开与关闭:
使用fopen函数以不同模式打开文件,决定文件的访问权限和操作方式。使用fclose函数关闭文件,释放资源,确保数据完整性。
文件读写操作:
文本文件操作:使用fgetc、fgets、fputc、fputs、fprintf、fscanf等函数处理人类可读的文本数据。二进制文件操作:使用fread、fwrite处理非文本数据,如图像、音频等,保持数据的原始格式。
文件指针与文件定位:
使用ftell、fseek、rewind等函数获取和设置文件指针的位置,实现随机访问文件中的数据。理解文件指针在文件读写过程中的重要性,优化数据访问效率。
文件操作中的错误处理:
使用perror、feof、ferror等函数检测和处理文件操作中的错误。确保在文件操作过程中,程序能够及时响应和处理错误,增强程序的健壮性。