C语言库函数中文件读写操作精讲


这篇文章主要解决如下几个问题:

  • 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..

可以观察到如下现象:

  1. 原本待写入文件中的0x0A在实际写入文件的过程中变为了0x0D 0x0A(即\r\n)。这就是我们上面提到的,默认文本模式情况下对于换行符的处理。
  2. 使用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..

对比文件内容和内存中的内容,有如下结论:

  1. 原本文件中的0x0D 0x0A在被读入内存的过程中,又被转换为了0x0A。这依然是文本模式下对于换行符的处理。
  2. 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..

根据上述内容,我们有如下结论:

  1. 使用%d格式化写入文件的数字最终是以ASCII码形式写入的,比如3的ASCII码(16进制)正好是0x33。
  2. 和上面分析类似,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);

程序执行结果如下:

  1. 第10行fscanf首先将3读出。
  2. 第一次执行到13行时,由于遇到0x0A,仅将"abc"读出,同时自动添加了字符串结束标志0x00(读出的字符串中不包含换行符0x0A)。
  3. 第二次执行到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..

可以观察到如下现象:

  1. 原本待写入文件中的0x0A在实际写入文件的过程中变为了0x0D 0x0A(即\r\n)。这就是我们上面提到的,默认文本模式情况下对于换行符的处理。
  2. 由于我们只向文件写入了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.烫烫烫(省略若干烫)

可以观察到如下现象:

  1. 原本文件中的0x0D 0x0A在被读入内存的过程中,又被转换为了0x0A。这依然是文本模式下对于换行符的处理。
  2. 由于文件中没有换行符,读出的字符串也自然不包含结束符。由于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);

则内存数据和最终硬盘上的数据则保持了一致,且不会再出现“烫烫烫”的问题。大家有兴趣的话,可以自行实验。

二进制模式下结构化数据的读写

考虑实现如下程序:

  1. 随机化生成一个100以内的数n,并根据n具体的值,再生成n个随机数
  2. 将n和n个随机数写入文件中
  3. 从文件中读取出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 );        // 获取当前文件指针位置
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值