第十三章 文件输入/输出

GitHub地址,欢迎 star

13.1 与文件进行通信

有时,需要程序从文件中读取信息或把信息写入文件。这种程序与文件交互的形式就是文件重定向。这种方法很简单,但是有一定限制。例如,假设要编写一个交互程序,询问用户书名并把完整的书名列表保存在文件中。如果使用重定向,应该类似于:books > bklist

用户的输入被重定向到 bklist 中。这样做不仅会把不符合要求的文本写入 bklist,而且用户也看不到要回答什么问题。

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

13.1.1 文件是什么

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

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

13.1.2 文本模式和二进制模式

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

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

UNIX 用同一种文件格式处理文本文件和二进制文件的内容。不奇怪,鉴于 C 是作为开发 UNIX 的工具而创建的,C 和 UNIX 在文本中都使用 \n(换行符)表示换行。UNIX 目录中有一个统计文件大小的计数,程序可使用该计数确定是否读到文件结尾。然而,其他系统在此之前已经有其他方法处理文件,专门用于保存文本。也就是说,其他系统已经有一种与 UNIX 模型不同的格式处理文本文件。

为了规范文本文件的处理,C 提供两种访问文件的途径:二进制模式和文本模式。在二进制模式中,程序可以访问文件的每个字节。而在文本模式中,程序所见的内容和文件的时间内容不同。程序以文本模式读取文件时,把本地环境表示的行末尾或文件结尾映射为 C 模式。

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

13.1.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。

13.1.4 标准文件

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

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

13.2 标准 I/O

与底层 I/O 相比,标准 I/O 包除了可移植以外还有两个好处。第一,标准 I/O 有许多专门的函数简化了处理不同 I/O 的问题。例如,printf() 把不同形式的数据转换成与终端适应的字符串输出。第二,输入和输出都是缓冲的。也就是说,一次转移一大块信息而不是一字节信息(通常至少 512 字节)。例如,当程序读取文件时,一块数据被拷贝到缓冲区(一块中介存储区域)。这种缓冲极大地提高了数据传输速率。程序可以检查缓冲区中的字节。缓冲在后台处理,所以让人有逐字符访问的错觉(如果使用底层 I/O,要自己完成大部分工作)。程序清单演示了如何用标准 I/O 读取文件和统计文件中的字符数。我们将在后面几节讨论程序清单中的一些特性。

13.2.1 检查命令行参数

首先,程序清单中的程序检查 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() 也能结束整个程序。

13.2.2 fopen() 函数

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

模式字符串含义
“r”以读模式打开文件
“w”以写模式打开文件,把现有文件的长度截为 0,如果文件不存在,则创建一个新文件
“a”以写模式打开文件,在现有文件末尾添加内容,如果文件不存在,则创建一个新文件
“r+”以更新模式打开文件(即可以读写文件)
“w+”以更新模式打开文件(即,读和写),如果文件存在,则将其长度截为 0;如果文件不存在,则创建一个新文件
“a+”以更新模式打开文件(即,读和写),在现有文件的末尾添加内容,如果文件不存在则创建一个新文件;可以读整个文件,但是只能从末尾添加内容
“rb”、“wb”、“ab”、“ab+”、“a+b”、“wb+”、“w+b”与上一个模式类似,但是以二进制模式而不是文本模式打开文件
“wx”、“wbx”、“w+x”、“wb+x” 或 “w+bx”(C11)类似非 x 模式,但是如果文件已存在或以独占模式打开文件,则打开文件失败

像 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 结构)。

13.2…3 getc() 和 putc() 函数

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

与此类似,下面语句的意思是 “把字符 ch 放入 FILE 指针 fpout 指定的文件中”:putc(ch, fpout); 在 putc() 函数的参数列表中,第 1 个参数是待写入的字符,第 2 个参数是文件指针。

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

为何该示例不同 putchar() 而要用 putc()?原因之一是为了介绍 putc() 函数;原因之二是,把 stdout 替换成别的参数,很容易将这段程序改写成文件输出。

13.2.4 文件结尾

从文件中读取数据的程序在读到文件结尾时要停止。如何告诉程序已经读到文件结尾?如果 getc() 函数在读取一个字符时发现是文件结尾,它将返回一个特殊值 EOF。所以 C 程序只有在读到超过文件末尾时才会发现文件的结尾。

为了避免读到空文件,应该使用入口条件循环进行文件输入。鉴于 getc() 的设计,程序应该在进入循环体之前先尝试读取。如下设计所示:

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

以上代码可简化为:

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

由于 ch = getc(fp) 是 while 测试条件的一部分,所以程序在进入循环体之前就读取了文件。不要设计成下面这样:

int ch;
FILE * fp;
fp = fopen("wacky.txt", "r");
while(ch != EOF) // 首次使用 ch 时,它的值尚未确定
{
	ch = getc(fp); // 获取输入
	putchar(ch); // 处理输入
}

第 1 个问题是,ch 首次与 EOF 比较时,其值尚未确定。第 2 个问题是,如果 getc() 返回 EOF,该循环会把 EOF 作为一个有效字符处理。这些问题都可以解决。例如,把 ch 初始化为一个哑值(dummy value),再把一个 if 语句加入到循环中。但是,何必多次一举,直接使用上面的设计范例即可。

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

13.2.5 fclose() 函数

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

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

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

13.2.6 指向标准文件的指针

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

标准文件文件指针通常使用的设备
标准输入stdin键盘
标准输出stdout显示器
标准错误stderr显示器

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

13.3 一个简单的文件压缩程序

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

注意

带参数的 main 运行:
D:\CProject\Capp\bin\Debug\Capp.exe D:\CProject\Capp\eddy.txt
char *argv[] 中的第一个参数为 运行的程序名 Capp.exe,依次为你传的参数

#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);
    }
    // 设置输入
    if((in = fopen(argv[1],"r")) == NULL)
    {
        fprintf(stderr,"I couldn't open the file \"%s\"\n",argv[1]);
        exit(EXIT_FAILURE);
    }
    // 设置输出
    strncpy(name, argv[1], LEN - 5); // 拷贝文件名
    name[LEN - 5] = '\0';
    strcat(name,".red"); // 在文件名后添加 .red
    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); // 打印 2 个字符串中的第 1 个字符
    // 收尾工作
    if(fclose(in) != 0 || fclose(out) != 0)
        fprintf(stderr,"Error in closing files\n");
    return 0;
}

假设可执行文件名是 reducto,待读取的文件名为 eddy,该文件中包含下面一行内容:So even Eddy came oven ready.
命令如下: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 个空字符在调用 strcar() 函数时,被 .red 的 . 覆盖,生成了 eddy.red。程序中还检查了是否成功打开名为 eddy.red 的文件。这个步骤在一些环境中相当重要,因为像 strange.c.red 这样的文件名可能是无效的。

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

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

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

13.4.1 fprintf() 和 fscanf() 函数

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

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX 41

int main(int argc,char *argv[])
{

    FILE *fp;
    char words[MAX];

    if((fp = fopen("wordy.txt","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;
}

本电脑运行:
D:\CProject\Demo1\Demo1.exe addaword
Enter words to add to the file; press the #
key at the beginning of a line to terminate.
The fabulous programmer
#
File contents:
The
fabulous
programmer
The
fabulous
programmer
Done!
D:\CProject\Demo1\Demo1.exe addaword
Enter words to add to the file; press the #
key at the beginning of a line to terminate.
enchanted the
large
#
File contents:
The
fabulous
programmer
The
fabulous
programmer
enchanted
the
large
Done!

如你所见,fprintf() 和 fscanf() 的工作方式与 printf() 和 scanf() 类似。但是,与 putc() 不同的是,fprintf() 和 fscanf() 函数都把 FILE 指针作为第 1 个参数,而不是最后一个参数。

13.4.2 fgets() 和 fputs() 函数

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

这里,buf 是 char 类型数组的名称,STLEN 是字符串的大小,fp 是指向 FILE 的指针。

fgets() 函数读取输入直到第 1 个换行符的后面,或读到文件结尾,或者读取 STLEN - 1 个字符。然后,fgets() 在末尾添加一个空字符使之成为一个字符串。字符串的大小是其字符数加上一个空字符。如果 fgets() 在读到字符上限之前已读完一整行,它会把表示行结尾的换行符放在空字符前面。fgets() 函数在遇到 EOF 时将返回 NULL 值,可以利用这一机制检查是否到达文件结尾:如果未遇到 EOF 则之前返回传给它的地址。

fputs() 函数接受两个参数:第 1 个是字符串的地址;第 2 个是文件指针。该函数根据传入地址找到的字符串写入指定的文件中。和 puts() 函数不同,fputs() 在打印字符串时不会在其末尾添加换行符。

fgets(bug,fp); 这里,buf 是字符串的地址,fp 用于指定目标文件。

由于 fgets() 保留了换行符,fputs() 就不会再添加换行符,它们配合得非常好。即使输入行比 STLEN 长,这两个函数依然处理的很好。

13.5 随机访问:fseek() 和 ftell()

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

#include <stdio.h>
#include <stdlib.h>
#define CNTL_Z '\032'
#define SLEN 81

int main(int argc,char *argv[])
{

    char file[SLEN];
    char ch;
    FILE *fp;
    long count,last;

    puts("Enter the name of the file to be processed:");
    scanf("%80s",file);
    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 != '\f')
            putchar(ch);
    }
    putchar('\n');
    fclose(fp);

    return 0;
}

下面是对一个文件的输出:
Enter the name of the file to be processed:
Cluv.txt
fsadfsafsafsafas

注意

如果通过命令行环境运行该程序,待处理文件要和可执行文件在同一个目录(或文件夹)中。如果在 IDE 中运行该程序,具体查找方案因实现而异。

接下来,我们要讨论 3 个问题:fseek() 和 ftell() 函数的工作原理、如何使用二进制流、如何让程序可移植。

13.5.1 fseek() 和 ftell() 的工作原理

fseek() 的第 1 个参数是 FILE 指针,指向待查找的文件,fopen() 应该已打开该文件。

fseek() 的第 2 个参数是偏移量(offset)。该参数表示从起始点开始要移动的距离。该参数必须是一个 long 类型的值,可以为正(前移)、负(后移)或 0(保持不动)。

fseek() 的第 3 个参数是模式,该参数确定起始点。根据 ANSI 标准,在 stdio.h 头文件中规定了几个表示模式的明示常量(manifest constant)。

模式偏移量的起始点
SEEK_SET文件开始处
SEEK_CUR当前位置
SEEK_END文件末尾

旧的实现缺少这些定义,可以使用数值 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,以此类推。ANSI C 规定,该定义适用于以二进制模式打开的文件,以文件模式打开文件的情况不同。

13.5.2 二进制模式和文本模式

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

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

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

13.5.3 可移植性

理论上,fseek() 和 ftell() 应该符合 UNIX 模型。但是,不同系统存在着差异,有时确实无法做到与 UNIX 模型一致。因此,ANSI 对这些函数降低了要求。下面是一些限制:

  • 在二进制模式中,实现不必支持 SEEK_END 模式。因此无法保证程序的可移植性。移植性更高的方法是逐字节读取整个文件知道文件末尾。C 预处理器的条件编译指令提供了一种系统方法来处理这种情况。
  • 在文本模式中,只有一下调用能保证其相应的行为。
函数调用效果
fseek(file,0L,SEEK_SET)定位至文件开始处
fseek(file,0L,SEEK_CUR)保持当前位置不动
fseek(file,0L,SEEK_END)定位至文件结尾
fseek(file,ftell - pos,SEEK_SET)到距文件开始处 ftell - pos 的位置,ftell - pos 是 ftell() 返回值

13.5.4 fgetpos() 和 fsetpos() 函数

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

ANSI C 定义了如果使用 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 类型的值应该通过之前调用 fsetpos() 获得。

13.6 标准 I/O 的机理

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

通常,使用标准 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。

输出函数以类似的方式把数据写入缓冲区。当缓冲区被填满时,数据被拷贝至文件中。

13.7 其他标准 I/O 函数

13.7.1 int ungetc(int c, FILE *fp) 函数

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

13.7.2 int fflush() 函数

fflush() 函数的原型如下:int fflush(FILE *fp);

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

13.7.3 int setvbuf() 函数

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 是一个派生的整数类型)。mode 的选择如下:_IOFBF 表示完全缓冲(在缓冲区满时刷新);_IOLBF 表示行缓冲(在缓冲区满时或协议一个换行符时);_IONBF 表示无缓冲。如果操作成功,函数返回 0,否则返回一个非 0 值。

假设一个程序要储存一种数据对象,每个数据对象的大小是 3000 字节。可以使用 setvbuf() 函数创建一个缓冲区,其大小是该数据对象大小的倍数。

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

介绍 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.33333333333333。改变转换说明将改变储存该值所需的空间数量,也会导致储存不同的值。把 num 储存为 0.33 后,读取文件时就无法将其恢复为更高的精度。一般而言,fprintf() 把数值转换为字符数据,这种转换可能会改变值。

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

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

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

13.7.5 size_t fwrite() 函数

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 的指针。在 ANSI C 函数原型中,这些实际参数都被转换成指向 void 的指针类型,这种指针可作为一种通用类型指针(在 ANSI C 之前,这些参数使用 char * 类型,需要把实参强制转换成 char * 类型)。

fwrite() 函数返回成功写入项的数量。正常情况下,该返回值就是 nmemb,但如果出现写入错误,返回值会比 nmemb 小。

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

13.7.7 int feof(FILE *fp) 和 int ferror(FILE *fp) 函数

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

13.7.8 一个程序示例

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

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

为演示 setvbuf() 函数的用法,该程序将使用它指定一个不同的缓冲区大小。下一步是细化程序打开目标文件的步骤:
1、以附加模式打开目标文件;
2、如果打开失败,则退出程序;
3、为该文件创建一个 4096 字节的缓冲区;
4、如果创建失败,则退出程序。

与此类似,通过以下具体步骤细化拷贝部分:
1、如果该文件与目标文件相同,则跳至下一个文件;
2、如果以读模式无法打开文件,则跳至下一个文件;
3、把文件内容添加至目标文件末尾。

最后,程序问到目标文件的开始处,显示当前整个文件的内容。作为练习,我们使用 fread() 和 fwrite() 函数进行拷贝。程序清单给出这个程序。

/* 把文件附加到另一个文件末尾 */
#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)
            {
                fputs("Can't 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;
}

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);
}

char * s_gets(char * st, int n)
{
    char * ret_val;
    char * find;

    ret_val = fgets(st,n,stdin);
    if(ret_val)
    {
        find = strchr(st,'\n'); // 查找换行符
        if(find) // 如果地址不是 NULL
            *find = '\0'; // 在此处放置一个空字符
        else
            while(getchar() != '\n')
                continue;
    }
    return ret_val;
}

如果 setvbuf() 无法创建缓冲区,则返回一个非零值,然后终止程序。可以用类似的代码为睁开拷贝的文件创建一块 4096 字节的缓冲区。把 NULL 作为 setvbuf() 的第 2 个参数,便可让函数分配缓冲区的存储空间。

该程序获取文件名所用的函数是 s_gets(),而不是 scanf(),因为 scanf() 会跳过空白,因此无法检测到空行。该程序还用 s_gets() 代替 fgets(),因为后者在字符串中保留换行符。

一下代码放置程序把文件附加在自身末尾:

if(strcmp(file_src, file_app) == 0)
	fputs("Can't append file to itself\n",stderr);

参数 file_app 表示目标文件名,file_src表示正在处理的文件名。

append() 函数完成拷贝任务。该函数使用 fread() 和 fwrite() 一次拷贝 4096 字节,而不是一次拷贝 1 字节:

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);
}

因为是以附加模式打开由 dest 指定的文件,所以所有的源文件都被依次添加至目标文件的末尾。注意,temp 数组具有静态存储期(意思是在编译时分配该数组,不是在每次调用 append() 函数时分配)和块作用域(意思是该数组属于它所在的函数私有)。

该程序示例使用文本模式的文件。使用 “ab+” 和 “rb” 模式可以处理二进制文件。

13.7.9 用二进制 I/O 进行随机访问

随机访问是用二进制 I/O 写入二进制文件最常用的方式,我们来看一个简短的例子。程序清单中的程序创建一个储存 double 类型数字的文件,然后让用户访问这些内容。

/* 用二进制 I/O 进行随机访问 */
#include <stdio.h>
#include <stdlib.h>
#define ARSIZE 1000

int main(void)
{
    double numbers[ARSIZE];
    double value;
    const char * file = "nmubers.dat";
    int i;
    long pos;
    FILE *iofile;

    // 创建一组 double 类型的值
    for(i = 0; i < ARSIZE; i++)
        numbers[i] = 100.0 * i + 1.0 / (i + 1);
    // 尝试打开文件
    if((iofile = fopen(file,"wb")) == NULL)
    {
        fprintf(stderr,"Could not open %s output.\n",file);
        exit(EXIT_FAILURE);
    }
    // 以二进制格式把数组写入文件
    fwrite(numbers,sizeof(double),ARSIZE,iofile);
    fclose(iofile);
    if((iofile = fopen(file,"rb")) == NULL)
    {
        fprintf(stderr,"Could not open %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!");

    return 0;
}

首先,该程序创建了一个数组,并在该数组中存放了一些值。然后,程序以二进制模式创建了一个名为 nmubers.dat 的文件,并使用 fwrite() 把数组中的内容拷贝到文件中。内存中数组的所有 double 类型值的位组合(每个位组合都是 64 位)都被拷贝至文件中。不能用文本编辑器读取最后的二进制文件,因为无法把文件中的值转换成字符串。然而,储存在文件中的每个值都与储存在内存中的值完全相同,没有损失任何精确度。此外,每个值在文件中也同样占用 64 为存储空间,所以可以很容易地计算出每个值的位置。

程序的第 2 部分用于打开待读取的文件,提示用户输入一个值的索引。程序通过把索引值和 double 类型值占用的字节相乘,即可得出文件中的一个位置。然后,程序调用 fseek() 定位到该位置,用 fread() 读取该位置上的数据值。注意,这里并未使用转换说明。fread() 从已定位的位置开始,拷贝 8 字节到内存中地址为 &value 的位置。然后,使用 printf() 显示 value。下面是该程序的一个运行示例:
Enter an index in the range 0-999.
500
The value there is 50000.001996.
Next index (out of range to quit):
900
The value there is 90000.001110.
Next index (out of range to quit):
0
The value there is 1.000000.
Next index (out of range to quit):
-1
Bye!

13.8 关键概念

C 程序把输入看作是字节流,输入流来源于文件、输入设备(如键盘),或者甚至是另一个程序的输出。类似地,C 程序把输出也看作是字节流,输出流的目的地可以是文件、视频显示等。

C 如何解释输入流或输出流取决于所使用的输入/输出函数。程序可以不做任何改动弄个地读取和存储字节,或者把字节依次解释成字符,随后可以把这些字节解释成普通文本以用文本表示数字。类似地,对于输出,所使用的函数决定了二进制值是被原样转移,还是被转换成文本或以文本表示数字。如果要在不损失精度的前提下保存或恢复数值数据,请使用二进制模式以及 fread() 和 fwrite() 函数。如果打算保存文本信息并创建能在普通文本编辑器查看的文本,请使用文本模式和函数(如 getc() 和 fprintf() )。

要访问文件,必须创建文件指针(类型是 FILE * )并把指针与特定文件名相关联。随后的代码就可以使用这个指针(而不是文件名)来处理该文件。

要重点理解 C 如何处理文件结尾。通常,用于读取文件的程序使用一个循环读取输入,直至到达文件结尾。C 输入函数在读过文件结尾后才会检测到文件结尾,这意味着应该在尝试读取之后立即判断是否是文件结尾。可以使用 13.2.4 节 “设计范例” 中的双文件输入模式。

13.9 本章小结

对于大多数 C 程序而言,写入文件和读取文件必不可少。为此,绝大多数 C 实现都提供底层 I/O 和标准高级 I/O。因为 ANSI C 库考虑到可移植性,包含了标准 I/O 包,但是未提供底层 I/O。

标准 I/O 包自动创建输入和输出缓冲区以加快数据传输。fopen() 函数为标准 I/O 打开一个文件,并创建一个用于存储文件和缓冲区信息的结构。fopen() 函数返回指向该结构的指针,其他函数可以使用该指针指定待处理的文件。feof() 和 ferror() 函数报告 I/O 操作失败的原因。

C 把输入视为字节流。如果使用 fread() 函数,C 把输入看作是二进制值并将其储存在指定存储位置。如果使用 fscanf()、getc()、fgets() 或其他相关函数,C 则将每个字节看作是字符码。然后 fscanf() 和scanf() 函数尝试把字节码翻译成转换说明指定的其他类型。例如,输入一个值 23,%f 转换说明会把 23 翻译成一个浮点值,%d 转换说明会把 23 翻译成一个整数值,%s 转换说明则会把 23 储存为字符串。getc() 和 fgetc() 系列函数把输入作为字节码储存,将其作为单独的字符保存在字符变量中或作为字符串储存在字符数组中。类似地,fwrite() 将二进制数据直接放入输入流,而其他蔬菜函数把非字符数据转换成用字节表示后才将其放入输出流。

ANSI C 提供两种文件打开模式:二进制和文本。以二进制模式打开文件时,可以逐字节读取文件;以文本模式打开文件时,会把文件内容从文本的系统表示法映射为 C 表示法。对于 UNIX 和 Linux 系统,这两种模式完全相同。

通常,输入函数 getc()、fgets()、fscanf() 和 fread() 都从文件开始处按顺序读取文件。然而,fseek() 和ftell() 函数让程序可以随机访问文件中的任意位置。fgetpos() 和 fsetpos() 把类似的功能扩展至更大的文件。与文本模式相比,二进制模式更容易进行随机访问。

13.10 复习题

1、下面的程序有什么问题?

int main(vlid)
{
	int * fp;
	int k;
	fp = fopen("gelatin");
	for(k = 0; k < 30; k++)
		fputs(fp,"Nanette eats gelatin.")'
	fclose("gelatin");
	return 0;
}

2、下面的程序完成什么任务?(假设在命令行环境中运行)

#include <stdio.h>
#include <stdlib.h>
#include < ctype.h>
int main(int argc, char *argv [])
{
	int ch;
	FILE *fp;
	if(argc < 2)
		exit(EXIT_FAILURE);
	if((tp = fopen(argv[1], "r")) == NULL)
		exit(EXIT_FAILURE);
	while((ch = getc(fp)) !+ EOF)
		if(isdigit(ch))
			putchar(ch);
	fclose(fp);
	return 0;
}

3、假设程序中有下列语句:

#include <stdio.h>
FILE * fp1, * fp2;
char ch;

fp1 = fopen("terky","r");
fp2 = fopen("jerky","w");

另外,假设成功打开了两个文件。补全项目函数调用中缺少的参数:
a、ch = getc();
b、fprintf( ,"%c\n", );
c、putc( , );
d、fclose(); /* 关闭 terky 文件 */

4、编写一个程序,不接受任何命令行参数或接受一个命令行参数。如果有一个参数将其解释为文件名;如果没有参数,使用标准输入(stdin)作为输入。假设输入完全是浮点数。该程序要计算和报告输入数字的算术平均值。

5、编写一个程序,接受两个命令行参数。第 1 个参数是字符,第 2 个参数是文件名。要求该程序只打印文件中包含给定字符的那些行。

注意

C 程序根据 ‘\n’ 识别文件中的行。假设所有行都不超过 256 个字符,你可能会想到用 fgets()

6、二进制文件和文本文件有何区别?二进制流和文本流有何区别?

7、
a、分别用 fprintf() 和 fwrite() 储存 8238201 有何区别?
b、分别用 putc() 和 fwrite() 储存字符 S 有何区别?

8、下面语句的区别是什么?
printf(“Hello, %s\n”, name);
fprintf(stdout,“Hello, %s\n”, name);
fprintf(stderr,“Hello, %s\n”, name);

9、“a+”、“r+” 和 “w+” 模式打开的文件都是可读写的。哪种模式更适合用来更改文件中已有的内容?

13.11 编程练习

1、修改程序清单中的程序,要求提示用户输入文件名,并读取用户输入的信息,不使用命令行参数。

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
	int ch; // 读取文件时,储存每个字符的地方
	FILE *fp; // “文件指针”
	unsigned long count = 0;
	if(argc != 2)
	{
		printf("Uaage: %s filename\n",argv[0]);
		exit(EXIT_FAILURE);
	}
	if((fp = fopen(argv[1],"r")) == NULL)
	{
		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;
}

2、编写一个文件拷贝程序,该程序通过命令行获取原始文件名和拷贝文件名。尽量使用标准 I/O 和二进制模式。

3、编写一个文件拷贝程序,提示用户输入文本文件名,并以该文件名作为原始文件名和输入文件名。该程序要使用 ctype.h 中的 toupper() 函数,在写入到输出文件时把所有文件转换成大写,使用标准 I/O 和文本模式。

4、编写一个程序,按顺序咋屏幕上显示命令行中列出的所有文件。使用 argc 控制循环。

5、修改程序清单中欧的程序,用命令行界面代替交互式界面。

/* 把文件附加到另一个文件末尾 */
#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)
            {
                fputs("Can't 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;
}

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);
}

char * s_gets(char * st, int n)
{
    char * ret_val;
    char * find;

    ret_val = fgets(st,n,stdin);
    if(ret_val)
    {
        find = strchr(st,'\n'); // 查找换行符
        if(find) // 如果地址不是 NULL
            *find = '\0'; // 在此处放置一个空字符
        else
            while(getchar() != '\n')
                continue;
    }
    return ret_val;
}

6、使用命令行参数的程序依赖于用户的内存如何正确地使用它们。程序程序清单中的程序,不使用命令行参数,而是提示用户输入所需信息。

7、编写一个程序打开两个文件。可以使用命令行参数或提示用户输入文件名。
a、该程序以这样的顺序打印:带引第 1 个文件的第 1 行,第 2 个文件的第 1 行,第 1 个文件的第 2 行,第 2 个文件的第 2 行,以此类推,打印到行数较多文件的最后一行。

b、修改程序,把行号相同的行打印成一行。

8、编写一个程序,以一个字符和任意文件名作为命令行参数。如果字符后面没有参数,该程序读取标准输入;否则,程序依次打开每个文件并报告每个文件中该字符出现的次数。文件名和字符本身也要一同报告。程序应包含错误检查,以确定参数数量是否正确和是否能打开文件。如果无法打开文件,程序应报告这一情况,然后继续处理下一个文件。

9、修改程序清单,从 1 开始,根据加入列表的顺序为每个单词编号。当程序瑕疵运行时,确保新的单词编号接着上次的编号开始。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX 41

int main(int argc,char *argv[])
{

    FILE *fp;
    char words[MAX];

    if((fp = fopen("wordy.txt","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;
}

10、编写一个程序打开一个文本文件,通过交互方式获得文件名。通过一个循环,提示用户输入一个文件位置。然后该程序打印从该位置开始到下一个换行符之前的内容。用户输入负数或非数值字符可以结束输入循环。

11、编写一个程序,接受两个命令行参数。第 1 个参数是一个字符串,第 2 个参数是一个文件名。然后该程序查找该文件,打印文件中包含该字符串的所有行。因为该任务是米昂新行而不是面向字符的,所有要使用 fgets() 而不是 getc()。使用标准 C 库函数 strstr() 在每一行中查找指定字符串。假设文件中的所有行都不超过 255 个字符。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值