目录
1 文件概述
【文件分类】
从文件功能角度对文件进行分类,通常分为:程序文件、数据文件。
程序文件:包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境后缀为.exe)。
数据文件:数据文件不是代码文件,而是包含程序运行时需要处理或存储的数据的文件。比如程序运行需要从中读取数据的文件,或者输出内容的文件。
【文件标识】
文件名包含3部分:文件路径+文件名主干+文件后缀。例如:c:\code\test.txt
文件标识通常被称为文件名。
2 文件的打开和关闭
2.1 文件指针
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用于存放文件的相关信息(比如:文件的名字、状态及当前位置)。这些信息保存在一个结构体变量中。该结构体由系统声明,名为FILE。
FILE* pf;
//pf是一个指向FILE类型数据的指针变量
每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构变量,并填充其中的信息。但使用者不需要关心细节,只需要通过FILE指针维护该结构体变量。
上述代码中,pf指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件。
2.2 文件的打开与关闭
//打开文件
FILE* fopen(const char* filename, const char* mode);
// 文件名 打开方式
//返回类型是FILE*,要用指针接收
//关闭文件
int fclose(FILE* stream); //成功时返回 0,失败时返回 EOF(通常是 -1)
【常用打开方式】
文件使用方式 | 含义 | if 目标文件不存在 |
---|---|---|
"r" | 只读 | 出错 |
"w" | 只写 | 创建一个新文件 |
"a" | 再文本文件末尾追加 | 创建一个新文件 |
【文件操作示例】
int main() {
FILE* pf = fopen("test.txt", "w"); //位置:当前文件所在目录内
if (pf == NULL) { //打开失败会返回空指针
perror("fopen");
return 1;
}
//文件相关操作
// ......
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
2.3 文件的顺序读写
2.3.1 顺序读写函数
功能 | 函数名 | 适用范围 |
---|---|---|
字符输入函数 | fgetc | 所有输入流 |
字符输出函数 | fputc | 所有输出流 |
文本行输入函数 | fgets | 所有输入流 |
文本行输出函数 | fputs | 所有输出流 |
格式化输入函数 | fscanf | 所有输入流 |
格式化输出函数 | fprintf | 所有输出流 |
二进制输入 | fread | 文件 |
二进制输出 | fwrite | 文件 |
【输入流】:数据从外部流入程序(如:键盘输入等)
【输出流】:数据从程序流向外部(如:数据打印在显示器等)
2.3.2 fputc() & fgetc()
fputc() 和 fgetc() 是C标准库中的两个文件操作函数,用于处理文件中的单个字符读写操作。
【fputc()函数】:用于将单个字符写入指定文件。
int fputc(int character, FILE *stream);
- character:要写入的字符(实际是unsigned char类型值,这里转换成int表示);
- stream:文件指针,指向被操作的文件;
- 返回值:成功——返回写入的字符;失败:返回EOF。
int main() {
FILE* pf = fopen("test.txt", "w");
if (pf == NULL) {
perror("Error opening file");
return 1;
}
for (int i = 0; i < 26; i++) {
fputc('a' + i, pf);
fputc(' ', pf);
}
fclose(pf);
pf = NULL;
return 0;
}
程序运行结果:
【fgetc()函数】:从指定文件读取单个字符。
int fgetc(FILE *stream);
- stream:文件指针,指向代操作文件;
- 返回值:成功——返回读取的字符,失败——返回EOF。
int main() {
FILE* pf = fopen("test.txt", "r");
if (pf == NULL) {
perror("Error opening file");
return 1;
}
int ch = 0; //定义变量接收读取到的字符
while ((ch = fgetc(pf)) != EOF) {
//fgetc读取成功后,文件指针会向后移动,指向下一个字符
printf("%c", ch);
}
fclose(pf);
pf = NULL;
return 0;
}
程序运行结果:a b c d e f g h i j k l m n o p q r s t u v w x y z
2.3.3 fputs() & fgets()
fputs() 和 fgets() 函数用于处理文件中的字符串(写入 or 读取)。
int main() {
FILE* writef = fopen("test2.txt", "w");
if (writef == NULL) {
perror("Error opening file");
return 1;
}
fputs("HELLO WORLD\n", writef);
fputs("HELLO C\n", writef);
fclose(writef);
writef = NULL;
//读取文件
FILE* readf = fopen("test2.txt", "r");
if (readf == NULL) {
perror("Error opening file");
return 1;
}
char arr[100];
while (fgets(arr, 100, readf) != NULL) {
//一次读取100个字符,直到文件结束
printf("%s", arr);
}
fclose(readf);
readf = NULL;
return 0;
}
程序运行结果:
fputs() 和 fgets() 与 fputc() 和 fgetc() 功能类似,但有以下几点区别:
- fputs() 和 fgets()处理字符串;
- fgets() 读取失败或读取完成返回NULL,而非EOF;
- fgets() 如果一次读取的内容长度小于文件内容长度,会在读取结束位置补上'\0'。(如:fgets(arr, 10, readf);只会读取到前9个字符,最后一个补上'\0')
2.3.4 fscanf() & fprintf()
【fscanf()函数】:从指定文件流中读取数据,并按照指定格式进行解析,然后存储到相应变量中。
int fscanf(FILE *stream, const char *format, ...);
- stream:代操作文件指针
- format:指定格式
- 返回值:操作成功——返回输入项数量,操作失败/错误/文件结束——返回EOF
【fprintf()函数】:将格式化数据写入到文件中。
int fprintf(FILE *stream, const char *format, ...);
- stream:代操作文件指针
- format:指定格式
- 返回值:操作成功——返回写入的字符输出,操作失败——返回负值
struct Stu
{
char name[20];
int age;
float score;
};
int main() {
struct Stu s1 = { "haha",18,99.92 };
struct Stu s2 = { "xixi",20,96.57 };
FILE* pf = fopen("test3.txt", "w");
if (pf == NULL) {
perror("Error opening file");
return 1;
}
//格式化数据写入文件
fprintf(pf, "%s %d %f\n", s1.name, s1.age, s1.score);
fprintf(pf, "%s %d %f\n", s2.name, s2.age, s2.score);
rewind(pf); //重置文件指针到文件开头
//格式化数据读取
fscanf(pf, "%s %d %f\n", s1.name, &(s1.age), &(s1.score));
printf("姓名:%s 年龄:%d 分数:%.2f\n", s1.name, s1.age, s1.score);
fscanf(pf, "%s %d %f\n", s2.name, &(s2.age), &(s2.score));
printf("姓名:%s 年龄:%d 分数:%.2f\n", s2.name, s2.age, s2.score);
fclose(pf);
pf = NULL;
return 0;
}
程序运行结果:
2.4 文件的随机读写
2.4.1 fseek
用于在文件中移动文件指针的位置。
int fseek(FILE *stream, long offset, int origin);
- stream:要操作的文件指针
- offset:相对位置的偏移量,以字节为单位
- origin:起始位置
- fseek() 执行成功返回0,失败返回-1
关于origin的取值:
SEEK_SET:从文件开头位置开始计算偏移量
SEEK_CUR:从当前位置开始
SEEK_END:从文件末尾开始
int main() {
FILE* pFile = fopen("example.txt", "r"); // 以只读模式打开文件
if (pFile == NULL) {
perror("Error opening file");
return 1;
}
// 将文件指针移动到第 9 个字符位置
fseek(pFile, 9, SEEK_SET);
int ch = fgetc(pFile); // 读取该位置的字符
printf("Character at position 9: %c\n", ch); // 输出 'n'
// 将文件指针移动到文件末尾的第一个字符
fseek(pFile, -1, SEEK_END);
ch = fgetc(pFile); // 读取文件的最后一个字符
printf("Last character in file: %c\n", ch); // 输出 '.'
// 将文件指针移动到当前位置的前 3 个字符
fseek(pFile, -3, SEEK_CUR);
ch = fgetc(pFile);
printf("Character at current-3: %c\n", ch); // 输出 'l'
fclose(pFile);
return 0;
}
2.4.2 ftell
返回文件指针当前相对于起始位置的偏移量。
long int ftell ( FILE * stream );
int main()
{
FILE* pFile;
pFile = fopen("example.txt", "w");
if (pFile == NULL) {
perror("Error opening file");
return 1;
}
fputs("This is an apple.", pFile); //T是第0个字符
fseek(pFile, 9, SEEK_SET);
printf("%d\n", ftell(pFile)); //9
fclose(pFile);
pFile = NULL;
return 0;
}
2.4.3 rewind
让文件指针回到文件的起始位置。
void rewind ( FILE * stream );
2.5 文件读取结束的判定
2.5.1 文件流的“状态”
FILE类型指针不仅仅指向文件,还维护文件操作的相关信息,包括文件状态标志。
这些标志记录了文件流当前的状态:是否与到文件末尾(EOF)、是否出现错误(如硬盘故障等)。
文件操作相关函数的返回值中会记录不同的文件状态。
2.5.2 文件读取结束的判定
因此,在文件读取的过程中,直接使用 feof() 函数判断是否结束并不可靠,更好的方法时检查文件操作函数(如:fgetc()、fgets()等)的返回值,区分具体状态。
feof() 用于确认读取是否因为文件结束而停止
ferror() 用于确认读取是否因为出现错误而停止
【文本文件读取结束判定】
int main(void)
{
int c; // 注意:int,非char,要求处理EOF
FILE* fp = fopen("test.txt", "r");
if (!fp) {
perror("File opening failed");
return 1;
}
//fgetc 当读取失败的时候或者遇到文件结束的时候,都会返回EOF
while ((c = fgetc(fp)) != EOF)
{
putchar(c);
}
//判断是什么原因结束的
if (ferror(fp))
puts("I/O error when reading");
else if (feof(fp))
puts("End of file reached successfully");
fclose(fp);
}
【二进制文件读取结束判定】
enum { SIZE = 5 };
int main(void)
{
double a[SIZE] = { 1.,2.,3.,4.,5. };
FILE* fp = fopen("test.bin", "wb"); // 必须用二进制模式
fwrite(a, sizeof * a, SIZE, fp); // 写 double 的数组
fclose(fp);
double b[SIZE];
fp = fopen("test.bin", "rb");
size_t ret_code = fread(b, sizeof * b, SIZE, fp); // 读 double 的数组
if (ret_code == SIZE) {
puts("Array read successfully, contents: ");
for (int n = 0; n < SIZE; ++n) printf("%f ", b[n]);
putchar('\n');
}
else { // error handling
if (feof(fp))
printf("Error reading test.bin: unexpected end of file\n");
else if (ferror(fp)) {
perror("Error reading test.bin");
}
}
fclose(fp);
}
- 通过fread()返回值与期望读取的数目对比判断是否达读取成功
- 结合feof()和ferror()进一步确认状态
2.6 文件缓冲区
【缓冲机制】
- 写操作:当你向文件中写入数据时,这些数据首先会被写入到内存中的缓冲区,而不是立即写入磁盘。这是为了提高性能,避免频繁的磁盘操作。
- 读操作:在从文件读取数据时,系统会先将数据从磁盘读取到内存中的缓冲区,然后再从缓冲区传输到程序使用的变量中。
- 缓冲区的大小根据C编译系统决定的。
【缓冲区优点】
- 提升性能: 通过减少与磁盘的直接交互,缓冲机制显著提高了文件读写的效率。
- 减少I/O操作:在很多情况下,程序会一次性读取或写入大量数据,而不是逐字节或逐行进行操作。
【刷新缓冲区】
- fclose() :关闭文件时,fclose() 也会自动刷新缓冲区。这意味着在关闭文件之前,所有在缓冲区中的数据都会被写入到磁盘。
因此: 由于缓冲机制,数据并不总是立即写入文件,因此在处理文件时要小心。如果你的程序在写入数据后没有刷新缓冲区或关闭文件,可能导致数据丢失。