1. 为什么使用文件
2. 什么是文件
磁盘上的文件是文件。
但是在程序设计中,我们一般谈的文件有两种:程序文件、数据文件(从文件功能的角度来分类的)。
2.1 程序文件
包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境 后缀为.exe)。
2.2 数据文件
文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件, 或者输出内容的文件。
在以前各章所处理数据的输入输出都是以终端为对象的,即从终端的键盘输入数据,运行结果显示到显 示器上。其实有时候我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使用,这里处理 的就是磁盘上文件。
2.3 文件名
- 1、一个文件要有一个唯一的文件标识,以便用户识别和引用。
- 2、文件名包含3部分:文件路径+文件名主干+文件后缀,例如: c:\code\test.txt
- 3、为了方便起见,文件标识常被称为文件名。
3. 文件的打开和关闭
3.1 文件指针
3.2 文件的打开和关闭
- 1、文件在读写之前应该先打开文件,在使用结束之后应该关闭文件。
- 2、在编写程序的时候,在打开文件的同时,都会返回一个FILE*的指针变量指向该文件,也相当于建立了指 针和文件的关系。
- 3、ANSIC 规定使用fopen函数来打开文件,fclose来关闭文件。
//打开文件:FILE * fopen ( const char * filename, const char * mode );
- 1、filename 是C字符串,包含要打开的文件的名称。其值应遵循运行环境的文件名规范,并可以包括路径(如果系统支持)。
- 2、mode 是文件的打开模式
//关闭文件:int fclose ( FILE * stream );
- stream 是指向指定要关闭的流的FILE对象的指针。
#include <stdio.h>
int main()
{
//下列是绝对路径,相对路径是在当前目录下
FILE* pf = fopen("D:\\2021_code\\class101\\test_7_20\\test.dat", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写文件
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
打开方式:
4. 文件的顺序读写
4.1 fgetc与fputc
int fputc(int character, FILE* stream);
将字符写入流
- character 是要写入的字符并进行int提升。写入时,该值会在内部转换为无符号字符。
- stream 是指向标识输出流的FILE对象的指针。
- 向流中写入一个字符并推进位置指示器。字符被写入流的内部位置是指示器所指示的位置,然后该指示器自动前进一个。
- 一旦成功,所写的字符就会返回。
- 如果发生写入错误,则返回EOF并设置错误指示器(ferror)。
int fgetc(FILE* stream);
从流中获取字符
- stream 是指向标识输入流的FILE对象的指针。
- 成功后,将返回读取的字符,返回类型为int
- 返回指定流的内部文件位置指示器当前指向的字符。然后,内部文件位置指示器前进到下一个字符。
- 如果调用时流位于文件末尾,则函数返回EOF并设置流的文件末尾指示符(feof)。
- 如果发生读取错误,函数将返回EOF并设置流的错误指示器(ferror)。
#include <stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "w");//用'w'写数据时,会销毁上次使用'w'写入的数据
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写文件
char i = 0;
for (i = 'a'; i <= 'z'; i++)
{
fputc(i, pf);
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
#include <stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读文件
//int ch = fgetc(pf);
//printf("%c ", ch);
//ch = fgetc(pf);
//printf("%c ", ch);
//ch = fgetc(pf);
//printf("%c ", ch);
int ch = 0;
while ((ch = fgetc(pf)) != EOF)//读取失败返回EOF
{
printf("%c ", ch);
//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
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
4.2 fgets与fputs
int fputs(const char* str, FILE* stream);
将str指向的C字符串写入流。
- str 是C字符串,其中包含要写入流的内容。
- stream 是指向标识输出流的FILE对象的指针。
- 函数开始从指定的地址(str)复制,直到到达终止的null字符(“\0”)。此终止null字符不会复制到流中。
- 成功后,将返回一个非负值。
- 出现错误时,函数返回EOF并设置错误指示器(ferror)。
char* fgets(char* str, int num, FILE* stream);
从流中获取字符串
- str 是指向将读取的字符串复制到其中的字符数组的指针。
- num 是要复制到str中的最大字符数(包括终止的null字符(\0))。
- stream 是指向标识输入流的FILE对象的指针。
- 从流中读取字符,并将其作为C字符串存储到str中,直到读取了(num-1)个字符,或者到达换行符或文件末尾,以先发生的为准。
- 在复制到str的字符之后,会自动附加一个终止的null字符(\0)。
- 换行符(\n)会使fgets停止读取,但函数会将其视为有效字符,并将其包含在复制到str的字符串中。
- 成功后,函数返回str。
- 如果在尝试读取字符时遇到文件结尾,则设置eof指示符(feof)。如果在读取任何字符之前发生这种情况,则返回的指针为空指针(NULL)(str的内容保持不变)。
- 如果发生读取错误,则会设置错误指示器(ferror),并返回一个空指针(NULL) (但str指向的内容可能已更改)。
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写一行数据
fputs("hello bit\n", pf);
fputs("hello world\n", pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读一行数据
//将pf里一行的(num-1)个数据(后面再补充一个\0)放进arr数组中
char arr[20];
fgets(arr, 4, pf);
printf("%s\n", arr);
fgets(arr, 20, pf);//最多只会打印一行的数据
printf("%s\n", arr);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
4.3 fscanf与fprintf
int fprintf(FILE* stream, const char* format, ...);
将格式化数据写入流
- stream 是指向标识输出流的FILE对象的指针。
- format 是C字符串,其中包含要写入流的文本。它可以选择性地包含嵌入的格式说明符,这些说明符被后续附加参数中指定的值替换,并根据请求进行格式化。
- 将格式化指向的C字符串写入流。如果format包含格式说明符(以%开头的子序列),则格式化后面的其他参数,并将其插入到结果字符串中,以替换其各自的说明符。
- 在format参数之后,函数需要至少与format指定的参数一样多的附加参数。
int fscanf(FILE* stream, const char* format, ...);
从流中读取格式化数据
- stream 是指向FILE对象的指针,该对象标识要从中读取数据的输入流。
- format 是C字符串,包含一系列字符,这些字符控制如何处理从流中提取的字符:
- 从流中读取数据,并根据参数格式将其存储到附加参数所指向的位置。
- 附加参数应指向由格式字符串中相应的格式说明符指定的类型的已分配对象。
//流
//FILE*
//scanf
//printf
//任何一个C程序,只要运行起来就会默认打开3个流
//FILE* stdin - 标准输入流(键盘)
//FILE* stdout - 标准输出流(屏幕)
//FILE* stderr - 标准错误流(屏幕)
#include<stdio.h>
struct S
{
char arr[10];
int num;
float sc;
};
int main()
{
struct S s = { "abcdef", 10, 5.5f };
//对格式化的数据进行写文件
FILE*pf = fopen("test.txt", "w");
if (NULL == pf)
{
perror("fopen");
return 1;
}
//写文件
//将s里的文件写入pf中去
fprintf(pf, "%s %d %f", s.arr, s.num, s.sc);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
#include<stdio.h>
struct S
{
char arr[10];
int num;
float sc;
};
int main()
{
struct S s = { 0 };
//对格式化的数据进行读文件
FILE* pf = fopen("test.txt", "r");
if (NULL == pf)
{
perror("fopen");
return 1;
}
//读文件
//读取pf里的文件,放入s中去
fscanf(pf, "%s %d %f", s.arr, &(s.num), &(s.sc));
//打印
printf("%s %d %f\n", s.arr, s.num, s.sc);
//fprintf(stdout, "%s %d %f\n", s.arr, s.num, s.sc);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
4.4 fread与fwrite
size_t fwrite(const void* ptr, size_t size, size_t count, FILE* stream);
将数据块写入流
- ptr 是指向要写入的元素数组的指针,转换为常量void*。
- size 是要写入的每个元素的大小(以字节为单位)。size_t是一个无符号整数类型。
- count 是元素的数量,每个元素的大小为字节。
- stream 是指向指定输出流的FILE对象的指针。
- 将count个元素的数组从ptr指向的内存块写入流中的当前位置,每个元素的大小为字节。
- 流的位置指示器按写入的总字节数提前。
- 在内部,该函数将ptr指向的块解释为无符号char类型的(size*count)元素数组,并将它们按顺序写入流,就像为每个字节调用fputc一样。
- 返回成功写入的元素总数。
- 如果此数字与计数参数不同,则写入错误会阻止函数完成。在这种情况下,将为流设置错误指示器(ferror)。如果大小或计数为零,函数将返回零,并且错误指示器保持不变。
size_t fread(void* ptr, size_t size, size_t count, FILE* stream);
从流中读取数据块
- ptr 是指向大小至少为(size*count)字节的内存块的指针,该内存块已转换为void*。
- size 是要读取的每个元素的大小(以字节为单位),size_t是一个无符号整数类型。
- count 是元素的数量,每个元素的大小为字节。
- stream 是指向指定输入流的FILE对象的指针。
- 从流中读取count个元素的数组,每个元素的大小为字节,并将它们存储在ptr指定的内存块中。
- 流的位置指示器提前读取的字节总数。
- 如果成功,读取的字节总数为(size*count)。返回成功读取的元素总数。
- 如果此数字与count参数不同,则表示发生读取错误,或者读取时已到达文件末尾。在这两种情况下,都设置了适当的指示器,可以分别用ferror和feof进行检查。
- 如果大小或计数为零,则函数返回零,并且ptr指向的流状态和内容保持不变。
//二进制的写
#include<stdio.h>
struct S
{
char arr[10];
int num;
float sc;
};
int main()
{
struct S s = { "abcde", 10, 5.5f };
//二进制的形式写
FILE*pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写文件
//写入一个struct S类型大小的数据放到pf里
fwrite(&s, sizeof(struct S), 1, pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
//fread读取
#include<stdio.h>
struct S
{
char arr[10];
int num;
float sc;
};
int main()
{
struct S s = {0};
//二进制的形式读
FILE*pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读文件
fread(&s, sizeof(struct S), 1, pf);
printf("%s %d %f\n", s.arr, s.num, s.sc);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
4.1 对比一组函数:
scanf / fscanf / sscanf
printf / fprintf / sprintf
#include <stdio.h>
struct S
{
char arr[10];
int age;
float f;
};
int main()
{
struct S s = { "hello", 20, 5.5f };
struct S tmp = { 0 };
char buf[100] = {0};
//sprintf 把一个格式化的数据,转换成字符串
sprintf(buf, "%s %d %f", s.arr, s.age, s.f);
printf("字符串:%s\n", buf);//"hello, 20, 5.5f"
//从字符串buf中获取一个格式化的数据到tmp中
sscanf(buf, "%s %d %f", tmp.arr, &(tmp.age), &(tmp.f));
printf("格式化:%s %d %f\n", tmp.arr, tmp.age, tmp.f);
return 0;
}
5. 文件的随机读写
5.1 fseek
int fseek(FILE* stream, long int offset, int origin);
根据文件指针的位置和偏移量来定位文件指针。
- stream 是指向标识流的FILE对象的指针。
- offset 二进制文件:从原点偏移的字节数。文本文件:要么为零,要么为ftell返回的值。
- origin 是用作偏移参考的位置。它由<cstdio>中定义的以下常量之一指定,专门用作此函数的参数: SEEK_SET 文件开头 SEEK_CUR 文件指针的当前位置 SEEK_END 文件结束*
- 将与流关联的位置指示器设置为新位置。
- 对于以二进制模式打开的流,通过向原点指定的参考位置添加偏移量来定义新位置。
- 对于以文本模式打开的流,偏移量应为零或先前调用ftell返回的值,并且原点必须为SEEK_SET。
5.2 ftell
long int ftell(FILE* stream);
返回文件指针相对于起始位置的偏移量
- stream 是指向标识流的FILE对象的指针。
- 返回流的位置指示器的当前值。
- 对于二进制流,这是从文件开始的字节数。
- 成功后,将返回位置指示器的当前值。
- 失败时,返回-1L,并将errno设置为系统特定的正值。
5.3 rewind
void rewind(FILE* stream);
让文件指针的位置回到文件的起始位置
- stream 是指向标识流的FILE对象的指针。
- 将与流关联的位置指示器设置为文件的开头。
- 成功调用此函数后,将清除与流相关的文件结尾和错误内部指示符,并删除以前调用该流上的ungetc的所有影响。
#include <stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "r");//文件放了"abcdef"
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读取文件
int ch = fgetc(pf);
printf("%c\n", ch);//a
//获取文件位置
int ret = ftell(pf);
printf("%d\n", ret);//1 - 指向b
//调整文件指针
fseek(pf, 2, SEEK_CUR);
ch = fgetc(pf);
printf("%c\n", ch);//d
ret = ftell(pf);
printf("%d\n", ret);//4 - 指向e
fseek(pf, 2, SEEK_SET);
ch = fgetc(pf);
printf("%c\n", ch);//c
ret = ftell(pf);
printf("%d\n", ret);//3 - 指向d
fseek(pf, -2, SEEK_END);
ch = fgetc(pf);
printf("%c\n", ch);//e
ret = ftell(pf);
printf("%d\n", ret);//5 - 指向f
//让文件指针回到起始位置
rewind(pf);
ch = fgetc(pf);
printf("%c\n", ch);//a
ret = ftell(pf);
printf("%d\n", ret);//1 - 指向b
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
6. 文本文件和二进制文件
#include <stdio.h>
int main()
{
int a = 10000;
FILE* pf = fopen("test.txt", "wb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写文件
fwrite(&a, sizeof(int), 1, pf);
fclose(pf);
pf = NULL;
return 0;
}
7. 文件读取结束的判定
7.1 被错误使用的 feof
ferror:int ferror(FILE* stream);
检查错误指示符
- stream 是指向标识流的FILE对象的指针。
- 检查是否设置了与流关联的错误指示器,如果设置了,则返回与零不同的值。
- 在设置了与流相关联的错误指示符的情况下,返回非零值。否则,返回零。
feof:int feof(FILE* stream);
检查文件结尾指示符
- stream 是指向标识流的FILE对象的指针。
- 检查是否设置了与流关联的文件结尾指示符,如果设置了,则返回一个不同于零的值。
- 此指示符通常由先前对试图在文件末尾或超过文件末尾读取的流进行的操作设置。
- 请注意,流的内部位置指示符可能指向下一个操作的文件结尾,但在操作尝试读取该点之前,可能不会设置文件结尾指示符。
7.2 正确使用的例子
文本文件的例子:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int c; // 注意:int,非char,要求处理EOF
FILE* fp = fopen("test.txt", "r");
if (!fp)
{
perror("File opening failed");
return EXIT_FAILURE;
}
//fgetc 当读取失败的时候或者遇到文件结束的时候,都会返回EOF
while ((c = fgetc(fp)) != EOF) // 标准C I/O读取文件循环
{
putchar(c);
}
//判断是什么原因结束的
if (ferror(fp))
puts("I/O error when reading");
else if (feof(fp))
puts("End of file reached successfully");
fclose(fp);
}
二进制文件的例子:
#include <stdio.h>
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);
fp = NULL;
}
8. 文件缓冲区
ANSIC 标准采用“缓冲文件系统”处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序 中每一个正在使用的文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装 满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓 冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根 据C编译系统决定的。
这里可以得出一个结论: 因为有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。 如果不做,可能导致读写文件的问题。
#include <stdio.h>
#include <windows.h>
//VS2013 WIN10环境测试
int main()
{
FILE* pf = fopen("test.txt", "w");
fputs("abcdef", pf);//先将代码放在输出缓冲区
printf("睡眠10秒-已经写数据了,打开test.txt文件,发现文件没有内容\n");
Sleep(10000);
printf("刷新缓冲区\n");
fflush(pf);//刷新缓冲区时,才将输出缓冲区的数据写到文件(磁盘)
//注:fflush 在高版本的VS上不能使用了
printf("再睡眠10秒-此时,再次打开test.txt文件,文件有内容了\n");
Sleep(10000);
fclose(pf);//注:fclose在关闭文件的时候,也会刷新缓冲区
pf = NULL;
return 0;
}