目录
在处理数据的时候,往往通过程序从文件中读取数据或将数据写入文件。本章将对文件的操作过程做一个汇总。
一、文件的打开和关闭
1.1 文件指针
使用C打开一个文件fopen,就会有一个文件信息区。例如,打开data.txt,就会有一个文件信息区自动创建,这个文件信息区是一个FILE类型的结构体。
创建一个FILE*的指针变量pf就可以找到对应的文件信息区,从而找到相应的文件。
FILE* fopen(const char* filename, const char* mode);//(文件名,打开方式)
int fclose(FILE* stream);
1.2 文件的打开和关闭
操作步骤如下:
- 打开文件fopen,创建文件信息区,同时返回文件信息区的地址;
- 读、写文件;
- 关闭文件fclose。
代码展示如下:
#include<stdio.h>
int main()
{
//1. 打开文件
FILE* pf = fopen("data.txt", "r");
//FILE* pf = fopen("..\\data.txt", "r");
//FILE* pf = fopen("..\\..\\data.txt", "r");//相对路径
// “\\”转义字符,表示一个斜杠
//FILE* pf = fopen("C:\\Users\\24_9_7\\24_9_7\\data.txt", "r");//绝对路径
//打开文件也可能失败
if (pf == NULL)
{
perror("fopen");
return 1;
}
//2. 写文件
//3. 关闭文件
fclose(pf);
pf = NULL;
return 0;
}
与动态内存管理流程类似:动态内存开辟是向内存申请内存资源,而打开文件是申请文件资源,都是对资源的申请和管理。
值得注意的是:打开文件的路径包括相对路径和绝对路径。
针对相对路径:.表示当前路径,..表示上一级路径。
1.3 文件的使用方式
文件使用方式 | 含义 | 如果指定文件不存在 |
"r"(只读) | 为了输入数据,打开一个已经存在的文本文件 | 出错 |
"w"(只写) | 为了输出数据,打开一个文本文件 | 建立一个新的文件 |
"a"(追加) | 向文本文件尾添加数据 | 建立一个新的文件 |
"rb"(只读) | 为了输入数据,打开一个二进制文件 | 出错 |
"wb"(只写) | 为了输出数据,打开一个二进制文件 | 建立一个新的文件 |
二、文件的顺序读写
2.1 (数据)流
流是高度抽象的概念!以输出流为例,我们知道输出流包括:文件流、标准输出流等。
在写数据的时候,有很多数据,可能会流向很多地方去,比如:显示到屏幕上、存到硬盘上、传到网络上、放到U盘上...等外部设备。
不同的外部设备读写方式肯定是不同的。写C语言代码把数据打印到屏幕上,...,需要掌握不同的数据传递方式。为了简化,在外部设备和数据之间再抽象一个“流”,所以有了定义:(数据)流,流淌的是数据。我们更关注数据与流之间的操作。
但是,在使用scanf从键盘上读取数据,使用printf向屏幕上打印数据,直接就操作了,没有进行打开键盘、打开屏幕的指令。这是因为,C只要运行起来,默认就打开了三个流:
- 标准输入流 - stdin
- 标准输出流 - stdout
- 标准错误流 - stderr
既然是流,那么其类型也必然是FILE*类型。
#include<stdio.h>
int main()
{
char ch = 0;
for (ch = 'a'; ch <= 'z'; ch++)
{
if (ch % 5 == 0)
fputc('\n', stdout);
fputc(ch, stdout);//在屏幕上打印信息,没有打印在文件中
}
return 0;
}
2.2 文件的读写顺序
功能 | 函数名 | 适用于 |
字符输入函数 | fgetc | 所有输入流 |
字符输出函数 | fputc | 所有输出流 |
本文行输入函数 | fgets | 所有输入流 |
文本行输出函数 | fputs | 所有输出流 |
格式化输入函数 | fscanf | 所有输入流 |
格式化输出函数 | fprintf | 所有输出流 |
二进制输入 | fread | 文件 |
二进制输出 | fwrite | 文件 |
2.2.1 fgetc
int fgetc(FILE * stream);
返回指定流的内部文件位置指示器当前指向的字符。然后,内部文件位置指示器前进到下一个字符。
如果调用时流位于文件末尾,则函数将返回EOF并设置流的文件结束标记(feof)。
如果发生读取错误,该函数将返回EOF并为流设置错误指示符(ferror)。
#include<stdio.h>
int main()
{
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读文件
//int ch = fgetc(pf);//返回的是读到字符的ASCII码,如果读取错误,返回EOF,同时会设置一个标记,可用ferror检测
//printf("%c ", ch);
//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)
{
printf("%c ", ch);
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
2.2.2 fputc
int fputc(int character, FILE * stream);
将字符写入流并前进位置指示器。
成功后,将返回写入的字符。
如果发生写入错误,则返回EOF并设置错误指示符(ferror)。
#include<stdio.h>
int main()
{
FILE* pf = fopen("data.txt", "w");//当打开文件的时候,文件中什么都没有,但是却有一个光标(文件状态指针)指向要输入字符的起始位置(指向文件内容的一个标记),每一次操作,光标会发生一次状态更新
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写文件
/*fputc('a', pf);
fputc('b', pf);
fputc('c', pf);
fputc('d', pf);*/
char ch = 0;
for (ch = 'a'; ch <= 'z'; ch++)
{
fputc(ch, pf);
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
2.2.3 fgets
char * fgets(char * str, int num, FILE * stream);
从stream(流)中读取字符并将其作为C字符串存储到str中,直到读取(num-1)个字符,或者到达换行符或文件末尾。
换行符会使fgets停止读取,但它被函数视为有效字符,并包含在复制到str的字符串中。
终止字符会自动附加到复制到str的字符之后。
成功后,该函数返回str。
如果在尝试读取字符时遇到文件结尾,则设置文件结束标记 (feof)。如果在读取任何字符之前发生这种情况,则返回的指针为NULL。(并且str的内容保持不变)。
如果发生读取错误,则设置错误指示符(ferror)并返回NULL指针(但str指向的内容可能已更改)。
#include<stdio.h>
int main()
{
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读文件
char arr[100] = { 0 };
fgets(arr, 100, pf);//hello,想读第二行,再来一次就可以了
printf("%s", arr);
fgets(arr, 100, pf);
printf("%s", arr);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
2.2.4 fputs
int fputs(const char * str, FILE * stream);
将str指向的C字符串写入流。
该函数从指定的地址(str)开始复制,直到到达终止字符 ('\0')。此终止字符不会复制到流中。
成功后,将返回非负值。
出错时,该函数返回EOF并设置错误指示符 (ferror)。
#include<stdio.h>
int main()
{
FILE* pf = fopen("data.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写文件
fputs("hello\n", pf);
char arr[] = "world";
fputs(arr, pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
2.2.5 fscanf
int fscanf(FILE* stream, const char* format, ...);
int scanf(const char* format, ...);
从流中读取数据,并根据参数格式将其存储到其他参数指向的位置。
成功后,该函数返回成功填充的参数列表的项目数。此计数可能与预期的项目数匹配,也可能由于匹配失败、读取错误或文件结束而更少(甚至为零)。
如果在读取时发生读取错误或到达文件末尾,则会设置正确的指示符(feof或ferror)。而且,如果在成功读取任何数据之前发生任何情况,则返回EOF。
#include<stdio.h>
struct S
{
float f;
char c;
int n;
};
int main()
{
struct S s = { 0 };
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读文件
fscanf(pf, "%f %c %d", &(s.f), &(s.c), &(s.n));//读取的时候要与写入的格式保持一致
printf("%f %c %d", s.f, s.c, s.n);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
2.2.6 fprintf
int fprintf(FILE* stream, const char* format, ...);
int printf(const char* format, ...);
将format指向的C字符串写入流。
成功后,将返回写入的字符总数。
如果发生写入错误,则设置错误指示符(ferror)并返回负数。
#include<stdio.h>
struct S
{
float f;
char c;
int n;
};
int main()
{
struct S s = { 3.14,'w',100 };
FILE* pf = fopen("data.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写文件
printf("%f %c %d", s.f, s.c, s.n);
fprintf(pf, "%f %c %d", s.f, s.c, s.n);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
2.2.7 fread 二进制方式读取文件
size_t fread(void *ptr, size_t size, size_t count, FILE* stream)
来源于ptr 每个元素大小 个数 写到流中
从流中读取count元素数组,每个元素的大小为size字节,并将它们存储在ptr指定的内存块中。 流的位置指示器按读取的总字节数前进。
#include<stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
//写文件
FILE* pf = fopen("data.txt", "wb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//二进制的写文件
fwrite(arr, sizeof(arr[0]), sizeof(arr) / sizeof(arr[0]), pf);
fclose(pf);
pf = NULL;
return 0;
}
fread要求读取count个大小为size字节的数据;
如果真的读取到count个数据,函数返回count;
如果没有读取到count个数据,返回的是真实的读取到的完整的数据个数。
2.2.8 fwrite 二进制方式写进文件
size_t fwrite(const void *ptr, size_t size, size_t count, FILE* stream)
将count元素数组(每个元素的大小为size字节)从ptr指向的内存块写入流中的当前位置。
#include<stdio.h>
int main()
{
int arr[10] = { 0 };
//写文件
FILE* pf = fopen("data.txt", "rb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//二进制的写文件
fread(arr, sizeof(arr[0]), sizeof(arr) / sizeof(arr[0]), pf);
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
fclose(pf);
pf = NULL;
return 0;
}
2.3 函数对比
scanf / fscanf / sscanf
printf / fprintf / sprintf
scanf是格式化的输入函数,针对的是标准输入流(键盘)
printf是格式化的输出函数,针对的是标准输出流(屏幕)
scanf和printf是针对标准输入/输出流的格式化输入/输出函数fscanf是针对所有输入流(文件流、标准输入流)的格式化输入函数
fprintf是针对所有输出流(文件流、标准输入流)的格式化输出函数int sscanf(const char* s, const char* format, ...);//将字符串转换成格式化的数据
int sprintf(char* str, const char* format, ...);//把格式化的数据转换成字符串
#include<stdio.h>
struct S
{
float f;
char ch;
int n;
};
int main()
{
struct S s = { 3.14, 'w', 100 };
char arr[10] = { 0 };
sprintf(arr, "%f-%c-%d", &(s.f), &(s.ch), &(s.n));
printf("%s\n", arr);
struct S tmp = { 0 };
sscanf(arr, "%f-%c-%d", &(tmp.f), &(tmp.ch), &(tmp.n));//从arr中以"%f-%c-%d"的格式提取数据,存放在tmp中
printf("%f\n", tmp.f);
printf("%c\n", tmp.ch);
printf("%d\n", tmp.n);
return 0;
}
三、文件的随机读写
3.1 fseek
int fseek(FILE* stream, long int offset, int origin);
流 偏移量 起始位置
- SEEK_SET 文件指针的初始位置
- SEEK_CUR 文件指针当前的位置
- SEEK_END 文件指针的结束位置
#include<stdio.h>
int main()
{
//打开文件
FILE* pf = fopen("data.txt", "r");//假设data文件中存放abcdef
if(pf == NULL)
{
ferror(fopen);
return 1;
}
//写文件
int ch = fgetc(pf);
printf("%c ", ch);//a
ch = fgetc(pf);
printf("%c ", ch);//b
fseek(pf, -2, SEEK_CUR);
//fseek(pf, 0, SEEK_SET);
//fseek(pf, -6, SEEK_END);
ch = fgetc(pf);
printf("%c ", ch);//a
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
3.2 ftell
返回文件指针相对于起始位置的偏移量,左移为负,右移为正。
#include<stdio.h>
int main()
{
FILE* pf = fopen("data.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
ch = fgetc(pf);
printf("%c\n", ch);//d
int pos = ftell(pf);
printf("pos = %d\n", pos);
fseek(pf, -pos, SEEK_CUR);
ch = fgetc(pf);
printf("%c\n", ch);//a
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
3.3 rewind
让文件指针的位置回到起始位置。
#include<stdio.h>
int main()
{
FILE* pf = fopen("data.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
ch = fgetc(pf);
printf("%c\n", ch);//d
rewind(pf);
ch = fgetc(pf);
printf("%c\n", ch);//a
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
四、文本文件和二进制文件
以ASCII码字符存储的文件就是文本文件;不加转换的输出到外存,就是二进制文件。所以数据文件被分为文本文件和二进制文件。
字符一律以ASCII码形式储存;
数据可以ASCII码形式储存,也可以二进制形式储存。
如:10000
//如果以ASCII码形式输出到磁盘,则占5个字节
0011 0001 0011 0000 0011 0000 0011 0000 0011 0000
1 0 0 0 0
//而以二进制形式输出,则占4个字节
0000 0000 0000 0000 0010 0111 0001 0000
通过代码展示如下:
#include<stdio.h>
int main()
{
int a = 10000;
FILE* pf = fopen("data.txt", "wb")
if(pf == NULL)
{
ferror("fopen");
return 1;
}
fwrite(a, 4, 1, pf);//二进制的形式写到文件中
fclose(pf);
pf = NULL;
return 0;
}
通过记事本打开相应文件为乱码,在VS上用二进制编辑器打开:
五、文件读取结束的判定
- ferror - 在文件读取结束后,用来判断文件是否因为读取过程中遇到错误而结束,遇到错误返回非0值。
- feof - 在文件读取结束后,用来判断文件是否因为读取过程中遇到文件结束标志而结束。
作用:当已知文件读取结束,但读取结束的原因可能是多样的,比如说读取失败、读取过程中发生错误等结束,也可能是正常读取到末位而结束。
feof当文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束。
写一个代码完成文件的拷贝,代码如下:
#include<stdio.h>
int main()
{
FILE* pfread = fopen("data1.txt", "r");
if (pfread == NULL)
{
perror("fopen");
return 1;
}
FILE* pfwrite = fopen("data2.txt", "w");
if (pfread == NULL)
{
perror("fopen");
fclose(pfread);
pfread = NULL;
return 1;
}
//拷贝数据
int ch = 0;
while ((ch = fgetc(pfread)) != EOF)//不能用char类型接收,因为ASCII码值不包括-1
{
fputc(ch, pfwrite);
}
//关闭文件
fclose(pfread);
fclose(pfwrite);
pfread = NULL;
pfwrite = NULL;
return 0;
}