C语言中的数据文件的操作

接下来我们开启今天的C语言之旅吧~

1. 为什么使用文件?

如果没有文件,我们写的程序的数据是储存在电脑中的内存中,如果程序退出,内存回收,数据就会回收,等再次运行程序,是看不到上次程序的数据的,如果将数据进行持久化的保存,我们就可以是文件。

2. 什么是文件?

磁盘(硬盘)上的文件。
从文件功能角度来分类一般有两种:数据文件、程序文件。

2.1 数据文件

⽂件的内容不⼀定是程序,而是程序运行时读写的数据,比如程序运⾏需要从中读取数据的文件,或者输出内容的文件。

2.2 程序文件

程序文件包括源程序⽂件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows 环境后缀为.exe)。

2.3 文件名

⼀个文件要有⼀个唯⼀的文件标识,以便用户识别和引用。

文件名包含3部分:⽂件路径+文件名主⼲+文件后缀 

例如:c:\code\test.txt

3. 文本文件和二进制文件?

根据数据的组织形式,数据文件通常被称为文本文件二进制文件

 1.文本文件:数据以 ASCII 码的形式存储,每个字符都对应一个 ASCII 码值。这种文件可以直接被文本编辑器等工具读取和修改。常见的文本文件包括.txt、.csv等格式。

2.二进制文件:数据以二进制形式存储,没有直接对应的字符表示。这种文件通常包含非文本数据,如图像、音频、视频等。它们不能被文本编辑器等工具直接解析,需要专门的程序进行解析和处理。

在内存中,数据通常以二进制形式存储。如果直接将内存中的二进制数据输出到外存文件中,则生成的文件是二进制文件。如果需要将数据以 ASCII 码的形式存储到外存中,则需要进行转换,这样生成的文件就是文本文件。

因此,要存储为文本文件,需要将数据从二进制形式转换为 ASCII 字符形式,然后存储到外存中。

接下来我们看一下数据文件是怎么在磁盘储存的?

对于一个整数10000(十进制),以ASCII码形式输出时需要5个字符('1', '0', '0', '0', '0'),而以二进制形式输出时只需要4个字节来存储这个整数的二进制表示(这里我们使用的是VS2022)。

这种差异导致了在磁盘上存储时的空间占用不同,所以使用适当的存储方式会节省一些不必要浪费的空间。

4. 文件的关闭和打开

在C语言中,要打开和关闭文件,通常使用标准库中的函数来完成。主要使用的函数是fopen()来打开文件,fclose()来关闭文件。这里我们要记住的是打开文件后,对文件操作完成后一定要关闭文件来释放资源。

下面是它们的基本用法:

4.1 打开文件

使用fopen()函数来打开一个文件,该函数原型如下:

FILE *fopen(const char *filename, const char *mode);

其中,

filename是文件名字符串:对应我们上文提到的文件名。

mode是打开文件的模式字符串。

  • 文本模式打开文件
  • "r":以只读方式打开文件,文件必须存在。文件中的内容将以文本形式读取,即会将文本中的换行符"\n"转换成程序中的换行符(可能是"\n"或"\r\n",取决于操作系统)。

  • "w":以写入方式打开文件,如果文件不存在则创建新文件,如果文件已存在则清空文件内容。将以文本形式写入数据,即会将程序中的换行符转换成文件中的换行符。

  • "a":以追加方式打开文件,如果文件不存在则创建新文件。将以文本形式写入数据,新内容将被追加到文件末尾,不会清空文件原有内容。

  • "r+":以读写方式打开文件,文件必须存在。文件中的内容将以文本形式读取,新数据将以文本形式写入。不会清空文件内容,文件指针会在文件开头。

  • "w+":以读写方式打开文件,如果文件不存在则创建新文件,如果文件已存在则清空文件内容。新数据将以文本形式写入。文件指针会在文件开头。

  • "a+":以读写方式打开文件,如果文件不存在则创建新文件。将以文本形式读取内容,并将新数据追加到文件末尾。文件指针会在文件末尾。

  • 二进制形式打开文件
  • "rb":只读方式打开文件。与 "r" 模式不同的是,"rb" 模式将文件以二进制形式进行读取,适用于二进制文件的读取,例如图像、音频等文件。

  • "wb":只写方式打开文件。与 "w" 模式不同的是,"wb" 模式将文件以二进制形式进行写入,适用于二进制文件的写入。

  • "ab":追加方式打开文件。与 "a" 模式不同的是,"ab" 模式将文件以二进制形式进行追加写入,适用于对二进制文件进行追加写入操作。

  • "rb+":读写方式打开文件。与 "r+" 模式不同的是,"rb+" 模式将文件以二进制形式进行读写,适用于对二进制文件进行读写操作。

  • "wb+":读写方式打开文件。与 "w+" 模式不同的是,"wb+" 模式将文件以二进制形式进行读写,适用于对二进制文件进行读写操作。

  • "ab+":读写方式打开文件。与 "a+" 模式不同的是,"ab+" 模式将文件以二进制形式进行读写,适用于对二进制文件进行读写操作,并且在文件末尾追加写入。

假设我们已经存在一个二进制文件,如何查看二进制的文件:

4.2 关闭文件

4.3 为什么要关闭文件?

关闭文件的一个重要原因是释放文件指针所占用的内存资源。

在C语言中,每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息。这些信息是保存在一个结构体变量中的,这个结构体类型就是系统声明的FILE类型。

FILE类型是C语言标准库中定义的,它是一个结构体类型,用来表示文件流(file stream)。这个结构体包含了文件的相关信息,例如文件描述符、读写位置、缓冲区等。通过fopen()函数打开文件时,会返回一个指向这个结构体的指针(即文件指针),程序可以通过这个指针来访问和操作文件的相关信息。

下面是一个简化的FILE结构体的示例,用来说明其可能包含的一些字段:

struct _IO_FILE {
    int fd; // 文件描述符
    char *buffer; // 缓冲区指针
    size_t buf_size; // 缓冲区大小
    size_t buf_pos; // 缓冲区当前位置
    size_t buf_len; // 缓冲区有效数据长度
    // 其他字段...
};
typedef struct _IO_FILE FILE;
 FILE* pf;//⽂件指针变量

不过,具体的实现是由C标准库和编译器提供的,开发者不需要关心FILE结构体的具体实现细节,只需使用文件指针进行文件操作即可。

如果不关闭文件,文件指针所占用的内存资源将一直保留在程序的内存中,即使文件已经不再需要。在处理大量文件或长时间运行的程序中,不及时关闭文件会导致内存资源的浪费,可能会导致内存泄漏等问题。

另外,及时关闭文件也可以确保文件的完整性和一致性。在写入文件时,如果不关闭文件,可能会导致部分数据还未写入文件就结束了程序,从而导致文件内容不完整。因此,关闭文件是一个良好的编程习惯,可以避免资源浪费和文件数据不一致等问题。

5. 文件的顺序读写

5.1 顺序读写函数分类

在使用这些函数时,需要包含 <stdio.h> 头文件,因为它包含了这些函数的声明。

通常来说,这些函数适用于标准输入流(stdin)、标准输出流(stdout)以及其他输入输出流(如文件输入输出流)。

  1. int fgetc(FILE *stream)

    • 函数功能:从指定的文件流中读取一个字符,并返回其 ASCII 码值。
    • 参数:stream 是指向已打开文件的指针。
    • 返回值:返回所读取字符的 ASCII 码值,如果到达文件结尾或出错,返回 EOF
  2. int fputc(int character, FILE *stream)

    • 函数功能:将指定的字符写入文件流中。
    • 参数:character 是要写入的字符的 ASCII 码值,stream 是指向已打开文件的指针。
    • 返回值:成功写入时返回写入的字符,如果出错返回 EOF
  3. char *fgets(char *str, int num, FILE *stream)

    • 函数功能:从指定的文件流中读取一行字符串。
    • 参数:str 是存储读取内容的字符数组,num 是要读取的字符数的最大值,stream 是指向已打开文件的指针。
    • 返回值:成功读取时返回 str,如果到达文件结尾或出错,返回 NULL
  4. int fputs(const char *str, FILE *stream)

    • 函数功能:将指定的字符串写入到文件流中。
    • 参数:str 是要写入的字符串,stream 是指向已打开文件的指针。
    • 返回值:成功写入时返回非负值,如果出错返回 EOF
  5. size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream)

    • 函数功能:从指定的文件流中读取数据块。
    • 参数:ptr 是用于存储读取数据的内存地址,size 是每个数据项的大小(以字节为单位),nmemb 是要读取的数据项的个数,stream 是指向已打开文件的指针。
    • 返回值:返回实际读取的数据项个数,如果出错或到达文件结尾,返回值可能小于 nmemb
  6. size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)

    • 函数功能:将数据块写入到指定的文件流中。
    • 参数:ptr 是指向要写入数据的内存地址,size 是每个数据项的大小(以字节为单位),nmemb 是要写入的数据项的个数,stream 是指向已打开文件的指针。
    • 返回值:返回实际写入的数据项个数,如果出错,返回值可能小于 nmemb

5.2 流

下面我们介绍一下流的概念:

流(stream)在计算机科学中通常被比作信息在程序和外部设备之间传输的通道,就像河流一样。这些"河流"负责将数据从内存传输到外部设备(如磁盘、网络、显示器等),或者从外部设备传输到内存。

通过流我们可以使内存与外部设备建立联系,流的概念使得程序能够以一种统一的方式来处理不同的输入和输出设备。

标准输入流和标准输出流是由操作系统提供的,默认情况下与终端相关联。这意味着你可以使用这些函数来读取用户的键盘输入并将输出显示在终端上。例如,你可以使用 fgetc(stdin) 来从键盘读取一个字符,使用 fputc(character, stdout) 来将字符输出到终端上。

另一方面,如果你打开了一个文件并将其与一个文件指针相关联,那么你也可以使用这些函数来操作该文件。例如,你可以使用 fgetc(file_pointer) 从文件中读取一个字符,使用 fputc(character, file_pointer) 将字符写入到文件中。

5.3 默认情况的流

在C语言程序启动时,默认会打开三个流:

  1. stdin:标准输入流,通常与键盘输入相关联。C语言中的输入函数(如 scanf())默认从标准输入流中读取数据。当你在程序中使用 scanf() 函数时,它会从标准输入流中读取用户输入的数据。

  2. stdout:标准输出流,通常与显示器界面相关联。C语言中的输出函数(如 printf())默认将信息输出到标准输出流中。当你在程序中使用 printf() 函数时,它会将输出内容显示在显示器界面上。

  3. stderr:标准错误流,也通常与显示器界面相关联。C语言中的错误信息通常输出到标准错误流中。与 printf() 不同,fprintf(stderr, ...) 函数用于将信息输出到标准错误流中。

下面是对函数的使用:

5.4 输出错误信息的函数

而在这里我要多加一句错误信息打印的另外一种方式:

strerror 是一个标准C库函数,用于将错误码转换为对应的错误消息字符串。它通常用于将系统函数返回的错误码(如 errno)转换为可读的错误消息,以便程序员更容易地理解和处理错误。

以下是 strerror 函数的用法示例:

#include <stdio.h>
#include <string.h>

int main() {
    // 模拟一个错误码
    int error_code = 2;

    // 使用 strerror 将错误码转换为错误消息字符串
    const char *error_message = strerror(error_code);

    // 输出错误消息字符串
    printf("错误消息: %s\n", error_message);

    return 0;
}

与之相比,

fprintf(stderr, ...) 函数用于将程序自定义的错误消息输出到标准错误流 stderr。这些错误消息通常是程序员自己定义的,用于描述程序中发生的特定错误情况。

strerror 则是将系统错误码转换为人类可读的错误消息字符串,用于描述系统函数调用可能发生的错误情况。

5.5 文本文件和二进制文件的操作示例

文本文件:

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int c; // 注意:使用 int 类型来存储字符和 EOF,而不是 char
    FILE *fp = fopen("test.txt", "r"); // 打开文件用于读取
    if (!fp) { // 检查文件是否成功打开
        perror("File opening failed"); // 如果失败,输出错误消息
        return EXIT_FAILURE; // 返回失败状态码
    }
    
    // 使用循环逐字符读取文件内容,直到遇到文件结束符 EOF
    while ((c = fgetc(fp)) != EOF) { 
        putchar(c); // 输出读取的字符到标准输出流
    }
    
    // 根据文件读取结束的原因输出相应的消息
    if (ferror(fp)) { // 检查是否发生了 I/O 错误
        puts("I/O error when reading");
    } else if (feof(fp)) { // 检查是否已经到达文件结尾
        puts("End of file reached successfully");
    }
    
    fclose(fp); // 关闭文件
    return EXIT_SUCCESS; // 返回成功状态码
}
5.5.1 ferror遇到错误的情况

在C语言中,IO(输入/输出)错误可能会发生在许多情况下。以下是一些可能导致IO错误的常见情况:

  1. 文件不存在或无法打开:尝试打开一个不存在的文件或者没有权限访问的文件会导致IO错误。

  2. 文件读写错误:尝试从文件中读取数据或向文件中写入数据时,如果发生了IO错误(如磁盘故障、文件损坏等),会导致IO错误。

  3. 文件结尾:尝试从文件中读取数据时,到达文件结尾会导致IO错误。这时 feof 函数可能会返回 true

  4. 文件被移动或删除:如果文件在IO操作期间被移动、删除或修改了访问权限,那么尝试对该文件进行IO操作会导致IO错误。

  5. 硬件故障:硬盘故障、IO设备故障等硬件问题可能导致IO错误。

在C语言中,通常通过检查IO函数的返回值来判断是否发生了IO错误。例如,对于 freadfwritefscanffprintf 等函数,它们在发生IO错误时会返回一个特殊的值(通常是负数),可以通过检查这个返回值来确定是否发生了IO错误。此外,可以使用 ferror 函数来检查文件流上是否发生了IO错误。

5.5.2 feof的使用规范(注意这是用来检查文件结束是否是因为遇到了文件末尾)

feof 函数会返回非零值(即 true),否则返回零值(即 false)。

feof 函数的作用是判断文件读取结束的原因是否是因为遇到了文件尾。

  1. 在文件读取过程中,不能仅凭 feof 函数的返回值来判断文件是否结束。这是因为 feof 函数只能检测到文件是否已经读到了文件尾,并不能检测到其他可能导致文件读取结束的原因。
  2. 对于文本文件的读取,通常应该根据文件读取函数的返回值来判断文件是否已经读取结束。例如,使用 fgetc 函数读取文本文件时,当函数返回 EOF 时表示文件读取结束。而使用 fgets 函数读取文本文件时,当函数返回 NULL字符 '\0'(ASCII码为0)被称为空字符空值终止符,它表示字符串的结束。当使用 fgets 函数从文件中读取字符串时,如果遇到了换行符 \n 或者文件结束符 EOF,则 fgets 函数会将这些字符添加到字符串的末尾,并在其后面添加一个空字符 \0,以表示字符串的结束。 时表示文件读取结束。

二进制文件:

#include <stdio.h>

enum { SIZE = 5 }; // 定义数组的大小为5

int main(void) {
    double a[SIZE] = {1., 2., 3., 4., 5.}; // 声明并初始化double类型的数组a

    FILE *fp = fopen("test.bin", "wb"); // 以二进制写入模式打开文件test.bin
    if (!fp) { // 检查文件是否成功打开
        perror("File opening failed"); // 输出错误消息
        return 1; // 返回错误码
    }

    fwrite(a, sizeof *a, SIZE, fp); // 将数组a写入到文件中
    fclose(fp); // 关闭文件

    double b[SIZE]; // 声明另一个double类型的数组b

    fp = fopen("test.bin", "rb"); // 以二进制读取模式打开文件test.bin
    if (!fp) { // 检查文件是否成功打开
        perror("File opening failed"); // 输出错误消息
        return 1; // 返回错误码
    }

    size_t ret_code = fread(b, sizeof *b, SIZE, fp); // 从文件中读取数据到数组b
    if (ret_code == SIZE) { // 检查是否成功读取了SIZE个元素
        puts("Array read successfully, contents: "); // 输出成功读取的消息
        for (int n = 0; n < SIZE; ++n) {
            printf("%f ", b[n]); // 输出数组b中的元素
        }
        putchar('\n'); // 换行
    } else { // 如果未成功读取SIZE个元素
        if (feof(fp)) {
            printf("Error reading test.bin: unexpected end of file\n"); // 输出意外的文件末尾消息
        } else if (ferror(fp)) {
            perror("Error reading test.bin"); // 输出错误消息
        }
    }

    fclose(fp); // 关闭文件
    return 0; // 返回成功码
}

6. 缓冲区

6.1 缓冲区的介绍存在的意义

我们想象一下,在课堂上,学生们难免会遇到学习上的问题,这时有一个同学来问问题,而下一秒另外一个同学也来问这个相同的问题,这无疑会在很大程度上减少了课堂的效率。而解决的办法就是等同学都将疑问抛出来后,老师再做解答。

这时我们就需要类似后者的工具来完成数据的一并传输,来保证效率,便是缓冲区

 

缓冲区处理数据的大小限制通常是由操作系统决定的,但也受到编译器和硬件的影响。以下是这些因素对缓冲区处理数据大小的影响:

  1. 操作系统:操作系统通常会限制单个进程能够分配的内存大小。缓冲区处理数据的大小受到操作系统对进程的内存限制的约束。不同的操作系统有不同的内存管理机制和内存限制,因此缓冲区处理数据的大小也可能不同。

  2. 硬件:硬件限制了系统的整体性能,包括内存大小和处理器速度等。较小的内存大小可能会限制缓冲区处理数据的大小,因为缓冲区需要占用一定的内存空间来存储数据。另外,较慢的处理器速度也可能会影响缓冲区处理数据的速度。

  3. 编译器:编译器可以影响程序的内存分配和优化方式,从而间接影响缓冲区处理数据的大小。一些编译器可能会对内存分配进行优化,使得程序可以分配更多的内存用于缓冲区。

接下来我们用一段程序演示缓冲区的存在:

#include <stdio.h>
#include <windows.h>

int main() {
    FILE *pf = fopen("test.txt", "w");
    
    // 先将数据放入输出缓冲区
    fputs("abcdef", pf);
    
    // 提示消息,输出到标准输出流(控制台)
    printf("睡眠10秒-已经写数据了,打开test.txt⽂件,发现⽂件没有内容\n");
    
    // 休眠10秒钟,模拟等待一段时间
    Sleep(10000);
    
    // 刷新缓冲区,将输出缓冲区的数据写入到文件(磁盘)
    fflush(pf);
    
    // 注:fflush 在⾼版本的VS上不能使⽤了
    
    // 提示消息,输出到标准输出流
    printf("刷新缓冲区\n");
    
    // 再次休眠10秒钟
    Sleep(10000);
    
    // 关闭文件流,同时刷新缓冲区,确保数据写入文件
    fclose(pf);
    
    // 注:fclose在关闭⽂件的时候,也会刷新缓冲区
    
    // 避免悬挂指针
    pf = NULL;
    
    return 0;
}

程序运行:

在刷新缓冲区之前我们没有看到数据。

我们快速在关闭文件之前查看txt文件,发现有了数据。

(而实际上关闭文件的操作也会刷新一次缓冲区:关闭文件时通常会执行一次缓冲区刷新操作,以确保所有数据都被写入到文件中。)

这次的分享就到此结束,感谢观看🌹,如有不足可以指出,欢迎大家来讨论😊

  • 25
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 8
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值