C文件读写

简介: C文件读写

可以将程序中的数据保存为一个文件。待下次重新启动程序时,从之前保存的文件中提取数据。这样,程序就不会在重启后失忆了。

创建并写入文件

#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 的第一个参数为字符串,内容为需要操作的文件路径,第二个参数也为字符串,内容为文件的操作模式

操作模式

读、写模式wr

  • "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函数的返回值判断。

可读可写模式

可以使用+rw模式从单一的模式,升级为读写均可模式。

  1. "w+" 模式,更新模式,可读可写。但是,会清空文件原有内容。
  2. "r+" 模式,更新模式,可读可写。

对于以更新模式 + 打开的文件,这里有一个必须要注意的地方:

  1. 文件从写操作转换为读操作前,必须使用fflushfseekrewind其中一个函数。
  2. 文件从读操作转换为写操作前,必须使用fseekrewind其中一个函数。

字符串输出到文件内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)。
但是,仅仅做了“回车”还不够,我们还需要将纸张上移一行,让“字车”对准新的空白一行。否则,两行字将被重叠打印在一起。这个动作被称作“换行”。
随着时代的发展,字符不仅仅只打印在纸上。例如,在屏幕上打印字符时,无需“字车”。
所以,当人们将开始新的一行引入到计算机上时,分成了两种惯例:

  1. 沿用这两个动作,回车加换行\r\n
  2. 简化为仅换行\n

两类具有代表性的系统分别使用了其中一种惯例:

  1. Windows系统使用\r\n
  2. 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
  • 如果遇到文件读取错误,返回NULLstr中有可能有部分已读取数据。

根据返回值规则,若读取一行字符成功将返回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种宏定义作为参数:

  1. SEEK_SET文件开头(文件第一个字节)
  2. SEEK_CUR当前文件位置
  3. 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修改为hfgetc读取到字符H后,文件指针已经指向了下一个字符。所以,若读取到字符H,需要将文件指针向前移动一个字节,再进行修改。

对于以更新模式 +开的文件,这里有一个必须要注意的地方:
  1. 文件从写操作转换为读操作前,必须使用fflushfseekrewind其中一个函数。
  2. 文件从读操作转换为写操作前,必须使用fseekrewind其中一个函数。

在代码中读写操作转换的地方加入必要函数。如果仅需要读写操作转换,但无需变动文件指针。可以在当前位置处偏移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函数了。写转读时,可以使用fflushfseek偏移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 *,其内部保存了数组的首地址。函数参数buffervoid *类型的指针,而void *类型的指针可以接收任何类型的指针。int *类型的指针在传递给void *类型的指针时,指针类型信息将丢失,仅留下首地址信息

参数size与count

fwrite会把待写入数据分为count块,每一块size个字节。例如:

  1. 将数组numbers分为1块,每一块sizeof(numbers)大小。
  2. 将数组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_tsizeof关键词返回值的类型,通常是unsigned int类型的别名。

FILE *stream

参数stream为使用fopen函数打开文件时返回的文件结构指针。

返回值

fwrite将返回成功写入文件的数据块的数量。

  1. 若将数组numbers分为1块,写入成功将返回1,写入失败将返回0。
  2. 若将数组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)时,读取应当停止。

目录
相关文章
|
6月前
|
存储 程序员 C语言
C文件读写
【2月更文挑战第14天】C文件读写。
28 1
|
6月前
|
存储 C++ iOS开发
C++文件操作
C++文件操作
|
30天前
读写文件使用
读写文件使用
17 2
|
存储 C语言
C 文件读写
C 文件读写。
43 0
|
6月前
|
存储 C语言
c文件读写
c文件读写
37 0
|
6月前
|
存储 移动开发 Linux
C++017-C++文件读写应用
C++017-C++文件读写应用
|
6月前
|
存储 程序员 C语言
文件操作详解
文件操作详解
56 0
|
存储 C++ iOS开发
70 C++ - 文件读写
70 C++ - 文件读写
56 0
|
程序员 编译器 C语言
文件操作(中)
文件操作(中)
36 0
|
存储 编译器 数据库
文件操作介绍(上)
文件操作介绍(上)
52 0