C语言结构体在文件I/O中的读写操作
C语言结构体在文件I/O中的读写操作基础
结构体基础回顾
在C语言中,结构体是一种用户自定义的数据类型,它允许将不同类型的数据组合在一起。例如,我们要描述一个学生的信息,可能包含姓名(字符串)、年龄(整数)和成绩(浮点数),就可以使用结构体来实现。
struct Student {
char name[50];
int age;
float score;
};
这里定义了一个名为Student
的结构体,它包含三个成员:name
(字符数组用于存储字符串)、age
(整数)和score
(浮点数)。
文件I/O基础
在C语言中,文件操作主要通过标准输入输出库<stdio.h>
来实现。文件I/O操作的基本流程通常是打开文件、进行读写操作,最后关闭文件。
- 打开文件:使用
fopen
函数,它的原型为FILE *fopen(const char *filename, const char *mode)
。filename
是要打开的文件名,mode
指定打开文件的方式,如"r"
表示只读,"w"
表示只写(如果文件存在则覆盖,不存在则创建),"a"
表示追加等。例如:
FILE *file = fopen("example.txt", "w");
if (file == NULL) {
perror("Failed to open file");
return 1;
}
这里尝试以只写模式打开example.txt
文件,如果打开失败,fopen
返回NULL
,并通过perror
打印错误信息。
- 关闭文件:使用
fclose
函数,原型为int fclose(FILE *stream)
。关闭文件可以释放系统资源,避免数据丢失。例如:
int result = fclose(file);
if (result != 0) {
perror("Failed to close file");
return 1;
}
fclose
返回0表示成功关闭,非零表示失败。
- 基本的读写函数
- 字符读写:
fputc
用于向文件写入一个字符,fgetc
用于从文件读取一个字符。 - 字符串读写:
fputs
用于向文件写入一个字符串,fgets
用于从文件读取一行字符串。
- 字符读写:
结构体在文件I/O中的写入操作
按成员逐个写入
一种简单的将结构体写入文件的方式是按结构体的成员逐个写入。以之前定义的Student
结构体为例:
#include <stdio.h>
#include <string.h>
struct Student {
char name[50];
int age;
float score;
};
int main() {
struct Student student = {"Alice", 20, 85.5};
FILE *file = fopen("students.txt", "w");
if (file == NULL) {
perror("Failed to open file");
return 1;
}
// 按成员逐个写入
fputs(student.name, file);
fputc('\n', file);
fprintf(file, "%d\n", student.age);
fprintf(file, "%.2f\n", student.score);
fclose(file);
return 0;
}
在这个例子中,我们先将学生的姓名用fputs
写入文件,并添加一个换行符。然后使用fprintf
分别将年龄和成绩以格式化的方式写入文件,每行一个数据。这种方式的优点是数据在文件中以文本形式存储,可读性强,方便查看和编辑。但缺点也很明显,读取时需要按照相同的格式解析,并且如果结构体成员较多,代码会显得繁琐。
使用fwrite
整体写入
fwrite
函数可以将一块内存中的数据直接写入文件,非常适合写入结构体。fwrite
的原型为size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)
,其中ptr
是指向要写入数据的指针,size
是每个数据项的大小,nmemb
是数据项的数量,stream
是文件指针。
#include <stdio.h>
struct Student {
char name[50];
int age;
float score;
};
int main() {
struct Student student = {"Bob", 22, 90.0};
FILE *file = fopen("students.bin", "wb");
if (file == NULL) {
perror("Failed to open file");
return 1;
}
// 使用fwrite整体写入结构体
size_t result = fwrite(&student, sizeof(struct Student), 1, file);
if (result != 1) {
perror("Failed to write to file");
fclose(file);
return 1;
}
fclose(file);
return 0;
}
这里我们以二进制写入模式"wb"
打开文件,然后使用fwrite
将整个student
结构体写入文件。fwrite
返回成功写入的数据项数量,如果返回值不等于nmemb
(这里是1),则表示写入失败。使用fwrite
写入结构体的优点是简单高效,适合大量结构体数据的写入。但缺点是数据以二进制形式存储,直接查看文件内容时可读性差。
结构体在文件I/O中的读取操作
按成员逐个读取
与按成员逐个写入相对应,我们也可以按成员逐个从文件中读取数据来填充结构体。假设文件students.txt
是按之前按成员逐个写入的方式生成的:
#include <stdio.h>
#include <string.h>
struct Student {
char name[50];
int age;
float score;
};
int main() {
struct Student student;
FILE *file = fopen("students.txt", "r");
if (file == NULL) {
perror("Failed to open file");
return 1;
}
// 按成员逐个读取
fgets(student.name, sizeof(student.name), file);
student.name[strcspn(student.name, "\n")] = '\0'; // 去掉换行符
fscanf(file, "%d", &student.age);
fscanf(file, "%f", &student.score);
fclose(file);
printf("Name: %s\n", student.name);
printf("Age: %d\n", student.age);
printf("Score: %.2f\n", student.score);
return 0;
}
在这个例子中,我们使用fgets
读取学生的姓名,并通过strcspn
去掉换行符。然后使用fscanf
分别读取年龄和成绩。这种方式在读取文本格式存储的结构体数据时比较方便,但如果文件格式不正确,可能会导致读取错误。
使用fread
整体读取
与fwrite
对应的读取函数是fread
,它的原型为size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream)
,参数含义与fwrite
类似,只不过它是从文件读取数据到内存。假设文件students.bin
是使用fwrite
写入的:
#include <stdio.h>
struct Student {
char name[50];
int age;
float score;
};
int main() {
struct Student student;
FILE *file = fopen("students.bin", "rb");
if (file == NULL) {
perror("Failed to open file");
return 1;
}
// 使用fread整体读取结构体
size_t result = fread(&student, sizeof(struct Student), 1, file);
if (result != 1) {
perror("Failed to read from file");
fclose(file);
return 1;
}
fclose(file);
printf("Name: %s\n", student.name);
printf("Age: %d\n", student.age);
printf("Score: %.2f\n", student.score);
return 0;
}
这里我们以二进制读取模式"rb"
打开文件,使用fread
将文件中的数据一次性读取到student
结构体中。同样,fread
返回成功读取的数据项数量,如果不等于nmemb
,则表示读取失败。
处理多个结构体的文件I/O
写入多个结构体
当需要处理多个结构体时,无论是使用按成员逐个写入还是fwrite
整体写入,都只需要在循环中进行相应操作即可。以下是使用fwrite
写入多个学生结构体的示例:
#include <stdio.h>
struct Student {
char name[50];
int age;
float score;
};
int main() {
struct Student students[] = {
{"Charlie", 21, 88.0},
{"David", 23, 92.5}
};
int numStudents = sizeof(students) / sizeof(students[0]);
FILE *file = fopen("students_list.bin", "wb");
if (file == NULL) {
perror("Failed to open file");
return 1;
}
for (int i = 0; i < numStudents; i++) {
size_t result = fwrite(&students[i], sizeof(struct Student), 1, file);
if (result != 1) {
perror("Failed to write to file");
fclose(file);
return 1;
}
}
fclose(file);
return 0;
}
在这个例子中,我们定义了一个students
数组,包含两个学生结构体。通过循环,使用fwrite
将每个学生结构体写入文件。
读取多个结构体
读取多个结构体同样可以在循环中使用fread
来实现。假设文件students_list.bin
是按上述方式写入的:
#include <stdio.h>
struct Student {
char name[50];
int age;
float score;
};
int main() {
struct Student student;
FILE *file = fopen("students_list.bin", "rb");
if (file == NULL) {
perror("Failed to open file");
return 1;
}
while (fread(&student, sizeof(struct Student), 1, file) == 1) {
printf("Name: %s\n", student.name);
printf("Age: %d\n", student.age);
printf("Score: %.2f\n", student.score);
}
fclose(file);
return 0;
}
这里使用while
循环,只要fread
成功读取一个结构体(返回值为1),就打印该结构体的信息。当fread
返回值不为1时(表示到达文件末尾或读取错误),循环结束。
结构体文件I/O中的注意事项
结构体对齐
在C语言中,结构体成员在内存中的存储存在对齐规则。不同的编译器和平台可能有不同的对齐方式,这会影响结构体的实际大小。例如,在某些平台上,为了提高内存访问效率,结构体成员会按照一定的字节数对齐。假设我们有如下结构体:
struct Example {
char c;
int i;
};
理论上,char
类型占1个字节,int
类型占4个字节,该结构体大小应该是5个字节。但实际上,由于对齐规则,在一些平台上,struct Example
的大小可能是8个字节,因为int
类型成员i
需要对齐到4字节边界,所以c
后面会填充3个字节。
在进行文件I/O操作时,如果涉及到结构体的写入和读取,尤其是在不同平台之间传输数据时,需要注意结构体对齐问题。一种解决方法是使用#pragma pack
指令来指定结构体的对齐方式。例如:
#pragma pack(push, 1)
struct Example {
char c;
int i;
};
#pragma pack(pop)
这里#pragma pack(push, 1)
表示将结构体对齐方式设置为1字节对齐,#pragma pack(pop)
则恢复原来的对齐方式。这样,struct Example
的大小就会是5个字节,无论在什么平台上都能保证一致的存储方式,有利于文件I/O操作的跨平台兼容性。
文件指针位置
在进行文件I/O操作时,文件指针的位置非常重要。每次读写操作后,文件指针会自动移动到下一个数据位置。例如,使用fwrite
写入一个结构体后,文件指针会移动到结构体数据之后。如果需要在文件的特定位置进行读写操作,可以使用fseek
函数来调整文件指针的位置。
fseek
的原型为int fseek(FILE *stream, long offset, int whence)
,其中stream
是文件指针,offset
是偏移量,whence
指定偏移的起始位置。whence
可以取值SEEK_SET
(从文件开头偏移)、SEEK_CUR
(从当前位置偏移)、SEEK_END
(从文件末尾偏移)。
假设我们已经向文件写入了多个结构体,现在想要读取文件中间某个结构体,可以先使用fseek
移动文件指针到指定位置:
#include <stdio.h>
struct Student {
char name[50];
int age;
float score;
};
int main() {
struct Student student;
FILE *file = fopen("students_list.bin", "rb");
if (file == NULL) {
perror("Failed to open file");
return 1;
}
// 假设要读取第3个结构体(从0开始计数)
int structIndex = 2;
fseek(file, structIndex * sizeof(struct Student), SEEK_SET);
size_t result = fread(&student, sizeof(struct Student), 1, file);
if (result != 1) {
perror("Failed to read from file");
} else {
printf("Name: %s\n", student.name);
printf("Age: %d\n", student.age);
printf("Score: %.2f\n", student.score);
}
fclose(file);
return 0;
}
在这个例子中,我们使用fseek
将文件指针移动到第3个结构体的位置(从文件开头偏移structIndex * sizeof(struct Student)
字节),然后使用fread
读取该结构体。
错误处理
在文件I/O操作中,错误处理至关重要。无论是打开文件、读写文件还是关闭文件,都可能发生错误。之前的示例中已经多次展示了使用perror
函数来打印错误信息。除了perror
,还可以使用ferror
函数来检查文件操作是否出错。ferror
的原型为int ferror(FILE *stream)
,如果文件操作出错,它返回非零值,否则返回0。
例如,在每次读写操作后,可以使用ferror
检查是否出错:
#include <stdio.h>
struct Student {
char name[50];
int age;
float score;
};
int main() {
struct Student student = {"Eve", 24, 95.0};
FILE *file = fopen("students.bin", "wb");
if (file == NULL) {
perror("Failed to open file");
return 1;
}
size_t result = fwrite(&student, sizeof(struct Student), 1, file);
if (result != 1 || ferror(file)) {
perror("Failed to write to file");
fclose(file);
return 1;
}
fclose(file);
return 0;
}
这里在fwrite
操作后,不仅检查了fwrite
的返回值,还使用ferror
检查是否有错误发生。如果有错误,通过perror
打印错误信息并进行相应处理。
结构体嵌套在文件I/O中的应用
结构体嵌套的定义
在C语言中,结构体可以嵌套,即一个结构体的成员可以是另一个结构体类型。例如,我们要描述一个教师的信息,教师除了基本的姓名、年龄,还可能有一个办公地址,而办公地址又可以用一个结构体来描述:
struct Address {
char street[100];
char city[50];
int zipCode;
};
struct Teacher {
char name[50];
int age;
struct Address address;
};
这里struct Teacher
结构体包含了一个struct Address
类型的成员address
。
嵌套结构体的写入操作
对于嵌套结构体的写入,同样可以使用fwrite
或按成员逐个写入的方式。使用fwrite
时,由于fwrite
是按内存块进行写入,只要保证结构体定义的一致性,就可以直接写入整个嵌套结构体。
#include <stdio.h>
struct Address {
char street[100];
char city[50];
int zipCode;
};
struct Teacher {
char name[50];
int age;
struct Address address;
};
int main() {
struct Teacher teacher = {
"Mr. Smith",
35,
{"123 Main St", "Anytown", 12345}
};
FILE *file = fopen("teachers.bin", "wb");
if (file == NULL) {
perror("Failed to open file");
return 1;
}
size_t result = fwrite(&teacher, sizeof(struct Teacher), 1, file);
if (result != 1) {
perror("Failed to write to file");
fclose(file);
return 1;
}
fclose(file);
return 0;
}
在这个例子中,我们定义了一个teacher
结构体变量,并使用fwrite
将其写入文件。
嵌套结构体的读取操作
读取嵌套结构体也可以使用fread
。假设文件teachers.bin
是按上述方式写入的:
#include <stdio.h>
struct Address {
char street[100];
char city[50];
int zipCode;
};
struct Teacher {
char name[50];
int age;
struct Address address;
};
int main() {
struct Teacher teacher;
FILE *file = fopen("teachers.bin", "rb");
if (file == NULL) {
perror("Failed to open file");
return 1;
}
size_t result = fread(&teacher, sizeof(struct Teacher), 1, file);
if (result != 1) {
perror("Failed to read from file");
fclose(file);
return 1;
}
printf("Name: %s\n", teacher.name);
printf("Age: %d\n", teacher.age);
printf("Address: %s, %s, %d\n", teacher.address.street, teacher.address.city, teacher.address.zipCode);
fclose(file);
return 0;
}
这里使用fread
将文件中的数据读取到teacher
结构体中,并打印出教师的信息,包括嵌套的地址信息。
结构体与动态内存分配在文件I/O中的结合
结构体中使用动态内存
在某些情况下,结构体的成员可能需要使用动态内存分配。例如,我们要描述一个包含可变长度字符串的学生信息,就可以使用字符指针并动态分配内存:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct Student {
char *name;
int age;
float score;
};
在使用这个结构体时,需要为name
成员分配内存:
struct Student student;
student.name = (char *)malloc(50 * sizeof(char));
strcpy(student.name, "Tom");
student.age = 20;
student.score = 80.0;
写入包含动态内存的结构体
当将包含动态内存分配的结构体写入文件时,不能简单地使用fwrite
,因为fwrite
只会写入指针的值,而不会写入指针指向的动态分配的内存中的数据。一种解决方法是先写入字符串的长度,然后写入字符串内容,再写入其他结构体成员。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct Student {
char *name;
int age;
float score;
};
int main() {
struct Student student;
student.name = (char *)malloc(50 * sizeof(char));
strcpy(student.name, "Tom");
student.age = 20;
student.score = 80.0;
FILE *file = fopen("students_dynamic.bin", "wb");
if (file == NULL) {
perror("Failed to open file");
free(student.name);
return 1;
}
int nameLength = strlen(student.name);
fwrite(&nameLength, sizeof(int), 1, file);
fwrite(student.name, sizeof(char), nameLength, file);
fwrite(&student.age, sizeof(int), 1, file);
fwrite(&student.score, sizeof(float), 1, file);
free(student.name);
fclose(file);
return 0;
}
在这个例子中,我们先写入name
字符串的长度,然后写入字符串内容,最后写入年龄和成绩。
读取包含动态内存的结构体
读取包含动态内存分配的结构体时,要按照写入的顺序,先读取字符串长度,然后根据长度分配内存并读取字符串内容,最后读取其他成员。
#include <stdio.h>
#include <stdlib.h>
struct Student {
char *name;
int age;
float score;
};
int main() {
struct Student student;
FILE *file = fopen("students_dynamic.bin", "rb");
if (file == NULL) {
perror("Failed to open file");
return 1;
}
int nameLength;
fread(&nameLength, sizeof(int), 1, file);
student.name = (char *)malloc((nameLength + 1) * sizeof(char));
fread(student.name, sizeof(char), nameLength, file);
student.name[nameLength] = '\0';
fread(&student.age, sizeof(int), 1, file);
fread(&student.score, sizeof(float), 1, file);
printf("Name: %s\n", student.name);
printf("Age: %d\n", student.age);
printf("Score: %.2f\n", student.score);
free(student.name);
fclose(file);
return 0;
}
这里我们先读取字符串长度,然后分配足够的内存来存储字符串,读取字符串后添加字符串结束符'\0'
,最后读取年龄和成绩。
通过以上内容,我们详细介绍了C语言结构体在文件I/O中的读写操作,包括基础操作、处理多个结构体、注意事项以及结构体嵌套和与动态内存分配的结合应用。希望这些知识能帮助你在实际编程中更好地处理结构体与文件I/O相关的任务。