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+" | 以“写入/更新”方式打开文件,相当于w和r+叠加的效果。既可以读取也可以写入,也就是随意更新文件。如果文件不存在,那么创建一个新文件;如果文件存在,那么清空文件内容(相当于删除原文件,再创建一个新文件)。 |
| "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()。 -
性能权衡:在某些场景下,无缓冲或显式刷新缓冲区可能会牺牲性能,但换取了数据的即时性和一致性。应根据具体需求权衡选择合适的缓冲策略。

浙公网安备 33010602011771号