C语言文件读写(1)-文本文件读操作

C语言文件读写——文本文件读操作

 

最近和几个初学C语言的朋友讨论文件读写,发现他们在使用C语言文件读写功能的时候遇到了不少问题,不是文件打开方式有问题,就是文件读写有问题,总是得不到自己想要的结果。

C语言文件读写操作,既简单又复杂,要熟练使用每一项功能,也确实不易,既有文本文件操作,也有二进制文件的读写。本文先比较详细地介绍C语言的文本文件读操作,下一篇会详细介绍文本文件的写使用。

什么是文本文件

文本文件是由一行一行的字符的有序序列组成的。一行是由0个或者若干个字符加上一个行结束符'\n'(换行符)组成的,但是最后一行的最后一个字符是否是'\n'可能在不同的平台的实现不太一样,但是由于是最后一行的最后一个字符,所以并不会影响正常读写,但是我们在写文本文件的时候,最好也保证最有一个字符是行结束符'\n'。

在Windows上,为了方便人阅读文本文档,在写入'\n'的时候,系统会自动将'\n'转换为'\r\n'(回车换行),在读取的时候会自动将'\r\n'转换为'\n'。在Linux上,并不会有这种转换,所以Windows上的文本文件在Linux上比较低的版本上进行读写的时候有时候需要进行一下转换,但是现在的Linux系统基本上已经能够正常处理'\r\n'了。

为了能够保证文本文件的正常读写,要求:

  • 写入的字符是可打印字符,或者一些特殊的控制符,比如'\t'(TAB键),'\n'。
  • 不要在空格字符后紧跟'\n'字符,否则可能在读取的时候空格会丢失。
  • 最后一个字符是'\n'。

读文本文件示例

说了这么多,我们先直接来一个读取文本文件内容的例子,然后再做一下具体分析,这个文本文件的内容就是本文档的部分内容,我们把它读取出来,然后在控制台显示,我们以Windows系统为例,Linux下面的操作方式是类似的,读取文本文件的示例代码如下:

#include <stdio.h>
void read_text(const char* file_name)
{
	char line[1024]={0};
	FILE *file = fopen(file_name,"rt");
	if(!file)
		return;
	while(1)
	{
		//文件读取结束
		if(EOF == fscanf(file,"%s",line))
			break;
		printf("%s\n",line);
	}
	fclose(file);

}
int main(int argc, char* argv[])
{
	read_text("test.txt");
	return 0;
}

然后运行程序,看效果是否是我们期望的那样,一行一行地显示在控制台,如下图所示,上边是程序的输出,下边是test.txt文件的内容。

读文本文件详解

C语言中打开文件,主要有两个函数,fopen和fopen_s,fopen_s是C11标准中新引入的,它们的函数申明是这样的:

		
	(1) 	
FILE *fopen( const char *filename, const char *mode );	(until C99)
FILE *fopen( const char *restrict filename, const char *restrict mode );(since C99)

	(2) 	
errno_t fopen_s(FILE *restrict *restrict streamptr, 
                const char *restrict filename,  
                const char *restrict mode);(since C11)

fopen_s会做一些额外的检查,更“安全”一些,C11标准后,有很多这样的_s的函数,都是为了更加安全,会做一些安全性的检查,比如函数传入的指针是否为空,传入的缓冲区是否会溢出等等。

返回值FILE*就代表了一个文件流对象,如果为NULL,则表示打开文件失败。

重点看一下mode,即打开文件的方式,常用的mode主要有:

mode 含义 说明 如果文件存在 如果文件不存在
“r” 以读的方式打开文件,打开以后只能读 成功打开,并从文件开始读 打开失败
"w" 创建一个文件进行写 文件内容会被清空 创建一个新文件
"a" 追加 追加内容到文件末尾 将文件内容追加到文件末尾 创建一个新文件
"r+" 扩展读 打开文件进行读写,可读可写 从文件开始读 打开失败
"w+" 扩展写 创建一个文件进行读写 文件内容会被清空 创建一个新文件
"a+" 扩展追加 打开文件进行读写 追加内容到文件末尾 创建一个新文件

 

 

 

 

 

11啊

 除了这些基本的打开模式之外,还可以附加一些别的模式,比如“t”,“b”等,例如“rb”,表示以二进制方式打开文件,"wt"以文本文件的方式创建文件,默认是文本文件方式,所以“t”一般可以省略。

从上面的表格我们可以简单总结出来,只要是有“r”标志,文件就要求存在,否则就会打开失败,其他的打开模式都不需要文件一定存在,如果文件不存在,则会创建一个新文件。所以,在进行文件读写之前,一定要明确自己的目的,是读文件,还是写文件,是从文件开始写,还是追加内容到文件末尾。

本节的主题是读文本文件,所以我们在使用fopen的时候,就使用"r"或者"rt"标志,不要使用别的模式。

说到读文本文件内容,不是一件简单事情,因为读文本文件内容也涉及到好些函数,如果选择不当,得到的结果也会不是我们期望的。

其实文本文件的内容虽然在前面介绍过,要作为文本文件是有要求的,比如前面提到的每行要求以'\n'结束等。但是文本文件内容本身又可以分为有格式的和没有格式的,比如我们前面的test.txt文件,就是没有格式的,就是纯文本文件,因为除了换行之外,没有任何别的格式,信息是杂乱的。如果我们查看一下一个普通的Excel文件,里面的数据就很有格式,一行一行,一列一列,非常整齐,这就是有格式的文本文件样子,我们看一个有格式的文本文件,这是一个简易的学生名单文件student.txt,每一行包含的内容为学号,姓名,所属学院,成绩,如下图所示。

这个student.txt文件内容就是格式化的,即每一行的数据格式都是一致的,包含的信息都是类似的,即都包含学号,姓名,学院和分数,而且它们之间是用空格隔开的,这就是格式化的文件数据。

在读取非格式化和格式化数据的时候,需要用到的函数是不一样的,否则,用非格式化数据读取函数去读取格式化的数据,读取到的内容我们也不好利用,同样,用格式化数据读取函数去读取非格式化的数据,有时候也得不到正确结果。

非格式化文本文件读取

 

非格式文本文件内容读取主要有两个函数,fgetc和fgets,fgetc的函数原型为:

int fgetc( FILE *stream );

 

每次从文件中读取一个字符,直到文件结尾。如果读到文件结尾,返回EOF。

fgetc不会对文件中的内容做任何处理,一次就读一个字符,'\n'也会想普通字符一样读取,我们来看一下使用fgetc读取我们前面提到的text.txt文件的效果,代码如下:

void read_text_by_getc(const char* file_name)
{
	int ch = 0;
	FILE *file = fopen(file_name,"rt");
	if(!file)
		return;
	while(EOF != (ch = fgetc(file)))
	{
		//在屏幕上输出读到的每一个字符
		putchar(ch);
	}
	fclose(file);
}

则执行效果下图所示。

fgetc对于读取非格式化的文本内容,就是最真实地反应了文本文件中的内容。

但是fgetc读取容易,处理起来却很麻烦,如果只是在屏幕上显示文件内容的话,没有问题,如果还要想对读取的内容进行处理的话,就麻烦一些。

我们再来看一下fgets函数。

fgets的函数原型为:

char *fgets( char *restrict str, int count, FILE *restrict stream );

 

(since C99)

每次从文件中读取指定长度的内容,读取的长度最多为count-1个字符。读到'\n'的时候,这一次读取就结束了,即使还没有读到count-1个字符。如果读取失败,返回NULL。

我们先看一下使用fgets读取test.txt文本文件的效果,代码如下:

void read_text_by_gets(const char* file_name)
{
	char buffer[20]={0};
	FILE *file = fopen(file_name,"rt");
	if(!file)
		return;
	while(NULL != (fgets(buffer,sizeof(buffer),file)))
	{
		//显示每一次读取到的内容
		printf("%s",buffer);
	}
	fclose(file);
}

然后运行,结果如下所示。

从图中可以看到,显示结果一塌糊涂,为什么呢?因为我们的缓冲区buffer大小是20,所以每次只读19个字符,有时候刚好得到半个汉字,就显示很凌乱了。

因此,如果我们想完整的一次读取一行的数据,这个缓冲区buffer必须足够大,至少要超过文件中最长的一行字符的长度,我们修改一下程序,我们知道我们的test.txt文件中每行都不会太长,所以我们把buffer的大小设置成128,看看效果如何,如下图所示。

因此,我们在对于非格式化文本进行读取的时候,要选择合适的函数来读取,如果想一行一行的读取文本文件中的内容,最好使用fgets,并且提供足够大的缓冲区。

格式化文本文件读取

格式化文本文件的读取主要用到函数fscanf和fscanf_s。

先看一下fscanf函数的原型。

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

(since C99)

fscanf和scanf一样,有一个格式化字符串format参数,因此在读取文本内容的时候会严格按照这个format格式去读,因此,对于我们的读取结构化的数据就非常方便,以我们的student.txt为例,我们再来看一下student.txt文件的内容。

1001 张三 计算机学院 90.0
1002 李四 计算机学院 98.7
1003 王五 软件学院 88
1004 黄渤海 人文学院 97

每一行代表一个学生,包含学号,姓名,所属学院以及成绩。

当使用fscanf读取的时候,同样,遇到'\n',一次读取就结束了,假设我们有四个变量ID,Name,College,Score,我们可以用scanf一次把一行的数据都读取到这四个变量中,我们看一下代码:

void read_formated_text(const char* file_name)
{
	int ID;
	char Name[32]={0};
	char College[128]={0};
	float Score = 0.0;
	FILE *file = fopen(file_name,"rt");
	if(!file)
		return;
	printf("学生名单为:\n");
	while(1)
	{
		//文件读取结束
		if(EOF == fscanf(file,"%d %s %s %f",&ID,Name,College,&Score))
			break;
		printf("学号:%d 姓名:%s 学院:%s 分数:%.2f\n",ID,Name,College,Score);		
	}
	fclose(file);
}

 

 运行结果下图所示。

如果我们有一个结构体Student的话,也可以通过这种方式把一行的数据直接读取到一个结构体中,代码如下:

void read_formated_text_by_struct(const char* file_name)
{
	struct Student stu;
	FILE *file = fopen(file_name,"rt");
	if(!file)
		return;
	printf("学生名单为:\n");
	while(1)
	{
		//文件读取结束
		if(EOF == fscanf(file,"%d %s %s %f",&stu.ID,stu.Name,stu.College,&stu.Score))
			break;
		printf("学号:%d 姓名:%s 学院:%s 分数:%.2f\n",stu.ID,stu.Name,stu.College,stu.Score);		
	}
	fclose(file);
}

 

结果是一样的。

现在简单介绍一下fscanf_s,其函数原型为:

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

  (since C11)

对于这个函数的使用,牢记住一点就好了,如果读取的是字符串类型,需要在字符串变量后面在跟一个字符串长度的值来指明字符串变量或者数组能够容纳多少个字符,比如我们前面的读取student.txt文件的代码就可以改为:

void read_formated_text_by_struct(const char* file_name)
{
	struct Student stu;
	FILE *file = fopen(file_name,"rt");
	if(!file)
		return;
	printf("学生名单为:\n");
	while(1)
	{
		//文件读取结束
		if(EOF == fscanf_s(file,"%d %s %s %f",&stu.ID,stu.Name,sizeof(stu.Name)-1,stu.College,sizeof(stu.College)-1,&stu.Score))
			break;
		printf("学号:%d 姓名:%s 学院:%s 分数:%.2f\n",stu.ID,stu.Name,stu.College,stu.Score);		
	}
	fclose(file);
}

因为Name和College都是字符串类型,所以在它们后面都跟了一个长度。执行结果仍然一样。

到了这里,大家以为文本文件的读取内容应该已经结束了,其实不是,如果你是C语言的初级选手或者刚学计算机不太久,后面的内容可以暂时跳过。

窄字符与宽字符

窄字符和宽字符都是指对字符的编码,窄字符就是一个字符占有的空间只有一个字节,宽字符通常一个字符占用两个字节的空间,我们也常常说宽字符为UNICODE编码字符。

我们前面所讲的读取文件,这些函数其实都是针对窄字符的,因为这些文件内容的编码都是窄字符编码,一个英文是一个字符,一个汉字是两个字符,它们都是一个字符占用一个字节。

如果是宽字符的话,一个字符占用两个字节,每一个英文字符还是一个字符,一个中文汉字通常也是一个字符,但是它们都占用两个字节。我们把test.txt文件另存一下,存为UNICODE编码,用记事本的另存功能,选择UNICODE,如图所示。

 

如果我们此时还是用前面的代码来读取test_unicode.txt文件,效果会是什么样呢?我们以read_text_by_getc来验证一下,结果如图所示。

可以再次用一塌糊涂来形容,什么也不是了。

这就涉及到另外的读取函数了,是专门针对宽字符的,主要也有这几个函数,与窄字符对应的函数分别为:

wint_t fgetwc( FILE *stream );

  (since C95)

wchar_t *fgetws( wchar_t * restrict str, int count, FILE * restrict stream );

  (since C99)

int fwscanf( FILE *restrict stream,
             const wchar_t *restrict format, ... );

(since C99)

int fwscanf_s( FILE *restrict stream,
               const wchar_t *restrict format, ...);

(5) (since C11)

 它们的用法和窄字符的用法完全一样,唯一的不同是它们都要求被读取的文件的内容是UNICODE即宽字符编码的。

这里不做试验了,有的朋友如果有兴趣的话,自己可以试一试,看看效果。

《完》

 

展开阅读全文

Windows版YOLOv4目标检测实战:训练自己的数据集

04-26
©️2020 CSDN 皮肤主题: 大白 设计师: CSDN官方博客 返回首页
实付39.90元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值