13.C 文件读写

C 文件读写

在C语言中,文件是一种抽象概念,代表了计算机系统中持久存储数据的实体。文件可以理解为一组有序的字节序列,这些字节按照特定的格式和结构组织在一起,用于保存各种类型的信息,如文本、图像、音频、程序代码等。在C语言编程中,文件操作是与操作系统交互的重要组成部分,允许程序创建、读取、写入、修改和删除文件,从而实现数据的持久化存储和交换。C语言提供了标准库中的文件操作函数,使程序员能够对文件进行创建、打开、读写、关闭、删除等操作。

文件流

所有的文件(保存在磁盘)都要载入内存才能处理,所有的数据必须写入文件(磁盘)才不会丢失。数据在文件和内存之间传递的过程叫做文件流。数据从文件复制到内存的过程叫做输入流,从内存保存到文件的过程叫做输出流。

文件是数据源的一种,除了文件,还有数据库、网络、键盘等;数据传递到内存也就是保存到C语言的变量(例如整数、字符串、数组、缓冲区等)。我们把数据在数据源和程序(内存)之间传递的过程叫做数据流(Data Stream)。相应的,数据从数据源到程序(内存)的过程叫做输入流(Input Stream),从程序(内存)到数据源的过程叫做输出流(Output Stream)。

输入输出(Input output,IO)是指程序(内存)与外部设备(键盘、显示器、磁盘、其他计算机等)进行交互的操作。几乎所有的程序都有输入与输出操作,如从键盘上读取数据,从本地或网络上的文件读取数据或写入数据等。通过输入和输出操作可以从外界接收信息,或者是把信息传递给外界。

1. 文件类型

  • 文本文件:包含可读字符序列,通常以ASCII码或Unicode编码表示。文本文件的内容可以直接被人阅读,也可以被程序解析。编辑器、记事本等应用程序通常用来创建和编辑文本文件。

  • 二进制文件:包含任意字节序列,不局限于可打印字符。二进制文件通常用于存储程序的可执行代码、数据库记录、图像、音视频等数据,这些数据的结构和编码方式由应用程序约定,直接打开可能显示为乱码。程序通常需要专门的解析逻辑来处理二进制文件。

  • 源代码文件:包含C语言或其他编程语言编写的程序源代码,通常以.c.cpp等扩展名标识。编译器读取源代码文件,将其翻译成可执行的机器指令。

  • 目标文件:编译器处理源代码文件产生的中间产物,包含汇编语言或机器语言指令。在链接阶段,目标文件会被合并成最终的可执行程序或动态链接库。

  • 可执行文件:经过编译和链接后形成的可以直接由操作系统加载执行的文件,通常以.exe(Windows)、.out.elf(Unix/Linux)等扩展名标识。

2. 文件操作

C语言通过标准库中的文件操作函数来与文件系统交互,主要包括以下几个方面:

1. 打开与关闭文件

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

  • 功能:打开一个文件,返回一个指向FILE结构体的指针(文件指针),用于后续对该文件的操作。如果无法打开文件或出现其他错误,则返回NULL

  • 参数

    • filename:指向包含文件路径和名称的字符串常量指针,指向要打开的文件。
    • mode:指向包含访问模式的字符串常量指针,指示如何打开文件。常见的模式包括:
      • "r":只读模式。打开已存在的文件,用于读取数据。如果文件不存在或无法以只读方式打开,fopen()失败。
      • "w":写入模式。创建一个新文件(如果文件已存在则覆盖其内容),用于写入数据。如果无法创建新文件或以写入方式打开文件,fopen()失败。
      • "a":追加模式。打开已存在的文件,所有写入操作都在文件末尾进行,不会覆盖现有内容。如果文件不存在,则创建一个新文件。
      • "r+":读写模式。打开已存在的文件,允许读取和写入。如果文件不存在,fopen()失败。
      • "w+":读写模式。类似"w",但同时允许读取。如果文件已存在,其内容会被清空。
      • "a+":读写模式。类似"a",但同时允许读取。所有写入操作都在文件末尾进行。
  • 返回值

    • 成功:返回指向FILE结构体的指针(文件指针)。fopen() 会获取文件信息,包括文件名、文件状态、当前读写位置等,并将这些信息保存到一个 FILE 类型的结构体变量中,然后将该变量的地址返回。
    • 失败:返回NULL,且可以通过errno获取具体错误代码。

int fclose(FILE *stream);

  • 功能:关闭指定的文件,释放与之关联的所有系统资源。

  • 参数

    • stream:由fopen()返回的文件指针,指向要关闭的文件。
  • 返回值

    • 成功:返回0。
    • 失败:返回非零值,可通过errno获取具体错误代码。
控制读写权限的字符串(必须指明)
打开方式 说明
"r" 以“只读”方式打开文件。只允许读取,不允许写入。文件必须存在,否则打开失败。
"w" 以“写入”方式打开文件。如果文件不存在,那么创建一个新文件;如果文件存在,那么清空文件内容(相当于删除原文件,再创建一个新文件)。
"a" 以“追加”方式打开文件。如果文件不存在,那么创建一个新文件;如果文件存在,那么将写入的数据追加到文件的末尾(文件原有的内容保留)。
"r+" 以“读写”方式打开文件。既可以读取也可以写入,也就是随意更新文件。文件必须存在,否则打开失败。
"w+" 以“写入/更新”方式打开文件,相当于wr+叠加的效果。既可以读取也可以写入,也就是随意更新文件。如果文件不存在,那么创建一个新文件;如果文件存在,那么清空文件内容(相当于删除原文件,再创建一个新文件)。
"a+" 以“追加/更新”方式打开文件,相当于a和r+叠加的效果。既可以读取也可以写入,也就是随意更新文件。如果文件不存在,那么创建一个新文件;如果文件存在,那么将写入的数据追加到文件的末尾(文件原有的内容保留)。
控制读写方式的字符串(可以不写)
打开方式 说明:读写权限和读写方式可以组合使用,但是必须将读写方式放在读写权限的中间或者尾部(换句话说,不能将读写方式放在读写权限的开头)
"t" 文本文件。如果不写,默认为"t"
"b 二进制文件。

2. 字符读写

字符读写fgetc()getc()getchar()用于从文件中读取单个字符,fputc()putc()putchar()用于向文件中写入单个字符。

int fgetc(FILE *stream);

  • 功能:从指定的文件中读取下一个字符,并将文件指针向前移动一位。

  • 参数

    • stream:指向要从中读取的文件的文件指针。
  • 返回值

    • 成功:返回读取到的字符(作为整数)。
    • 到达文件末尾(EOF):返回EOF(通常定义为-1)。
    • 失败:返回EOF,可通过feof()ferror()判断具体原因。

int fputc(int c, FILE *stream);

  • 功能:将指定的字符写入到指定的文件中,并将文件指针向前移动一位。

  • 参数

    • c:要写入的字符(作为整数)。
    • stream:指向要写入的文件的文件指针。
  • 返回值

    • 成功:返回写入的字符(与参数c相同)。
    • 失败:返回EOF,可通过ferror()判断具体原因。

3. 块读写

块读写fgets()从文件中读取一行文本(包括换行符),fputs()将一个字符串写入文件。fread()fwrite()则用于以二进制方式读写固定大小的数据块。

char *fgets(char * restrict s, int n, FILE * restrict stream);

  • 功能:从指定的文件中读取最多n - 1个字符,直到遇到换行符、文件末尾或读取到n - 1个字符为止。读取到的字符串以\0结束,并存储在s指向的缓冲区内。文件指针移动到读取的下一个字符。

  • 参数

    • s:指向用于存储读取到的字符串的缓冲区。
    • n:指定缓冲区的最大长度(包括结尾的\0)。
    • stream:指向要从中读取的文件的文件指针。
  • 返回值

    • 成功:返回指向s的指针。
    • 到达文件末尾(EOF):返回NULL,但s可能包含已读取的部分数据。
    • 失败:返回NULL,可通过feof()ferror()判断具体原因。

int fputs(const char * restrict s, FILE * restrict stream);

  • 功能:将指定的字符串s(不包括结尾的\0)写入到指定的文件中。文件指针移动到写入的下一个位置。
  • 参数
    • s:指向要写入的字符串。
    • stream:指向要写入的文件的文件指针。
  • 返回值
    • 成功:返回非负值。
    • 失败:返回EOF,可通过ferror()判断具体原因。

size_t fread(void * restrict ptr, size_t size, size_t nmemb, FILE * restrict stream);

  • 功能:从指定的文件中读取数据块,并将数据存储到ptr指向的缓冲区。读取的数据量为size乘以nmemb字节。文件指针移动到读取的下一个位置。

  • 参数

    • ptr:指向用于存储读取数据的缓冲区的指针。
    • size:单个数据元素的大小(单位:字节)。
    • nmemb:要读取的数据元素数量。
    • stream:指向要从中读取的文件的文件指针。
  • 返回值

    • 成功:返回实际读取的数据元素数量(可能小于nmemb,如遇到文件末尾)。
    • 失败:返回0,可通过feof()ferror()判断具体原因。

size_t fwrite(const void * restrict ptr, size_t size, size_t nmemb, FILE * restrict stream);

  • 功能:将ptr指向的缓冲区中的数据块写入到指定的文件中。写入的数据量为size乘以nmemb字节。文件指针移动到写入的下一个位置。

  • 参数

    • ptr:指向包含要写入数据的缓冲区的指针。
    • size:单个数据元素的大小(单位:字节)。
    • nmemb:要写入的数据元素数量。
    • stream:指向要写入的文件的文件指针。
  • 返回值

    • 成功:返回实际写入的数据元素数量(可能小于nmemb,如遇到磁盘空间不足或写入错误)。
    • 失败:返回0,可通过ferror()判断具体原因。

4. 格式化读写

格式化读写fprintf()fscanf()类似于printf()scanf(),但针对文件操作,允许按照指定格式写入或读取数据。

int fprintf(FILE * restrict stream, const char * restrict format, ...);

  • 功能:类似于printf(), 但将格式化输出写入到指定的文件中。接受一个可变参数列表,根据format字符串中的格式说明符,将相应参数的值转换为字符串并写入到文件。

  • 参数

    • stream:指向要写入的文件的文件指针。
    • format:包含格式说明符的字符串,用于控制输出格式。
    • ...:与format中格式说明符对应的可变参数列表。
  • 返回值

    • 成功:返回实际写入的字符数。
    • 失败:返回负值,可通过ferror()判断具体原因。

5. 定位与查询

int fseek(FILE *stream, long int offset, int whence);

  • 功能:改变文件指针的位置,使其指向文件中的特定位置。根据whence参数指定的起始位置(文件开头、当前位置、文件末尾),加上offset偏移量来确定新的文件指针位置。

  • 参数

    • stream:指向要修改的文件的文件指针。
    • offset:相对于whence指定起始位置的偏移量(单位:字节)。
    • whence:起始位置标志,取值可以是:
      • SEEK_SET:从文件开头开始计算偏移量。
      • SEEK_CUR:从当前文件指针位置开始计算偏移量。
      • SEEK_END:从文件末尾开始计算偏移量。
  • 返回值

    • 成功:返回0。
    • 失败:返回非零值,可通过errno获取具体错误代码。

long int ftell(FILE *stream);

  • 功能:返回当前文件指针在文件中的位置(字节偏移量)。

  • 参数

    • stream:指向要查询的文件的文件指针。
  • 返回值

    • 成功:返回文件指针位置(字节偏移量)。
    • 失败:返回-1L,可通过errno获取具体错误代码。

void rewind(FILE *stream);

  • 功能:将文件指针重置到文件开头。相当于fseek(stream, 0L, SEEK_SET)

  • 参数

    • stream:指向要重置的文件的文件指针。
  • 返回值:无。

int feof(FILE *stream);

  • 功能:检测是否到达文件末尾。如果最后一次读取操作尝试越过文件末尾,feof()返回非零值;否则返回0。

  • 参数

    • stream:指向要检测的文件的文件指针。
  • 返回值

    • 到达文件末尾:返回非零值。
    • 未到达文件末尾:返回0。

int ferror(FILE *stream);

  • 功能:检查是否存在读写错误。如果最后一次文件操作发生错误,ferror()返回非零值;否则返回0。

  • 参数

    • stream:指向要检查的文件的文件指针。
  • 返回值

    • 存在错误:返回非零值。
    • 无错误:返回0。

6. 其他操作

remove():删除一个文件,rename():更改文件的名称,tmpfile():创建并打开一个临时文件,关闭时自动删除。

int remove(const char *filename);

  • 功能:删除指定的文件。

  • 参数

    • filename:指向包含要删除文件路径和名称的字符串常量指针。
  • 返回值

    • 成功:返回

3. 标准流

标准流是C语言中预定义的三个特殊的文件流,它们在程序启动时自动打开,不需要程序员通过fopen()显式打开,且在程序结束时会自动关闭。标准流提供了程序与外部世界(如用户、终端设备)进行交互的标准接口,极大地简化了输入输出操作。以下是关于标准流的详细说明:

1. 标准输入流(stdin

定义:标准输入流通常与键盘设备关联,允许程序接收用户的输入数据。

标识符FILE *stdin,预定义在<stdio.h>头文件中。

模式:标准输入流默认以只读模式("r")打开。

使用:程序员可以通过诸如fscanf()fgets()getchar()等函数从标准输入流读取数据。例如:

int age;
char name[50];

if (fscanf(stdin, "%d %s", &age, name) == 2) {
    printf("Age: %d, Name: %s\n", age, name);
} else {
    printf("Input parsing failed.\n");
}

2. 标准输出流(stdout

定义:标准输出流通常与显示器(终端窗口)关联,允许程序向用户显示信息。在大多数情况下,这是程序输出结果的主要途径。

标识符FILE *stdout,预定义在<stdio.h>头文件中。

模式:标准输出流默认以写入模式("w")打开。

使用:程序员可以通过诸如printf()fprintf(stdout, ...)puts()fputs()putc()putchar()等函数向标准输出流写入数据。例如:

printf("Hello, World!\n");

3. 标准错误流(stderr

定义:标准错误流同样与显示器(终端窗口)关联,但主要用于输出程序错误信息、警告和调试信息。与标准输出流分离,便于程序在重定向标准输出时仍然能在终端显示错误信息,保持错误报告的可见性。

标识符FILE *stderr,预定义在<stdio.h>头文件中。

模式:标准错误流默认以写入模式("w")打开。

使用:程序员通常通过fprintf(stderr, ...)fputs()putc()putchar()等函数向标准错误流写入数据。例如:

fprintf(stderr, "An error occurred: %s\n", strerror(errno));

4.特性与注意事项

  • 重定向:在命令行环境中,标准输入、标准输出和标准错误流都可以通过 shell 命令进行重定向。例如,将标准输出重定向到文件:

    ./my_program > output.txt
    

    或者将标准错误重定向到标准输出:

    ./my_program 2>&1
    
  • 缓冲:标准流通常带有缓冲机制。标准输出和标准错误流可能是行缓冲(当一行数据结束或缓冲区满时刷新)或全缓冲(当缓冲区满时刷新),具体取决于它们与终端的关联情况。标准输入通常是全缓冲。理解缓冲机制有助于确保数据及时、完整地写入或读取。必要时,可以使用fflush()函数强制刷新缓冲区。

  • 同步:在多线程环境中,如果多个线程同时访问标准流,尤其是标准输出和标准错误,可能会导致输出交错。此时,可能需要使用锁或其他同步机制来确保输出的顺序性。

综上所述,标准流是C语言中预定义的特殊文件流,包括标准输入(stdin)、标准输出(stdout)和标准错误(stderr),它们为程序与外部世界(如用户、终端设备)之间的交互提供了便捷、统一的接口。理解并熟练使用标准流是编写C语言程序的基础技能之一。

4.文件缓冲区

1. 文件缓冲区概念

文件缓冲区是C语言标准库为提高文件I/O效率而引入的一种内存区域。当程序进行文件读写操作时,数据并不直接与物理硬盘进行交互,而是先临时存储在内存中的缓冲区中。这样做的好处在于:

  • 减少磁盘I/O:磁盘I/O操作相比内存操作速度慢得多。通过缓冲区,程序可以批量读写数据,减少频繁的磁盘访问,显著提升性能。
  • 优化数据传输:对于连续的大规模数据读写,缓冲区可以提供更大的数据块,有利于操作系统进行更高效的数据传输。
  • 简化编程模型:程序员无需关心底层的硬件细节,只需通过标准库提供的文件操作函数与缓冲区交互,简化了编程工作。

2. 缓冲区的工作原理

1.写入操作

当程序使用fprintf(), fwrite()等函数向文件写入数据时,数据首先被放入与文件关联的缓冲区。在以下情况下,缓冲区的内容会被刷新到磁盘:

  • 缓冲区满:当写入的数据使缓冲区达到预设容量时,标准库会自动将缓冲区内容写入文件,并清空缓冲区以接收新的数据。
  • 遇到特定字符:对于行缓冲模式(如标准输出stdout在连接到终端时),当写入数据中包含换行符(\n)时,标准库会立即将缓冲区内容刷新到磁盘。
  • 显式刷新:程序员调用fflush()函数强制将缓冲区内容写入文件。
  • 文件关闭:当调用fclose()关闭文件时,标准库会确保缓冲区中所有未写入的数据被刷新到文件。

2.读取操作

对于文件读取操作(如fscanf(), fread()),标准库也会使用缓冲区。读取数据时,程序从缓冲区中获取数据,而非直接从磁盘读取。当缓冲区为空时,标准库会一次性从磁盘读取一定数量的数据填充缓冲区。这样,后续的读取操作就可以快速从内存中的缓冲区获取数据,而不是每次都发起磁盘I/O请求。

3. 缓冲区的管理

缓冲区的管理是指C语言标准库如何为打开的文件分配缓冲区、选择合适的缓冲策略以及在文件操作过程中维护缓冲区状态的过程。以下是关于文件缓冲区管理的详细解释:

1. 缓冲区的分配

当通过fopen()函数打开一个文件时,C标准库会为该文件分配一个缓冲区。缓冲区是程序内存空间中的一块区域,其大小通常由标准库内部设定,也可能受到系统资源限制。分配的缓冲区将与打开的文件句柄(即返回的FILE *指针)关联起来,后续对该文件的所有读写操作都将通过这个缓冲区进行。

2. 缓冲策略的选择

根据文件的打开模式和类型,标准库会选择合适的缓冲策略。常见的缓冲策略包括:

  • 全缓冲(Fully Buffered):对于大多数打开为读或写模式的文件,标准库默认采用全缓冲策略。在这种模式下,程序写入的数据先存入缓冲区,直到缓冲区满或显式刷新时才一次性写入磁盘;读取数据时,标准库一次性从磁盘读取大量数据填充缓冲区,然后程序从缓冲区中逐次读取。

  • 行缓冲(Line Buffered):对于连接到终端(如标准输入stdin、标准输出stdout和标准错误stderr)的文件,或者打开模式为"r+""w+""a+"的文件,标准库可能采用行缓冲策略。在这种模式下,当写入数据中包含换行符(\n)时,标准库会立即将缓冲区内容刷新到磁盘。读取数据时,依然一次性从磁盘读取大量数据填充缓冲区,但遇到换行符时,会立即返回当前行的数据。

  • 无缓冲(Unbuffered):对于某些需要即时写入或读取数据的应用场景,可以使用setvbuf()函数设置为无缓冲模式。在这种模式下,每次读写操作都会直接与磁盘交互,不使用缓冲区。无缓冲模式通常用于实时性要求极高或需要精确控制数据流的应用,如设备驱动程序、实时控制系统等。

3. 缓冲区状态的维护

在文件操作过程中,标准库会维护缓冲区的状态,包括:

  • 缓冲区满/空:对于写入操作,当缓冲区中的数据量达到预设容量时,标记缓冲区为满,触发数据刷新到磁盘;对于读取操作,当缓冲区中的数据已被读完时,标记缓冲区为空,触发从磁盘读取更多数据填充缓冲区。

  • 错误状态:当发生读写错误时,标准库会在缓冲区中记录错误状态,可通过feof()ferror()函数查询。调用clearerr()函数可以清除这些错误状态。

  • 缓冲区刷新:当缓冲区满、遇到特定字符(如行缓冲模式下的换行符)或调用fflush()函数时,标准库负责将缓冲区中的数据刷新到磁盘,并更新缓冲区状态。

  • 文件关闭时的缓冲区处理:当调用fclose()关闭文件时,标准库会确保缓冲区中所有未写入的数据被刷新到文件,然后释放缓冲区资源。

4. 缓冲区的控制与调整

程序员可以通过以下方式对缓冲区进行控制或调整:

  • 显式刷新缓冲区:使用fflush()函数可以强制将缓冲区内容写入磁盘,这对于确保重要数据立即写入或在程序退出前清理缓冲区非常有用。

  • 更改缓冲模式:使用setvbuf()函数可以更改文件的缓冲模式(全缓冲、行缓冲、无缓冲)或设置自定义缓冲区大小。自定义缓冲区大小通常仅适用于全缓冲模式。

  • 查询缓冲状态:通过feof()ferror()函数可以查询文件流是否到达文件末尾(读取操作)或是否有读写错误。fileno()函数可以获取与文件流关联的文件描述符,有助于与其他低级I/O函数配合使用。

5. 缓冲区与多线程/进程

在多线程或多进程环境中,对同一个文件进行并发读写时,需要特别注意缓冲区的同步问题,以避免数据竞争、顺序混乱等问题。通常需要使用互斥锁等同步机制来保护对文件流及其缓冲区的访问。

综上所述,文件缓冲区的管理包括缓冲区的分配、缓冲策略的选择、缓冲区状态的维护以及对缓冲区的控制与调整。理解并合理运用这些管理机制,可以帮助程序员编写高效、安全且符合预期的文件操作代码。同时,应对多线程/进程环境下的缓冲区同步问题保持警惕,确保数据一致性与正确性。

4. 缓冲区的影响与注意事项

虽然文件缓冲区带来了诸多好处,但在使用时需要注意以下几点:

  • 数据一致性:在程序异常终止或系统崩溃的情况下,缓冲区中尚未刷新到磁盘的数据可能会丢失。对于重要数据,应在关键点使用fflush()确保数据写入磁盘,尤其是在程序退出前。

  • 顺序输出:由于缓冲机制,多个线程或进程同时写入同一个文件可能导致输出顺序混乱。需要通过同步机制(如互斥锁)协调对共享文件的访问,保证输出顺序正确。

  • 实时性:对于需要即时更新磁盘数据或依赖于文件状态的应用(如监控文件大小变化),应考虑使用无缓冲或行缓冲模式,或者适时调用fflush()

  • 性能权衡:在某些场景下,无缓冲或显式刷新缓冲区可能会牺牲性能,但换取了数据的即时性和一致性。应根据具体需求权衡选择合适的缓冲策略。

posted @ 2026-08-09 21:29  littlecamel  阅读(5)  评论(0)    收藏  举报