文章目录
这篇文章主要解决如下几个问题:
- C语言中如何进行文件读写?
- fopen时的文本模式和二进制模式有什么区别?
- fread、fgets和fscanf各有什么区别,如何使用?
- fwrite、fputs和fprintf各有什么区别,如何使用?
概述
在C语言中进行文件操作,可以使用C库提供的文件读写库函数。我们经常使用的库函数如下:
FILE *fopen( char *file, char *open_mode ); //打开文件,读文件到内存,返回文件信息结构指针
size_t fread(void *ptr, size_t size, size_t n, FILE *stream)//按字节读取文件内容到s中
size_t fwrite(const void *ptr, size_t size, size_t n, FILE *stream)//按字节将s地址中的数据写到文件中
char *fgets( char *s, int max_size, FILE * stream); //读一行数据到缓冲区s中
int fputs(const char *str, FILE *stream) ;//将字符串s写入文件
int fscanf(FILE *stream, const char *format, ...) ;//fscanf(文件流指针,格式字符串,输出表列);
int fprintf(FILE *stream, const char *format, ...);//fprintf(文件流指针,格式字符串,输出表列);
int fseek( FILE * stream, long offset, int whence); //移动文件指针到指定位置(0,1,2):SEEK_SET,SEEK_CUR,SEEK_END
void rewind(FILE * stream); //回到文件头
long ftell(FILE * stream); //得到当前文件偏移位置
int feof(FILE *stream);//文件是否结束
fclose(FILE * stream); //关闭文件,刷新缓存到物理磁盘上
针对上述函数,更为详细的描述请大家参考cppreference上的文档描述:https://en.cppreference.com/w/c/io/
常用函数
fopen
用于打开一个文件,用于后续读写。该函数原型如下:
FILE *fopen( const char *filename, const char *mode ); // C99标准
errno_t fopen_s( FILE *restrict *restrict streamptr, const char *restrict filename, const char *restrict mode ); // C11标准
需要注意的是:
- 该函数返回的是一个FILE结构的指针,如果文件打开失败,则该指针返回值为空
- 第一个参数filename为文件名,有相对路径和绝对路径两种传入方式
- 绝对路径:包含完整的路径,如
E:\SRC\FileIO\fgets_fputs\mystr.txt
- 绝对路径:包含完整的路径,如
- 相对路径:以当前exe所在路径为参考的文件路径,如果需要返回上级目录,使用
..
。比如当前exe位于E:\src\Cheese\Debug
目录下,而待读取的hello.txt位于E:\src\Cheese\Cheese\hello.txt
,则使用相对路径为:..\Cheese\hello.txt
。
- 如果直接运行exe,则“当前路径”是以exe所在路径为基准的,如果fopen时直接输入文件名,则意味着对应要打开的文件和exe处于同级目录下。
- 如果通过VS运行exe,则“当前路径”以源码所在的工程路径为基准的,如果fopen时直接输入文件名,则意味着对应要打开的文件位于对应的VS工程目录下。
- 如果路径使用
\
来标识,需要进行转义:\\
- 第二个参数是打开文件时使用的模式,主要有以下六种:
File access mode string | 含义 | 解释 | 文件存在时的操作 | 文件不存在时的操作 |
---|---|---|---|---|
"r" | read | 打开文件供读取 | 从文件头部开始读取 | 打开失败 |
"w" | write | 创建文件供写入 | 清空文件原有内容,重新写入 | 创建新文件 |
"a" | append | 向文件尾部追加新内容 | 向文件尾部追加 | 创建新文件 |
"r+" | read extended | 打开文件供读取和写入 | 从文件头部开始读取 | 打开失败 |
"w+" | write extended | 创建文件供读取和写入 | 清空文件原有内容,重新写入 | 创建新文件 |
"a+" | read extended | 打开文件供读取和写入 | 向文件尾部追加 | 创建新文件 |
- 需要注意的是"a"及"a+"模式下,当文件已存在的情况下,是向文件尾部追加内容,这一点与"w"和"w+"不同。这也是课堂练习题不选第二选项的原因。
- 在一些Windows下C语言的资料中,会提到另一个名为"b"或"t"的后缀,这是所谓的二进制和文本模式。
- 在POSIX标准中(适用于Unix、Linux等环境),文本和二进制模式本身没有差异
- 在Windows环境下,由于所采用的换行符是CRLF(16进制的0x0D 0x0A),出于兼容性考虑,在读写过程中会进行与标准换行符(0x0A)的自动转换。具体说来,在文本模式下,对文件写入时,如果遇到0x0D 0x0A,在写入文件时会被转换成0x0A;而在文件读取时,如果只遇到0x0A,会将其转为0x0D 0x0A。另外,Windows下对于文本模式的文件结尾也有特殊规定,具体可参见MSDN中的论述:https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/fopen-wfopen
In addition to the earlier values, the following characters can be appended to mode to specify the translation mode for newline characters.
In text mode, CTRL+Z is interpreted as an EOF character on input. In files that are opened for reading/writing by using “a+”, fopen checks for a CTRL+Z at the end of the file and removes it, if it’s possible. It’s removed because using fseek and ftell to move within a file that ends with CTRL+Z may cause fseek to behave incorrectly near the end of the file.
In text mode, carriage return-line feed (CRLF) combinations are translated into single line feed (LF) characters on input, and LF characters are translated to CRLF combinations on output. When a Unicode stream-I/O function operates in text mode (the default), the source or destination stream is assumed to be a sequence of multibyte characters. Therefore, the Unicode stream-input functions convert multibyte characters to wide characters (as if by a call to the mbtowc function). For the same reason, the Unicode stream-output functions convert wide characters to multibyte characters (as if by a call to the wctomb function).
- fopen函数返回为FILE对象的指针,如果文件打开失败,则该指针为空指针。因此,严谨的写法应该在函数返回后,对指针值进行判断。
fclose
fclose应与fopen配对使用,关闭一个被fopen打开的文件对象指针。其函数原型如下:
int fclose( FILE *stream );
需要注意的是,如果对文件内容进行了写入或修改,只有当调用fclose之后,才能保证对应的修改一定被同步到了对应文件中。
fgets和fputs
概述
这一组函数用于对字符串的读取和写入。函数原型如下:
int fputs( const char *str, FILE *stream );
char* fgets( char *str, int count, FILE *stream );
- str参数是待写入文件或从文件读出的字符串
- stream是用于传入fopen打开的FILE*指针,指定读写的文件对象。
- fgets中的count用于指定最大可从文件读到内存中的字符串长度(通常被设置为str的最大长度)
文本模式
我们先来看一个简单的例子:
FILE* fp = fopen("mystr.txt", "w");
if (!fp)
{
printf("open file error\n");
return 0;
}
const char* target = "abc\ndef\n";
fputs(target, fp);
fclose(fp);
先在第3行下断点,观察target的内存:
61 62 63 0a 64 65 66 0a 00 abc.def.
接着让程序执行完fclose,使用16进制编辑器观察刚才写入的文件:
61 62 63 0D 0A 64 65 66 0D 0A abc..def..
可以观察到如下现象:
- 原本待写入文件中的0x0A在实际写入文件的过程中变为了0x0D 0x0A(即\r\n)。这就是我们上面提到的,默认文本模式情况下对于换行符的处理。
- 使用fputs写入字符串时,不会写入包含字符串的结束符\0(即0x00),如果此时继续向文件写入数据,则很难再将之前的字符串与随后写入的数据分隔开。
接下来,我们再使用fgets从刚才的文件中读出相应内容到内存中:
#define MAXLEN 50
char str[MAXLEN];
fp = fopen("mystr.txt", "r");
if (!fp)
{
printf("open file error\n");
return 0;
}
while (!feof(fp))
{
fgets(str, MAXLEN, fp);
printf("%s ", str);
}
fclose(fp);
在第12行下断点,观察读到的str的内存:
61 62 63 0a 00 abc..
对比文件内容和内存中的内容,有如下结论:
- 原本文件中的0x0D 0x0A在被读入内存的过程中,又被转换为了0x0A。这依然是文本模式下对于换行符的处理。
- fgets每次只读取一行内容,输出换行符后即返回,并且会在其后自动补充结束符\0。
我们继续运行,但不取消断点,第二次命中12行时,查看内存中的内容如下:
64 65 66 0a 00 def..
二进制模式
如果我们使用二进制方式打开文件进行写入,即使用如下代码:
FILE* fp = fopen("mystr.txt", "wb"); // 写入
FILE* fp = fopen("mystr.txt", "rb"); // 读出
再观察写入和读出的内容,就会发现0x0A在写入文件后依然为0x0A,读出也依然是0x0A:
61 62 63 0A 64 65 66 0A abc.def.
fscanf和fprintf
概述
大家对于scanf和printf都很熟悉,而fscanf和fprintf只不过将输入或输出的目标由标准输入/输出变成了FILE*对象。
int fscanf( FILE *stream, const char *format, ... );
int fprintf( FILE *stream, const char *format, ... );
文本模式
还是先用一个简单的例子:
FILE* fp = fopen("mystr.txt", "w");
if (!fp)
{
printf("open file error\n");
return 0;
}
const char* target = "abc\ndef\n";
int num = 3;
fprintf(fp, "%d%s", num, target);
fclose(fp);
分析最终写入的文件:
33 61 62 63 0D 0A 64 65 66 0D 0A 3abc..def..
根据上述内容,我们有如下结论:
- 使用%d格式化写入文件的数字最终是以ASCII码形式写入的,比如3的ASCII码(16进制)正好是0x33。
- 和上面分析类似,0x0A在写入文件时,也被转换为了0x0D 0x0A。
接着我们再使用fscanf尝试对文件进行读出:
#define MAXLEN 50
char str[MAXLEN];
fp = fopen("mystr.txt", "r");
if (!fp)
{
printf("open file error\n");
return 0;
}
int num1;
fscanf(fp, "%d", &num1);
while (!feof(fp))
{
fscanf(fp, "%s", str);
printf("%s ", str);
}
fclose(fp);
程序执行结果如下:
- 第10行fscanf首先将3读出。
- 第一次执行到13行时,由于遇到0x0A,仅将"abc"读出,同时自动添加了字符串结束标志0x00(读出的字符串中不包含换行符0x0A)。
- 第二次执行到13行时,由于遇到0x0A,仅将"def"读出,同时自动添加了字符串结束标志0x00(读出的字符串中不包含换行符0x0A)。
二进制模式
如果我们对程序进行修改,将读写文件时fopen的第二个参数加入二进制读写方式,即使用如下代码:
FILE* fp = fopen("mystr.txt", "wb"); // 写入
FILE* fp = fopen("mystr.txt", "rb"); // 读出
最终写入文件的内容如下:
33 61 62 63 0A 64 65 66 0A 3abc.def.
则写入和读出的文件不再会被添加和去除0x0D。
fread和fwrite
概述
这一组函数用于对结构化数据的读取和写入,如对数组进行读取。
size_t fread( void *buffer, size_t size, size_t count, FILE *stream );
size_t fwrite( const void *buffer, size_t size, size_t count, FILE *stream );
- 第一个参数buffer是用于接收读取到的数据或待写入数据的指针
- 第二个参数size是待读取数据的单位长度(以字节为单位)
- 第三个参数count是待读取数据的个数
- 第四个参数stream用于传入fopen打开的FILE*指针,指定读写的文件对象。
文本模式
我们先来看一个简单的例子:
FILE* fp = fopen("mystr.txt", "w");
if (!fp)
{
printf("open file error\n");
return -1;
}
const char* target = "abc\ndef\n";
fwrite(target, sizeof(char), strlen(target), fp);
fclose(fp);
需要注意的是,fread和fwrite中的第二个参数,使其具有了批量读取同一系列数据的能力。我们先调试运行程序,并观察target的内存。其中左边是文件的16进制内容,右边是对应ASCII码的情况。可以看到,\n
对应的16进制编码为0x0A:
61 62 63 0a 64 65 66 0a 00 abc.def..
让程序继续运行,执行完fclose后,我们使用16进制编辑器查看写入的文件,得到如下内容。其中左边是文件的16进制内容,右边是对应ASCII码的情况:
61 62 63 0D 0A 64 65 66 0D 0A abc..def..
可以观察到如下现象:
- 原本待写入文件中的0x0A在实际写入文件的过程中变为了0x0D 0x0A(即\r\n)。这就是我们上面提到的,默认文本模式情况下对于换行符的处理。
- 由于我们只向文件写入了strlen获取的长度,而该长度不包含字符串的结束符\0(即0x00),如果此时继续向文件写入数据,则很难再将之前的字符串与随后写入的数据分隔开。
接下来我们再尝试从文件中读出所写的内容:
// 从文件中读取
#define MAXLEN 50
char str[MAX_LEN];
FILE* fp1 = fopen("mystr.txt", "r");
if (!fp)
{
printf("open file error\n");
return 0;
}
while (!feof(fp))
{
fread(str, 1, MAXLEN, fp);
printf("%s", str);
}
fclose(fp);
我们在第13行下断点,观察str对应的内存:
61 62 63 0a 64 65 66 0a cc cc cc cc cc (省略后续若干cc) abc.def.烫烫烫(省略若干烫)
可以观察到如下现象:
- 原本文件中的0x0D 0x0A在被读入内存的过程中,又被转换为了0x0A。这依然是文本模式下对于换行符的处理。
- 由于文件中没有换行符,读出的字符串也自然不包含结束符。由于Debug模式下VC编译器默认会向局部数组填充0xcc,数组中没有被覆盖的0xcc 0xcc正好就被解释为了中文的烫字。
二进制模式
基于上述分析,我们将之前的代码修改如下:
FILE* fp = fopen("mystr.txt", "wb"); // 此处模式代码改为wb
if (!fp)
{
printf("open file error\n");
return -1;
}
const char* target = "abc\ndef\n";
fwrite(target, sizeof(char), strlen(target) + 1, fp); // 此处第三个参数改为了strlen(target) + 1
fclose(fp);
同理,读出程序也改为:
// 从文件中读取
#define MAXLEN 50
char str[MAX_LEN];
FILE* fp1 = fopen("mystr.txt", "r");
if (!fp)
{
printf("open file error\n");
return 0;
}
while (!feof(fp))
{
fread(str, 1, MAXLEN, fp);
printf("%s", str);
}
fclose(fp);
则内存数据和最终硬盘上的数据则保持了一致,且不会再出现“烫烫烫”的问题。大家有兴趣的话,可以自行实验。
二进制模式下结构化数据的读写
考虑实现如下程序:
- 随机化生成一个100以内的数n,并根据n具体的值,再生成n个随机数
- 将n和n个随机数写入文件中
- 从文件中读取出n和n个随机数,然后对其进行排序
我们使用fwrite和fread实现,代码如下:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int cmp(const void* a, const void* b)
{
return *(int*)a - *(int*)b;
}
int main()
{
//写n个随机数
srand(time(NULL));
int n = n = rand() % 100 + 1;
int rd, i;
FILE* fp = fopen("myrandint.txt", "wb");
printf("n=%d\n", n);
fwrite(&n, sizeof(int), 1, fp);
for (i = 0; i < n; ++i)
{
rd = rand() % n;
printf("%d ", rd);
fwrite(&rd, sizeof(int), 1, fp);
}
printf("\n");
fclose(fp);
// 读出n个随机数
fp = fopen("myrandint.txt", "rb");
fread(&n, sizeof(int), 1, fp);
printf("n=%d\n", n);
int* data = (int*)malloc(n * sizeof(int));
if (!data)
{
return NULL;
}
for (i = 0; i < n; ++i)
{
fread(&rd, sizeof(int), 1, fp);
data[i] = rd;
printf("%d ", rd);
}
printf("\n");
// 排序
qsort(data, n, sizeof(data[0]), cmp);
for (i = 0; i < n; i++)
{
printf("%d ", data[i]);
}
return 0;
}
需要提醒大家注意的是,查看生成的myrandint.txt会发现,内容并非如我们设想中的可读数字。使用16进制编辑器打开,内容如下:
00000000: 1A 00 00 00 05 00 00 00 04 00 00 00 08 00 00 00 ................
00000010: 00 00 00 00 01 00 00 00 13 00 00 00 01 00 00 00 ................
00000020: 0E 00 00 00 02 00 00 00 14 00 00 00 18 00 00 00 ................
00000030: 18 00 00 00 17 00 00 00 10 00 00 00 00 00 00 00 ................
00000040: 0C 00 00 00 0B 00 00 00 12 00 00 00 0F 00 00 00 ................
00000050: 08 00 00 00 01 00 00 00 03 00 00 00 18 00 00 00 ................
00000060: 11 00 00 00 15 00 00 00 07 00 00 00 ............
这是因为fwrite是将对应内存中的数据写入了文件,而int在内存中的呈现形式(以x86这一小端机为例),就是4字节为一个int,从高位向低位展开。
另外我们使用了C语言库函数自带的排序函数qsort,关于这个函数,请大家使用我们课上学过的方法,查询文档了解其用法。
其他函数
使用C库函数打开文件后,C库在对应的FILE对象中会维护一根指针,指向文件待读取或写入的位置。当打开一个文件后,一般来说这根指针默认指向文件的头部,即从头开始读写,并随着每一次使用上面三组函数而自动更新位置。但如果使用“追加模式”(“a”)打开文件时,该指针会指向文件结束位置。
我们也可以通过fseek来人工指定或改变该指针的位置:
int fseek( FILE *stream, long offset, int origin );
// origin的可选值:
#define SEEK_SET // offset相对于文件头部计算
#define SEEK_CUR // offset相对于当前位置计算
#define SEEK_END // offset相对于文件末尾计算
如果希望让文件指针回到文件头部,也可以使用rewind函数:
// 以下两种方式等价
void rewind( FILE *stream );
fseek(FILE *stream, 0, SEEK_SET);
也可以获取当前文件指针距文件头部的长度:
long ftell( FILE *stream );
当然,也可以直接获取及设置文件指针的位置:
int fsetpos( FILE *stream, const fpos_t *pos ); // 设置文件指针位置
int fgetpos( FILE *stream, fpos_t *pos ); // 获取当前文件指针位置