文件(file),是数据存储的一种形式,与我们在编程中的即时性数据不同,文件中的数据具有持久性。那我们可以将程序运行过程中生成的数据写入文件中,或者从文件中得到数据在编程中使用吗?当然可以,不过是将我们之前的控制台换成文件而已。
而在这可转换的背后,是一种名为“流”(strem)之物。可以说这是一种很形象也很抽象的东西,它形象化数据成水流一样,从一端流向另一端,但是却很泛滥,因为有很多“流”。
![](https://i-blog.csdnimg.cn/direct/a3295b80424844aea5965879975c7b59.png)
值得庆幸的是,在C中,我们可以用一种通用的接口去表示不同的流,而不用像Java一样那么细分。因为不管数据是字节还是字符,我们都可以统称为I/O流,在C中我们通过类型的对象来表示,这些对象只能通过
类型的指针进行访问和操作。每个流都与一个外部物理设备(文件、标准输入流、打印机、串行端口等)相关联。
一、FIFE对象
是一个定义在标准库
中的一个结构体,它长得像这样
有很多成员,但是相关文档貌似并没有特别指明这些成员的作用和含义,标准制定者的意思是希望开发者把其当作一个整体使用,所以为我们提供了一系列的函数。
文档是这么说的
Each
FILE
object denotes a C stream.C standard does not specify whether
FILE
is a complete object type. While it may be possible to copy a validFILE
, using a pointer to such a copy as an argument for an I/O function invokes unspecified behavior. In other words,FILE
may be semantically non-copyable.每个FILE对象表示一个C流。 C标准没有指定FILE是否是完整的对象类型。虽然可以复制有效的FILE,但使用指向此类副本的指针作为I/O函数的参数会调用未指定的行为。换句话说,FILE在语义上可能是不可复制的。
尽管如此,在后面的文件操作中,我们仍然可以推测出这些成员的含义。
标准输入、输出和错误输出
在C语言中,标准输入(stdin)、标准输出(stdout)和标准错误输出(stderr)是三个预定义的流对象,用于处理输入和输出。
它们可以在文件中找到
- 标准输入(stdin)是程序从用户获取输入的流,它通常是键盘输入。可以使用scanf、getchar等函数从标准输入读取数据。
- 标准输出(stdout)是程序输出结果的流,它通常是屏幕输出。可以使用printf、putchar等函数将数据输出到标准输出。
- 标准错误输出(stderr)是程序输出错误信息的流,它也通常是屏幕输出。可以使用fprintf函数将错误信息输出到标准错误输出。
这三个流对象在C语言中是预先定义的,因此程序可以直接使用它们进行输入和输出操作,而不需要先进行初始化或打开文件。
二、基本文件IO操作
(一)文件读写
在C语言中,用于文件操作的一些常见函数包括:
函数 | 描述 |
---|---|
文件访问 | |
| 打开一个文件,并返回一个文件指针,失败将是一个空指针。默认文件类型为文本文件,当未指明文件类型时,C编译系统按文本文件进行处理。 |
| 文件操作结束后,应当关闭文件以释放所占内存缓冲区空间,把更新后的数据写回硬盘,否则文件可能会被误用,造成信息混乱或丢失。关闭成功返回值0,否则返回非零值; |
读写字符 | |
| 把参数 c 的字符值写入到 fp 所指向的输出流中。如果写入成功,它会返回写入的字符,如果发生错误,则会返回 EOF。 |
| 从 fp 所指向的输入文件中读取一个字符。返回值是读取的字符,如果发生错误则返回 EOF。 |
读写字符串 | |
| 从 fp 所指向的输入流中读取 n - 1 个字符。它会把读取的字符串复制到缓冲区 buf,并在最后追加一个 \0 字符来终止字符串。如果这个函数在读取最后一个字符之前就遇到一个换行符 '\n' 或文件的末尾 EOF,则只会返回读取到的字符,包括换行符。非正常结束时返回NULL值 |
| 字符串 s 写入到 fp 所指向的输出流中。如果写入成功,它会返回写入文件的字符个数,如果发生错误,则会返回NULL。 |
文件成块读写 | |
| 从文件中读取指定数量的数据块。从文件指针fp指向的文件中读取n个大小为size字节的数据项到以buffer为首地址的内存区域 |
| 将指定数量的数据块写入文件。从以buffer为首地址的内存区域中读取n个大小为size字节的数据项到文件指针fp指向的文件 |
文件定位 | |
| 移动文件指针到指定位置。fseek 设置当前读写点到 offset 处, whence 可以是 SEEK_SET,SEEK_CUR,SEEK_END 这些值决定是从文件头、当前点和文件尾计算偏移量 offset。 |
| 返回偏离文件头部的字节数,失败返回-1L |
| 设置文件位置为给定流 fp 的文件的开头 |
格式化读写 | |
| 发送格式化输出到流 中。如果成功,则返回写入的字符总数,否则返回一个负数。 |
| 从流 读取格式化输入。如果成功,该函数返回成功匹配和赋值的个数。如果到达文件末尾或发生读错误,则返回 EOF。 |
文件检测 | |
| 检查文件是否结束,结束返回非零值,否则返回0; |
| 检测文件读写有无出错,未出错返回0 |
| 清除出错标志与文件结束标志,使它们为0值; |
1、打开与关闭文件
C的标准函数提供了打开和关闭文件的功能,其中主要使用的函数是fopen
和fclose
。
fopen
函数用于打开文件,它的原型如下:
FILE *fopen(const char *filename, const char *mode);
其中,filename
是一个字符串,表示要打开的文件名,mode
是一个字符串,表示打开文件的模式。
mode 说明:
模式 | 描述 |
r | 打开一个已有的文本文件,允许读取文件。 |
w | 打开一个文本文件,允许写入文件。如果文件不存在,则会创建一个新文件。在这里,的程序会从文件的开头写入内容。如果文件存在,则该会被截断为零长度,重新写入。 |
a | 打开一个文本文件,以追加模式写入文件。如果文件不存在,则会创建一个新文件。在这里,的程序会在已有的文件内容中追加内容。 |
r+ | 打开一个文本文件,允许读写文件。 |
w+ | 打开一个文本文件,允许读写文件。如果文件已存在,则文件会被截断为零长度,如果文件不存在,则会创建一个新文件。 |
a+ | 打开一个文本文件,允许读写文件。如果文件不存在,则会创建一个新文件。读取会从文件的开头开始,写入则只能是追加模式。 |
如果处理的是二进制文件,则需使用下面的访问模式来取代上面的访问模式:
"rb", "wb", "ab", "rb+", "r+b", "wb+", "w+b", "ab+", "a+b"
下表列出了在使用fopen
函数打开二进制文件时允许的mode
参数及其说明:
mode参数 | 说明 |
---|---|
"rb" | 以只读方式打开二进制文件,文件必须存在 |
"wb" | 以写入方式创建或打开二进制文件,清空文件内容,若文件存在则截断为0字节 |
"ab" | 以追加方式打开二进制文件,若文件不存在则创建,若文件存在则在文件末尾追加 |
"r+b" | 以读写方式打开二进制文件,文件必须存在,允许读和写操作,文件内容从文件开始读取或写入内容 |
"w+b" | 以读写方式创建或打开二进制文件,清空文件内容,若文件存在则截断为0字节,允许读和写操作 |
"a+b" | 以读写方式打开二进制文件,若文件不存在则创建,若文件存在则在文件末尾追加,允许读和写操作 |
"x+b" | 以读写方式创建或打开二进制文件,若文件已存在则函数失败 |
注意:以上表格中的b
表示所打开的文件为二进制文件。
fopen
函数返回一个指向FILE
结构的指针,该结构描述了文件的属性和状态。如果打开文件成功,则返回指向文件的指针,否则返回NULL
。
fclose
函数用于关闭文件,它的原型如下:
int fclose(FILE *stream);
其中,stream
是一个指向要关闭文件的指针。fclose
函数会关闭指定的文件,并在关闭成功时返回0,否则返回非零值。
示例代码:
#include <stdio.h>
int main() {
FILE *file = fopen("test.txt", "w");
if (file != NULL) {
// 文件打开成功
fprintf(file, "Hello, World!");
fclose(file);
} else {
// 文件打开失败
printf("Failed to open file.\n");
}
return 0;
}
关于打开文件的一些问题
编码问题
以下为一个测试的程序,为了避免是不是文件编码问题,这里标注了UTF-8的字符编码,这对于英文字符不太有影响(当然,读者也可以试试不添加),然后文件都是同一文件夹下
文件都存在,但是有中文的会出现找不到
原因在于,在C语言中使用fopen
函数打开含有中文路径的文件,尤其是在Windows平台上,因为fopen
默认使用的是ANSI编码,而中文字符在ANSI编码下可能无法正确解析。
The
fopen
function opens the file specified byfilename
. By default, a narrowfilename
string is interpreted using the ANSI codepage (CP_ACP
).fopen 函数用于打开由文件名指定的文件。默认情况下,一个窄文件名字符串是使用 ANSI 代码页(CP_ACP)来解释的。
在这种情况下使用另一个替换函数,
是
的宽字符版本;
的参数是宽字符字符串,除此之外,
和
的行为完全相同。仅仅使用
不会影响文件流中使用的编码字符集。
除了更换函数,记得在字符串之前添加,使其转换为宽字符格式
也可以
如果用户输入中文文件路径,这里笔者在本人VScode终端是完成不了中文路径的输入(倘若有读者参考了之前笔者在多文件编译的批处理命令,可能也会出现这个问题),
注:如果读者没有类似情况可以跳过下面这块说明
但是,应当是可以的,在纯粹的命令行,这是可行的,可能是因为命令行GBK编码
所以可以将VScode的输入终端改为GBK(一般默认),此时如果文件编码不是GBK,则应当也改为GBK,避免终端中文乱码,修改后应该可以正常
下面为,针对之前本人多文件编译批处理命令的更新,有需要的读者可以参考
@echo off > nul
cls
echo Begin to build...
rem 删除所有obj文件
del /Q /S %cd%\obj\*.o > nul && echo All obj files have been deleted.
rem 删除所有bin文件
del /Q /S %cd%\bin\*.exe > nul && echo All bin files have been deleted.
rem 进入到obj文件夹下
cd /d %cd%\obj
rem 编译所有工程文件
gcc -c %cd%\..\util\*.c || echo "Compile all files failed." && exit /b 1
rem 编译主源文件
gcc -c %cd%\..\main.c || echo "Compile main file failed." && exit /b 1
rem 链接所有文件
gcc -o %cd%\..\bin\main.exe *.o || echo "Link all files failed." && exit /b 1
rem 返回主目录
cd /d %cd%/..
rem 编码选择 GBK 936 UTF-8 65001
chcp 936 > nul
cls
rem 提示信息
echo Main program has been built successfully.
echo Begin running...
echo Current execute path: %cd%
rem 执行程序
%cd%\bin\main.exe
echo End running.
echo Press any key to exit...
pause > nul
作为补充,单独创建一个文件在同级如
,写入以下内容
#include <windows.h>
int main(int argc, char const *argv[])
{
system("D:/CsProjects/C/basic/build.cmd"); // 替换为您的路径
return 0;
}
单独运行,可以使程序在一个独立的CMD中运行
当然,如果不涉及中文文件路径,可能不需要这么麻烦,但是谁也不能保证。
路径问题
fopen 接受在执行文件系统上的有效的路径;fopen 接受 UNC 路径和涉及映射网络驱动器的路径,只要执行代码的系统在执行时能够访问共享或映射的驱动器。在为 fopen 构造路径时,请确保驱动器、路径或网络共享在执行环境中可用。在路径中,可以使用正斜杠(/)或反斜杠(\)作为目录分隔符。
此外
The format of
filename
is implementation-defined, and does not necessarily refer to a file (e.g. it may be the console or another device accessible though filesystem API). On platforms that support them,filename
may include absolute or relative filesystem path.文件名的格式由具体实现来定义,不一定指的是一个文件(例如,它可能是控制台或者通过文件系统 API 可访问的其他设备)。在支持这些的平台上,文件名可能包含绝对或相对的文件系统路径。
文件编码问题
函数可以支持以什么编码格式打开文件,但是这并不是C标准,笔者没有成功实现。以下为参考fopen, _wfopen | Microsoft Learn
支持Unicode文件流,只需在mode参数中传递 ccs=encoding指定所需编码的标志,如下。
允许的值为
UNICODE
,UTF-8
, andUTF-16LE
.
当文件以Unicode模式打开时,输入函数会将从文件中读取的数据转换为以
wchar_t
类型存储的UTF-16数据。向以Unicode模式打开的文件写入数据的函数则期望包含以wchar_t
类型存储的UTF-16数据的缓冲区。如果文件是以UTF-8编码的,则在写入时UTF-16数据会被转换为UTF-8,而在读取时,文件的UTF-8编码内容会被转换为UTF-16。在Unicode模式下尝试读取或写入奇数个字节会导致参数验证错误。若要在程序中读取或写入以UTF-8存储的数据,应使用文本或二进制文件模式而非Unicode模式,并且你需要负责任何必要的编码转换。如果文件已存在且是以读取或追加模式打开的,则文件中的任何字节顺序标记(BOM)将决定其编码。BOM编码优先于通过
ccs
标志指定的编码。ccs
编码仅在不存在BOM或文件是新文件时才使用。注意
BOM 检测仅适用于以 Unicode 模式(即通过传递 ccs 标志)打开的文件。
2、读写字符
fgetc
和 getc
- 功能:从文件流中读取一个字符。
- 区别:
fgetc
是一个标准C库函数,而getc
是fgetc
的宏实现,可能在某些编译器中通过宏展开来优化性能。两者在功能上是等价的,但getc
可能会有额外的副作用(比如修改宏参数)。 - 原型:
int fgetc(FILE *stream); int getc(FILE *stream);
当
为
相当于
-
返回值:成功时返回读取的字符(以无符号字符形式转换为int),遇到文件结束或错误时返回EOF。
fputc
和 putc
- 功能:向文件流中写入一个字符。
- 区别:类似于
fgetc
和getc
,fputc
是标准函数,而putc
是其宏实现。 - 原型:
int fputc(int char, FILE *stream); int putc(int char, FILE *stream);
当
为
相当于
- 返回值:成功时返回写入的字符,失败时返回EOF。
ungetc
- 功能:将一个字符退回到文件流中,使其在下一次读取时重新可用。
- 原型:
int ungetc(int c, FILE *stream);
返回值:成功时返回c,失败时返回EOF。
- 注意:并非所有文件流都支持
ungetc
,且退回到文件流的字符通常只能被退回一次。
#include <stdio.h>
int main() {
FILE *file = fopen("example.txt", "r"); // 打开文件
if (file == NULL) {
// 如果文件打开失败,则退出程序
perror("Failed to open the file");
return -1;
}
int ch;
while ((ch = fgetc(file)) != EOF) { // 读取字符直到文件末尾
printf("%c\n", ch); // 打印字符
}
fclose(file); // 关闭文件
return 0;
}
在这个示例中,首先使用fopen
打开一个名为"example.txt"的文件。然后使用fgetc
不断地读取文件中的字符,直到达到文件末尾(此时fgetc
会返回EOF
)。每个读取到的字符都会被打印出来。最后,使用fclose
关闭文件。
#include <stdio.h>
int main() {
FILE *file = fopen("output.txt", "w"); // 打开文件准备写入
if (file == NULL) {
// 如果文件打开失败,则退出程序
perror("Failed to open the file for writing");
return -1;
}
fputc('H', file); // 写入字符 'H'
fputc('E', file); // 写入字符 'E'
fputc('L', file); // 写入字符 'L'
fputc('O', file); // 写入字符 'O'
fputc('\n', file); // 写入换行符
fclose(file); // 关闭文件
return 0;
}
在这个示例中,首先使用fopen
打开一个名为"output.txt"的文件准备写入。然后使用fputc
将一系列字符和一个换行符写入到文件中。最后,使用fclose
关闭文件。
值得注意的是,这不能读写中文字符,因为中文字符的一个字往往不是一个字符表示的,这时候就要用宽字符的函数,如下,它们包含在中,另外需要注意的是,文件的编码和输出的终端应该匹配,避免乱码。以下使用的都是GBK,文件为文本文件,编码为
#include<stdio.h>
#include <locale.h>
#include <wchar.h>
int main(int argc, char const *argv[])
{
setlocale(LC_ALL, "zh_CN.GBK"); // 设置语言环境为GBK
FILE *fp = fopen("./data/test.txt", "r");
if (fp == NULL){
printf("Error: Unable to open file \n");
return 1;
}else{
printf("File opened successfully\n");
}
wint_t ch;
while ((ch = fgetwc(fp)) != WEOF) { // 读取字符直到文件末尾
putwchar(ch);; // 打印字符
}
fclose(fp); // 关闭文件
return 0;
}
3、读写字符串
fgets
- 功能:从指定的文件流中读取一行文本,直到遇到换行符('\n')、文件结束符EOF或已读取了
n-1
个字符为止(其中n
是第二个参数指定的最大字符数,包括最后的空字符'\0')。 - 原型:
char *fgets(char *str, int n, FILE *stream);
当
为
时和
差不多
- 参数:
str
:指向用于存储读取数据的字符数组的指针。n
:要读取的最大字符数(包括最后的空字符'\0')。stream
:指向FILE
对象的指针,该对象标识了要从中读取数据的输入流。
- 返回值:成功时,返回指向
str
的指针;如果发生错误或到达文件末尾而没有读取任何字符,则返回NULL
。
fputs
- 功能:将一个字符串写入到指定的文件流中,但不包括空字符('\0')。
- 原型:
int fputs(const char *str, FILE *stream);
当
为
时和
差不多,
也不会终止符写入标准输出,但是会添加换行符。
- 参数:
str
:指向要写入文件的空终止字符串的指针。stream
:指向FILE
对象的指针,该对象标识了要写入数据的输出流。
- 返回值:成功时,返回非负值;失败时,返回
EOF
。
#include <stdio.h>
#include <stdlib.h>
int main() {
FILE *file;
char filename[] = "example.txt";
char buffer[100];
// 打开文件以写入。如果文件不存在,将创建它。
file = fopen(filename, "w");
if (file == NULL) {
perror("Error opening file");
return 1;
}
// 使用fputs写入字符串到文件
fputs("Hello, World!\n", file);
fputs("This is a test.\n", file);
// 关闭文件
fclose(file);
// 重新打开文件以读取
file = fopen(filename, "r");
if (file == NULL) {
perror("Error opening file");
return 1;
}
// 使用fgets从文件中读取字符串
while (fgets(buffer, sizeof(buffer), file) != NULL) {
printf("%s", buffer);
}
// 关闭文件
fclose(file);
return 0;
}
在这个示例中,我们首先使用fopen函数以写入模式("w")打开一个名为example.txt的文件。如果文件不存在,fopen会创建它。然后,我们使用fputs函数将两个字符串写入文件。
在写入字符串后,我们关闭文件,然后重新以读取模式("r")打开它。接下来,我们使用fgets函数从文件中读取字符串,直到文件的末尾。每次调用fgets时,它都会读取最多sizeof(buffer)-1个字符(为末尾的null字符留出空间)并存储在buffer中。当fgets到达文件末尾或发生错误时,它会返回NULL。
最后,我们打印出从文件中读取的每个字符串,并关闭文件。
正常情况下,使用此对函数读写中文没有问题,注意编码统一。
4、成块读写
首先,我们使用 fwrite 将一些数据写入一个文件。
#include <stdio.h>
int main() {
FILE *file = fopen("example.bin", "wb"); // 打开文件以写入二进制数据
if (file == NULL) {
perror("Error opening file");
return 1;
}
int data[] = {1, 2, 3, 4, 5}; // 要写入的数据
size_t items = sizeof(data) / sizeof(data[0]); // 数据项的数量
size_t size = sizeof(data[0]); // 每个数据项的大小
// 使用fwrite写入数据
size_t written = fwrite(data, size, items, file);
if (written != items) {
perror("Error writing to file");
fclose(file);
return 1;
}
fclose(file); // 关闭文件
return 0;
}
接下来,我们使用 fread 从刚才写入的文件中读取数据。
#include <stdio.h>
int main() {
FILE *file = fopen("example.bin", "rb"); // 打开文件以读取二进制数据
if (file == NULL) {
perror("Error opening file");
return 1;
}
int data[5]; // 用于存储读取的数据的数组
size_t items = sizeof(data) / sizeof(data[0]); // 数据项的数量
size_t size = sizeof(data[0]); // 每个数据项的大小
// 使用fread读取数据
size_t read = fread(data, size, items, file);
if (read != items) {
perror("Error reading from file");
fclose(file);
return 1;
}
// 打印读取的数据
for (size_t i = 0; i < items; ++i) {
printf("%d ", data[i]);
}
printf("\n");
fclose(file); // 关闭文件
return 0;
}
5、文件定位
在C语言中,文件定位函数(File Positioning Functions)用于操作文件指针的位置,即控制读写操作在文件中的位置。
C语言提供了以下几个文件定位函数:
-
long ftell(FILE *stream):获取文件指针的当前位置。该函数返回一个long类型的值,表示文件指针在文件中的偏移量(以字节为单位)。如果函数执行成功,返回的值大于等于0;如果函数执行失败,返回-1。
-
int fseek(FILE *stream, long offset, int origin):将文件指针定位到指定位置。offset表示相对于origin的偏移量,该偏移量可以是正数或负数。origin表示相对于哪个位置进行偏移,可以取以下三个值:
- SEEK_SET:从文件开头开始偏移。
- SEEK_CUR:从当前位置开始偏移。
- SEEK_END:从文件末尾开始偏移。 若函数执行成功(返回值为0),则文件指针将被移动到指定位置;若执行失败(返回值为非0),文件指针的位置不变。
-
void rewind(FILE *stream):将文件指针重新定位到文件开头。该函数相当于fseek(stream, 0L, SEEK_SET)。它无返回值。
这些文件定位函数可以用于对文件进行随机访问,即可以直接定位到文件中任意位置进行读写操作。文件定位函数在处理大型文件、数据库文件等场景中非常有用。
#include <stdio.h>
int main() {
FILE *file;
long position;
char buffer[100];
// 打开一个文件,假设它存在且可读
file = fopen("example.txt", "r");
if (file == NULL) {
perror("Error opening file");
return 1;
}
// 使用ftell获取并打印当前文件位置
printf("Initial file position: %ld\n", ftell(file));
// 读取一些数据,然后再次使用ftell
fgets(buffer, sizeof(buffer), file);
printf("After reading: %s\n", buffer);
printf("File position after reading: %ld\n", ftell(file));
// 使用fseek移动到文件的某个位置
if (fseek(file, 10, SEEK_SET) != 0) {
perror("Error seeking in file");
fclose(file);
return 1;
}
printf("File position after fseek: %ld\n", ftell(file));
// 使用rewind将文件位置重置为文件开头
rewind(file);
printf("File position after rewind: %ld\n", ftell(file));
// 关闭文件
fclose(file);
return 0;
}
在Windows系统中这可以间接查看一个文件的大小,如
只需将文件指针移到最后
除了上面,比较常见的文件定位函数,C还提供了两个具有差不多功能的函数,如下:
-
fgetpos函数:
- 函数原型:
int fgetpos(FILE *stream, fpos_t *pos)
- 功能:获取文件位置指针的当前位置,并将其保存在给定的fpos_t类型的变量中。
- 参数:
- stream: 文件指针,指向要获取位置的文件流。
- pos: fpos_t类型的变量指针,用于保存文件位置指针的当前位置。
- 返回值:如果成功,返回0;如果失败,返回非零值。
- 函数原型:
-
fsetpos函数:
- 函数原型:
int fsetpos(FILE *stream, const fpos_t *pos)
- 功能:设置文件位置指针的位置,使其指向给定的fpos_t类型变量中保存的位置。
- 参数:
- stream: 文件指针,指向要设置位置的文件流。
- pos: fpos_t类型的变量指针,指定文件位置指针要设置的位置。
- 返回值:如果成功,返回0;如果失败,返回非零值。
- 函数原型:
6、格式化读写
以下是一个简单的示例,演示如何使用 fprintf 将一些文本写入文件:
#include <stdio.h>
int main() {
FILE *file;
file = fopen("example.txt", "w"); // 打开文件以写入数据
if (file == NULL) {
printf("无法打开文件\n");
return 1;
}
fprintf(file, "Hello, World!\n"); // 写入字符串
fprintf(file, "整数:%d\n", 123); // 写入整数
fprintf(file, "浮点数:%f\n", 3.14); // 写入浮点数
fclose(file); // 关闭文件
return 0;
}
以下是一个简单的示例,演示如何使用 fscanf 从文件中读取数据:
#include <stdio.h>
int main() {
FILE *file;
char str[100];
int num;
float floatNum;
file = fopen("example.txt", "r"); // 打开文件以读取数据
if (file == NULL) {
printf("无法打开文件\n");
return 1;
}
fscanf(file, "%s", str); // 读取字符串
printf("读取的字符串:%s\n", str);
fscanf(file, "整数:%d", &num); // 读取整数
printf("读取的整数:%d\n", num);
fscanf(file, "浮点数:%f", &floatNum); // 读取浮点数
printf("读取的浮点数:%f\n", floatNum);
fclose(file); // 关闭文件
return 0;
}
这个示例将打开之前使用 fprintf 写入的 example.txt 文件,并使用 fscanf 从中读取数据。注意,我们使用 fopen 函数以读取模式("r")打开文件,并在完成读取后使用 fclose 关闭文件。在调用 fscanf 时,我们需要传递变量的地址(使用 & 运算符)来存储从文件中读取的数据。
7、错误处理
在C语言中,处理文件输入/输出(I/O)时,经常会遇到需要检测和处理错误或文件结束(EOF)的情况。clearerr
、feof
、ferror
和 perror
是标准I/O库中用于这些目的的重要函数。
1. clearerr
函数
功能:清除与文件流相关的错误指示符和文件结束指示符。
原型:void clearerr(FILE *stream);
- 参数:
stream
是一个指向FILE
对象的指针,该对象标识了一个打开的文件流。 - 作用:当文件流发生错误(如读写错误)或达到文件末尾(EOF)时,相应的错误指示符和文件结束指示符会被设置。
clearerr
函数会清除这些指示符,使得文件流再次处于“干净”状态,可以继续进行读写操作。 - 返回值:无返回值。
2. feof
函数
功能:检查文件流是否已到达文件末尾(EOF)。
原型:int feof(FILE *stream);
- 参数:
stream
是一个指向FILE
对象的指针,该对象标识了一个打开的文件流。 - 返回值:如果文件流已经到达EOF,则返回非零值(通常为1);否则返回0。
- 注意:
feof
只有在尝试读取操作之后才能准确地指示是否已到达文件末尾。仅因为feof
返回0,并不意味着接下来读取操作会成功。
3. ferror
函数
功能:检查文件流是否发生了错误。
原型:int ferror(FILE *stream);
- 参数:
stream
是一个指向FILE
对象的指针,该对象标识了一个打开的文件流。 - 返回值:如果文件流在最近的I/O操作中发生了错误,则返回非零值(通常为1);否则返回0。
- 用途:在尝试读写文件后,可以使用
ferror
来检查操作是否成功。如果返回非零值,则说明发生了错误,可能需要清除错误状态(使用clearerr
),或者根据具体情况采取其他措施。
4. perror
函数
功能:打印最后发生的系统错误的描述到标准错误(stderr)。
原型:void perror(const char *s);
- 参数:
s
是一个指向以空字符终止的字符串的指针,该字符串将被输出到标准错误流之前,通常用于描述发生错误的上下文。 - 作用:
perror
函数首先输出传递给它的字符串(如果非NULL),后跟一个冒号、一个空格和最后发生的系统错误的文本描述。这个描述通常来自于全局变量errno
,该变量在出错时被设置。 - 用途:当程序遇到系统级错误(如文件打开失败)时,
perror
可以用来向用户报告错误的具体原因。
#include <stdio.h>
int main() {
FILE *file;
int ch;
file = fopen("example.txt", "r");
if (file == NULL) {
perror("无法打开文件");
return 1;
}
// 循环读取文件内容,直到文件结束或发生错误
while ((ch = fgetc(file)) != EOF) {
putchar(ch);
if (feof(file)) {
printf("\n文件结束\n");
break;
}
}
fclose(file);
return 0;
}
在这个例子中,我们使用 fgetc 函数从文件中逐个字符地读取内容,并在每次读取后检查 feof。当 feof 返回非零值时,我们知道已经到达了文件的末尾,于是打印出“文件结束”并退出循环。
#include <stdio.h>
int main() {
FILE *file;
int ch;
file = fopen("nonexistent.txt", "r");
if (file == NULL) {
perror("无法打开文件");
return 1;
}
// 尝试读取文件内容
ch = fgetc(file);
if (ferror(file)) {
printf("读取文件时发生错误\n");
clearerr(file); // 清除错误标志
} else {
printf("读取的字符是:%c\n", ch);
}
fclose(file);
return 0;
}
在这个例子中,我们尝试打开一个不存在的文件。由于文件不存在,fgetc 调用将失败,并且 ferror 将返回非零值。我们检测到错误后,使用 clearerr 函数清除错误标志,以便进行后续的文件操作。
(二) IO缓冲
在C语言中,缓冲区是内存中的一块区域,用于临时存储数据,以减少对底层硬件(如磁盘或网络)的直接访问次数,从而提高程序的效率。
![](https://i-blog.csdnimg.cn/direct/9e3bd995782645cb979a3dd2ccb26377.png)
对于文件I/O操作,C标准库提供了三种类型的缓冲区:无缓冲(unbuffered)、行缓冲(line buffered)和全缓冲(fully buffered)。
- 无缓冲(Unbuffered):无缓冲意味着每次对文件或流的读写操作都会直接作用于底层硬件,不经过缓冲区。这通常用于需要即时反馈的情况,比如错误信息的输出(在某些系统上,stderr是默认无缓冲的)。
- 行缓冲(Line Buffered):行缓冲意味着输入/输出数据在遇到换行符(如'\n')或缓冲区满时会进行实际的I/O操作。标准输出(stdout)在大多数系统上默认是行缓冲的(当输出到终端时),这有助于在程序运行时即时看到输出结果。
- 全缓冲(Fully Buffered):全缓冲意味着只有在缓冲区满或显式刷新缓冲区时(如使用
fflush
函数),才会进行实际的I/O操作。对于磁盘文件,标准库通常会使用全缓冲,以提高磁盘I/O的效率。
1、设置缓冲区
这些缓冲机制主要通过stdio.h
库中的函数如setvbuf
、setbuf
等来设置,但默认情况下,标准输入(stdin)、标准输出(stdout)和标准错误(stderr)的缓冲类型可能依赖于具体的系统和库实现。
这两个函数要用到三个宏用于设置缓冲的模式
setbuf函数
void setbuf(FILE *stream, char *buf);
- stream: 指向要设置缓冲的文件流的指针。
- buf: 如果不为NULL,则指向一个缓冲区,其大小至少为BUFSIZ(在stdio.h中定义)字节。如果为NULL,则文件流被设置为无缓冲模式。
注意:
- 一旦文件流被使用(即进行了读/写操作),就不能再调用
setbuf
来改变其缓冲模式。 - 并非所有系统都支持无缓冲的文件流,特别是在标准输入和标准输出上。
如果 BUFSIZ 不是合适的缓冲区大小,可以使用 setvbuf 来更改它。setvbuf 还应当用于检测错误,因为 setbuf 不会表明成功或失败。此函数只能在流与打开的文件相关联之后,但在任何其他操作(除了 setbuf/setvbuf 的失败调用)之前使用。一个常见的错误是将 stdin 或 stdout 的缓冲区设置为一个生存期在程序终止前结束的数组。
setvbuf函数
int setvbuf(FILE *stream, char *buf, int mode, size_t size);
- stream: 指向要设置缓冲的文件流的指针。
- buf: 如果不为NULL,则指向一个用户提供的缓冲区。如果为NULL,则库会尝试分配一个大小为
的缓冲区。
- mode: 指定缓冲模式,可以是
_IOFBF
(全缓冲)、_IOLBF
(行缓冲)或_IONBF
(无缓冲)。 - size: 缓冲区的大小,如果
buf
非NULL,则忽略此值,否则此值决定了分配的缓冲区的大小。
返回值:
- 成功时返回0;失败时返回非0值,并设置errno以指示错误。
#include <stdio.h>
int main() {
FILE *fp = fopen("test.txt", "w");
if (fp == NULL) {
printf("Failed to open file\n");
return 1;
}
char buffer[BUFSIZ];
if (setvbuf(fp, buffer, _IOFBF, BUFSIZ) != 0) {
printf("Failed to set buffer\n");
return 1;
}
fputs("Hello, World!", fp);
fclose(fp);
return 0;
}
fflush函数
int fflush(FILE *stream);
- stream: 指向要清空输出缓冲区的文件流的指针。如果为NULL,则清空所有输出流的缓冲区。
返回值:
- 成功时返回0;失败时返回EOF,并设置errno以指示错误。
#include <stdio.h>
int main() {
char buffer[1024];
FILE *fp = fopen("example.txt", "w");
if (fp == NULL) {
perror("Failed to open file");
return 1;
}
setvbuf(fp, buffer, _IOFBF, sizeof(buffer)); // 将fp设置为全缓冲,使用自定义缓冲区
fputs("This will be written when the buffer is full or flushed.\n", fp);
fflush(fp); // 显式刷新缓冲区
fclose(fp);
return 0;
}
二、IO重定向
前面我们知道如何文件与程序之间数据传递,也知道程序和控制台之间如何数据传递,现在我们试试能不能从控制台到文件直接的IO流传输,因为文件和控制台都可以通过指针表示。
(一)freopen
freopen函数是一个用于文件重定向的函数。它可以重新定向一个标准的输入输出流到一个指定的文件。
函数原型:
FILE *freopen(const char *filename, const char *mode, FILE *stream);
参数说明:
- filename:要打开的文件名,可以是相对路径或绝对路径。
- mode:文件打开模式,包括"r"(只读)、"w"(只写)、"a"(追加)、"rb"(二进制读)、"wb"(二进制写)等。
- stream:要重定向的流,可以是stdin(标准输入流)、stdout(标准输出流)、stderr(标准错误流)等。
返回值:
- 成功:返回一个指向FILE结构的指针,指向已打开的文件。
- 失败:返回NULL,并设置errno错误码。
函数作用:
- 通过调用freopen函数,可以将输入输出流重定向到指定文件,从而实现文件输入输出操作。
- 通过重定向标准输入输出流,可以实现文件的读取和写入。
使用示例:
#include <stdio.h>
int main() {
FILE *fp;
char ch;
// 打开文件并将标准输入流重定向到该文件
fp = freopen("input.txt", "r", stdin);
if (fp == NULL) {
printf("Failed to open file.\n");
return 1;
}
// 从文件中读取字符,并输出到标准输出流
while ((ch = getchar()) != EOF) {
putchar(ch);
}
// 关闭文件
fclose(fp);
return 0;
}
由于标准错误是不会通过标准输出显式,所以可以通过重定向将其重定向进一个文件中
三、非文本文件的操作
以上都是对普通的字符文件的操作,换句话说,以上的操作对于能使用普通的记事本打开的文件格式基本都OK,接下来对其它的文件进行尝试,即不能简单使用记事本打开的文件。这些文件通常由一些专门的处理软件打开,在其二进制形式中包含大量的一些元数据,处理这些文件往往需要知道其文件的格式。
(一)文字文件
在一个docx文档里面写一点文字,你就会发现,它的大小就有右图这么多,并且如果简单的从文件的二进制形式看,也找不到这些文字
但如果将其文件后缀改了,使其变成一个压缩文件,就会发现别有洞天
而我们的内容,可以在一个xml里面找到
那像如果要操作这样的文件,可以想到的一点就是,通过
函数使其变成一个压缩文件,通过比如
函数调用解压缩程序解压,之后的事情就好办多了。
(二)图片
如果是简单的复制之类的,像上面那样的文件操作完全就可以,但是图片的信息能不能通过C获取,或者C能不能通过修改二进制数据改变图片的呈现效果。
这里以一些简单的图片,比如位图BMP(Bitmap)为例,来操作图片。它由微软公司为其Windows操作系统开发。BMP图像文件几乎不进行压缩,因此它们通常比其他格式的图像文件要大。也因此,我们更容易得到图片的相关信息。
BMP文件的格式,可以参考:BMP格式详解_bmp头部信息-CSDN博客。也可以查考更官方的文档BMP file format - Wikipedia。
BMP文件的格式说简单也需要一些功夫,特别是数据部分。元数据比较好获取,不过要注意数据通常是小端存储。
这里使用VScode的插件Hex Editor打开的一张位图如下,其大小存储在第2到第4字节,所以要像下面一样反过来看。
我们可以设计一个结构体表示其头部的数据,比如:
typedef struct{
unsigned char type[2] ; // 文件类型,2字节
unsigned char size[4]; // 文件大小,4个字节
unsigned char reserved1[2]; // 2字节,保留,必须为 0
unsigned char reserved2[2]; // 2字节,保留,必须为 0
unsigned char offset[4]; // 从文件头到实际图像数据的偏移量,4个字节
unsigned char informationSize[4]; // 信息头的大小,4个字节
unsigned char width[4]; // 图像的宽度,4个字节
unsigned char height[4]; // 图像的高度,4个字节
unsigned char planes[2]; // 颜色平面数,2个字节
unsigned char bitPerPixel[2]; // 每个像素的位数,2个字节
unsigned char compression[4]; // 压缩类型,4个字节
unsigned char imageSize[4]; // 图像数据大小,4个字节
unsigned char xPixelsPerMeter[4]; // 水平分辨率,4个字节
unsigned char yPixelsPerMeter[4]; // 垂直分辨率,4个字节
unsigned char colorsUsed[4]; // 使用的颜色数,4个字节
unsigned char importantColors[4]; // 重要的颜色数,4个字节
}__attribute__((packed)) BMP, * BMP_PTR;
然后通过指针转换可以更方便得到对应的数据区,得到的数据还需要经过转换为实际的值
char buffer[sizeof(BMP)] = {0};
BMP_PTR bmp = (BMP_PTR)buffer;
fread(buffer, sizeof(BMP), 1, fp);
printf("BMP file type: %c%c\n", bmp->type[0], bmp->type[1]);
printf("BMP file size: %f Mb\n", endianSwap(&bmp->size, sizeof(bmp->size)) / 1024.0 / 1024 );
free(data);
fclose(fp);
一个小端转换的参考代码:
unsigned long endianSwap(void* ptr, int size){
// 将指针转换为 unsigned char* 类型,方便单字节操作
unsigned char *buffer = (unsigned char*)ptr;
unsigned long result = 0;
short isBegin = 0;
for(int i = 0; i < size; i++){
if(!isBegin && buffer[size - i - 1]){
isBegin = 1;
}
if(isBegin){
result = result << 8 | buffer[size - i - 1];
}
}
return result;
}
四、控制字符序列
在前面,简要介绍过通过输出特殊的转义字符可以控制输出产生不同的效果。这些特殊的字符序列通常被称为转义序列(Escape Sequences)或控制字符(Control Characters)。
以下参考【Console Virtual Terminal Sequences - Windows Console | Microsoft Learn】
(一)光标定位
通过将光标定位,在某些时候可以很好控制输入输出效果。
CSI(Control Sequence Introducer)序列\x1b[
(其中\x1b
是ESC字符的十六进制表示)用于引入控制序列,在某些教程中也会使用\033[,这两者其实是一样的。
可以将\x1b[先定义为宏
序列 | 说明 |
---|---|
将光标移动到当前列的前n行 | |
将光标移动到当前列的后n行 | |
将光标移动到当前位置的前n列 | |
将光标移动到当前位置的后n列 | |
将光标移动到后n行,列变为第1列 | |
将光标移动到前n行,列变为第1列 | |
将光标移动到当前列的第n行 | |
将光标移动到当前行的第n 列 | |
将光标移动到当前视界的第r行第c列 | |
将光标移动到当前视界的第r行第c列 |
<n> 表示移动的距离,是一个可选参数。如果 <n> 被省略或者等于 0 ,则将其视为 1 。<n> 不能大于 32767 (最大短整型值),<n> 不能为负数。
示例
printf(ESC"[10;10H(1)这里是第10行第10列");
printf(ESC"[5d(2)这里是第5行第10列");
printf(ESC"[20g(3)这里是第5行第20列");
printf(ESC"[10;10f(4)这是也是第10行第10列");
printf(ESC"[1A(5)上移动一行");
printf(ESC"[1B(6)下移动一行");
printf(ESC"[1C(7)前移动");
printf(ESC"[1D(8)后移动");
printf(ESC"[3E(9)下移动3行");
printf(ESC"[3F(10)上移动3行");
效果
(二)光标状态
以下控制序列,可以设置光标的状态
序列 | 说明 |
---|---|
ESC [ ? 12 h | 启动文本光标闪烁效果 |
ESC [ ? 12 l | 禁用文本光标闪烁效果 |
ESC [ ? 25 h | 显示文本光标 |
ESC [ ? 25 l | 隐藏文本光标 |
ESC[0 SP q | 默认光标形状 |
ESC[1 SP q | 闪烁的块光标形状 |
ESC[2 SP q | 块光标形状 |
ESC[3 SP q | 闪烁的下划线光标形状 |
ESC[4 SP q | 下划线光标形状 |
ESC[5 SP q | 闪烁的条光标形状 |
ESC[6 SP q | 条光标形状 |
其中SP是一个小空白字符(0x20),q是字符(0x71)。可以自行设计宏定义,方便操作
滚动控制台窗口的控制序列\x1b[%dT
和\x1b[%dS
分别用于向下和向上滚动指定的行数。然而,这些序列的广泛支持可能因终端而异。
(三)文本修改
以下序列可以进行一些文本操作
序列 | 说明 |
---|---|
在光标当前位置插入n个空格,将所有现有文本向右移动。退出右侧屏幕的文本将被删除。 | |
删除当前光标位置的n个字符,从屏幕右边缘移位空格字符。 | |
从当前光标位置擦除n个字符,方法是用空格字符覆盖它们。 | |
插入n行到光标位置的缓冲区中。光标所在的行及其下方的行将向下移动。 | |
从光标当前行开始删除n行 | |
以上的n将是0如果忽略不写,以下的序列的n有且仅有三种有效值
| |
将当前视口/屏幕中由 <n> 指定的所有文本用空格字符替换。 | |
将由 <n> 所指定的、光标所在行上的所有文本替换为空格字符。 |
示例
printf("This is first line\n"); //换行,刷新缓冲区
printf("Next Line"); // 使光标有所移动
printf(ESC"[0K"); // 清除当前行后面的内容
如果,不换行,效果就不同了
printf("This is first line"); //不换行
printf(ESC"[5C"); // 使光标有所移动
printf(ESC"[0K"); // 清除当前行后面的内容
(四)文本颜色
SGR(Select Graphic Rendition)序列可以设置输出的文本颜色。
序列 | 说明 |
---|---|
n是由分号分割的若干数字,可以接受0~16个参数,但是将会从右向左应用,并选择第一个有效的前景色与背景色 |
前景色 (Foreground Colors)
值 | 描述 | 行为描述 |
---|---|---|
0 | 默认 | 将所有属性恢复到修改前的状态 |
1 | 粗体/高亮 | 对前景色应用亮度/强度标志 |
22 | 无粗体/高亮 | 从前景色移除亮度/强度标志 |
30 | 前景色黑色 | 对前景色应用非粗体/高亮的黑色 |
31 | 前景色红色 | 对前景色应用非粗体/高亮的红色 |
32 | 前景色绿色 | 对前景色应用非粗体/高亮的绿色 |
33 | 前景色黄色 | 对前景色应用非粗体/高亮的黄色 |
34 | 前景色蓝色 | 对前景色应用非粗体/高亮的蓝色 |
35 | 前景色品红色 | 对前景色应用非粗体/高亮的品红色 |
36 | 前景色青色 | 对前景色应用非粗体/高亮的青色 |
37 | 前景色白色 | 对前景色应用非粗体/高亮的白色 |
39 | 前景色默认 | 仅应用默认设置的前景部分(见0) |
90 | 粗体/高亮黑色 | 对前景色应用粗体/高亮的黑色 |
91 | 粗体/高亮红色 | 对前景色应用粗体/高亮的红色 |
92 | 粗体/高亮绿色 | 对前景色应用粗体/高亮的绿色 |
93 | 粗体/高亮黄色 | 对前景色应用粗体/高亮的黄色 |
94 | 粗体/高亮蓝色 | 对前景色应用粗体/高亮的蓝色 |
95 | 粗体/高亮品红色 | 对前景色应用粗体/高亮的品红色 |
96 | 粗体/高亮青色 | 对前景色应用粗体/高亮的青色 |
97 | 粗体/高亮白色 | 对前景色应用粗体/高亮的白色 |
背景色 (Background Colors)
值 | 描述 | 行为描述 |
---|---|---|
40 | 背景色黑色 | 对背景色应用非粗体/高亮的黑色 |
41 | 背景色红色 | 对背景色应用非粗体/高亮的红色 |
42 | 背景色绿色 | 对背景色应用非粗体/高亮的绿色 |
43 | 背景色黄色 | 对背景色应用非粗体/高亮的黄色 |
44 | 背景色蓝色 | 对背景色应用非粗体/高亮的蓝色 |
45 | 背景色品红色 | 对背景色应用非粗体/高亮的品红色 |
46 | 背景色青色 | 对背景色应用非粗体/高亮的青色 |
47 | 背景色白色 | 对背景色应用非粗体/高亮的白色 |
49 | 背景色默认 | 仅应用默认设置的背景部分(见0) |
100 | 粗体/高亮黑色背景 | 对背景色应用粗体/高亮的黑色 |
101 | 粗体/高亮红色背景 | 对背景色应用粗体/高亮的红色 |
102 | 粗体/高亮绿色背景 | 对背景色应用粗体/高亮的绿色 |
103 | 粗体/高亮黄色背景 | 对背景色应用粗体/高亮的黄色 |
104 | 粗体/高亮蓝色背景 | 对背景色应用粗体/高亮的蓝色 |
105 | 粗体/高亮品红色背景 | 对背景色应用粗体/高亮的品红色 |
106 | 粗体/高亮青色背景 | 对背景色应用粗体/高亮的青色 |
107 | 粗体/高亮白色背景 | 对背景色应用粗体/高亮的白色 |
示例
printf(ESC"[34;46m前景色蓝色,背景色青色\r\n");
printf(ESC"[0m恢复\n");
(五)设置窗口标题
OSC(Operating System Command)序列,以\x1b]开头,可以设置
设置终端或控制台的窗口标题。这个序列不是所有终端都支持,但在许多Unix-like系统和较新版本的Windows 10控制台中是有效的。
序列 | 说明 |
---|---|
设置窗口标题为string | |
ST为字符结束符,可以是ESC \
(0x1B 0x5C
)或BEL
(0x07
) ,这里是0x07。