0. 引言
什么是文件:
文件通常是在磁盘或固态硬盘上一段已命名的存储区。C把文件看做是一系列连续的字节,每个字节都能单独读取。C提供两种文件模式:文本模式和二进制模式。文本内容和二进制内容:
所有文件内容都是二进制存储的。如果文件最初使用二进制编码的字符(例如ASCII或Unicode)表示文本,那么该文件就是文本文件,包含文本内容;如果文件中的二进制值代表机器语言代码或数值数据或图片或音乐编码,那么就是二进制文件,包含二进制内容。文本文件格式和二进制文件格式:
Unix用同一种文件格式处理文本文件和二进制文件的内容。C和Unix在文本中都用\n(换行符)表示换行。其他系统例如OS X Macintosh文件用\r(回车符)表示新的一行,MS DOS文件用\r\n表示新的一行,用嵌入的Ctrl+Z表示文件结尾。-
二进制模式和文本模式:
为了规范文本文件的处理,C提供两种访问文件的途径:文本模式和二进制模式。二进制模式中,程序可以访问文件的每个字节。而在文本模式中,程序所见的内容和文件的实际内容不同,程序以文本模式读取文件时,会把本地环境表示的行末尾或文件结尾映射为C模式。例如,C程序在旧式Macintosh中以文本模式读取文件时,把文件中的\r转换成\r\n;以文本模式写入文件时,把\n转换成\r。或者,C程序在MS-DOS平台以文本模式读取文件时,将\r\n转换成\n;以文本模式写入文件时,将\n转换成\r\n。二进制模式读写本地文本文件则不会发生映射。在Unix/Linux中,由于只有一种文件格式,所以上面两种模式的实现相同。
I/O级别
底层I/O使用操作系统提供的基本I/O服务,标准高级I/O使用C库的标准包和stdio.h头文件定义。因为无法保证所有操作系统使用相同的底层I/O, C标准只支持标准I/O包。
2. 标准I/O
本章例子的入口函数和头文件如下:
//fileio.c
#include <stdio.h>
#include <stdlib.h> /*提供exit()函数的原型*/
#include "fileio.h" /*内含函数声明*/
int main(int argc, char * argv[])
{
puts("**********1.使用标准I/O读取文件并统计文件中的字符数,将文件1的内容追加到文件2的末尾,如果文件2不存在,则创建一个**********");
count_and_copy(argc, argv);
putchar('\n');
puts("**********2.使用标准I/O实现简单的文件压缩**********");
reducto(argc, argv);
putchar('\n');
puts("**********3.文件I/O函数fprintf()、fscanf()、rewind()*****************");
add_a_word(argc, argv);
putchar('\n');
puts("*************4.随机访问:fseek()和ftell()******************");
reverse_file_content(argc, argv);
putchar('\n');
puts("*************5.其他标准I/O函数示例:追加若干文件的内容到目标文件末尾***********");
append_file();
return 0;
}
//fileio.h
#ifndef FILEIO_H_INCLUDED
#define FILEIO_H_INCLUDED
#define LEN 40
#define CNTL_Z '\032' /*DOS文本文件中的文件结尾标记*/
#define SLEN 81
static unsigned long count = 0;
static FILE * in;
static FILE * out;
static FILE * fp;
void reducto(int, char * []);
void count_and_copy(int, char * []);
void add_a_word(int, char * []);
void reverse_file_content(int, char * []);
void rand_bin();
#endif // FILEIO_H_INCLUDED
2.1 一个简单的例子
下面是一个利用标准I/O读取文件和做一些操作的程序:
//将count.c 和fileio.c一起编译
#include <stdio.h>
#include <stdlib.h> /*提供exit()函数的原型*/
#include "fileio.h"
void count_and_copy(int argc, char * argv[])
{
int ch; //读取文件时,用于存储每个字符
extern FILE *in; //声明式引用
extern FILE *out;
extern unsigned long count;
if (argc !=3)
{
printf("Usage: %s filename filename2 \n", argv[0]);
exit(EXIT_FAILURE);
}
if ((in=fopen(argv[1],"a+b"))== NULL)
{
printf("Cannot open %s\n", argv[1]);
exit(EXIT_FAILURE);
}
if ((out=fopen(argv[2],"a+b"))== NULL)
{
printf("Cannot open %s\n", argv[2]);
exit(EXIT_FAILURE);
}
while((ch = getc(in)) != EOF)
{
// putc(ch, stdout); //与putchar(ch);相同
putc(ch, out);
count++;
}
fclose(in);
fclose(out);
printf("File %s has %lu characters, and has append all those to file %s\n", argv[1], count, argv[2]);
}
2.2 fopen()函数
fopen()函数用来打开文件,该函数声明在stdio.h中,它的第一个参数是待打开的文件名称,更确定地说是一个包含该文件名称的字符串地址。第2个参数是一个字符串,指定待打开文件的模式。下表列出了fopen()函数的一些模式。
模式字符串 | 含义 |
---|---|
"r" | 以读模式打开文件,文件不存在则fopen()返回NULL |
"w" | 以写模式打开文件,把现有文件长度截为0,如果文件不存在,创建一个新的文件。(所以要小心,以带w的模式打开已存在的文件会清空该文件的内容) |
"a" | 以写模式打开文件,在现有文件末尾添加内容,如果文件不存在,创建一个新的文件。 |
"r+" | 以更新模式打开文件(即,可以读写文件) |
"w+" | 以更新模式打开文件(即,可以读写文件),如果文件存在,则将其文件长度截为0;如果文件不存在,创建一个新文件(所以要小心,以带w的模式打开已存在的文件会清空该文件的内容) |
"a+" | 以更新模式打开文件(即,可以读写文件),在现有文件末尾添加内容,如果文件不存在则创建一个新文件;可以读整个文件,但是只能从文件末尾追加内容。 |
"rb" "wb" "ab" "rb+" "r+b" "wb+" "w+b" "ab+" "a+b" | 与上一个模式类似,但是以二进制模式而不是文本模式打开文件 |
"wx" "wbx" "w+x" "wb+x" "w+bx" | (C11) 类似非X模式,但是如果文件已存在或以独占模式打开文件,则打开文件失败,返回NULL |
注意,像Unix和Linux这样只有一种文件类型的系统,带b字母的模式和不带b字母的模式相同。
新的C11新增了带x字母的写模式,与以前的写模式相比有以下特性:
- 如果以传统的写模式(不带x,带w)打开一个已存在的文件,会清空文件内容。以带x的模式,无法打开现存的文件,这避免了文件被意外清空;
- 如果环境允许,x模式的独占特性使得其他程序或线程无法访问正在被打开的文件。
fopen()函数的返回值是文件指针(指向FILE的指针),其它I/O函数可以使用该指针指定该文件。文件指针fp不指向实际的文件,它指向一个包含文件信息的数据对象(C结构类型),其中包括操作文件的I/O函数所用的缓冲区信息。因为标准库中的I/O函数使用缓冲区,所以它们不仅要知道缓冲区的位置,还要知道缓冲区被填充的程度以及操作哪一个文件。
2.3 getc()和putc()函数
getc()和putc()函数与getchar()和putchar()函数类似,不同的是,要告诉getc()和putc()函数使用哪个文件指针。
ch = getchar();/*从标准输入中获取一个字符,实际上getchar()一般通过getc()定义*/
ch = getc(fp); /*从fp指定的文件中获取一个字符*/
putc(ch, fpout); /*将字符ch放入FILE指针fpout指定的文件中*/
putchar(ch); /*将字符ch输出到标准输出,实际上putchar()一般通过putc()定义*/
2.4 文件结尾
如果getc()函数在读取一个字符时发现是文件结尾,它将返回一个特殊值EOF(宏定义,其值时-1)。
//设计范例
while((ch=getc(fp)!=EOF)
{
putchar(ch);
}
2.5 fclose()函数
fclose()函数关闭fp指定的文件,必要时刷新缓冲区。对于较正式的程序,应检查是否关闭成功。如果关闭成功,fclose返回0, 否则返回EOF。
if(fclose(fp) != 0)
printf("Error in closing file %s\n", argv[1]);
如果磁盘已满、移动硬盘被移除或出现I/O错误,都会导致fclose()函数失败。
2.6.指向标准文件的指针
标准文件 | 文件指针 | 通常使用的设备 |
---|---|---|
标准输入 | stdin | 键盘 |
标准输出 | stdout | 屏幕 |
标准错误输出 | stderr | 屏幕 |
2.7 一个简单的文件压缩程序
//reducto.c 和fileio.c一起编译
#include <stdio.h>
#include <stdlib.h> /*提供exit()函数的原型*/
#include "fileio.h"
//压缩第一个入参文件,每三个字符取第一个字符,输出文件是原文件名加".red"后缀
void reducto(int argc, char * argv[])
{
FILE * in, * out; /*声明两个指向FILE的指针*/
char ch;
char name[LEN]; //存储输出文件名
//设置输入
if((in = fopen(argv[1],"r"))== NULL)
{
fprintf("Cannot open file %s\n", argv[1]);
exit(EXIT_FAILURE);
}
//获取压缩文件名, 如果原文件1中有.txt这样的后缀存在,去掉后缀,只保留'.'以前的字符串
char * find;
find = strchr(argv[1],'.');
if (find)
*find = '\0';
strncpy(name,argv[1],LEN-5);
name[LEN-1] = '\0';
strcat(name,".red"); //文件名加".red"后缀
// strncat(name,argv[1],".red",LEN-1-strlen(name));/*没必要这么写,因为第二个字符串已知了*/
//设置输出
if((out=fopen(name,"w"))==NULL)
{
printf("Cannot open file %s\n", name);
exit(EXIT_FAILURE);
}
while((ch=getc(in))!=EOF )
{
/*因为我们是以"r"模式打开文件的,所以不论原来文件行末尾是什么格式的,都转换Unix格式的换行符'\n'。
如果碰到换行符,我们将其写入压缩文件,但是不计数,从下一行开始继续计数,
这样做主要是在每三个字符取第一个字符的过程中,我们不想将换行符也算作一个字符*/
if (ch=='\n')
{
printf("copy char '\\n' to file %s\n", name);
putc(ch,out);
continue;
}
if(count%3==0)
{
printf("copy char %c to file %s\n", ch, name);
putc(ch,out);
}
count++;
}
printf("source file %s has %d characters and has copied %d to file %s", argv[1], count, count/3, name );
if(fclose(in)!=0 || fclose(out)!=0)
fprintf(stderr, "Error in closing files");
}
3. 文件I/O函数fprintf()、fscanf()、fgets()、fputs()、rewind()
fprintf():工作方式和printf()差不多,区别在于第一个参数用来指定输出的文件。
fscanf(): 工作方式和scanf()差不多,区别在于第一个参数用来指定从哪个文件读。
rewind(fp): 返回fp所指的文件的开头处
char * fgets(char *, int, FILE *):
第一个参数是字符串的地址;
第二个参数指明了读入字符的最大数量,如果该值是n, 那么读入(n-1)个字符,或者读到遇到的第一个换行符位置;fgets()读到一个换行符,会将它存储在字符串中,这点与gets()不同,gets()会丢弃换行符;
第三个参数指明要读入的文件,如果从键盘读入,则以stdin作为参数;fputs(char *, FILE *) :
第一个参数是字符串的地址;
第二个参数指定目标文件;
3.1 一个简单的程序,在文件中添加单词
// addaword.c 和fileio.c一起编译
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void add_a_word(int argc, char * argv[])
{
FILE * fp;
char words[41];
if ((fp=fopen("C:\woody","a+"))==NULL) //以"a+"模式打开文件,如果文件不存在将新建,可读写,追加写
{
printf("Cannot open file \"C:\woody\" \n");
exit(EXIT_FAILURE);
}
printf("Enter words to add to the file, q to quit\n");
while(fscanf(stdin,"%40s",words)==1 && words[0]!='q')
fprintf(fp,"%s\n",words);
rewind(fp); /*文件指针回到文件头*/
puts("File content:");
while(fscanf(fp,"%s",words)==1)
puts(words);
puts("Done");
if (fclose(fp) !=0)
fprintf(stderr,"Error in closing file\n");
}
4. 随机访问: fseek()和ftell()
4.1 一个简单的例子,倒序输出文件内容
#include <stdio.h>
#include <stdlib.h>
#include "fileio.h"
void reverse_file_content(int argc, char * argv[])
{
extern FILE * fp; //引用式声明,告诉编译器,fp在别处定义
extern unsigned long count;
char file[SLEN];
char ch;
long last;
puts("Please enter the name of the file to be processed.");
scanf("%80s",file);
if ((fp=fopen(file,"rb"))==NULL)
{
printf("cannot open file %s \n", file);
exit(EXIT_FAILURE);
}
fseek(fp,0L, SEEK_END); /*定位到文件末尾*/
last = ftell(fp); /*ftell()返回long值,表示文件中的当前位置*/
for (count=1; count<=last; count++)
{
fseek(fp, -count, SEEK_END); /*回退*/
ch = getc(fp);
if(ch != CNTL_Z && ch != '\r') /*由于采用"rb"模式打开文件,即用二进制模式,为了兼容MS-DOS文件,略过MS-DOS文件结束标志符CNTL_Z和'\r'字符*/
putchar(ch);
}
putchar('\n');
if (fclose(fp) != 0)
fprintf(stderr, "Error in closing file %s\n", argv[1]);
}
关于这个例子,我们要讨论三个问题:fseek()和ftell()函数的工作原理、如何使用二进制流、如何让程序可移植。
4.2 fseek()和ftell()的工作原理
fseek()的第一个参数是FILE指针,指向待查找的文件,fopen()应该已打开此文件。
fseek()的第二个参数是偏移量(offset)。该参数表示从起始点开始要移动的距离(参见下表列出的文件的起始点模式), 该参数必须是一个long类型的值,可以为正(前移)、负(后移)或0(保持不动)。
fseek()的第三个参数是模式,该参数确定起始点。根据ANSI标准,在stdio.h中规定了几个表示模式的明示常量(manifest constant)。如下表所示:
模式 | 偏移量的起始点 |
---|---|
SEEK_SET | 文件开始处 |
SEEK_CUR | 当前位置 |
SEEK_END | 文件末尾 |
旧的实现可能缺少这些定义,可以使用0L、1L、2L分别表示这3种模式。
下面是调用fseek()函数的一些示例:
fseek(fp, 0L, SEEK_SET); /*定位至文件开始处*/
fseek(fp, 10L, SEEK_SET); /*定位至文件中的第10个字节*/
fseek(fp, 2L, SEEK_CUR); /*从文件当前文件前移2个字节*/
fseek(fp, 0L, SEEK_END); /*定位至文件末尾*/
fseek(fp, -10L, SEEK_END); /*从文件结尾处回退10个字节*/
如果一切正常,fseek()返回值为0,如果出现错误(如试图移动的距离超过文件的范围),其返回值为-1。
ftell()函数的返回类型为long,它返回的是参数指向文件的当前位置的距文件开始处的字节数。ANSI把它定义在stdio.h中。在最初实现的UNIX中,ftell()通过返回距文件开始处的字节数来确定文件的位置。文件的第1个字节到文件开始处的距离是0,以此类推。ANSI规定,该定义适合于以二进制模式打开的文件,以文件模式打开的文件情况不同。所以上面的例子,我们以"rb"模式打开文件。
下面的语句:fseek(fp, 0L, SEEK_END);
把当前位置设置为距离文件末尾0字节偏移量的位置,也就是设置为文件末尾。
下一条语句:last = ftell(fp);
把从文件开始处到文件末尾处的字节数赋给last。
然后是一个for循环:
for (count =1L; count <= last; count++)
{
fseek(fp, -count, SEEK_END);
ch = getc(fp);
}
实现了从文件末尾的第一个字符(即,文件的最后一个字符)倒序打印到文件第一个字符。
4.2 二进制模式和文本模式
上述例子在Unix和MS-DOS环境下都可以运行。许多MS-DOS编辑器都以Ctrl+Z标记文本文件的结尾,以文本模式打开这样的文件,C将Ctrl+Z视作文件结尾标记的字符。但以二进制模式打开相同,Ctrl+Z视作文件中的一个字符,所以上述例子过滤了Ctrl+Z字符的打印。
此外,二进制模式和文本模式的另一个不同之处是:MS-DOS用\r\n组合表示文本文件换行,以文本模式打开相同的文件,C把"\r\n"看成'\n',但是以二进制模式打开该文件,程序能看到这两个字符。因此上述例子过滤了'\r'字符的打印。
ftell()函数在文本模式和二进制模式中的工作方式不同,许多系统的文本文件格式与Unix的模型有很大不同,导致从文件开头统计的字节数成为一个毫无意义的值。ANSI C规定,对于文本文件,ftell()返回的值可以作为fseek()的第二个参数。对于MS-DOS,ftell()返回的值把"\r\n"当成一个字节计数。
4.3 fgetpos()和fsetpos()函数
fseek()和ftell()函数的潜在问题是它们都把文件大小限制在long类型能表示的范围内。ANSI C新增两个处理较大文件的新定位函数:fgetpos()和fsetpos()。这两个函数不使用long类型表示位置,而用一种新类型:fpos_t(文件定位类型)。fpos_t类型不是基本类型,它根据其他类型来定义。fpos_t类型的变量或数据对象可以在文件中指定一个位置,它不能是数组类型。实现可以提供一个满足特殊平台要求的类型,例如,fpos_t可以实现为结构。
fgetpos()函数原型如下:
int fgetpos(FILE * restrict stream, fpos_t * restrict pos);
调用该函数,它把fpos_t类型的值放在pos指向的位置上,该值描述了文件中的当前位置距文件开头的字节数。如果成功,fgetpos()函数返回0;如果失败,返回非0。
fsetpos()函数的原型如下:
int fsetpos(FILE * stream, const fpos_t * pos);
调用该函数时,使用pos指向位置上的fpos_t类型值来表示文件指针的偏移量。如果成功,fsetpos()函数返回0;如果失败,返回非0。fpos_t类型的值应通过之前调用fgetpos()函数获得。
5. 标准I/O的机理
通常,使用标准I/O的第一步是调用fopen()打开文件。fopen()函数不仅打开一个文件,还创建一个缓冲区(读写模式下会创建两个)以及一个包含文件和缓冲区数据的结构。另外,fopen()返回一个指向该结构的指针。假设把该指针赋给一个指针变量fp,我们说fopen()函数"打开一个流"。如果以文本模式打开该文件,就获得一个文本流;如果以二进制模式打开该文件,就获得一个二进制流。
该结构通常包含一个指定流中当前位置的文件位置指示器。除此以外,还包括错误和文件结尾的指示器、一个指向缓冲区开始处的指针、一个文件标识符和一个计数(统计实际拷贝进缓冲区的字节数)。
使用标准I/O的第2步是调用一个定义在stdio.h中的输入函数,如fscanf()、getc()、fgets()等。一调用这些函数,文件中的缓冲大小数据块就被拷贝到缓冲区中。除了填充缓冲区外,还要设置fp所指向结构中的值。尤其要设置流中的当前位置和拷贝进缓冲区中的字节数。
在初始化结构和缓冲区后,输入函数按要求从缓冲区中读取数据。stdio.h中所有输入函数都使用相同的缓冲区,所以调用任何一个函数都将从上一次函数停止调用的位置开始。
当输入函数发现已读完缓冲区中所有字符时,会请求把下一个缓冲大小的数据块从文件拷贝到该缓冲区。在读完缓冲区最后一个字符后,把结尾指示器设置为真。于是,下一次调用的输入函数将返回EOF。
输出函数以类似的方式将数据写入缓冲区。当缓冲区被填满,数据将被拷贝至文件中。
6. 其他标准I/O函数
6.1 int ungetc(int c, FILE * fp)函数
int ungetc()函数将c指定的字符放回到输入流中。如果把一个字符放回到输入流,下次调用标准输入函数时将读取该字符。
6.2 int fflush(FILE * fp)函数
调用fflush()函数引起输出缓冲区中所有未写入数据被发送到fp指定的输出文件。这个过程称为刷新缓冲区。如果fp是空指针,所有输出缓冲区都被刷新。在输入流使用fflush()函数的效果是未定的。只要最新一次操作不是输入操作,就可以用该函数来更新流(任何读写模式)。
6.3 int setvbuf(FILE * restrict fp, char * restrict buf, int mode, size_t size)函数
setvbuf()函数创建了一个供标准I/O函数替换使用的缓冲区。在打开文件后且未对流进行其他操作之前,调用该函数。指针fp识别待处理的流,buf指向待使用的存储区。如果buf的值不是NULL,则必须创建一个缓冲区。例如,声明一个内含1024字符的数组,并传递该数组的值。然而,如果把NULL作为buf的值,该函数会为自己分配一个缓冲区。变量size告诉setvbuf()数组的大小。mode的选择如下:_IOFBF表示完全缓冲(在缓冲区满时刷新); _IOLBF表示行缓冲(在缓冲区满时或写入一个换行符时); _IONBF表示无缓冲。如果操作成功,函数返回0, 否则返回一个非0值。
假设一个程序要存储一种数据对象,每个数据对象的大小是3000字节,那么可以使用setvbuf()函数创建一个缓冲区,其大小是该数据对象大小的倍数。
6.4 二进制I/O: fread()和fwrite()
前面介绍的fprintf()将数值转换为字符数据存储,这种转换会丢失精度,例如:
double num = 1./3.;
fprintf(fp, "%f", num); /*将num存储为8个字符:0.333333*/
前面介绍的标准I/O函数也都是面向文本的,用于处理字符和字符串,那么为了保证数值在存储前后的一致性,最精确的做法是使用和计算机相同的位组合来存储。因此double类型的值应存储在double大小的单元中,也即用二进制形式存储数据。对于标准I/O,fread()和fwrite()函数用于以二进制形式处理数据。
6.5 size_t fwrite()函数
fwrite()函数的原型如下:
size_t fwrite(const void * restrict ptr, size_t size, size_t nmemb, FILE * restrict fp)
fwrite()函数将二进制数据写入文件。size_t是sizeof运算符的返回类型,通常是unsigned int。指针ptr是待写入数据块的地址。size是待写入数据块的大小(以字节为单位),nmemb表示待写入数据块的数量。和其它函数一样,fp指定待写入的文件。例如,要保存一个大小为256字节的数据对象(如数组),可以这么做:
char buffer[256];
fwrite(buffer, 256, 1, fp);
以上调用把一块256字节的数据从buffer写入文件。另举一例,要保存一个内含10个double类型值的数组,可以这么做:
double earnings[10];
fwrite(earnings, sizeof(double), 10, fp);
以上调用将earnings数组的数据写入文件,数据被分为10块,每块都是double的大小。
fwrite()函数返回成功写入项的数量,正常情况下,该返回值就是nmemb,但如果出现写入错误,返回值会比nmemb小。
6.6 size_t fread()函数
size_t fread()函数的原型如下:
size_t fread(void * restrict ptr, size_t size, size_t nmemb, FILE * restrict fp)
fread()函数接受的参数和fwrite()函数相同。在fread()函数中,ptr是待读取文件数据在内存中的地址,fp指定待读取的文件。该函数用于读取被fwrite()写入文件中的数据。例如,要恢复上例中保存的内含10个double类型值的数组,可以这样做:
double earnings[10];
fread(earnings, sizeof(double), 10, fp);
该调用将10个double大小的值拷贝进earnings数组中。
fread()函数返回成功读取项的数量。正常情况下,返回值就是nmemb,但如果出现读取错误或读到文件末尾,该返回值就会比nmemb小。
6.7 int feof(FILE *fp)和int ferror(FILE *fp)函数
如果标准输入函数返回EOF,则通常表示函数已到达文件末尾。然而,如果出现读取错误,也会返回EOF。feof()和ferror()用于区别这两种情况。当上一次输入调用检测到文件末尾时,feof()返回非零值,否则返回0。当读或写出现错误,ferror()函数返回一个非零值,否则返回0。
6.8 一个程序示例
// append_file.c 和fileio.c一起编译
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include "fileio.h"
#define BUFSIZE 4096
char * s_gets(char *, int);
void append(FILE * source , FILE * dest);
int append_file(void)
{
FILE * fa, *fs; //fa指向目标文件,fs指向源文件
int files = 0; //附加的文件数量
char file_app[SLEN]; //目标文件名
char file_src[SLEN]; //源文件名
int ch;
puts("Enter name of destination file:");
while (getchar() != '\n')
continue;
s_gets(file_app, SLEN);
if ( (fa=fopen(file_app,"a+")) == NULL)
{
fprintf(stderr, "Can't open dest file %s\n", file_app);
exit(EXIT_FAILURE);
}
if (setvbuf(fa, NULL, _IOFBF, BUFSIZE)!= 0) /*为目标文件创建一个4096字节大小的缓冲区*/
{
fputs("Cannot create output buffer\n", stderr);
exit(EXIT_FAILURE);
}
puts("Enter name of first source file(empty line to quit):");
while( NULL != s_gets(file_src, SLEN) && file_src[0] != '\0')
{
if (strcmp(file_src, file_app) == 0)
fputs("Cannot open file to itself\n", stderr);
else if ((fs=fopen(file_src, "r")) == NULL)
fprintf(stderr, "Cannot open file %s\n", file_src);
else
{
if ( setvbuf(fs, NULL, _IOFBF, BUFSIZE)!= 0 )
{
fputs("cannot open create input buffer\n", stderr);
continue;
}
append(fs, fa);
if (ferror(fs) != 0)
fprintf(stderr, "Error in reading file %s\n", file_src);
if (ferror(fa) !=0)
fprintf(stderr, "Error in writing file %s\n", file_app);
fclose(fs);
files++;
printf("File %s appended.\n", file_src);
puts("Next file (empty line to quit):");
}
}
printf("Done appending. %d files appended. \n", files);
rewind(fa);
printf("%s contents:\n", file_app);
while((ch = getc(fa))!= EOF)
putchar(ch);
puts("Done displaying");
fclose(fa);
return 0;
}
// 强化fgets(), 去除fgets()函数读入字符数组中的换行符,并用'\0'替换; 如果输入行超长(超过n-1),丢弃输入缓冲区多余的字符.
char * s_gets(char *st, int n)
{
char * ret_val;
char * find;
int i = 0;
ret_val = fgets(st, n, stdin);
if (ret_val)
{
find = strchr(st, '\n');
if (find)
* find = '\0';
else
while (getchar() != '\n')
continue;
}
return ret_val;
// char * ret_val;
//
// int i = 0;
// ret_val = fgets(st, n, stdin);
// if (ret_val)
// {
// while (st[i] != '\n' && st[i] != '\0')
// i++;
// if (st[i] == '\n')
// st[i] = '\0';
// else
// while (getchar() != '\n')
// continue;
// }
// return ret_val;
}
void append(FILE * source , FILE * dest)
{
size_t bytes;
static char temp[BUFSIZE]; //只分配一次,静态存储期,块作用域
while((bytes=fread(temp, sizeof(char), BUFSIZE, source)) > 0)
fwrite(temp, sizeof(char), bytes, dest);
}
6.9 用二进制I/O进行随机访问
随机访问时用二进制I/O写入二进制文件最常用的方式,下面的程序创建了一个存储double类型数字的文件,然后让用户访问这些内容。
/*randbin.c 用二进制I/O进行随机访问*/
#include <stdio.h>
#include <stdlib.h>
#define ARSIZE 1000
void rand_bin(void)
{
double members[ARSIZE];
const char * file = "numbers.dat";
int i;
long pos;
double value;
FILE * iofile;
/*创建一组double类型的值*/
for (i = 0; i< 1000; i++)
members[i] = 100.0 * i + 1.0/ (i+1);
//尝试以二进制写模式打开文件
if((iofile = fopen(file, "wb")) == NULL)
{
fprintf(stderr, "cannot open file %s for output\n", file);
exit(EXIT_FAILURE);
}
//以二进制格式把数组写入文件
fwrite(members, sizeof(double), ARSIZE, iofile );
fclose(iofile);
//尝试以二进制读模式打开文件
if ((iofile=fopen(file, "rb")) == NULL)
{
fprintf(stderr, "cannot open file %s for random access\n", file);
exit(EXIT_FAILURE);
}
//从文件中读取选定的内容(指定偏移量)
printf("Enter an index in the range 0-%d.\n", ARSIZE-1);
while(scanf("%d", &i )==1 && i >=0 && i<ARSIZE)
{
pos = (long) i * sizeof(double); //计算偏移量
fseek(iofile, pos, SEEK_SET);
fread(&value, sizeof(double), 1, iofile);
printf("The value there is %f.\n", value);
printf("Next index (out of range to quit):\n");
}
fclose(iofile);
puts("bye");
}