《C Primer Plus》学习笔记—第13章

《C Primer Plus》学习笔记

第13章 文件输入/输出

1.与文件进行通信

有时,需要程序从文件中读取信息或把信息写入文件。这种程序与文件交互的形式就是文件重定向(第8章介绍过)。这种方法很简单,但是有一定限制。例如,假设要编写一个交互程序,询问用户书名并把完整的书名列表保存在文件中。如果使用重定向,应该类似于:
books > bklist
用户的输入被重定向到bklist中。这样做不仅会把不符合要求的文本写入bklist,而且用户也看不到要回答什么问题。

C提供了更强大的文件通信方法,可以在程序中打开文件,然后使用特殊的I/O函数读取文件中的信息或把信息写入文件。

1.文件是什么

文件(file)通常是在磁盘或固态硬盘上的一段已命名的存储区。对我们而言,stdio.h就是一个文件的名称,该文件中包含一些有用的信息。然而,对操作系统而言,文件更复杂一些。例如,大型文件会被分开储存,或者包含一些额外的数据,方便操作系统确定文件的种类。然而,这都是操作系统所关心的,程序员关心的是C程序如何处理文件(除非你正在编写操作系统)。

C把文件看作是一系列连续的字节,每个字节都能被单独读取。这与UNIX环境中(C的发源地)的文件结构相对应。由于其他环境中可能无法完全对应这个模型,C提供两种文件模式:文本模式和二进制模式。

2.文本模式和二进制模式

首先,要区分文本内容和二进制内容文本文件格式和二进制文件格式,以及文件的文本模式和二进制模式

所有文件的内容都以二进制形式(0或1)储存。但是,如果文件最初使用二进制编码的字符(例如,ASCII或Unicode)表示文本(就像C字符串那样),该文件就是文本文件,其中包含文本内容。如果文件中的二进制值代表机器语言代码或数值数据(使用相同的内部表示,假设,用于long 或double类型的值)或图片或音乐编码,该文件就是二进制文件,其中包含二进制内容

UNIX用同一种文件格式处理文本文件和二进制文件的内容。鉴于C是作为开发UNIX的工具而创建的,C和UNIX在文本中都使用\n (换行符)表示换行。UNIX目录中有一个统计文件大小的计数,程序可使用该计数确定是否读到文件结尾。然而,其他系统在此之前已经有其他方法处理文件,专门用于保存文本。也就是说,其他系统已经有一种与UNIX模型不同的格式处理文本文件。例如,以前的OS X Macintosh文件用\r(回车符)表示新的一行。早期的MS-DOS文件用\r\n组合表示新的一行,用嵌入的Ctrl+Z字符表示文件结尾,即使实际文件用添加空字符的方法使其总大小是256的倍数(在Windows中,Notepad仍然生成MS-DOS格式的文本文件,但是新的编辑器可能使用类UNIX格式居多)。其他系统可能保持文本文件中的每一行长度相同,如有必要,用空字符填充每一行, 使其长度保持一致。或者,系统可能在每行的开始标出每行的长度。

为了规范文本文件的处理,C提供两种访问文件的途径:二进制模式和文本模式。在二进制模式中,程序可以访问文件的每个字节。而在文本模式中,程序所见的内容和文件的实际内容不同。

程序以文本模式读取文件时,把本地环境表示的行末尾或文件结尾映射为C模式。例如,C程序在旧式Macintosh中以文本模式读取文件时,把文件中的\r转换成\n;以文本模式写入文件时,把\n转换成\r。或者,C文本模式程序在MS-DOS平台读取文件时,把\r\n转换成\n;写入文件时,把\n转换成\r\n。在其他环境中编写的文本模式程序也会做类似的转换。

除了以文本模式读写文本文件,还能以二进制模式读写文本文件。如果读写一个旧式MS-DOS文本文件,程序会看到文件中的\r和\n字符,不会发生映射(图13.1演示了一些文本)。如果要编写旧式Mac格式、MS-DOS格式或UNIX/Linux格式的文件模式程序,应该使用二进制模式,这样程序才能确定实际的文件内容并执行相应的动作。

C提供了二进制模式和文本模式,但是这两种模式的实现可以相同。前面提到过,因为UNIX使用一种文件格式,这两种模式对于UNIX实现而言完全相同。Linux也是如此。

二进制模式和文本模式

3.I/O的级别

除了选择文件的模式,大多数情况下,还可以选择I/O的两个级别(即处理文件访问的两个级别)。底层I/O(low-level I/O)使用操作系统提供的基本I/O服务。标准高级I/O(standard high-level I/O)使用C库.的标准包和stdio.h头文件定义。因为无法保证所有的操作系统都使用相同的底层I/O模型,C标准只支持标准I/O包。有些实现会提供底层库,但是C标准建立了可移植的I/O模型,接下来主要讨论这些I/O。

4.标准文件

C程序会自动打开3个文件,它们被称为标准输入(standard input)、标准输出(standard output)和标准错误输出(standard error output)。在默认情况下,标准输入是系统的普通输入设备,通常为键盘;标准输出和标准错误输出是系统的普通输出设备,通常为显示屏

通常,标准输入为程序提供输入,它是getchar()和scanf()使用的文件。程序通常输出到标准输出,它是putchar()、puts()和printf()使用的文件。第8章提到的重定向把其他文件视为标准输入或标准输出。标准错误输出提供了一个逻辑上不同的地方来发送错误消息。例如,如果使用重定向把输出发送给文件而不是屏幕,那么发送至标准错误输出的内容仍然会被发送到屏幕上。这样很好,因为如果把错误消息发送至文件,就只能打开文件才能看到。

2.标准I/O

与底层I/O相比,标准I/O包除了可移植以外还有两个好处。第一,标准I/O有许多专门的函数简化了处理不同I/O的问题。例如,printf()把不同形式的数据转换成与终端相适应的字符串输出。第二,输入和输出都是缓冲的。也就是说,一次转移一大块信息而不是一字节信息(通常至少512字节)。

例如,当程序读取文件时,一块数据被拷贝到缓冲区(一块中介存储区域)。这种缓冲极大地提高了数据传输速率。程序可以检查缓冲区中的字节。缓冲在后台处理,所以让人有逐字符访问的错觉(如果使用底层I/O,要自己完成大部分工作)。程序count.c演示了如何用标准I/O读取文件和统计文件中的字符数。该程序使用命令行参数,如果是Windows用户,在编译后必须在命令提示窗口运行该程序;或者,如第11章所述,如果在IDE中运行该程序,可以使用Xcode的Product菜单提供命令行参数。或者也可以用puts()和fgets()函数替换命令行参数来获得文件名。

1.程序count.c
/* count.c 使用标准I/O */
#include <stdio.h>
#include <stdlib.h> // exit()函数原型 

int main(int argc, char *argv[])
{
   
    int ch;         // 读取文件时,存储每个字符的地方 
    FILE *fp;       // 文件指针 
    unsigned long count = 0;
    if (argc != 2)
    {
   
        printf("Usage: %s filename\n", argv[0]);
        exit(EXIT_FAILURE);
    }
    if ((fp = fopen(argv[1], "r")) == NULL)//打开文件,r表示读模式 
    {
   
        printf("Can't open %s\n", argv[1]);
        exit(EXIT_FAILURE);
    }
    while ((ch = getc(fp)) != EOF)
    {
   
        putc(ch,stdout);  //与putchar(ch)相同 
        count++;
    }
    fclose(fp);//关闭文件 
    printf("File %s has %lu characters\n", argv[1], count);
    
    return 0;
}
1.检查命令行参数

首先,程序count.c中的程序检查argc的值,查看是否有命令行参数。如果没有,程序将打印一条消息并退出程序。字符串**argv[0]**是该程序的名称。显式使用argv[0]而不是程序名,错误消息的描述会随可执行文件名的改变而自动改变。这一特性在像UNIX这种允许单个文件具有多个文件名的环境中也很方便。但是,一些操作系统可能不识别argv[0],所以这种用法并非完全可移植。

exit()函数关闭所有打开的文件并结束程序。exit()的参数被传递给一些操作系统,包括UNIX、Linux、Windows和MS-DOS,以供其他程序使用。通常的惯例是:正常结束的程序传递0,异常结束的程序传递非零值。不同的退出值可用于区分程序失败的不同原因,这也是UNIX和DOS编程的通常做法。但是,并不是所有的操作系统都能识别相同范围内的返回值。因此,C标准规定了一个最小的限制范围。尤其是,标准要求0或宏EXIT_SUCCESS用于表明成功结束程序,宏EXIT_FAILURE用于表明结束程序失败。这些宏和exit()原型都位于stdlib.h头文件中。
根据ANSI C的规定,在最初调用的main()中使用return与调用exit()的效果相同。因此,在main(),下面的语句:

return 0;

和下面这条语句的作用相同:

exit(0);

但是要注意,我们说的是“最初的调用”。如果main()在一个递归程序中,exit()仍然会终止程序,但是return只会把控制权交给上一级递归,直至最初的一级。然后return结束程序。return和exit()的另一个区别是,即使在其他函数中(除main()以外)调用exit()也能结束整个程序

2.fopen()函数

继续分析程序count.c,该程序使用fopen()函数打开文件。该函数声明在stdio.h中。它的第1个参数是待打开文件的名称,更确切地说是一个包含该文件名的字符串地址第2个参数是一个字符串,指定待打开文件的模式。表13.1列出了C库提供的一些模式。

fopen()各种模式

像UNIX和Linux这样只有一种文件类型的系统,带b字母的模式和不带b字母的模式相同

新的C11新增了带x字母的写模式,与以前的写模式相比具有更多特性。第一,如果以传统的一种写模式打开一个现有文件,fopen()会把文件的长度截为0,这样就丢失了该文件的内容。但是使用带x字母的写模式,即使fopen()操作失败,原文件的内容也不会被删除。第二,如果环境允许,x模式的独占特性使得其他程序或线程无法访问正在被打开的文件。

警告
如果使用任何一种"w"模式(不带x字母)打开一个现有文件,该文件的内容会被删除,以便程序在一个空白文件中开始操作。然而,如果使用带x字母的任何一种模式,将无法打开一个现有文件。程序成功打开文件后,fopen()将返回文件指针(file pointer),其他I/O函数可以使用这个指针指定该文件。文件指针(该例中是fp)的类型是指向FILE的指针,FILE是一个定义在stdio.h中的派生类型。文件指针fp并不指向实际的文件,它指向一个包含文件信息的数据对象,其中包含操作文件的I/O函数所用的缓冲区信息。因为标准库中的I/O函数使用缓冲区,所以它们不仅要知道缓冲区的位置,还要知道缓冲区被填充的程度以及操作哪一个文件。标准I/O函数根据这些信息在必要时决定再次填充或清空缓冲区。fp指向的数据对象包含了这些信息(该数据对象是一个C结构,将在第14章中介绍)。

3.getc()和putc()函数

getc()和putc()函数与getchar()和putchar()函数类似。所不同的是,要告诉getc()和putc()函数使用哪一个文件。下面这条语句的意思是“从标准输入中获取一个字符”:

ch = getchar();

然而,下面这条语句的意思是“从fp指定的文件中获取一个字符”:

ch = getc(fp);

与此类似,下面语句的意思是“把字符ch放入FILE指针fpout指定的文件中”:

putc(ch,fpout);

在putc()函数的参数列表中,第1个参数是待写入的字符,第2个参数是文件指针

程序count.c把stdout作为putc()的第2个参数。stdout作为与标准输出相关联的文件指针,定义在stdio.h中,所以putc(ch,stdout)与putchar(ch)的作用相同。实际上,putchar()函数一般通过putc()来定义。与此类似,getchar()也通过使用标准输入的getc()来定义。

4.文件结尾

从文件中读取数据的程序在读到文件结尾时要停止。如何告诉程序已经读到文件结尾?如果getc()函数在读取一个字符时发现是文件结尾,它将返回一个特殊值EOF。所以C程序只有在读到超过文件末尾时才会发现文件的结尾(一些其他语言用一个特殊的函数在读取之前测试文件结尾,C语言不同)。
为了避免读到空文件,应该使用入口条件循环(不是do while循环)进行文件输入。鉴于getc()(和其他C输入函数)的设计,程序应该在进入循环体之前先尝试读取。如下面设计所示:

//设计范例#1
int ch;//用int类型的变量储存EOF
FILE * fp;
fp = fopen("wacky.txt", "r");
ch = getc(fp);//获取初始输入
while(ch != EOF)
{
   
    putchar(ch); //处理输入
	ch = getc(fp);//获取下一个输入
}

以上代码可简化为:

//设计范例#2
int ch;
FILE * fp;
fp = fopen("wacky.txt", "r");
while (( ch = getc(fp)) != EOF)
{
   
    putchar(ch); //处理输入
}

由于ch = getc(fp)是while测试条件的一部分,所以程序在进入循环体之前就读取了文件

其他输入函数也会用到这种处理方案,它们在读到文件结尾时也会返回一个错误信号(EOF或NULL指针)

5.fclose()函数

fclose(fp)函数关闭fp指定的文件,必要时刷新缓冲区。对于较正式的程序,应该检查是否成功关闭文件。如果成功关闭,fclose()函数返回0,否则返回EOF:

if (fclose(fp) != 0)
	printf("Error in closing file &s \n", argv[1]);

如果磁盘已满、移动硬盘被移除或出现I/O错误,都会导致调用fclose()函数失败

6.指向标准文件的指针

stdio.h头文件把3个文件指针与3个标准文件相关联,C程序会自动打开这3个标准文件。如表13.2所示:

标准文件

这些文件指针都是指向FILE的指针,所以它们可用作标准I/O函数的参数,如fclose(fp)中的fp。

7.count.c输出示例

C-Free编译运行后输出:

Usage: D:\桌面\C\第十三章\count.exe filename

在count.c的目录下创建一个1.txt文件,其中随意存储一些信息,在该目录下运行cmd,输入count.exe 1.txt,得到结果:

D:\桌面\C\第十三章>count 1.txt
hello
open
File 1.txt has 11 characters

open后有个换行符,所以文件一共有11个字符。

3.一个简单的文件压缩程序

下面的程序示例把一个文件中选定的数据拷贝到另一个文件中。该程序同时打开了两个文件,以"r"模式打开一个,以"w"模式打开另一个。程序reducto.c以保留每3个字符中的第1个字符的方式压缩第1个文件的内容。最后,把压缩后的文本存入第2个文件。第2个文件的名称是第1个文件名加上.red后缀(此处的red代表reduced)。使用命令行参数,同时打开多个文件,以及在原文件名后面加上后缀,都是相当有用的技巧。这种压缩方式有限,但是也有它的用途(很容易把该程序改成用标准I/O而不是命令行参数提供文件名)。

1.程序reducto.c
// reducto.c -- 把文件压缩成原来的1/3 
#include <stdio.h>
#include <stdlib.h>    // exit()原型 
#include <string.h>    // strcpy(), strcat()原型 
#define LEN 40

int main(int argc, char *argv[])
{
   
    FILE  *in, *out;   // 声明两个指向FILE的指针 
    int ch;
    char name[LEN];    // 存储输出文件名 
    int count = 0;
    
    // 检查命令行参数 
    if (argc < 2)
    {
   
        fprintf(stderr, "Usage: %s filename\n", argv[0]);
        exit(EXIT_FAILURE);
    }
    // 设置输入 eddy 
    if ((in = fopen(argv[1], "r")) == NULL)
    {
   
        fprintf(stderr, "I couldn't open the file \"%s\"\n",
                argv[1]);
        exit(EXIT_FAILURE);
    }
    // 设置输出 eddy.red 
    strncpy(name,argv[1], LEN - 5); // 拷贝文件名 
    name[LEN - 5] = '\0';
    strcat(name,".red");            // 在文件名后面添加.add 
    if ((out = fopen(name, "w")) == NULL)
    {
                          // 写模式打开文件 
        fprintf(stderr,"Can't create output file.\n");
        exit(3);
    }
    // 拷贝数据
    while ((ch = getc(in)) != EOF)
        if (count++ % 3 == 0)
            putc(ch, out);  //打印3个字符中的第1个字符 
    // 收尾工作,关闭文件 
    if (fclose(in) != 0 || fclose(out) != 0)
        fprintf(stderr,"Error in closing files\n");
    
    return 0;
}

在C-Free中编译运行,输出如下:

Usage: D:\桌面\C\第十三章\reducto.exe filename

假设可执行文件名是reducto,待读取的文件名为eddy(可以使用Notepad++打开该文件,这两个文件在同一目录下),该文件中包含下面一行内容:
So even Eddy came oven ready.
在目录下运行cmd,命令如下:
reducto eddy
待写入的文件名为eddy.red(同一目录)。该程序把输出显示在eddy.red中,而不是屏幕上。打开eddy.red, 内容如下:
Send money

fprintf()和printf()类似,但是fprintf()第1个参数必须是一个文件指针。程序中使用stderr指针把错误消息发送至标准错误,C标准通常都这么做。

为了构造新的输出文件名,该程序使用strncpy()把名称eddy拷贝到数组name中。参数LEN-5为.red后缀和末尾的空字符预留了空间。如果argv[2]字符串比LEN-5长,就拷贝不了空字符。出现这种情况时,程序会添加空字符。调用strncpy()后,name中的第1个空字符在调用strcat()函数时,被.red的.覆盖,生成了eddy.red。程序中还检查了是否成功打开名为eddy.red的文件。这个步骤在一些环境中相当重要,因为像strange.c.red这样的文件名可能是无效的。例如,在传统的DOS环境中,不能在后缀名后面添加后缀名(MS-DOS使用的方法是用. red替换现有后缀名,所以strange.c将变成strange.red。例如,可以用strchr()函数定位(如果有的话),然后只拷贝点前面的部分即可)。

该程序同时打开了两个文件,所以要声明两个FIFL指针。注意,程序都是单独打开和关闭每个文件。同时打开的文件数量是有限的,这个限制取决于系统和实现,范围一般是10~20。相同的文件指针可以处理不同的文件,前提是这些文件不需要同时打开

4.文件I/O:fprintf()、fscanf()、fgets()和fputs()

前面章节介绍的I/O函数都类似于文件I/O函数。它们的主要区别是,文件I/O函数要用FILE指针指定待处理的文件。与getc()、putc()类似,这些函数都要求用指向FILE的指针(如,stdout) 指定一个文件,或者使用fopen()的返回值。

1.fprintf()和fscanf()函数

文件I/O函数fprintf()和fscanf()函数的工作方式与printf()和scanf()类似,区别在于前者需要用第1个参数指定待处理的文件。在前面用过fprintf()。程序addaword.c演示了这两个文件I/O函数和rewind()函数的用法。

1.程序addaword.c
/* addaword.c -- 使用fprintf(),fscanf(),和rewind() */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX 41

int main(void)
{
   
    FILE *fp;
    char words[MAX];
    
    if ((fp = fopen("wordy", "a+")) == NULL)
    {
   
        fprintf(stdout,"Can't open \"wordy\" file.\n");
        exit(EXIT_FAILURE);
    }
    
    puts("Enter words to add to the file; press the #");
    puts("key at the beginning of a line to terminate.");
    while ((fscanf(stdin,"%40s", words) == 1)  && (words[0] != '#'))
        fprintf(fp, "%s\n", words);
    
    puts("File contents:");
    rewind(fp);           /* 返回到文件开始处 */
    while (fscanf(fp,"%s",words) == 1)
        puts(words);
    puts("Done!");
    if (fclose(fp) != 0)
        fprintf(stderr,"Error closing file\n");
    
    return 0;
}

该程序可以在文件中添加单词。使用"a+"模式, 程序可以对文件进行读写操作。首次使用该程序,它将在addaword.c所在的目录下创建wordy文件,以便把单词存入其中。

第一次编译运行输出示例:

Enter words to add to the file; press the #
key at the beginning of a line to terminate.
hello who
are you
#
File contents:
hello
who
are
you
Done!

随后再使用该程序,可以在wordy文件后面添加单词。虽然"a+"模式只允许在文件末尾添加内容,但是该模式下可以读整个文件。rewind()函数让程序回到文件开始处,方便while循环打印整个文件的内容。注意,rewind()接受一个文件指针作为参数。第二次编译运行输出示例:

Enter words to add to the file; press the #
key at the beginning of a line to terminate.
i am fine
#
File contents:
hello
who
are
you
i
am
fine
Done!
2.fgets()和fputs()函数

第11章时介绍过fgets()函数。它的第1个参数和gets()函数元一样,也是表示储存输入位置的地址(char *类型);第2个参数是一个整数,表示待输入字符串的大小(字符串大小和字符串长度不同。前者指该字符串占用多少空间,后者指该字符串的字符个数),最后一个参数是文件指针,指定待读取的文件。下面是一个调用该函数的例子:

cfgets(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()就不会再添加换行符。如第11章的程序fgets2.c所示,即使输入行比STLEN长,这两个函数依然处理得很好。

5.随机访问: fseek()和ftell()

有了fseek()函数,便可把文件看作是数组,在fopen()打开的文件中直接移动到任意字节处。创建一个程序reverse.c演示fseek()和ftell()的用法。注意,fseek()有3个参数,返回int类型的值; ftell()函数返回一个long类型的值,表示文件中的当前位置

1.程序reverse.c
/* reverse.c -- 倒序显示文件的内容 */
#include <stdio.h>
#include <stdlib.h>
#define CNTL_Z '\032'   /* DOS文本文件中的文件标记结尾 */
#define SLEN 81
int main(void)
{
   
    char file[SLEN];
    char ch;
    FILE *fp;
    long count, last;
    
    puts("Enter the name of the file to be processed:");
    scanf("%80s", file);//2.txt 
    if ((fp = fopen(file,"rb")) == NULL)
    {
                                  /* 二进制只读模式 */
        printf("reverse can't open %s\n", file);
        exit(EXIT_FAILURE);
    }
    
    fseek(fp, 0L, SEEK_END);        /* 定位到文件末尾 */
    last = ftell(fp);
    for (count = 1L; count <= last; count++)
    {
   
        fseek(fp, -count, SEEK_END); /* 回退 */
        ch = getc(fp);
		if (ch != CNTL_Z && ch != '\r')  /* MS-DOS文件 */
            putchar(ch);
    }
    putchar('\n');
    fclose(fp);
    
    return 0;
}

输出示例:

Enter the name of the file to be processed:
2.txt
reverse can't open 2.txt

必须在目录下创建2.txt才行,因为二进制只读模式不会创建文件。创建2.txt,文件内容为:this is reverse.c。运行程序,输出示例:

Enter the name of the file to be processed:
2.txt
c.esrever si siht

该程序使用二进制模式,以便处理MS-DOS文本和UNIX文件。但是,在使用其他格式文本文件的环境中可能无法正常工作。

注意:
如果通过命令行环境运行该程序,待处理文件要和可执行文件在同一个目录(或文件夹)中。如果在IDE中运行该程序,具体查找方案序因实现而异。例如,默认情况下,Microsoft Visual Studio 2012在源代码所在的目录中查找,而Xcode4.6则在可执行文件所在的目录中查找。

2.fseek()和ftell()的工作原理

fseek()第1个参数是FILE指针,指向待查找的文件,fopen()应该已打开该文件。fseek()的第2个参数是偏移量(ffset)。 该参数表示从起始点开始要移动的距离(参见表13.3列出的起始点模式)。该参数必须是一个long类型的值,可以为正(前移)、负(后移)或0(保持不动)。fseek()的第3个参数是模式,该参数确定起始点。根据ANSI标准,在stdio.h头文件中规定了几个表示模式的明示常量(manifest constant),如表13.3所示。

文件的起始点

旧的实现可能缺少这些定义,可以使用数值0L、1L、2L分别表示这3种模式。L后缀表明其值是long类型。或者,实现可能把这些明示常量定义在别的头文件中。如果不确定,请查阅实现的使用手册或在线帮助。
下面是调用fseek()函数的一些示例,fp是一个文件指针:

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 C把它定义在stdio.h中。在最初实现的UNIX中,ftell()通过返回距文件开始处的字节数来确定文件的位置。文件的第1个字节到文件开始处的距离是0,以此类推。ANSIC规定,该定义适用于以二进制模式打开的文件,以文件模式打开文件的情况不同。这也是程序reverse.c以二进制模式打开文件的原因。

下面来分析程序reverse.c中的基本要素。首先,下面的语句:

fseek(fp, 0L, SEEK_END);

把当前位置设置为距文件末尾0字节偏移量。也就是说,该语句把当前位置设置在文件结尾。下一条语句:

last = ftell(fp);

把从文件开始处到文件结尾的字节数赋给last。
然后是一个for循环:

For (count = 1L; count <= last; count++)
{
   
    fseek(fp, -count, SEEK_END); /* go backward */
	ch = getc(fp);
}

第1轮迭代,把程序定位到文件结尾的第1个字符(即,文件的最后一个字符)。然后,程序打印该字符。下一轮迭代把程序定位到前一个字符,并打印该字符。重复这一过程直至到达文件的第1个字符,并打印。

3.二进制模式和文本模式

程序reverse.c在UNIX和MS-DOS环境下都可以运行。UNIX只有一种文件格式,所以不需要进行特殊的转换。然而MS-DOS要格外注意。许多MS-DOS编辑器都用Ctrl+Z标记文本文件的结尾
以文本模式打开这样的文件时,C能识别这个作为文件结尾标记的字符。但是,以二进制模式打开相同的文件时,Ctrl+Z字符被看作是文件中的一个字符,而实际的文件结尾符在该字符的后面。文件结尾符可能紧跟在Ctrl+Z字符后面,或者文件中可能用空字符填充,使该文件的大小是256的倍数。在DOS环境下不会打印空字符,程序reverse.c中就包含了防止打印Ctrl+Z字符的代码。

二进制模式和文本模式的另一个不同之处是: MS-DOS用\r\n组合表示文本文件换行。以文本模式打开相同的文件时,C程序把\r\n“看成”\n。但是,以二进制模式打开该文件时,程序能看见这两个字符。
因此,程序reverse.c中还包含了不打印\r的代码。通常,UNIX文本文件既没有Ctrl+Z,也没有\r,所以这部分代码不会影响大部分UNIX文本文件。

ftell()函数在文本模式和二进制模式中的工作方式不同。许多系统的文本文件格式与UNIX的模型有很大不同,导致从文件开始处统计的字节数成为一个毫无意义的值。ANSIC规定,对于文本模式,ftell()返回的值可以作为fseek()的第2个参数。对于MS-DOS,ftell()返回的值把\r\n当作一个字节计数。

4.可移植性

理论上,fseek()和ftell()应该符合UNIX模型。但是,不同系统存在着差异,有时确实无法做到与UNIX模型一致。因此,ANSI对这些函数降低了要求。下面是一些限制。
1.在二进制模式中,实现不必支持SEEK_END模式。因此无法保证程序reverse.c的可移植性。移植性更高的方法是逐字节读取整个文件直到文件末尾。C预处理器的条件编译指令(第16章介绍)提供了一种系统方法来处理这种情况。

2.在文本模式中,只有以下调用能保证其相应的行为。

fseek()示例

不过,许多常见的环境都支持更多的行为。

5.fgetpos()和fsetpos()函数

fseek()和ftell()潜在的问题是,它们都把文件大小限制在long类型能表示的范围内。也许20亿字节看起来相当大,但是随着存储设备的容量迅猛增长,文件也越来越大。鉴于此,ANSI C新增了两个处理较大文件的新定位函数: fgetpos()和fsetpos()。这两个函数不使用long类型的值表示位置,它们使用一种新类型: **fpos_t(**代表file position type, 文件定位类型)。fpos_t类型不是基本类型,它根据其他类型来定义。fpos_t类型的变量或数据对象可以在文件中指定一个位置,它不能是数组类型,除此之外,没有其他限制。实现可以提供一个满足特殊平台要求的类型,例如,fpos_t可以实现为结构。
ANSIC定义了如何使用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()获得。

6.标准I/O的机理

前面学习了标准I/O包的特性,本节研究一个典型的概念模型,分析标准I/O的工作原理

1.通常,使用标准I/O的第1步是调用fopen()打开文件(前面介绍过,C程序会自动打开3种标准文件。fopen()函数不仅打开一个文件,还创建了一个缓冲区(在读写模式下会创建两个缓冲区)以及一个包含文件和缓冲区数据的结构。另外,fopen()返回一个指向该结构的指针,以便其他函数知道如何找到该结构。假设把该指针赋给一个指针变量fp,就说fopen()函数“打开一个流”。如果以文本模式打开该文件,就获得一个文本流;如果以二进制模式打开该文件,就获得一个二进制流。
这个结构通常包含一个指定流中当前位置的文件位置指示器。除此之外,它还包含错误和文件结尾的指示器、一个指向缓冲区开始处的指针、一个文件标识符和一个计数(统计实际拷贝进缓冲区的字节数)。

2.我们主要考虑文件输入。通常,使用标准I/O的第2步是调用一个定义在stdio.h中的输入函数,如fscanf()、getc()或fgets()。 一调用这些函数,文件中的数据块就被拷贝到缓冲区中。缓冲区的大小因实现而异,一般是512字节或是它的倍数,如4096或16384(随着计算机硬盘容量越来越大,缓冲区的大小也越来越大)。最初调用函数,除了填充缓冲区外,还要设置fp所指向的结构中的值。尤其要设置流中的当前位置和拷贝进缓冲区的字节数。通常,当前位置从字节0开始。

3.在初始化结构和缓冲区后,输入函数按要求从缓冲区中读取数据。在它读取数据时,文件位置指示器被设置为指向刚读取字符的下一个字符。由于stdio.h系列的所有输入函数都使用相同的缓冲区,所以调用任何一个函数都将从上一次函数停止调用的位置开始。
当输入函数发现已读完缓冲区中的所有字符时,会请求把下一个缓冲大小的数据块从文件拷贝到该缓冲区中。以这种方式,输入函数可以读取文件中的所有内容,直到文件结尾。函数在读取缓冲区中的最后一个字符后,把结尾指示器设置为真。于是,下一次被调用的输入函数将返回EOF。输出函数以类似的方式把数据写入缓冲区。当缓冲区被填满时,数据将被拷贝至文件中。

7.其他标准I/O函数

ANSI标准库的标准I/O系列有几十个函数。这里列出函数的原型,表明函数的参数和返回类型。我们要讨论的这些函数,除了setvbuf(),其他函数均可在ANSI之前的实现中使用。

1.int ungetc(int c, FILE *fp)函数

int ungetc()函数c指定的字符放回输入流中。如果把一个字符放回输入流,下次调用标准输入函数时将读取该字符(见图13.2)。例如,假设要读取下一个冒号之前的所有字符,但是不包括冒号本身,可以使用getchar() 或getc()函数读取字符到冒号,然后使用ungetc()函数把冒号放回输入流中。ANSI C标准保证每次只会放回一个字符。如果实现允许把一行中的多个字符放回输入流,那么下一次输入函数读入的字符顺序与放回时的顺序相反。

ungetc()函数

2.int fflush(FILE *fp)函数

调用fflush()函数引起输出缓冲区中所有的未写入数据被发送到fp指定的输出文件。这个过程称为刷新缓冲区。如果fp是空指针,所有输出缓冲区都被刷新。在输入流中使用fflush()函数的效果是未定义的。只要最近一次操作不是输入操作,就可以用该函数来更新流(任何读写模式)。

3.int setvbuf()函数
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()数组的大小(size_t是一种派生的整数类型,第5章介绍过)。mode的选择如下:_ IOFBF表示完全缓冲(在缓冲区满时刷新);_ IOLBF表示行缓冲(在缓冲区满时或写入一个换行符时);_IONBF表示无缓冲。如果操作成功,函数返回0,否则返回一个非零值。
假设一个程序要储存一种数据对象,每个数据对象的大小是3000字节。可以使用setvbuf()函数创建一个缓冲区,其大小是该数据对象大小的倍数。

4.二进制I/O:fread()和fwrite()

之前用到的标准I/O函数都是面向文本的,用于处理字符和字符串。如何要在文件中保存数值数据?用fprintf()函数和%f转换说明只是把数值保存为字符串。例如,下面的代码:

double num = 1./3;
fprintf(fp, "%f", num);

把num储存为8个字符:0.333333。使用%.2f转换说明将其储存为4个字符:0.33,用%.12f转换说明则将其储存为14个字符: 0.3333333333。改变转换说明将改变储存该值所需的空间数量,也会导致储存不同的值。把num储存为0.33后,读取文件时就无法将其恢复为更高的精度。一般而言**,fprintf()把数值转换为字符数据,这种转换可能会改变值**。

为保证数值在储存前后一致,最精确的做法是使用与计算机相同的位组合来储存。因此,double类型的值应该储存在一个double大小的单元中。如果以程序所用的表示法把数据储存在文件中,则称以二进制形式储存数据。不存在从数值形式到字符串的转换过程。对于标准I/O,fread()和fwrite()函数用于以二进制形式处理数据(见图13.3)。

二进制输出和文本输出

实际上,所有的数据都是以二进制形式储存的,甚至连字符都以字符码的二进制表示来储存。如果文件中的所有数据都被解释成字符码,则称该文件包含文本数据。如果部分或所有的数据都被解释成二进制形式的数值数据,则称该文件包含二进制数据(另外,用数据表示机器语言指令的文件都是二进制文件)。

二进制和文本的用法很容易混淆。ANSI C和许多操作系统都识别两种文件格式:二进制和文本。能以二进制数据或文本数据形式存储或读取信息。可以用二进制模式打开文本格式的文件,可以把文本储存在二进制形式的文件中。可以调用getc()拷贝包含二进制数据的文件。然而,一般而言,用二进制模式在二进制格式文件中储存二进制数据。类似地,最常用的还是以文本格式打开文本文件中的文本数据(通常文字处理器生成的文件都是二进制文件,因为这些文件中包含了大量非文本信息,如字体和格式等)。

5.size_t fwrite()函数
size_t fwrite(const void * restrict ptr, size_t size, size_t nmemb, FILE * restrict fp);

fwrite()函数二进制数据写入文件。size_t是根据标准C类型定义的类型,它是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()原型中的const void * restrict ptr声明。fwrite()的一个问题是,它的第1个参数不是固定的类型。例如,第1个例子中使用buffer,其类型是指向char的指针;而第2个例子中使用earnings,其类型是指向double的指针。在ANSIC函数原型中,这些实际参数都被转换成指向void的指针类型,这种指针可作为一种通用类型指针(在ANSI C之前,这些参数使用char *类型,需要把实参强制转换成char *类型).
fwrite()函数返回成功写入项的数量。正常情况下,该返回值就是nmemb, 但如果出现写入错误,返回值会比nmemb小。

6.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小。

7.int feof(FILE *fp)和int ferror(FILE *fp)函数

如果标准输入函数返回EOF,则通常表明函数已到达文件结尾。然而,出现读取错误时,函数也会返回EOF。feof()和ferror()函数用于区分这两种情况。当上一次输入调用检测到文件结尾时,feof()函数返回一个非零值,否则返回0。当读或写出现错误,ferror()函数返回一个非零值,否则返回0。

8.一个程序示例

用一个程序示例说明这些函数的用法。该程序把一系列文件中的内容附加在另一个文件的末尾。该程序存在一个问题:如何给文件传递信息。可以通过交互或使用命令行参数来完成,先采用交互式的方法。下面列出了程序的设计方案。

1.询问目标文件的名称并打开它。
2.使用一个循环询问源文件。
3.以读模式依次打开每个源文件,并将其添加到目标文件的末尾。

为演示setvbuf()函数的用法,该程序将使用它指定一个不同的缓冲区大小。下一步是细化程序打开目标文件的步骤:

1.以附加模式打开目标文件;
2.如果打开失败,则退出程序;
3.为该文件创建一个4096字节的缓冲区;
4.如果创建失败,则退出程序。

与此类似,通过以下具体步骤细化拷贝部分:

1.如果该文件与目标文件相同,则跳至下一个文件;
2.如果以读模式无法打开文件,则跳至下一个文件;
3.把文件内容添加至目标文件末尾。
最后,程序回到目标文件的开始处,显示当前整个文件的内容。
使用fread()和fwrite()函数进行拷贝。程序append.c给出了这个程序。

1.程序append.c
/* append.c -- 把文件附加到另一个文件末尾 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define BUFSIZE 4096
#define SLEN 81
void append(FILE *source, FILE *dest);
char * s_gets(char * st, int n);

int main(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:");
    s_gets(file_app, SLEN);
    if ((fa = fopen(file_app, "a+")) == NULL)
    {
   
        fprintf(stderr, "Can't open %s\n", file_app);
        exit(EXIT_FAILURE);
    }
    if (setvbuf(fa, NULL, _IOFBF, BUFSIZE) != 0)
    {
   //文件流+缓冲区 
        fputs("Can't create output buffer\n", stderr);
        exit(EXIT_FAILURE);
    }
    puts("Enter name of first source file (empty line to quit):");
    while (s_gets(file_src, SLEN) && file_src[0] != '\0')
    {
   
        if (strcmp(file_src, file_app) == 0)
            fputs("Can't append file to itself\n",stderr);
        else if ((fs = fopen(file_src, "r")) == NULL)
            fprintf(stderr, "Can't open %s\n", file_src);
        else
        {
   
            if (setvbuf(fs, NULL, _IOFBF, BUFSIZE) !=
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值