在此之前,我极少使用C语言处理文件。因为我认为使用Python、matlab处理文件是及其方便的。
事实果真如此吗?
文件(file)通常是在磁盘或固态硬盘上的一段已命名的存储区。对我们而言,stdio.h就是一个文件的名称,该文件中包含一些有用的信息。然而,对操作系统而言,文件更复杂一些。例如,大型文件会被分开储存,或者包含一些额外的数据,方便操作系统确定文件的种类,这都是操作系统所关心的,程序员关心的是C程序如何处理文件。
C把文件看作是一系列连续的字节,每个字节都能被单独读取。这与UNIX环境中的文件结构相对应。由于其他环境中可能无法完全对应这个模型,C提供两种文件模式:文本模式和二进制模式。
需要区分:文本内容和二进制内容、文本文件格式和二进制文件格式,以及文件的文本模式和二进制模式。
所有文件的内容都以二进制形式(0或1)储存(即在物理层面上,文本文件和二进制文件没有区别)。
如果文件最初使用二进制编码的字符(例如,ASCII或Unicode)表示文本(就像C字符串那样),该文件就是文本文件,其中包含文本内容。
如果文件中的二进制值代表机器语言代码或数值数据(使用相同的内部表示,假设,用于long或double类型的值)或图片或音乐编码,该文件就是二进制文件,其中包含二进制内容。
比如 bmp文件,它开始的部分是文件头信息,前2个字节表示文件格式为BMP格式,接着的 8个字节表示文件的长度,再接着的4个字节表示 bmp文件头的长度。从中可以看出,解析bmp文件时是不定长度的,2、4、8字节长度的都有。因此bmp文件是二进制文件。
不同系统在文件处理方面有着不同的标准(如换行符在不同系统中可以是:\n,\r,\r\n),为了规范文本文件的处理,C提供两种访问文件的途径:二进制模式和文本模式。
在二进制模式中,程序可以访问文件的每个字节。
在文本模式中,程序所见的内容和文件的实际内容不同。程序以文本模式读取文件时,把本地环境表示的行末尾或文件结尾映射为C模式。
例如:
C程序在旧式Macintosh中以文本模式读取文件时,把文件中的\r转换成\n;以文本模式写入文件时,把\n转换成\r。
或者,C文本模式程序在MS-DOS平台读取文件时,把\r\n转换成\n;写入文件时,把\n转换成\r\n。在其他环境中编写的文本模式程序也会做类似的转换。
除了以文本模式读写文本文件,还能以二进制模式读写文本文件。如果读写一个旧式MS-DOS文本文件,程序会看到文件中的\r和\n字符,不会发生映射。如果要编写旧式Mac格式、MS-DOS格式或UNIX/Linux格式的文件模式程序,应该使用二进制模式,这样程序才能确定实际的文件内容并执行相应的动作。
虽然C提供了二进制模式和文本模式,但是这两种模式的实现可以相同。前面提到过,因为UNIX使用一种文件格式,这两种模式对于UNIX实现而言完全相同。Linux也是如此。
除了选择文件的模式,大多数情况下,还可以选择I/O的两个级别(即处理文件访问的两个级别)。
底层I/O(low-level I/O)使用操作系统提供的基本I/O服务。
标准高级I/O(standardhigh-level I/O)使用C库的标准包和stdio.h头文件定义。因为无法保证所有的操作系统都使用相同的底层I/O模型,C标准只支持标准I/O包。有些实现会提供底层库,但是C标准建立了可移植的I/O模型,本文主要讨论这些I/O。
C程序会自动打开3个文件,它们被称为标准输入(standard input)、标准输出(standard output)和标准错误输出(standard error output)。
在默认情况下,标准输入是系统的普通输入设备,通常为键盘;
标准输出和标准错误输出是系统的普通输出设备,通常为显示屏。
通常,标准输入为程序提供输入,它是getchar()和scanf()使用的文件。
程序通常输出到标准输出,它是putchar()、puts()和printf()使用的文件。
重定向把其他文件视为标准输入或标准输出。
标准错误输出提供了一个逻辑上不同的地方来发送错误消息。例如,如果使用重定向把输出发送给文件而不是屏幕,那么发送至标准错误输出的内容仍然会被发送到屏幕上。这样很好,因为如果把错误消息发送至文件,就只能打开文件才能看到。
与底层I/O相比,标准I/O包除了可移植以外还有两个好处。
例如,当程序读取文件时,一块数据被拷贝到缓冲区(一块中介存储区域)。这种缓冲极大地提高了数据传输速率。程序可以检查
缓冲区中的字节。缓冲在后台处理,所以让人有逐字符访问的错觉(如果使用底层I/O,要自己完成大部分工作)
函数原型:
FILE* fopen(char const* _FileName,char const* _Mode
第二个参数值指定以什么模式打开文件,是一个字符串,可取的值有:
模式字符串 | 含义 |
---|---|
“r” | 以读模式打开文件 |
“w” | 以写模式打开文件,把现有文件的长度截为0,如果文件不存在,则创建一个新文件 |
“a” | 以写模式打开文件,在现有文件末尾添加内容,如果文件不存在,则创建一个新文件 |
“r+” | 以更新模式打开文件(即可以读写文件) |
“w+” | 以更新模式打开文件(即,读和写),如果文件存在,则将其长度截为0;如果文件不存在,则创建一个新文件 |
“a+” | 以更新模式打开文件(即,读和写),在现有文件的末尾添加内容,如果文件不存在则创建一个新文件;可以读整个文件,但是只能从末尾添加内容 |
“rb”、“wb”、“ab”、“rb+”、“r+b”、“wb+”、“w+b”、“ab+”、“a+b” | 与上一个模式类似,但是以二进制模式而不是文本模式打开文件 |
“wx”、“wbx”、“w+x”、“wb+x"或"w+bx” | (C11)类似非x模式,但是如果文件已存在或以独占模式打开文件,则打开文件失败 |
像UNIX和Linux这样只有一种文件类型的系统,带b字母的模式和不带b字母的模式相同。
上面的“将长度截为0”意思是删除现有文件已有的内容。使用任何一种不带X的“w”模式,都会这样。
示例:
FILE* fp = fopen("test.txt","r");
这两个函数用于从文件中获取一个字符和将一个字符写入文件,每次执行后,位置都会自动向后移动一个字符。
和getchar()、putchar()类似。
示例:
ch = getc();
putc(ch,fp);
getc()函数在读取一个字符时发现是文件结尾,它将返回一个特殊值EOF。
它定义在stdio.h中:
#define EOF (-1)
现在就可以使用C语言来读取一个文本文件了:
#include
int main()
{
FILE* fp = fopen("origin.txt","r");
int ch;
while ((ch=getc(fp))!=EOF)
{
putchar(ch);
}
fclose(fp);
return 0;
}
输出:
Hello,CSDN.
fclose(fp)函数关闭fp指定的文件,必要时刷新缓冲区。对于较正式的程序,应该检查是否成功关闭文件。如果成功关闭,fclose()函数返回0
,否则返回EOF:
if (fclose(fp) != 0)
printf("Error in closing file %s\n", argv[1]);
如果磁盘已满、移动硬盘被移除或出现I/O错误,都会导致调用fclose()函数失败。
都是指向文件的指针。
写一个程序,读取一个文件,并copy内容到新文件中。
#include
#include
#include
#define LEN 30
void file_copy(const char *file_in);
int main()
{
file_copy("origin.txt");
return 0;
}
void file_copy(const char *file_in)
{
FILE *in, *out;
char name[LEN];
int ch;
// 检查文件名称
if ((in = fopen(file_in, "r")) == NULL)
{
fprintf(stderr,"The file: %s is not exit.\n",file_in);
exit(EXIT_FAILURE);
}
strncpy(name, file_in, LEN - 9);
name[LEN - 5] = '\0';
strcat(name,".new.txt");
if ((out = fopen(name, "w+")) == NULL)
{
fprintf(stderr, "Can't creat output file.\n");
exit(EXIT_FAILURE);
}
while ((ch = getc(in)) != EOF)
{
putc(ch, out);
}
if (fclose(in) != 0 || fclose(out) != 0)
{
fprintf(stderr, "Error with closing files.\n");
exit(EXIT_FAILURE);
}
puts("Processing success!");
}
注:
exit()函数关闭所有打开的文件并结束程序。exit(EXIT_FAILURE);表示技术程序失败,并将标准错误(stderr)输出到屏幕,exit(EXIT_SUCCESS);即exit(0);在main()函数中和return效果一样。
文件I/O函数fprintf()和fscanf()函数的工作方式与printf()和scanf()类似,区别在于前者需要用第1个参数指定待处理的文件。
例:
void fp_s()
{
FILE* fp;
char words[30];
if ((fp = fopen("word.txt", "a+")) == NULL)
{
fprintf(stderr, "打开文件失败。\n");
exit(EXIT_FAILURE);
}
puts("输入单词,q表示结束\n");
while ((fscanf(stdin, "%s", words) == 1) && words[0] != 'q')
{
fprintf(fp, "%s\n", words);
}
rewind(fp); //将指针移到开头
puts("你输入了以下单词:");
while ((fscanf(fp, "%s\n", words)) == 1)
puts(words);
if ((fclose(fp)) != NULL)
fprintf(stderr,"关闭文件失败。\n");
}
输出:
输入单词,q表示结束
dog
cat
Jay Chou
你好
123
q
你输入了以下单词:
dog
cat
Jay
Chou
你好
123
fgets()函数的第1个参数和gets()函数一样,也是表示储存输入位置的地址(char * 类型);第2个参数是一个整数,表示待输入字符串的大小(不是长度);最后一个参数是文件指针,指定待读取的文件。下面是一个调用该函数的例子:
fgets(buf, STLEN, fp);
这里,buf是char类型数组的名称,STLEN是字符串的大小,fp是指向FILE的指针。fgets()函数读取输入直到第1个换行符的后面,或读到文件结尾,或者读取STLEN-1个字符(以上面的fgets()为例)。然后,fgets()在末尾添加一个空字符使之成为一个字符串。字符串的大小是其字符数加上一个空字符。如果fgets()在读到字符上限之前已读完一整行,它会把表示行结尾的换行符放在空字符前面。fgets()函数在遇到EOF时将返回NULL值,可以利用这一机制检查是否到达文件结尾;如果未遇到EOF则返回之前传给它的第一个参数地址。
fputs()函数接受两个参数:第1个是字符串的地址;第2个是文件指针。该函数根据传入地址找到的字符串写入指定的文件中。和puts()函数不同,fputs()在打印字符串时不会在其末尾添加换行符。下面是一个调用该函数的例子:
fputs(buf, fp);
这里,buf是字符串的地址,fp用于指定目标文件。由于fgets()保留了换行符,fputs()就不会再添加换行符。
fseek()函数原型:
int fseek( FILE* _Stream,long _Offset, int _Origin);
该函数的功能是修改当前位置。
成功返回0,出错返回-1。
ftell()函数的返回类型是long,就一个参数:文件指针,它返回的是参数指向文件的当前位置距文件开始处的字节数。
编写一个函数将文件中的内容逆序添加到文件结尾,原内容:
ABCDEFGHIJK
运行一次:
ABCDEFGHIJK
KJIHGFEDCBA
代码:
void fseek_tell()
{
FILE* pt;
long count;
int ch;
pt = fopen("sort.txt","a+");
fseek(pt,0L,SEEK_END);
count = ftell(pt);
fprintf(pt,"%s","\n"); //换行
while (count != -1)
{
fseek(pt,count,SEEK_SET);
ch = getc(pt);
count--;
fseek(pt, 0L, SEEK_END);
fprintf(pt, "%c", ch);
}
fclose(pt);
}
fseek()和ftell()潜在的问题是,它们都把文件大小限制在long类型能表示的范围内。也许20亿字节看起来相当大,但是随着存储设备的容量迅猛增长,文件也越来越大。鉴于此,ANSI C新增了两个处理较大文件的新定位函数:fgetpos()和fsetpos().
通常,使用标准I/O的第1步是调用fopen()打开文件(前面介绍过,C程序会自动打开3种标准文件)。
fopen()函数不仅打开一个文件,还创建了一个缓冲区(在读写模式下会创建两个缓冲区)以及一个包含文件和缓冲区数据的结构。
另外,fopen()返回一个指向该结构的指针,以便其他函数知道如何找到该结构。假设把该指针赋给一个指针变量fp,我们说fopen()函数“打开一个流”。如果以文本模式打开该文件,就获得一个文本流;如果以二进制模式打开该文件,就获得一个二进制流。
这个结构通常包含一个指定流中当前位置的文件位置指示器。除此之外,它还包含错误和文件结尾的指示器、一个指向缓冲区开始处的指针、一个文件标识符和一个计数(统计实际拷贝进缓冲区的字节数)。
通常,使用标准I/O的第2步是调用一个定义在stdio.h中的输入函数,如fscanf()、getc()或fgets()。一调用这些函数,文件中的缓冲大小数据块就被拷贝到缓冲区中。缓冲区的大小因实现而异,一般是512字节或是它的倍数,如4096或16384。最初调用函数,除了填充缓冲区外,还要设置fp所指向的结构中的值。尤其要设置流中的当前位置和拷贝进缓冲区的字节数。通常,当前位置从字节0开始。
在初始化结构和缓冲区后,输入函数按要求从缓冲区中读取数据。在它读取数据时,文件位置指示器被设置为指向刚读取字符的下一个字符。由于stdio.h系列的所有输入函数都使用相同的缓冲区,所以调用任何一个函数都将从上一次函数停止调用的位置开始。
当输入函数发现已读完缓冲区中的所有字符时,会请求把下一个缓冲大小的数据块从文件拷贝到该缓冲区中。以这种方式,输入函数可以读取文件中的所有内容,直到文件结尾。函数在读取缓冲区中的最后一个字符后,把结尾指示器设置为真。于是,下一次被调用的输入函数将返回EOF。输出函数以类似的方式把数据写