在前面学习结构体和动态开辟函数的时候,我们学习通讯录程序的时候,可以给通讯录增加、删除数据,此时数据是存放在内存中的,退出程序的时候数据就不存在了。那怎么样我们才可以将录入通讯录的信息保存下来,使其持久化呢我们平时一般数据持久化的方法有,把数据存放在磁盘文件、存放到数据库等方式。所以我们可以将数据直接存放在电脑的硬盘上,使数据持久化。
硬盘上的文件是文件。但是在程序中,我们一般谈的文件有两种:程序文件、数据文件(从功能的角度来分类的)。
程序文件包括源程序文件(后缀为
.c
),目标文件(Windows环境后缀为.obj
),可执行程序(Windows环境后缀为.exe
)。
文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。
本章讨论的是数据文件,在以前各章所处理的输入输出都是以终端为对象的,即从终端的“键盘”输入数据,运行结果输出到“显示器”上。其实有时候我们会把信息输出到磁盘上,当需要的时候把数据读取到内存中使用,这里处理的就是磁盘上的文件。
一个文件要有一个唯一的文件标识,以便用户识别和引用。文件包括3部分:文件路径+文件名主干+文件后缀,例如:c:\code\test.txt文件中c:\code为文件路径,test为文件主干,txt为文件后缀。为了方便起见,文件标识常被称为文件名。
缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针“。每个被使用的文件都在内存中开辟了一个文件信息区,用来存放文件的相关信息(比如文件的名字、文件的状态、文件的当前位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是有类型声明的,该结构体类型被typedef关键字重命名为FILE,该结构体定义包含在收stdio.h的头文件中,因此,使用文件的程序都要包含
#include
。
例如,VS2013编译环境提供的stdio.h头文件的文件类型申明:
struct _iobuf {
char* _ptr;
int _cnt;
char* _base;
int _flag;
int _file;
int _charbuf;
int _bufsiz;
char* _tmpfname;
};
typedef struct _iobuf FILE;
不同编译器的FILE类型包含的内容不完全相同,但是大同小异;每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中的信息,我们不需要关心细节,一般都是通过一个FILE的指针来维护这个FILE结构的变量,这样使用起来更加方便。
下面我们来创建一个FILE的指针变量:
FILE*pf;//文件指针变量
定义pf是一个指向FILE类型数据的指针变量,可以使pf指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能访问该信息。也就是说,通过文件指针变量能找到与它关联的文件。✨✨✨
例如:
文件在读写之前应该先打开文件,在使用之后应该关闭文件,ANSIC规定打开文件的函数为fopen
函数、关闭文件的函数为fclose
函数。
FILE* fopen(const char* filename, const char* mode);
头文件为:
#include
参数介绍:
filename为有效的文件名(即文件的路径),是一个字符串。
mode为文件的访问的方式,有r\r+\rb\rb+\w\w+等模式。
函数介绍:
①打开filename文件,如果打开成功返回返回FILE类型的文件指针,打开文件失败,则返回一个空指针;②返回的文件指针在不需要使用的时候(一般为程序末尾),使用
fclose
关闭文件。
打开方式为:
文件的打开方式 | 含义 |
---|---|
“r”(只读) | 已输入的方式打开一个文本文件,文件只能读 |
“r+”(读写) | 为输入/输出打开一个文本文件 |
“rb”(只读) | 已输入的方式打开一个二进制文件,文件只能读 |
“rb+”(读写) | 为输入/输出打开一个二进制文件 |
“w”(只写) | 以输出方式建立一个文本文件,文件只能写 |
“w+”(读写) | 为输入/输出建立一个二进制文件 |
“wb”(只写) | 以输出方式建立一个二进制文件,文件只能写 |
“wb+”(读写) | 为输入/输出建立一个二进制文件 |
“a”(追加) | 向文本文件尾数添加数据,文件只能写 |
“a+”(读写) | 为输入/输出打开一个文本文件 |
“ab”(追加) | 向二进制文件为添加数据,文件只能写 |
“ab+”(读写) | 为输入/输出打开一个二进制文件 |
fopen函数以“r”、“r+”、“rb“、“rb+”等方式打开文件,文件必须存在,否则fopen()函数将返回NULL;fopen函数以 “w”“w+”“wb”“wb+”等方式打开文件,不要求文件一定存在,若文件不存在则创建该文件,如果文件已经存在,则清空文件原有内容。如果打开操作失败则fopen函数返回空指针(NULL)。fopen函数以“a"“a+”“ab”“ab+”等方式打开文件,不要求文件一定存在,若文件不存在则创建该文件,若文件已经存在则只能在原有数据之后添加内容,如果打开操作失败则fopen函数返回空指针(NULL)。
int fclose(FILE* stream);
头文件:
#include
参数介绍:
stream为指向要关闭文件的文件指针
函数介绍:
①将文件指针与文件取消关联(与文件指针相关联的内部缓冲区也取消关联)②写入任何未写入的输出缓冲区的内容,丢弃任何未读入的输入缓冲区的内容,即刷新缓冲区③如果关闭文件成功,则返回零值;如果关闭文件失败,则会返回EOF的值。
fopen函数和fclose函数使用的案例:
#include
int main()
{
FILE* fp = fopen("test.txt", "r");
if (fp == NULL)
{
perror("fopen");
return 1;
}
//读取操作
//...
//关闭文件
fclose(fp);
fp = NULL;
return 0;
}
当前存放源文件的位置没有test.txt文件代码运行的结果为:
在当前源文件添加test.txt文件后代码运行的结果为:
补充: 在上面的代码中我们使用的都是相对路径,而文件的路径包括相对路径和绝对路径。
相对路径: 如果文件在和源文件同一文件中,文件只需要写文件主干和文件后缀即可,如果该文件在源文件的上一级目录中在前面加上..\
(两个点一个斜杠),如果该文件在在源文件的上一级目录的上一级目录中,在前面多加一个..\
,以此类推。.\
(一个点一个斜杠表示与源文件同级目录,一般可以省略)。
绝对路径: 文件路径+文件名主干+文件后缀。
int fputc(int character, FILE* stream);
头文件:
#include
参数介绍:
①character为字符,写入文件指针流,进行整型提升,在内部转化为无符号字符;②stream为指向输出流的文件指针。
函数介绍:
①写入成功后,返回所写入的字符,如果写入错误,返回EOF并可以设置ferror(错误指示器)函数进行判断。
fputc函数的使用案例:
#include
int main()
{
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写文件
char ch = 0;
for (ch = 'a'; ch <= 'z'; ch++)
{
fputc(ch, pf);
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
int fgetc(FILE* stream);
头文件:
#include
参数介绍:
stream: 指向标识输入流的FILE对象的指针。
函数介绍:
①从流中获取字符,返回文件指针流当前指向的字符,然后文件流指针自动后移,指向下一个字符。
②如果函数读取成功,将返回读取的字符(提升为int的值)
③如果调用该函数时,指针移至末尾结束,函数返回EOF到文件指针流中,可以设置feof函数进行判断。
④如果发生读取错误而导致程序结束,函数返回EOF设置ferror函数判断。
fgetc函数的使用案例:
//fgetc函数
#include
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读文件
int ch = 0;
for (ch = 'a'; ch <= 'z'; ch++)
{
ch = fgetc(pf);
printf("%c ", ch);
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
int fputs(const char* str, FILE * stream);
头文件:
#include
参数介绍:
①str为文件指针指向的C字符串②stream为FILE类型输出流的指针。
函数介绍:
①写入从str的地址开始写入字符串,一直到终止字符串
'\0'
②fputs可以指定流进行写入,不会自动写入其他字符,puts只能在标准输出流中写入,会自动添加换行符\n
③如果写入成功,返回非负值,写入失败会返回EOF设置ferror函数进行判断。
fputs函数的使用案例:
#include
int main()
{
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//测试写一行数据
fputs("hello wrold\n", pf);
fclose(pf);
pf = NULL;
return 0;
}
char* fgets(char* str, int num, FILE* stream);
头文件:
#include
参数介绍:
①str指向存储字符串的字符数组指针②num为最大的读取个数,包括终止字符
'\0'
③stream为指向输入流(文件输入流或标准输入流)的文件指针。
函数介绍:
①从输入流中读取字符到str指向的数组中,直到读取到(num-1)个字符或者遇到换行符或者遇到终止字符停止②换行符使fgets停止读取,并拷贝到str指向的字符串中,终止字符会自动附加在str指向的字符串中③与gets相比,
fgets
不仅可以指定读取的流,还可以指定读取的最大字符数,而且会自动加上终止字符。
fgets函数的使用案例:
#include
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//测试读一行数据
char arr[20] = { 0 };
fgets( arr,12,pf);
printf("%s\n", arr);
fclose(pf);
pf = NULL;
return 0;
}
int printf(const char* format, ...);
fprintf函数原型:
int fprintf(FILE* stream, const char* format,...);
参数介绍:
①stream为指向输出流(文件输入流或标准输入流)的文件指针②format为写入的格式
函数介绍:
①将format格式的C字符串写入到输出流中去②如果写入成功,则返回写入的字符总个数,如果写入失败,可以设置ferror函数进行判断并返回负数。
fprintf函数的使用案例:
#include
struct S
{
char name[20];
int age;
double scores;
};
int main()
{
struct S s = { "zhangsan",20,98.5 };
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//格式化写入文件
fprintf(pf, "%s %d %lf", s.name, s.age, s.scores);
fclose(pf);
pf = NULL;
return 0;
}
int scanf(const char* format, ...);
fscanf函数原型:
int fscanf(FILE*stream,const char* format, ...);
头文件:
#include
参数介绍:
①stream为指向读取数据的输入流的FILE类型的指针②format为读取的格式
函数介绍:
①从输入流中,按参数格式存储数据到指定地址的变量中去②读取成功后返回参数列表的项目数,也会由于匹配失败、读取错误或遇到文件末尾减少(甚至为零)③如果发生读取错误或读取到文件末尾,可以设置feof函数(ferror函数)进行判断④如果在读取数据之前,匹配失败、读取错误或遇到文件末尾,则会返回EOF。
fscanf函数的使用案例:
#include
struct S
{
char name[20];
int age;
double scores;
};
int main()
{
struct S s = { 0 };
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//格式化读取文件
fscanf(pf,"%s %d %lf", s.name, &(s.age),&(s.scores));
printf("%s %d %lf\n", s.name,s.age, s.scores);
fclose(pf);
pf = NULL;
return 0;
}
size_t fwrite(const void* ptr, size_t size, size_t count, FILE* stream);
头文件:
#include
参数介绍:
①ptr为指向要写入元素数组的指针②size为每个元素的大小③count为元素的个数④指向输出流的FILE类型的指针。
函数介绍:
①将ptr指向数组的内存块以二进制的形式写入到输出流中去,FILE类型的指针的按总字节数前进②如果写入成功,返回写入的元素总数。如果该数字和count相同,则说明数据成功写入;如果该数字和count不同,则说明函数写入错误,可以设置ferror函数进行判断。
fwrite函数的使用案例:
//测试二进制写入函数
#include
struct S
{
char name[20];
int age;
double scores;
};
int main()
{
struct S s = { "张三",20,98.5 };
FILE* pf = fopen("test.txt", "wb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//以二进制方式写入文件
fwrite(&s, sizeof(struct S), 1, pf);
fclose(pf);
pf = NULL;
return 0;
}
size_t fread(void* ptr, size_t size, size_t count, FILE* stream);
头文件:
#include
参数介绍:
①ptr为指向至少count * size个字节的内存块,转化为
void*
类型②size为每个元素的大小③count为元素的个数④指向输入流的FILE类型的指针。
函数介绍:
①从输出流中读取数据存储到ptr指向的内存中去,FILE类型的指针的按总字节数前进②如果读取成功,返回读取的元素总数。如果该数字和count相同,则说明数据成功读取;如果该数字和count不同,则说明函数读取错误,可以设置ferror函数进行判断。
fread函数的使用案例:
//测试二进制读取函数
#include
struct S
{
char name[20];
int age;
double scores;
};
int main()
{
struct S s = { 0 };
FILE* pf = fopen("test.txt", "rb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//以二进制方式读取文件
fread(&s, sizeof(struct S), 1, pf);
printf("%s %d %lf\n", s.name, s.age, s.scores);
fclose(pf);
pf = NULL;
return 0;
}
功能 | 函数名 | 适用于 |
---|---|---|
字符输出函数 | fputc | 所有输出流 |
字符输入函数 | fgetc | 所有输入流 |
文本行输出函数 | fputs | 所有输出流 |
文本行输入函数 | fgets | 所有输入流 |
格式化输出函数 | fprintf | 所有输出流 |
格式化输入函数 | fscanf | 所有输入流 |
二进制输出函数 | fwrite | 文件 |
二进制输入函数 | fread | 文件 |
1.上面的stream(流)是什么意思呢?首先,我们得知道如果想把数据输出到屏幕、文件、网络等输出设备,由于输出设备不同那么输出方式也不同,会大大增加了程序的复杂度。于是,C语言规定程序中的数据先输入到stream(流)中,再从stream(流)输出到设备中(流到输出设备的方式,已经被封装好了,不需要我们参与),同理,程序读取数据也是从输入流中读取到内存中的。所以进行文件操作的时候,我们需要打开
FILE*
类型的流,才能进行输入、输出操作。而文件操作中的FILE*类型的流,也是如此。
2.大家有没有想过平时我们写C程序的scanf、printf进行读取的时候,没有打开流就可以进行键盘输入、屏幕输出操作,这是因为任何一个C语言程序运行的时候,默认打开3个流:
int sprintf(char* s, const char* format, ...);
头文件:
#include
函数介绍:
sprintf函数的使用案例:
#include
struct S
{
char name[20];
int age;
double scores;
};
int main()
{
struct S s = { "zhangsan",20,98.5 };
char arr[100] = { 0 };
sprintf(arr, "%s %d %lf", s.name, s.age, s.scores);
printf("%s\n",arr);//按照字符串方式打印
return 0;
}
int sscanf(const char* s, const char* format,...);
头文件:
#include
sscanf函数的使用案例:
#include
struct S
{
char name[20];
int age;
double scores;
};
int main()
{
struct S s = { "zhangsan",20,98.5 };
char arr[100] = { 0 };
sprintf(arr, "%s %d %lf", s.name, s.age, s.scores);
printf("%s\n",arr);//按照字符串方式打印
struct S tmp = { 0 };
sscanf(arr, "%s %d %lf", tmp.name, &(tmp.age), &(tmp.scores));
printf("%s %d %lf\n", tmp.name, tmp.age, tmp.scores);
//打印结构体数据
return 0;
}
代码运行的结果:
对比scanf/fscanf/sscanf、printf/fprintf/sprintf函数
scanf
–从键盘中读取格式化的数据到内存(stdin)
printf
–把内存中格式化的数据输出到屏幕上(stdout)
fscanf
–针对所有输入流的格式化的输入函数:stdin、文件指针流
fprintf
–针对所有输出流的格式化的输出函数:stdout、文件指针流
sscanf
–从一个字符串中,还原出格式化的数据。
sprintf
–把格式化的数据,转化为(存放在)一个字符串(中)。
前面介绍的函数,都是进行文件的顺序读写的函数,即文件的指针都是从头到尾移动的,那么有时候文件指针跳过的文件数据需要打印两次,有没有可能使文件指针往回挪呢?接下来我们可以使文件随机读写相关的函数!
int fseek(FILE* stream, long int offset, int origin);
头文件:
#include
参数介绍:
①stream为FILE类型的指针②offset为偏移量③origin为文件指针当前指向的位置
函数介绍:
可以将文件指针设置到任意位置,用于读取重复的数据,如果成功返回零,否则返回非零值
fseek函数的使用案例:
#include
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
int ch=fgetc(pf);
printf("%c\n", ch);//a
ch = fgetc(pf);
printf("%c\n", ch);//b
ch = fgetc(pf);
printf("%c\n", ch);//c
//接下去读取的是d
//可以使用fseek函数进行调整读取字符b
//1.SEEK_SET指针指向文件起始位置
//fseek(pf, 1, SEEK_SET);
//ch = fgetc(pf);
//printf("%c\n", ch);//b
//2.SEEK_CUR文件指向指向文件当前位置
//fseek(pf, -2, SEEK_CUR);
//ch = fgetc(pf);
//printf("%c\n", ch);//b
//2.SEEK_END文件指向文件的末尾
fseek(pf, -5, SEEK_END);
ch = fgetc(pf);
printf("%c\n", ch);//b
fclose(pf);
pf = NULL;
return 0;
}
long int ftell(FILE* stream);
头文件:
#include
参数介绍:
stream为FILE类型的指针
函数介绍:
如果函数调用成功,返回FILE类型指针的当前位置,失败则返回-1L。
ftell函数的使用案例:
#include
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
int ch=fgetc(pf);
printf("%c\n", ch);//a
printf("%d\n", ftell(pf));
ch = fgetc(pf);
printf("%c\n", ch);//b
printf("%d\n", ftell(pf));
ch = fgetc(pf);
printf("%c\n", ch);//c
printf("%d\n", ftell(pf));
fclose(pf);
pf = NULL;
return 0;
}
void rewind(FILE* stream);
头文件:
#include
参数介绍:
stream为FILE类型的指针
函数介绍:
将文件指针重新指向开头,无返回值。
rewind函数的使用案例:
#include
int main()
{
int n;
FILE* pFile;
char buffer[27];
pFile = fopen("myfile.txt", "w+");
for (n = 'A'; n <= 'Z'; n++)
fputc(n, pFile);
rewind(pFile);
//让文件指针重新指向文件的起始位置
fread(buffer, 1, 26, pFile);
fclose(pFile);
buffer[26] = '\0';
puts(buffer);
return 0;
}
1.根据数据的组织形式,数据文件被称为文本文件或二进制文件;数据在内存中以二进制的形式存储,如果不加转换输出到外存,就是二进制文件;如果要求在外存上以ASCII码的形式存储,则需要在存储前转换,以ASCII字符的形式存储的文件就是文本文件。
2.一个数据在内存中怎么存储的呢?
字符一律以ASCII形式存储,数值型的数据既可以用ASCII形式存储,也可以使用二进制形存储。
测试代码:
#include
int main()
{
int a = 1000;
FILE* pf = fopen("test.txt", "wb");
//以二进制形式写到文件
if (pf == NULL)
{
perror("fopen");
return 1;
}
fwrite(&a, 4, 1, pf);
fclose(pf);
pf = NULL;
return 0;
}
代码调试的过程:
我们可以发现我们看不懂文件里面的数据,那么接下来我们一起操作一下把!
添加文件test.txt
到源文件中,然后以二进制编辑器方式显示
图形理解:
1.文本文件读取是否结束,判断返回值是否为EOF(fgetc),或者NULL(fgets)
例如:
fgetc
判断是否为EOFfgets
判断返回值是否为NULL
2.二进制文件的都区结束判断,判断返回值是否小于实际读取要读的个数。
例如:- fread判断返回值是否小于实际要读的个数。
feof函数原型:
int feof(FILE * stream);
头文件:
#include
参数介绍:
stream 为指向标识流的FILE的指针
函数介绍:
①feof函数为真,说明文件正常读取遇到了文件结束指示符而结束②如果设置了文件结束的指示符,返回非零值,否则返回零值。
int ferror(FILE * stream);
头文件:
#include
参数介绍:
stream 为指向标识流的FILE的指针
函数介绍:
f①erro如果返回真,就说明是文件在读取过程中遇到了错误指示符,即出出现错误结束;②如果设置了文件结束的指示符,返回非零值,否则返回零值
文本文件的使用案例:
#include
int main()
{
int c = 0;//注意: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 successful");
}
fclose(fp);
fp = NULL;
return 0;
}
#include
enum { SIZE = 5 };
int main()
{
double a[SIZE] = { 1.0,2.0,3.0,4.0,5.0 };
FILE* fp = fopen("test.bin", "wb");//用二进制写double的数组
fwrite(a, sizeof(*a), SIZE, fp);
fclose(fp);
fp = NULL;
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 handing
{
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;
return 0;
}
ANSIC标准采用“缓冲文件系统”处理数据文件,所谓缓冲系统是系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。从内存向磁盘中输出数据先送到内存中的缓冲区,等缓冲区装满后再输出到磁盘。如果从磁盘向计算机读入数据,磁盘文件中的数据先输入到内存缓冲区中,等缓冲区装满后再将数据输送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决定。
缓冲区刷新例子:
#include
#include
//在Devc++ WIN11环境测试
int main()
{
FILE* pf = fopen("test.txt", "w");
fputs("abcdef", pf);
printf("睡眠10秒-已经在写数据,打开test.txt文件,发现文件没有内容\n");
Sleep(1000);
printf("刷新缓冲区\n");
fflush(pf);//刷新缓冲区时,才将输出缓冲区的数据写到文件(磁盘)
//注意:fflush函数在高版本的VS上不能使用了
printf("再睡眠10秒-已经在写数据,再打开test.txt文件,文件有内容了\n");
Sleep(1000);
fclose(pf);
//注意:fclose在关闭文件的时候,也会刷新缓冲区
pf = NULL;
return 0;
}
结论: 因为有缓冲区的存在,C语言在操作文件的时候,需要刷新缓冲区或则在文件结束的时候关闭文件;如果不刷新缓冲区,可能导致读写文件的问题。
本章我们一起学习了对文件进行操作的函数以及文件的相关知识,希望对大家了解文件操作又些许帮助,感谢大家阅读!如有不对,欢迎大家纠正!