可以将程序中的数据保存为一个文件。待下次重新启动程序时,从之前保存的文件中提取数据。这样,程序就不会在重启后失忆了。
创建并写入文件
#include <stdio.h>
int main()
{
// 创建一个名为data.txt的文件
FILE* pFile = fopen("data.txt", "w");
if (pFile == NULL)
{
// 文件创建失败
return -1;
}
// 文件创建成功
int n = 123;
double f = 3.1415;
char ch = 'A';
// fprintf第一个参数为文件结构指针,其后参数与printf一致
fprintf(pFile, "%d\n", n);
fprintf(pFile, "%f\n", f);
fprintf(pFile, "%c\n", ch);
// 关闭文件
fclose(pFile);
return 0;
}
打开文件data.txt
,我们可以发现,里面有刚刚写入的三个变量的值,并且每打印一个变量换行一次。
123
3.141500
A
为了操作文件,我们需要借助几个在头文件stdio.h
中声明的库函数。
创建或打开文件fopen
函数。
FILE *fopen (const char * filename, const char * mode);
输入:const char * filename
文件路径,可以使用相对路径或绝对路径。const char * mode
操作模式
输出:
如果文件创建或打开成功,则返回一个指针。这个指针指向一个记录文件信息的结构FILE
。其他各种文件操作函数,需要这个结构指针才能对fopen
打开或创建的文件进行操作。我们无需过多地关注这个结构的具体组成,仅需要将这个结构指针传递给各种文件操作函数即可。
例如,我们使用相对路径data.txt
,将在当前目录下,创建一个名为data.txt
的文件。
也可以在windows上使用形如F:/projects/data.txt
的绝对路径,在F盘下的project文件夹中,创建data.txt文件。
函数 fopen 的第一个参数为字符串,内容为需要操作的文件路径,第二个参数也为字符串,内容为文件的操作模式。
操作模式
读、写模式w
、r
- "r" 模式,读模式,取自read的首字母。对文件进行读取操作。
- "w" 模式,写模式,取自write的首字母。对文件进行写入操作。如果文件存在,清空原文件内容,不存在则创建一个新文件。
追加模式a
如果,现在想在第一行后,再增加更多的HelloWorld
,若函数fopen
使用的是w
写入模式,文件将清空原内容再写入。现在,我们需要保留原有内容,继续在文件尾部添加新内容。这时候,需要使用追加模式a
。字符a
为单词追加append
的首字母。
#include <stdio.h>
int main()
{
FILE* pFile = fopen("data.txt", "a"); // 追加模式
if (pFile == NULL)
{
return -1;
}
char str[] = "HelloWorld\n";
char* p = str;
while (*p != '\0')
{
fputc(*p, pFile);
p++;
}
fclose(pFile);
return 0;
}
多运行几次,可以发现,文件中有了多行HelloWorld
了。
注意,代码从未将\0
写入过文件,文件中的每一行都是由换行分隔。且\0
也不标记文件结尾。文件是否结尾可以通过文件操作函数返回值和feof
函数的返回值判断。
可读可写模式
可以使用+
将r
和w
模式从单一的模式,升级为读写均可模式。
- "w+" 模式,更新模式,可读可写。但是,会清空文件原有内容。
- "r+" 模式,更新模式,可读可写。
对于以更新模式 + 打开的文件,这里有一个必须要注意的地方:
- 文件从写操作转换为读操作前,必须使用
fflush
,fseek
,rewind
其中一个函数。 - 文件从读操作转换为写操作前,必须使用
fseek
,rewind
其中一个函数。
字符串输出到文件内fprintf
int fprintf (FILE * stream, const char * format, ...);
若需要将字符串输出到文件内,有一个非常类似于printf
的函数fprintf
。它就相当于在函数printf
第一个参数前,加了一个文件结构指针参数,用于指明操作哪个文件。其他的使用方法和printf
几乎一致。
字符输出到文件内fputc
fputc()
函数用于向文件中写入一个字符。
fputc 的函数原型:int fputc(int character, FILE* stream);
输入:int character
写入文件的字符FILE* stream
文件结构指针
输出 :
如果写入成功,返回刚刚写入的字符。如果文件结尾或失败,则返回EOF
。并且ferror
可以检测到文件读写出错。
使用指针p
的移动遍历"HelloWorld\n"
字符串,直到指针指向字符为\0
为止。遍历结束前的字符,均被fputc
函数写入到文件当中。
请注意,目前函数fopen
使用的是w
写入模式。因此,文件将清空原内容再写入。
#include <stdio.h>
int main()
{
FILE* pFile = fopen("data.txt", "w"); // 写模式
if (pFile == NULL)
{
return -1;
}
char str[] = "HelloWorld\n";
char* p = str;
while (*p != '\0')
{
// 向文件中写入一个字符
fputc(*p, pFile);
p++;
}
fclose(pFile);
return 0;
}
程序运行完成后,将会在文件中看到一串字符HelloWorld
并换行。
关闭文件fclose(pFile);
虽然程序结束会为我们自动关闭文件。如果在程序运行期间,不需要再次操作文件了,可以调用函数fclose
关闭文件。并且,关闭所有资源再结束程序是一个良好的编程习惯。
文本模式与二进制模式
使用十六进制查看器,打开这个文件
很显然,这个文件里面记录了刚刚写入字符的ASCII码。
十六进制0A
,换行符,转义序列为'\n'
。
十六进制0D
,回车,转义序列为'\r'
。
为什么会出现回车和换行两个字符
在早期的电传打字机上,有一个部件叫“字车”,类似于打印机的喷头。“字车”从最左端开始,每打一个字符,“字车”就向右移动一格。当打满一行字后,“字车”需要回到最左端。这个动作被称作“回车”(return carriage)。
但是,仅仅做了“回车”还不够,我们还需要将纸张上移一行,让“字车”对准新的空白一行。否则,两行字将被重叠打印在一起。这个动作被称作“换行”。
随着时代的发展,字符不仅仅只打印在纸上。例如,在屏幕上打印字符时,无需“字车”。
所以,当人们将开始新的一行引入到计算机上时,分成了两种惯例:
- 沿用这两个动作,回车加换行
\r
、\n
。 - 简化为仅换行
\n
。
两类具有代表性的系统分别使用了其中一种惯例:
- Windows系统使用
\r
加\n
。 - Linux系统使用
\n
。
C语言本身采取了第二种惯例,仅使用一个字符\n
。但是,为了适配各系统下的惯例,C语言写入、读取文件时,若系统惯例与C语言使用的不一致,则会自动进行转换。
Linux系统和C语言采用同一种惯例\n
,无需转换。
C语言在Windows系统上写入文件时,会将\n
写入为\r
、\n
。而读取文件时,会将\r
、\n
读取为\n
。
如果在windows系统上运行刚刚的代码,文件内换行将是\r
、\n
两个字符。
如果在linux系统上运行刚刚的代码,文件内换行将是\n
一个字符。
正是因为C语言把对文件输入输出的数据当做一行行的文本来处理,才会有这种换行时的自动转换的现象。这种文件操作模式被称作文本模式。
二进制模式
如果,不希望C语言把对文件输入输出的数据当做文本,不进行换行时的自动转换。可以在打开文件时使用二进制模式。在函数fopen
的第二个参数的字符串中添加字符b
,代表二进制binary
。FILE *pFile = fopen("data.txt", "wb"); // 二进制写模式
FILE *pFile = fopen("data.txt", "rb"); // 二进制读模式
读取文件
fscanf函数
fscanf
相当于在函数scanf
第一个参数前,加了一个文件结构指针参数,用于指明操作哪个文件。其他的使用方法和scanf
几乎一致。
fscanf
的函数原型:
int fscanf(FILE* stream, const char* format, ...);
现在需要从文件中读取数据,所以使用只读r
模式打开文件。
#include <stdio.h>
int main()
{
// 读取一个名为data.txt的文件
FILE* pFile = fopen("data.txt", "r");
if (pFile == NULL)
{
// 文件打开失败
return -1;
}
// 文件打开成功
int n;
double f;
char ch;
// fscanf第一个参数为文件结构指针,其后参数与fscanf一致
fscanf(pFile, "%d", &n);
fscanf(pFile, "%lf", &f);
fscanf(pFile, "%c", &ch);
printf("%d\n", n);
printf("%f\n", f);
printf("%c\n", ch);
// 关闭文件
fclose(pFile);
return 0;
}
函数fscanf
成功地从文件中读取出了前两个数据,第三个数据读取失败了。这是因为第三个fscanf
的%c
占位符期望获取一个字符。而上一行末尾中,刚好有一个\n
。因此,第三个fscanf
读取了\n
并赋值给了变量ch
。
可以使用类似于getchar()
函数的fgetc
,从文件中读取一个字符,吸收这个\n
。
fgetc函数
int fgetc(FILE* stream);
输入:FILE * stream
文件结构指针
输出:
如果读取成功,返回读取到的字符。如果文件结尾或失败,则返回EOF
。
#include <stdio.h>
int main()
{
FILE* pFile = fopen("data.txt", "r");
if (pFile == NULL)
{
return -1;
}
int n;
double f;
char ch;
fscanf(pFile, "%d", &n);
fscanf(pFile, "%lf", &f);
// 吸收上一行末尾的'\n'
fgetc(pFile);
fscanf(pFile, "%c", &ch);
printf("%d\n", n);
printf("%f\n", f);
printf("%c\n", ch);
fclose(pFile);
return 0;
}
fgets函数
char* fgets(char* str, int num, FILE* stream);
输入:
str
将读取的一行字符串存储在 str 为首地址的空间中。num
最大的读取字符数,包括 '\n' 在内。stream
文件结构指针
例如,我们先声明100个字节的 char 类型的数组,数组名为 str ,用于放置从文件中读取的一行字符串。若文件中有一行超过100个字符,将这一行字符串放置到str
数组中,将导致越界。因此,我们可以使用第二个参数num
来限制最大读取的字符数。第三个参数则是文件结构指针。
char buffer[100];
fgets(buffer, 100, pFile);
输出:
- 如果读取成功,函数返回
str
。 - 如果遇到文件结尾,已读取到部分数据,那么返回
str
。 - 如果遇到文件结尾,未读取到任何数据,那么返回
NULL
。 - 如果遇到文件读取错误,返回
NULL
。str
中有可能有部分已读取数据。
根据返回值规则,若读取一行字符成功将返回str
,即可再次读取下一行字符。若返回NULL
,则结束读取。
在运行程序前,别忘记刚刚文件已经被清空了。先向文件写入些内容再运行程序。
#include <stdio.h>
int main()
{
FILE* pFile = fopen("data.txt", "r");
if (pFile == NULL)
{
return -1;
}
char buffer[100];
while (fgets(buffer, 100, pFile) != NULL)
{
printf("%s", buffer);
}
fclose(pFile);
return 0;
}
正常输出
123
3.141500
A
EOF
EOF
,是文件结尾,End Of File
的首字符缩写。为头文件stdio.h
中定义的一个宏,通常定义为:#define EOF (-1)
它被用于头文件stdio.h
中一些函数的返回值,用于指示文件结尾或者是一些其他错误。
#include <stdio.h>
int main()
{
FILE* pFile = fopen("data.txt", "r");
if (pFile == NULL)
{
return -1;
}
char ch;
while (1)
{
ch = fgetc(pFile);
if (ch == EOF)
{
// 文件结尾或者是一些其他错误
break;
}
putchar(ch);
}
fclose(pFile);
return 0;
}
文件状态判断
假设文件data.txt
内容为
123
3.141500
A
feof
用于测试是否文件结尾。ferror
用于测试文件是否读写出错。
feof
函数原型
int feof(FILE* stream);
输入:FILE * stream
文件结构指针
输出:
如果文件结尾,返回值为非0。否则,返回值为0。
ferror
函数原型
int ferror(FILE* stream);
输入:FILE * stream
文件结构指针
输出:
如果文件读写出错,返回值为非0。否则,返回值为0。
我们可以在fgetc
函数返回EOF
后,再次根据上述两个函数,判断究竟是文件结尾了,还是遇到了错误。
#include <stdio.h>
int main()
{
FILE* pFile = fopen("data.txt", "r");
if (pFile == NULL)
{
return -1;
}
char ch;
while (1)
{
ch = fgetc(pFile);
if (ch == EOF)
{
// 文件结尾或者是一些其他错误
if (feof(pFile) != 0) // 测试文件是否结尾
{
printf("end of file\n");
}
else if (ferror(pFile) != 0) // 测试文件是否读写出错
{
printf("file access error\n");
}
break;
}
putchar(ch);
}
fclose(pFile);
return 0;
}
正常结尾
123
3.141500
A
end of file
如果把文件打开模式换成w
写模式。那么,文件将无法被读取,尝试读取文件将产生读写错误。并且,由于**w**
写模式会将已有文件清空,所以现在文件内容为空。
// 改为"w"写模式
FILE* pFile = fopen("data.txt", "w");
读写错误
file access error
文件缓存
fputs函数
fputs()
函数用于向文件中写入一串字符串。int fputs(const char* str, FILE* stream);
输入:const char* str
待写入文件的字符串FILE* stream
文件结构指针
输出 :
如果写入成功,返回一个非负值。如果写入失败,则返回EOF
。并且,ferror
可以检测到文件读写出错。
由于用fopen
函数打开文件时,使用了w
写模式。因此,文件原内容将清空,写入5行Have a good time\n
。
#include <stdio.h>
#include <stdlib.h>
int main()
{
FILE* pFile = fopen("data.txt", "w"); // 写模式
if (pFile == NULL)
{
return -1;
}
char str[] = "Have a good time\n";
for (int i = 0; i < 5; i++)
{
fputs(str, pFile);
}
// 关闭文件前,先暂停一下
system("pause");
fclose(pFile);
return 0;
}
虽然在运行到暂停时,向文件中写入数据的fputs(str, pFile)
语句已经运行过了。但是,现在打开文件,文件内没有任何内容。
让暂停继续。程序结束后,文件内出现了内容。
fflush函数
C语言中提供的文件操作函数是带有缓存的,数据会先写入到缓存中。待缓存中的数据积累到一定数量时,再一起写入文件。因此,刚刚暂停时,数据还在缓存区内,未写入到文件当中。
只有将缓存区的数据写入文件,数据才真正保存在了文件中。此时缓存区的数据无需保留将被清空。这个动作被称之为刷新缓存。
而文件关闭fclose
或程序结束会刷新缓存。所以,关闭文件fclose
后,文件内出现了内容。
除此之外,还可以主动调用fflush
函数,主动刷新文件缓存。int fflush(FILE* stream);
输入:FILE * stream
文件结构指针
输出
刷新缓存区成功返回0,否则返回EOF
,并且ferror
可以检测到文件读写出错。
现在,稍微改一点代码。在程序暂停前刷新缓存区。
#include <stdio.h>
#include <stdlib.h>
int main()
{
FILE* pFile = fopen("data.txt", "w"); // 写模式
if (pFile == NULL)
{
return -1;
}
char str[] = "Have a good time\n";
for (int i = 0; i < 5; i++)
{
fputs(str, pFile);
}
// 刷新文件缓存区后暂停程序
fflush(pFile);
system("pause");
fclose(pFile);
return 0;
}
现在,即使未运行到fclose
及程序关闭,文件中也已经有内容了。
Have a good time
Have a good time
Have a good time
Have a good time
Have a good time
文件偏移
假设现在文件data.txt
内容为
Have a good time
Have a good time
Have a good time
Have a good time
Have a good time
#include<stdio.h>
void fileEofOrError(FILE* pFile)
{
if (feof(pFile) != 0) // 测试文件是否结尾
{
printf("end of file\n");
}
else if (ferror(pFile) != 0) // 测试文件是否读写出错
{
printf("file access error\n");
}
}
int main()
{
FILE* pFile = fopen("data.txt", "r");
if (pFile == NULL)
{
return -1;
}
char ch;
while (1)
{
ch = fgetc(pFile);
if (ch == EOF)
{
fileEofOrError(pFile);
break;
}
putchar(ch);
}
fclose(pFile);
return 0;
}
输出结果
Have a good time
Have a good time
Have a good time
Have a good time
Have a good time
end of file
为什么每一次的 fgetc 函数能顺序获取到文件中的字符呢?
文件指针
文件结构pFile
中,保存了一个当前文件读写位置的指针。文件由fopen
函数打开后,这个指针指向文件中第一个字节。当任意文件操作函数读写相应长度的字节后,指针也会偏移相应的长度。fgetc
函数每次获取一个字节。因此,文件指针向后移动一个字节。所以,重复调用fgetc
函数可以逐个读取文件内的字符。fgets
函数每次获取一行字符。因此,文件指针向后移动到下一行开始。所以,重复调用fgets
函数可以逐行读取文件内的字符。
文件指针移动函数fseek
int fseek(FILE* stream, long offset, int origin);
输入:FILE* stream
文件结构指针long offset
文件指针偏移量origin
从什么位置开始偏移。
其中origin
可以使用以下3种宏定义作为参数:
SEEK_SET
文件开头(文件第一个字节)SEEK_CUR
当前文件位置SEEK_END
文件结尾(文件最后一个字节后)
输出 :
如果成功,返回0。否则,则返回一个非零值。并且,ferror
可以检测到文件读写出错。
从文件开头偏移5个字节,文件指针将指向
a
。
fseek(pFile, 5, SEEK_SET);
从文件结尾偏移-5个字节,文件指针将指向
i
。
fseek(pFile, -5, SEEK_END);
ftell
函数获取当前文件指针位置
ftell 的函数原型:long ftell (FILE * stream);
输入:FILE * stream
文件结构指针
输出:
如果成功,则返回当前文件指针位置。如果失败,则返回-1。
获取文件大小
如果将文件指针先偏移到末尾,再获取文件指针当前的位置,就能知道该文件内有多少个字节。即该文件的大小。
#include <stdio.h>
int main()
{
FILE* pFile = fopen("data.txt", "r");
if (pFile == NULL)
{
return -1;
}
char ch;
// 偏移到文件结尾
fseek(pFile, 0, SEEK_END);
// 获取当前文件指针位置
long length = ftell(pFile);
printf("size of file %ld\n", length);
fclose(pFile);
return 0;
}
输出文件大小
函数rewind
,将文件指针回到文件最开始。
如果想让文件指针回到最开始,从文件开头偏移0个字节。fseek(pFile, 0, SEEK_SET);
也可以使用函数rewind
,将文件指针回到文件最开始。rewind
的函数原型:void rewind(FILE * stream);
输入:FILE * stream
文件结构指针
输出:
无
更新文件
假设现在data.txt
文件内容为:
Hello world
Hello world
Hello world
Hello world
Hello world
现在要将H
全部改为h
为了满足需求,我们选用保留原文件内容的r+
更新模式。
代码中使用fgetc
读取文件中的每个字符,若读到字符H
,则把这个字符使用fputc
修改为h
。fgetc
读取到字符H
后,文件指针已经指向了下一个字符。所以,若读取到字符H
,需要将文件指针向前移动一个字节,再进行修改。
对于以更新模式
+
开的文件,这里有一个必须要注意的地方:
- 文件从写操作转换为读操作前,必须使用
fflush
,fseek
,rewind
其中一个函数。 - 文件从读操作转换为写操作前,必须使用
fseek
,rewind
其中一个函数。
在代码中读写操作转换的地方加入必要函数。如果仅需要读写操作转换,但无需变动文件指针。可以在当前位置处偏移0字节。fseek(pFile, 0, SEEK_CUR);
#include <stdio.h>
void fileEofOrError(FILE* pFile)
{
if (feof(pFile) != 0) // 测试文件是否结尾
{
printf("end of file\n");
}
else if (ferror(pFile) != 0) // 测试文件是否读写出错
{
printf("file access error\n");
}
}
int main()
{
FILE* pFile = fopen("data.txt", "r+");
if (pFile == NULL)
{
return -1;
}
char ch;
while (1)
{
ch = fgetc(pFile);
if (ch == EOF)
{
fileEofOrError(pFile);
break;
}
if (ch == 'H')
{
// 读转写
fseek(pFile, -1, SEEK_CUR);
ch = fputc('h', pFile);
if (ch == EOF)
{
fileEofOrError(pFile);
break;
}
// 写转读
fflush(pFile);
}
}
fclose(pFile);
return 0;
}
读转写时已经调用过fseek
函数了。写转读时,可以使用fflush
或fseek
偏移0字节。
运行后,文件中的字符H
已修改为小写的h
。
读写字符串
将数值转为字符串保存
#include <stdio.h>
int main()
{
// 创建一个名为data.txt的文件
FILE* pFile = fopen("data.txt", "w");
if (pFile == NULL)
{
// 文件创建失败
return -1;
}
// 装有数值的数组
int numbers[8] = { 1, 12, 123, 1234, 12345, 10, 123456, 1234567 };
for (int i = 0; i < 8; i++)
{
// 将数值打印至文件,每行一个数值
fprintf(pFile, "%d\n", numbers[i]);
}
// 关闭文件
fclose(pFile);
return 0;
}
编译并运行后,使用文本编译器打开文件data.txt
可以发现,数值已经被转为换行分隔的字符串并保存在文件中了。
若数值的十进制位数越多,字符串的字符也就越多,需要占用的空间也越大。
例如:
"1" 有1个十进制位,需要1个字节。
"12345" 有5个十进制位,需要5个字节。
"1234567" 有7个十进制位,需要7个字节。
读取字符串转为数值
#include <stdio.h>
void fileEofOrError(FILE* pFile)
{
if (feof(pFile) != 0) // 测试文件是否结尾
{
printf("end of file\n");
}
else if (ferror(pFile) != 0) // 测试文件是否读写出错
{
printf("file access error\n");
}
}
int main()
{
// 创建一个名为data.txt的文件
FILE* pFile = fopen("data.txt", "r");
if (pFile == NULL)
{
// 文件创建失败
return -1;
}
int numbers[8] = { 0 };
int count = 0;
while (1)
{
// 如果数组已经填满8个元素,则不继续读取
if (count >= 8)
{
printf("numbers is full\n");
break;
}
int get = fscanf(pFile, "%d", &numbers[count]);
printf("%d,", get);
if (get == EOF)
{
fileEofOrError(pFile);
break;
}
count++;
}
putchar('\n');
// 打印数组中的数值
for (int i = 0; i < 8; i++)
printf("%d\n", numbers[i]);
// 关闭文件
fclose(pFile);
return 0;
}
判断是否读完
除了使用固定长度的循环,还可以通过函数fscanf
的返回值判断是否已经读完文件。
函数fscanf
的返回值的意义为:参数列表中成功填充的参数个数。若文件读取失败或文件结尾,将返回EOF
。
若返回EOF
,此时可以通过feof
以及ferror
函数查询具体的原因。
防止数组越界
若文件中的字符串小于8个:数组numbers
未填满,但文件已经结尾。那么fscanf
将返回EOF
指示文件结尾,并终止读取文件内容。
若文件中的字符串大于等于8个:数组numbers
已填满,但文件内还有内容,这时没有地方再放置读取上来的数据了。也必须终止读取文件内容。
输出结果
1,1,1,1,1,1,1,1,-1,end of file
1
12
123
1234
12345
10
123456
1234567
以二进制形式读写
将数值以二进制形式保存
除了将数值转为字符串保存,数值还能不经过任何处理,直接以二进制形式保存成文件。下面介绍一个新函数fwrite
,用于将数据直接写入到文件。
fwrite函数
size_t fwrite(const void* buffer, size_t size, size_t count, FILE* stream);
输入:const void *buffer
待写入文件数据的首地址size_t size
每一块数据的大小size_t count
一共有多少块数据FILE *stream
文件结构指针
返回值:size_t
成功写入多少块数据。
参数buffer
第一个参数 buffer 为待写入文件的数据的首地址。数组 numbers 出现在表达式中将会转为首元素指针,指向第一个int
元素,类型为int *
,其内部保存了数组的首地址。函数参数buffer
为void *
类型的指针,而void *
类型的指针可以接收任何类型的指针。int *
类型的指针在传递给void *
类型的指针时,指针类型信息将丢失,仅留下首地址信息。
参数size与count
fwrite
会把待写入数据分为count
块,每一块size
个字节。例如:
- 将数组
numbers
分为1块,每一块sizeof(numbers)
大小。 - 将数组
numbers
分为8块,每一块sizeof(int)
大小。
两种方式都能将整个数组写入文件,以下是对应的代码。
// 将数组numbers分为1块,每一块`sizeof(numbers)`大小
fwrite(numbers, sizeof(numbers), 1, pFile);
// 将数组numbers分为8块,每一块sizeof(int)大小
fwrite(numbers, sizeof(int), 8, pFile);
而参数类型size_t
为sizeof
关键词返回值的类型,通常是unsigned int
类型的别名。
FILE *stream
参数stream
为使用fopen
函数打开文件时返回的文件结构指针。
返回值
fwrite
将返回成功写入文件的数据块的数量。
- 若将数组
numbers
分为1块,写入成功将返回1,写入失败将返回0。 - 若将数组
numbers
分为8块,写入成功将返回8,部分成功将返回小于8大于0的数值,写入失败将返回0。
二进制模式
字节0A
是数值int
类型的数值0A 00 00 00
的前1个字节,刚好为\n
的ASCII码。在文本模式下,字符\n
将会被自动替换为\n\r
,再输出到文件中。其ASCII码为十六进制0D 0A
。因此,数据0A 00 00 00
前会出现一个OD
。很显然,这里的字节0A
并不代表换行,而是与其他3个十六进制字节一起表示一个int
类型的数据。因此,以二进制形式存储为文件并不需要做这个转换。
默认情况下,文件是以文本模式打开的,文本模式下会做换行符的转换。而在函数fopen
的第二个参数中,添加字符b
。以二进制模式打开文件,二进制模式不进行换行符的转换。FILE* pFile = fopen("data.txt", "w");
修改为FILE* pFile = fopen("data.txt", "wb");
现在,字节0A
前不会再自动添加0D
了。
从文件中读取二进制
与之前讨论的直接将数据写入文件的fwrite
函数对应,fread
函数可以将文件中的数据直接读取到内存当中。由于现在需要读取文件,函数fopen
的第二个参数,文件打开模式改为r
。size_t fread(void* buffer, size_t size, size_t count, FILE* stream);
输入:const void* buffer
接收数据的首地址size_t size
每一块数据的大小size_t count
一共有多少块数据FILE* stream
文件结构指针
返回值 :size_t
成功读取多少块数据。
函数fread
的各个参数用法类似于fwrite
函数,不同的是将写入换成了读取。它将从文件中读取count
块数据,每一块数据size
大小,读取出来的数据存放到buffer
为首地址的空间中。返回值为成功读取的块的数量。
#include <stdio.h>
int main()
{
// 创建一个名为data.txt的文件
FILE* pFile = fopen("data.txt", "r");
if (pFile == NULL)
{
// 文件创建失败
return -1;
}
// 接收数值的数组
int numbers[8] = { 0 };
// 每块读取sizeof(numbers)字节,一共读取1块
fread(numbers, sizeof(numbers), 1, pFile);
for (int i = 0; i < 8; i++)
printf("%d\n", numbers[i]);
// 关闭文件
fclose(pFile);
return 0;
}
除了读取固定大小的数据,我们也能让fread
每次读取一字节数据,直到文件结尾或接收的空间存满为止。
#include <stdio.h>
void fileEofOrError(FILE* pFile)
{
if (feof(pFile) != 0) // 测试文件是否结尾
{
printf("end of file\n");
}
else if (ferror(pFile) != 0) // 测试文件是否读写出错
{
printf("file access error\n");
}
}
int main()
{
// 创建一个名为data.txt的文件
FILE* pFile = fopen("data.txt", "r");
if (pFile == NULL)
{
// 文件创建失败
return -1;
}
// 接收数据的数组
int numbers[8] = { 0 };
// 接收数据的首地址
char* p = (char*)(numbers);
// 已读取的字节
int count = 0;
while (1)
{
// 如果数组已经填满8个元素,则不继续读取
if (count >= sizeof(numbers))
{
printf("numbers is full\n");
break;
}
// 每块读取1字节,一共读取1块
int get = fread(p, 1, 1, pFile);
if (get == EOF)
{
fileEofOrError(pFile);
break;
}
p++;
count++;
}
for (int i = 0; i < 8; i++)
printf("%d\n", numbers[i]);
// 关闭文件
fclose(pFile);
return 0;
}
由于fread
函数每次读取1字节并存放到第一个参数指示的地址当中。因此,在下一次读取前,需要将接收数据的地址向后移动一字节。我们将数组首地址存放到一个char *
类型的指针p
当中。fread
函数将读取到的1字节数据,存放到指针 p 中保存的地址当中。在下一次读取开始前,让指针p++
,使得指针中保存的地址向后移动1字节。
注意,文件中的数据可能超过numbers
数组的长度,因此,需要在程序中判断已读取到的数据大小。若数组已经装满,也不应该继续读取了,否则会造成数组越界。代码中使用count
记录已经读取到的数据大小,当count
大于数组长度sizeof(numbers)
时,读取应当停止。