C现代方法(第22章)笔记——输入/输出

文章目录

  • 第22章 输入/输出
    • 22.1 流
      • 22.1.1 文件指针
      • 22.1.2 标准流和重定向
      • 22.1.3 文本文件与二进制文件
    • 22.2 文件操作
      • 22.2.1 打开文件
      • 22.2.2 模式
      • 22.2.3 关闭文件
      • 22.2.4 为打开的流附加文件
      • 22.2.5 从命令行获取文件名
      • 22.2.6 临时文件
      • 22.2.7 文件缓冲
      • 22.2.8 其他文件操作
    • 22.3 格式化的输入/输出
      • 22.3.1 ...printf函数
      • 22.3.2 ...printf转换说明
      • 22.3.3. C99对...printf转化说明的修改(C99)
      • 22.3.4 ...printf转换说明示例
      • 22.3.5 ...scanf函数
      • 22.3.6 ..scanf格式串
      • 22.3.7 ...scanf转换说明
      • 22.3.8 C99对...scanf转换说明的改变(C99)
      • 22.3.9 scanf示例
      • 22.3.10 检测文件末尾和错误条件
    • 22.4 字符的输入/输出
      • 22.4.1 输出函数
      • 22.4.2 输入函数
        • 22.4.2.1 程序——复制文件
    • 22.5 行的输入/输出
      • 22.5.1 输出函数
      • 22.5.2 输入函数
    • 22.6 块的输入/输出
    • 22.7 文件定位
      • 22.7.1 程序——修改零件记录文件
    • 22.8 字符串的输入/输出
      • 22.8.1 输出函数
      • 22.8.2 输入函数
    • 问与答
    • 写在最后

第22章 输入/输出

——在人与机器共存的世界中,懂得思变的一定是人,别指望机器。

C语言的输入/输出库是标准库中最大且最重要的部分。由于输入/输出是C语言的高级应用,因此这里将用一整章(篇幅最长)来讨论头——输入/输出函数的主要存储位置。

从第2章开始,我们已经在使用了,而且已经对printf函数、scanf函数、putchar函数、getchar函数、puts函数以及gets函数的使用有了一定的了解。本章会提供有关这6个函数的更多信息,并介绍一些新的用于文件处理的函数。值得高兴的是,许多新函数和我们已经熟知的函数有着紧密的联系。例如,fprintf函数就是printf函数的“文件版”。

本章将首先讨论一些基本问题:流的概念、FILE类型、输入和输出重定向,以及文本文件和二进制文件的差异(22.1节)。随后将讨论特别为使用文件而设计的函数,包括打开和关闭文件的函数(22.2节)。在讨论完printf函数、scanf函数以及与“格式化”输入/输出相关的函数(22.3节)后,我们将着眼于读/写非格式化数据的函数。

  • 每次读写一个字符的getc函数、putc函数以及相关的函数(22.4节)。
  • 每次读写一行字符的gets函数、puts函数以及相关的函数(22.5节)。
  • 读/写数据块的fread函数和fwrite函数(22.6节)。

随后,22.7节会说明如何对文件执行随机的访问操作。最后,22.8节会描述sprintf函数、snprintf函数和sscanf函数,它们是printf函数和scanf函数的变体,后两者分别用于写入和读取一个字符串。

本章涵盖了中的绝大部分函数,但忽略了其中8个函数。perror函数是这8个函数中的一个,它与头紧密相关,所以把它推迟到24.2节讨论头时再来介绍。26.1节涵盖了其余7个函数(vfprintfvprintfvsprintfvsnprintfvfscanfvscanfvsscanf)。这些函数依赖于va_list类型,该类型在26.1节介绍。

C89中,所有的标准输入/输出函数都属于。但从C99开始有所不同,有些输入/输出函数在头(25.5节)中声明。中的函数用于处理宽字符而不是普通字符,但大多数函数与中的函数紧密相关。中用于读或写数据的函数称为字节输入/输出函数,而中的类似函数则称为宽字符输入/输出函数


22.1 流

C语言中,术语流(stream)表示任意输入的源或任意输出的目的地。许多小型程序(就像前面章节中介绍的那些)都是通过一个流(通常和键盘相关)获得全部的输入,并且通过另一个流(通常和屏幕相关)写出全部的输出。

较大规模的程序可能会需要额外的流。这些流常常表示存储在不同介质(如硬盘驱动器、CDDVD和闪存)上的文件,但也很容易和不存储文件的设备(如网络端口、打印机等)相关联。这里将集中讨论文件,因为它们常见且容易理解。但是,请千万记住一点:中的许多函数可以处理各种形式的流,而不仅限于表示文件的流


22.1.1 文件指针

C程序中对流的访问是通过文件指针(file pointer)实现的。此指针的类型为FILE *FILE类型在中声明)。用文件指针表示的特定流具有标准的名字;如果需要,还可以声明另外一些文件指针。例如,如果程序除了标准流之外还需要两个流,则可以包含如下声明:

FILE *fp1, *fp2;

虽然操作系统通常会限制可以同时打开的流的数量,但程序可以声明任意数量的FILE *类型变量。


22.1.2 标准流和重定向

提供了3个标准流(见表22-1)。这3个标准流可以直接使用,不需要对其进行声明,也不用打开或关闭它们。

表22-1 标准流

文件指针 默认的含义
stdin 标准输入 键盘
stdout 标准输出 屏幕
stderr 标准误差 屏幕

前面章节使用过的函数(printfscanfputchargetcharputsgets)都是通过stdin获得输入,并且用stdout进行输出的。默认情况下,stdin表示键盘,stdoutstderr表示屏幕。然而,许多操作系统允许通过一种称为重定向(redirection)的机制来改变这些默认的含义。

通常,我们可以强制程序从文件而不是从键盘获得输入,方法是在命令行中放上文件的名字,并在前面加上字符<

demo <in.dat

这种方法叫作输入重定向(input redirection),它本质上是使stdin流表示文件(此例中为文件in.dat)而非键盘。重定向的绝妙之处在于,demo程序不会意识到正在从文件in.dat中读取数据,它会认为从stdin获得的任何数据都是从键盘输入的

输出重定向(output redirection)与之类似。对stdout流的重定向通常是通过在命令行中放置文件名,并在前面加上字符>实现的:

demo >out.da

现在所有写入stdout的数据都将进入out.dat文件中,而不是出现在屏幕上。

顺便说一下,我们还可以把输出重定向和输入重定向结合使用:

demo <in.dat >out.da

字符<>不需要与文件名相邻,重定向文件的顺序也是无关紧要的,所以下面的例子是等效的:

demo < in.dat > out.dat 
demo >out.dat <in.d

输出重定向的一个问题是,会把写入stdout的所有内容都放入文件中。如果程序运行失常并且开始写出错消息,那么我们在看文件的时候才会知道,而这些应该是出现在stderr中的。通过把出错消息写到stderr而不是stdout中,可以保证即使在对stdout进行重定向时,这些出错消息仍能出现在屏幕上。(不过,操作系统通常也允许对stderr进行重定向。)


22.1.3 文本文件与二进制文件

支持两种类型的文件:文本文件二进制文件。在文本文件(text file)中,字节表示字符,这使人们可以检查或编辑文件。例如,C程序的源代码是存储在文本文件中的。另外,在二进制文件(binary file)中,字节不一定表示字符,字节组还可以表示其他类型的数据,比如整数和浮点数。如果试图查看可执行C程序的内容,你会立刻意识到它是存储在二进制文件中的。

文本文件具有2种二进制文件没有的特性:

  • 文本文件分为若干行。文本文件的每一行通常以一两个特殊字符结尾, 特殊字符的选择与操作系统有关。在Windows中,行末的标记是回车符('\x0d')与一个紧跟其后的回行符('\x0a')。在UNIXMacintosh操作系统(Mac OS)的较新版本中,行末的标记是一个单独的回行符。旧版本的Mac OS使用一个单独的换行符。
  • 文本文件可以包含一个特殊的“文件末尾”标记。一些操作系统允许在文本文件的末尾使用一个特殊的字节作为标记。在Windows中,标记为'\x1a'(Ctrl+Z)Ctrl+Z不是必需的,但如果存在,它就标志着文件的结束,其后的所有字节都会被忽略。使用Ctrl+Z的这一习惯继承自DOS,而DOS中的这一习惯又是从CP/M(早期用于个人计算机的一种操作系统)来的。大多数其他操作系统(包括UNIX)没有专门的文件末尾字符。

二进制文件不分行,也没有行末标记和文件末尾标记所有字节都是平等对待

向文件写入数据时,我们需要考虑是按文本格式存储还是按二进制格式来存储。为了搞清楚其中的差别,考虑在文件中存储数32767的情况。一种选择是以文本的形式把该数按字符32767写入。假设字符集为ASCII,那么就可以得到下列5个字节:

00110011 00110010 00110111 00110110 00110111
‘3’ ‘2’ ‘7’ ‘6’ ‘7’

另一种选择是以二进制的形式存储此数,这种方法只会占用2个字节:

01111111 11111111

[在按小端顺序(20.3节)存储数据的系统中,这两个字节的顺序相反。]从上述示例可以看出,用二进制形式存储数可以节省相当大的空间。

编写用来读写文件的程序时,需要考虑该文件是文本文件还是二进制文件。在屏幕上显示文件内容的程序可能要把文件视为文本文件。但是,文件复制程序就不能认为要复制的文件是文本文件。如果那样做,就不能完全复制含有文件末尾字符的二进制文件了。在无法确定文件是文本形式还是二进制形式时,安全的做法是把文件假定为二进制文件


22.2 文件操作

简单性是输入和输出重定向的魅力之一:不需要打开文件、关闭文件或者执行任何其他的显式文件操作。可惜的是,重定向在许多应用程序中受到限制。当程序依赖重定向时,它无法控制自己的文件,甚至无法知道这些文件的名字。更糟糕的是,如果程序需要在同一时间读入两个文件或者写出两个文件,重定向都无法做到

当重定向无法满足需要时,我们将使用提供的文件操作。本节将探讨这些文件操作,包括打开文件、关闭文件、改变缓冲文件的方式、删除文件以及重命名文件。


22.2.1 打开文件

FILE *fopen(const char * restrict filename, const char * restrict mode);

如果要把文件用作流,打开时就需要调用fopen函数。fopen函数的第一个参数是含有要打开文件名的字符串。(“文件名”可能包含关于文件位置的信息,如驱动器符或路径。)第二个参数是“模式字符串”,它用来指定打算对文件执行的操作。例如,字符串"r"表明将从文件读入数据,但不会向文件写入数据。

注意!!在fopen函数的原型中,restrict关键字(17.8节)出现了两次。restrict是从C99开始引入的关键字,表明filenamemode所指向的字符串的内存单元不共享C89中的fopen原型不包含restrict,但也有这样的要求。restrictfopen的行为没有影响,因此通常可以忽略。

请注意!!提醒Windows程序员:在fopen函数调用的文件名中含有字符\时,一定要小心。这是因为C语言会把字符\看作转义序列(7.3节)的开始标志。

fopen("c:\project\test1.dat", "r");

以上调用会失败,因为编译器会把\t看作转义字符。(\p不是有效的转义字符,但看上去像。根据C标准,\p的含义是未定义的。)有两种方法可以避免这一问题。一种方法是用``\代替\

fopen("c:\\project\\test1.dat", "r"); 

//另一种方法更简单,只要用/代替\就可以了:
fopen("c:/project/test1.dat", "r");

Windows会把/认作目录分隔符。

fopen函数返回一个文件指针。程序可以(且通常)把此指针存储在一个变量中,稍后在需要对文件进行操作时使用它。fopen函数的常见调用形式如下所示,其中fpFILE*类型的变量:

fp = fopen("in.dat", "r");  /* opens in.dat for reading */ 

当程序稍后调用输入函数从文件in.dat中读数据时,会把fp作为一个实际参数。

当无法打开文件时,fopen函数会返回空指针。这可能是因为文件不存在,也可能是因为文件的位置不对,还可能是因为我们没有打开文件的权限

请注意!!永远不要假设可以打开文件,每次都要测试fopen函数的返回值以确保不是空指针。


22.2.2 模式

fopen函数传递哪种模式字符串不仅依赖于稍后将要对文件采取的操作,还取决于文件中的数据是文本形式还是二进制形式。要打开一个文本文件,可以采用表22-2中的一种模式字符串:

表22-2 用于文本文件的模式字符串

字符串 含义
“r” 打开文件用于读
“w” 打开文件用于写(文件不需要存在)
“wx” 创建文件用于写(文件不能已经存在)①
“w+x” 创建文件用于更新(文件不能已经存在)①
“a” 打开文件用于追加(文件不需要存在)
“r+” 打开文件用于读和写,从文件头开始
“w+” 打开文件用于读和写(如果文件存在就截去)
“a+” 打开文件用于读和写(如果文件存在就追加)

① 从C11开始引入的模式(独占的创建-打开模式)。

当使用fopen打开二进制文件时,需要在模式字符串中包含字母b表22-3列出了用于二进制文件的模式字符串。

表22-3 用于二进制文件的模式字符串

字符串 含义
“rb” 打开文件用于读
“wb” 打开文件用于写(文件不需要存在)
“wbx” 创建文件用于写(文件不能已经存在)①
“ab” 打开文件用于追加(文件不需要存在)
“r+b"或者"rb+” 打开文件用于读和写,从文件头开始
“w+b"或者"wb+” 打开文件用于读和写(如果文件存在就截去)
“w+bx"或者"wb+x” 创建文件用于更新(文件不能已经存在)①
“a+b"或者"ab+” 打开文件用于读和写(如果文件存在就追加)

① 从C11开始引入的模式(独占的创建-打开模式)。

表22-2表22-3可以看出对写数据和追加数据进行了区分。当给文件写数据时,通常会对先前的内容进行覆盖。然而,当为追加打开文件时,向文件写入的数据添加在文件末尾,因而可以保留文件的原始内容。另外,带有字母“x”的打开模式是从C11才开始引入的,这个字母表示独占模式。在这种模式下,如果文件已经存在或者无法创建,fopen函数将执行失败;否则文件将以独占(非共享)模式打开。

顺便说一下,当打开文件用于读和写(模式字符串包含字符+)时,有一些特殊的规则。如果没有先调用一个文件定位函数(22.7节),那么就不能从读模式转换成写模式,除非读操作遇到了文件的末尾。类似地,如果既没有调用fflush函数(稍后会介绍)也没有调用文件定位函数,那么就不能从写模式转换成读模式。


22.2.3 关闭文件

int fclose(FILE *stream);

fclose函数允许程序关闭不再使用的文件。fclose函数的参数必须是文件指针,此指针来自fopen函数或freopen函数(本节稍后会介绍)的调用。如果成功关闭了文件,fclose函数会返回零;否则,它会返回错误代码EOF(在中定义的宏)。

为了说明如何在实践中使用fopen函数和fclose函数,下面给出了一个程序的框架。此程序打开文件example.dat进行读操作,并要检查打开是否成功,然后在程序终止前再把文件关闭:

#include  
#include  

#define FILE_NAME "example.dat" 

int main(void) 
{ 
    FILE *fp; 
    
    fp = fopen(FILE_NAME, "r"); 
    if (fp == NULL) { 
        printf("Can’t open %s\n", FILE_NAME); 
        exit(EXIT_FAILURE); 
    } 
    ... 
    fclose(fp); 
    return 0; 
}

当然,按照C程序员的编写习惯,通常也可以把fopen函数的调用和fp的声明结合在一起使用:

FILE *fp = fopen(FILE_NAME, "r");

还可以把函数调用与NULL判定相结合:

if ((fp = fopen(FILE_NAME, "r")) == NULL) ...

22.2.4 为打开的流附加文件

FILE *freopen(const char * restrict filename, 
              const char * restrict mode, 
              FILE * restrict stream); 

freopen函数为已经打开的流附加一个不同的文件。最常见的用法是把文件和一个标准流(stdin、stdout 或stderr)相关联。例如,为了使程序开始往文件foo中写数据,可以使用下列形式的freopen函数调用:

if (freopen("foo", "w", stdout) == NULL) { 
    /* error; foo can’t be opened */ 
} 

在关闭了先前(通过命令行重定向或者之前的freopen函数调用)与stdout相关联的所有文件之后,freopen函数将打开文件foo,并将其与stdout相关联。

freopen函数的返回值通常是它的第三个参数(一个文件指针)。如果无法打开新文件,那么freopen函数会返回空指针。(如果无法关闭旧的文件,那么freopen函数会忽略错误。)

C99开始新增了一种机制。如果filename是空指针,freopen会试图把流的模式修改为mode参数指定的模式。不过,具体的实现可以不支持这种特性;如果支持,则可以限定能进行哪些模式改变。


22.2.5 从命令行获取文件名

当正在编写的程序需要打开文件时,马上会出现一个问题:如何把文件名提供给程序呢?把文件名嵌入程序自身的做法不太灵活,而提示用户输入文件名的做法也很笨拙。通常,最好的解决方案是让程序从命令行获取文件的名字。例如,当执行名为demo的程序时,可以通过把文件名放入命令行的方法为程序提供文件名:

demo names.dat dates.dat 

13.7节中,我们了解到如何通过定义带有两个形式参数的main函数来访问命令行参数:

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

argc是命令行参数的数量,而argv是指向参数字符串的指针数组。argv[0]指向程序的名字,从argv[1]argv[argc-1]都指向剩余的实际参数,而argv[argc]是空指针。在上述例子中,argc3argv[0]指向含有程序名的字符串,argv[1]指向字符串"names.dat",而argv[2]则指向字符串"dates.dat"

下面举例一个程序,该程序判断文件是否存在,如果存在,则判断它是否可以打开并读入。在运行程序时,用户将给出要检查的文件的名字:

canopen file

然后程序将显示出file can be opened或者显示出file can't be opened。如果在命令行中输入的实际参数的数量不对,那么程序将显示出消息usage: canopen filename来提醒用户canopen需要一个文件名。

/*
canopen.c 
--Checks whether a file can be opened for reading 
*/
#include  
#include  

int main(int argc, char *argv[]) 
{ 
    FILE *fp;
    if (argc != 2) { 
        printf("usage: canopen filename\n"); 
        exit(EXIT_FAILURE); 
    }
    if ((fp = fopen(argv[1], "r")) == NULL) { 
        printf("%s can’t be opened\n", argv[1]); 
        exit(EXIT_FAILURE); 
    } 
    
    printf("%s can be opened\n", argv[1]); 
    fclose(fp); 
    return 0; 
}

注意!!可以使用重定向来丢弃canopen的输出,并简单地测试它返回的状态值。


22.2.6 临时文件

FILE *tmpfile(void);    
char *tmpnam(char *s); 

现实世界中的程序经常需要产生临时文件,即只在程序运行时存在的文件。例如,C编译器就常常产生临时文件。编译器可能先把C程序翻译成一些存储在文件中的中间形式,稍后把程序翻译成目标代码时,编译器会读取这些文件。一旦程序完全通过了编译,就不再需要保留那些含有程序中间形式的文件了。提供了两个函数用来处理临时文件,即tmpfile函数和tmpnam函数。

tmpfile函数创建一个临时文件(用"wb+"模式打开),该临时文件将一直存在,除非关闭它或程序终止。tmpfile函数的调用会返回文件指针,此指针可以用于稍后访问该文件:

FILE *tempptr; 
... 
tempptr = tmpfile();   /* creates a temporary file */ 
//如果创建文件失败,tmpfile函数会返回空指针。

虽然tmpfile函数很易于使用,但它有两个缺点:

  • 无法知道tmpfile函数创建的文件名是什么
  • 无法在以后使文件变为永久的。如果这些缺陷导致了问题,备选的解决方案就是用fopen函数产生临时文件。当然,我们不希望此文件拥有和前面已经存在的文件相同的名字,因此需要一种方法来产生新的文件名。这也是tmpnam函数出现的原因。

tmpnam函数为临时文件产生名字。如果它的实际参数是空指针,那么tmpnam函数会把文件名存储到一个静态变量中,并且返回指向此变量的指针:

char *filename; 
... 
filename = tmpnam(NULL); /* creates a temporary file name */ 

否则,tmpnam函数会把文件名复制到程序员提供的字符数组中:

char filename[L_tmpnam]; 
... 
tmpnam(filename);   /* creates a temporary file name */ 

在后一种情况下,tmpnam函数也会返回指向数组第一个字符的指针。L_tmpnam中的一个宏,它指明了保存临时文件名的字符数组的长度。

请注意!!确保tmpnam函数所指向的数组至少有L_tmpnam个字符。此外,还要当心不能过于频繁地调用tmpnam函数。宏TMP_MAX(在中定义)指明了程序执行期间由tmpnam函数产生的临时文件名的最大数量。如果生成文件名失败,tmpnam返回空指针。


22.2.7 文件缓冲

int fflush(FILE *stream);                           
void setbuf(FILE * restrict stream, char * restrict buf);             
int setvbuf(FILE * restrict stream, char * restrict buf, int mode, size_t size);

向磁盘驱动器传入数据或者从磁盘驱动器传出数据都是相对较慢的操作。因此,在每次程序想读或写字符时都直接访问磁盘文件是不可行的。获得较好性能的诀窍就是缓冲(buffering):把写入流的数据存储在内存的缓冲区域内;当缓冲区满了(或者关闭流)时,对缓冲区进行“清洗”(写入实际的输出设备)。输入流可以用类似的方法进行缓冲:缓冲区包含来自输入设备的数据;从缓冲区读数据而不是从设备本身读数据缓冲可以大幅提升效率,因为从缓冲区读字符或者在缓冲区内存储字符几乎不花什么时间。当然,把缓冲区的内容传递给磁盘,或者从磁盘传递给缓冲区是需要花时间的,但是一次大的“块移动”比多次小字节移动要快很多

中的函数会在缓冲有用时自动进行缓冲操作。缓冲是在后台发生的,我们通常不需要关心它的操作。然而,极少的情况下我们可能需要更主动。如果真是如此,可以使用fflush函数、setbuf函数和setvbuf函数。

当程序向文件中写输出时,数据通常先放入缓冲区中。当缓冲区满了或者关闭文件时,缓冲区会自动清洗。然而,通过调用fflush函数,程序可以按我们所希望的频率来清洗文件的缓冲区。调用

fflush(fp);    /* flushes buffer for fp */

为和fp相关联的文件清洗了缓冲区。调用

fflush(NULL);  /* flushes all buffers */

清洗了全部输出流。如果调用成功,fflush函数会返回零;如果发生错误,则返回EOF

setvbuf函数允许改变缓冲流的方法,并且允许控制缓冲区的大小和位置。函数的第三个实际参数指明了期望的缓冲类型,该参数应为以下三个宏之一:

  • _IOFBF(满缓冲)。当缓冲区为空时,从流读入数据;当缓冲区满时,向流写入数据。
  • _IOLBF(行缓冲)。每次从流读入一行数据或者向流写入一行数据。
  • _IONBF(无缓冲)。直接从流读入数据或者直接向流写入数据,而没有缓冲区。

(所有这三种宏都在中进行了定义。)对于没有与交互式设备相连的流来说,满缓冲是默认设置。

setvbuf函数的第二个参数(如果它不是空指针的话)是期望缓冲区的地址。缓冲区可以有静态存储期、自动存储期,甚至可以是动态分配的。使缓冲区具有自动存储期可以在块退出时自动为其重新申请空间。动态分配缓冲区可以在不需要时释放缓冲区。setvbuf函数的最后一个参数是缓冲区内字节的数量。较大的缓冲区可以提供更好的性能,而较小的缓冲区可以节省空间。

例如,下面这个setvbuf函数的调用利用buffer数组中的N个字节作为缓冲区,而把stream的缓冲变成了满缓冲:

char buffer[N]; 
... 
setvbuf(stream, buffer, _IOFBF, N);

请注意!!setvbuf函数的调用必须在打开stream之后(流在前,缓冲在后),在对其执行任何其他操作之前。

用空指针作为第二个参数来调用setvbuf也是合法的,这样做就要求setvbuf创建一个指定大小的缓冲区。如果调用成功,那么setvbuf函数返回零。如果mode参数无效或者要求无法满足,那么setvbuf函数会返回非零值。

setbuf函数是一个较早期的函数,它设定了缓冲模式和缓冲区大小的默认值。如果buf是空指针,那么setbuf(stream, buf)调用就等价于

(void) setvbuf(stream, NULL, _IONBF, 0); 

否则,它就等价于

(void) setvbuf(stream, buf, _IOFBF, BUFSIZ);

这里的BUFSIZ是在中定义的宏。我们把setbuf函数看作陈旧的内容,不建议大家在新程序中使用

请注意!!使用setvbuf函数或者setbuf函数时,一定要确保在释放缓冲区之前已经关闭了流流在前,缓冲在后)。特别是,如果缓冲区是局部于函数的,并且具有自动存储期,一定要确保在函数返回之前关闭流。


22.2.8 其他文件操作

int remove(const char *filename);                
int rename(const char *old, const char *new);

remove函数和rename函数允许程序执行基本的文件管理操作。不同于本节中大多数其他函数,remove函数和rename函数对文件名(而不是文件指针)进行处理。如果调用成功,那么这两个函数都返回零;否则,都返回非零值。

remove函数删除文件:

remove("foo");  /* deletes the file named "foo" */

如果程序使用fopen函数(而不是tmpfile函数)来创建临时文件,那么它可以使用remove函数在程序终止前删除此文件。一定要确保已经关闭了要移除的文件,因为对于当前打开的文件,移除文件的效果是由实现定义的。

rename函数改变文件的名字:

rename("foo", "bar");  /* renames "foo" to "bar" */

对于用fopen函数创建的临时文件,如果程序需要使文件变为永久的,那么用rename函数改名是很方便的。如果具有新名字的文件已经存在了,改名的效果会由实现定义。

请注意!!如果打开了要改名的文件,那么一定要确保在调用rename函数之前关闭此文件。对打开的文件执行改名操作会失败


22.3 格式化的输入/输出

本节将介绍使用格式串来控制读/写的库函数。这些库函数包括已经知道的printf函数和scanf函数,它们可以在输入时把字符格式的数据转换为数值格式的数据,并且可以在输出时把数值格式的数据再转换成字符格式的数据。其他的输入/输出函数不能完成这样的转换。


22.3.1 …printf函数

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

fprintf函数和printf函数向输出流中写入可变数量的数据项,并且利用格式串来控制输出的形式。这两个函数的原型都是以...符号(省略号26.1节)结尾的,表明后面还有可变数量的实际参数。这两个函数的返回值是写入的字符数,若出错则返回一个负值。

fprintf函数和printf函数唯一的不同就是printf函数始终向stdout(标准输出流)写入内容,而fprintf函数则向它自己的第一个实际参数指定的流中写入内容:

printf("Total: %d\n", total);   /* writes to stdout */ 
fprintf(fp, "Total: %d\n", total); /* writes to fp */

printf函数的调用等价于fprintf函数把stdout作为第一个实际参数而进行的调用。

但是,不要以为fprintf函数只是把数据写入磁盘文件的函数。中的许多函数一样,fprintf函数可以用于任何输出流。事实上,fprintf函数最常见的应用之一(向标准误差流stderr写入出错消息)和磁盘文件没有任何关系。下面就是这类调用的一个示例:

fprintf(stderr, "Error: data file can’t be opened.\n");

stderr写入消息可以保证消息能出现在屏幕上,即使用户重定向stdout也没关系。

中还有另外两个函数也可以向流写入格式化的输出。这两个函数很不常见,一个是vfprintf函数,另一个是vprintf函数(26.1节)。它们都依赖于中定义的va_list类型,因此将和一起讨论。


22.3.2 …printf转换说明

printf函数和fprintf函数都要求格式串包含普通字符或转换说明。普通字符会原样输出,而转换说明则描述了如何把剩余的实参转换为字符格式显示出来。3.1节简要介绍了转换说明,其后的章节中还添加了一些细节。现在,我们将对已知的转换说明内容进行回顾,并且把剩余的内容补充完整。

...printf函数的转换说明由字符%和跟随其后的最多5个不同的选项构成。假设格式串为%#012.5Lg,分析如下:

标志 最小栏宽 精度 长度指定符 转换指定符
% #0 12 .5 L g

下面对上述这些选项进行详细的描述,选项的顺序必须与上面一致:

  • 标志(可选项,允许多于一个)。标志导致在栏内左对齐,而其他标志则会影响数的显示形式。表22-4给出了标志的完整列表。

表22-4 用于…printf函数的标志

标志 含义
- 在栏内左对齐(默认右对齐)
+ 有符号转换得到的数总是以+-开头(通常,只有负数前面附上-
空格 有符号转换得到的非负数前面加空格(+标志优先于空格标志)
# 0开头的八进制数,以0x0X开头的十六进制非零数。浮点数始终有小数点。不能删除由gG转换输出的数的尾部零
0(零) 前导零在数的栏宽内进行填充。如果转换是d、i、o、u、xX,而且指定了精度,那么可以忽略标志0-标志优先于0标志)
  • 最小栏宽(可选项)。如果数据项太小以至于无法达到这一宽度,那么会进行填充。(默认情况下会在数据项的左侧添加空格,从而使其在栏内右对齐。)如果数据项过大以至于超过了这个宽度,那么会完整地显示数据项。栏宽既可以是整数也可以是字符*。如果是字符*,那么栏宽由下一个参数决定。如果这个参数为负,它会被视为前面带-标志的正数。

  • 精度(可选项)。精度的含义依赖于转换指定符:如果转换指定符是d、i、o、u、x、X,那么精度表示最少位数(如果位数不够,则添加前导零);如果转换指定符是a、A、e、E、f、F,那么精度表示小数点后的位数;如果转换指定符是g、G,那么精度表示有效数字的个数;如果转换指定符是s,那么精度表示最大字节数。精度是由小数点(.)后跟一个整数或字符*构成的。如果出现字符*,那么精度由下一个参数决定。(如果这个参数为负,效果与不指定精度一样。)如果只有小数点,那么精度为零。

  • 长度指定符(可选项)。长度指定符配合转换指定符,共同指定传入的实际参数的类型(例如,%d通常表示一个int值,%hd用于显示short int值,%ld用于显示long int值)。表22-5列出了每一个长度指定符、可以使用的转换说明以及两者相结合时的类型(表中没有给出的长度指定符和转换指定符的结合会引起未定义的行为)。

表22-5 用于…printf函数的长度指定符

长度指定符 转换指定符 含义
hh① d、i、o、u、x、X signed char, unsigned char
hh① n signed char *
h d、i、o、u、x、X short int, unsigned short int
h n short int *
l(ell) d、i、o、u、x、X long int, unsigned long int
l(ell) n long int *
l(ell) c wint_t
l(ell) s wchar_t *
l(ell) a、A、e、E、f、F、g、G 无作用
ll①(ell-ell) d、i、o、u、x、X long long int, unsigned long long int
ll①(ell-ell) n long long int *
j① d、i、o、u、x、X intmax_t, uintmax_t
j① n intmax_t *
z① d、i、o、u、x、X size_t
z① n size_t *
t① d、i、o、u、x、X ptrdiff_t
t① n ptrdiff_t *
L a、A、e、E、f、F、g、G long double

①仅C99及之后的标准才有。

  • 转换指定符。转换指定符必须是表22-6中列出的某一种字符。注意f、F、e、E、g、G、aA全部设计用来输出double类型的值,但把它们用于float类型的值也可以:由于有默认实参提升(9.3节)float类型实参在传递给带有可变数量实参的函数时会自动转换为double类型。类似地,传递给...printf函数的字符也会自动转换为int类型,所以可以正常使用转换指定符c

表22-6 …printf 函数的转换指定符

转换指定符 含义
d、i int类型值转换为十进制形式
o、u、x、X 把无符号整数转换为八进制(o)十进制(u)十六进制(x、X)形式。x表示用小写字母a~f来显示十六进制数,X表示用大写字母A~F来显示十六进制数
f、F① double类型值转换为十进制形式,并且把小数点放置在正确的位置上。如果没有指定精度,那么在小数点后面显示6个数字
e、E double类型值转换为科学记数法形式。如果没有指定精度,那么在小数点后面显示6个数字。如果选择e,那么要把字母e放在指数前面;如果选择E,那么要把字母E放在指数前面
g、G g会把double类型值转换为f形式或者e形式。当数值的指数部分小于-4,或者指数部分大于等于精度值时,会选择e形式显示。尾部的零不显示(除非使用了#标志),且小数点仅在后边跟有数字时才显示出来。G会在F形式和E形式之间进行选择
a①、A① 使用格式[-]0xh.hhhhp±d的格式把double类型值转换为十六进制科学记数法形式。其中[-]是可选的负号,h代表十六进制数位,±是正号或者负号,d是指数。d为十进制数,表示2的幂。如果没有指定精度,在小数点后将显示足够的数位来表示准确的数值(如果可能的话)。a表示用小写形式显示a~fA表示用大写形式显示A~F。选择a还是A也会影响字母xp的情况
c 显示无符号字符的int类型值
s 写出由实参指向的字符。当达到精度值(如果存在)或者遇到空字符时,停止写操作
p void *类型值转换为可打印形式
n 相应的实参必须是指向int类型对象的指针。在该对象中存储...printf函数调用已经输出的字符数量,不产生输出
% 写字符%

①仅C99及之后的标准才有。

请注意!!请认真遵守上述规则。使用无效的转换说明会导致未定义的行为。


22.3.3. C99对…printf转化说明的修改(C99)

C99printf函数和fprintf函数的转换说明做了不少修改:

  • 增加了长度指定符C99中增加了hhlljzt长度指定符。hhll提供了额外的长度选项,j允许输出最大宽度整数(27.1节)zt分别使对size_tptrdiff_t类型值的输出变得更方便了。

  • 增加了转换指定符C99中增加了F、aA转换指定符。Ff一样,区别在于书写无穷数和NaN(见下面的讨论)的方式。aA转换指定符很少使用,它们和十六进制浮点常量相关,后者在第7章末尾的“问与答”部分讨论过。

  • 允许输出无穷数和NaNIEEE 754浮点标准允许浮点运算的结果为正无穷数负无穷数NaN(非数)。例如,1.0除以0.0会产生正无穷数,-1.0除以0.0会产生负无穷数,而0.0除以0.0会产生NaN(因为该结果在数学上是无定义的)。在C99中,转换指定符a、A、e、E、f、F、gG能把这些特殊值转换为可显示的格式。a、e、fg将正无穷数转换为infinfinity(都是合法的),将负无穷数转换为-inf-infinity,将NaN转换为nan-nan(后面可能跟着一对圆括号,圆括号里面有一系列的字符)。A、E、FGa、e、fg是等价的,区别仅在于使用大写字母(INFINFINITYNAN)。

  • 支持宽字符。从C99开始的另一个特性是使用fprintf来输出宽字符。%lc转换说明用于输出一个宽字符,%ls用于输出一个由宽字符组成的字符串。

  • 之前未定义的转换指定符现在允许使用了。在C89中,使用%le%lE%lf%lg以及%lG的效果是未定义的。这些转换说明在C99及其之后都是合法的(l长度指定符被忽略)。


22.3.4 …printf转换说明示例

现在来看一些示例。在前面的章节中我们已经看过大量日常转换说明的例子了,所以下面将集中说明一些更高级的应用示例。与前面的章节一样,这里将用·表示空格字符。

我们首先来看看标志作用于%d转换的效果(对其他转换的效果也是类似的)。表22-7的第一行显示了不带任何标志的%8d的效果。接下来的四行分别显示了带有标志-+空格以及0的效果(标志#从不用于%d)。剩下的几行显示了标志组合所产生的效果。

表22-7 标志作用于%d转换的效果

转换说明 对123应用转换说明的结果 对-123应用转换说明的结果
%8d •••••123 ••••-123
%-8d 123••••• -123••••
%+8d ••••+123 ••••-123
% 8d •••••123 ••••-123
%08d 00000123 -0000123
%-+8d +123•••• -123••••
%- 8d •123•••• -123••••
%+08d +0000123 -0000123
% 08d •0000123 -0000123

表22-8说明了标志#作用于oxXgG转换的效果。

表22-8 标志#的效果

转换说明 对123应用转换说明的结果 对123.0应用转换说明的结果
%8o •••••173
%#8o ••••0173
%8x ••••••7b
%#8x ••••0x7b
%8X ••••••7B
%#8X ••••0X7B
%8g •••••123
%#8g •123.000
%8G •••••123
%#8G •123.000

在前面的章节中,表示数值时已经使用过最小栏宽和精度了,所以这里不再给出更多的示例,只在表22-9中给出最小栏宽和精度作用于%s转换的效果。

表22-9 最小栏宽和精度作用于转换%s的效果

转换说明 对"bogus"应用转换说明的结果 对"buzzword"应用转换说明的结果
%6s •bogus buzzword
%-6s bogus• buzzword
%.4s bogu buzz
%6.4s ••bogu ••buzz
%-6.4s bogu•• buzz••

表22-10说明了%g转换如何以%e%f的格式显示数。表中的所有数都用转换说明%.4g进行了书写。前两个数的指数至少为4,因此它们是按照%e的格式显示的。接下来的8个数是按照%f的格式显示的。最后两个数的指数小于-4,所以也用%e的格式来显示。

表22-10 %g转换的示例

对数应用转换%.4g的结果
123456.00000000000 1.235e+05
12345.60000000000 1.235e+04
1234.56000000000 1235
123.45600000000 123.5
12.34560000000 12.35
1.23456000000 1.235
0.12345600000 0.1235
0.01234560000 0.01235
0.00123456000 0.001235
0.00012345600 0.0001235
0.00001234560 1.235e-05
0.00000123456 1.235e-06

过去,我们假设最小栏宽和精度都是嵌在格式串中的常量。用字符*取代最小栏宽或精度通常可以把它们作为格式串之后的实际参数加以指定。例如,下列printf函数的调用都产生相同的输出:

printf("%6.4d", i); 
printf("%*.4d", 6, i); 
printf("%6.*d", 4, i); 
printf("%*.*d", 6, 4, i)

注意!!为字符*填充的值刚好出现在待显示的值之前。顺便说一句,字符*的主要优势就是它允许使用宏来指定栏宽或精度:

printf("%*d", WIDTH, i)

我们甚至可以在程序执行期间计算栏宽或精度:

printf("%*d", page_width / num_cols, i)

最不常见的转换说明是%p%n%p转换允许显示指针的值:

printf("%p", (void *) ptr); /* displays value of ptr 

虽然在调试时%p偶尔有用,但它不是大多数程序员日常使用的特性。C标准没有指定用%p显示指针的形式,但很可能会以八进制十六进制数的形式显示。

转换%n用来找出到目前为止由...printf函数调用所显示的字符数量。例如,在调用:

printf("%d%n\n", 123, &len)

之后len的值将为3,因为在执行转换%n的时候printf函数已经显示3个字符(123)了。注意,在len前面必须要有&(因为%n要求指针),这样就不会显示len自身的值。


22.3.5 …scanf函数

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

fscanf函数和scanf函数从输入流读入数据,并且使用格式串来指明输入的格式。格式串的后边可以有任意数量的指针(每个指针指向一个对象)作为额外的实际参数。输入的数据项根据格式串中的转换说明进行转换并且存储在指针指向的对象中。

scanf函数始终从标准输入流stdin中读入内容,而fscanf函数则从它的第一个参数所指定的流中读入内容:

scanf("%d%d", &i, &j);  /* reads from stdin */ 
fscanf(fp, "%d%d", &i, &j);  /* reads from fp */

scanf函数的调用等价于以stdin作为第一个实际参数的fscanf函数调用。

如果发生输入失败(即没有输入字符可以读)或者匹配失败(即输入字符和格式串不匹配),那么...scanf函数会提前返回。(在C99中,输入失败还可能由编码错误导致。编码错误意味着我们试图按多字节字符的方式读取输入,但输入字符不是有效的多字节字符。)这两个函数都返回读入并且赋值给对象的数据项的数量。如果在读取任何数据项之前发生输入失败,那么会返回EOF

C程序中测试scanf函数的返回值的循环很普遍。例如,下列循环逐个读取一串整数,在首个遇到问题的符号处停止:

//惯用法
while (scanf("%d", &i) == 1) { 
    ...  
}

22.3.6 …scanf格式串

...scanf函数的调用类似于...printf函数的调用。然而,这种相似可能会产生误导,实际上...scanf函数的工作原理完全不同于...printf函数。我们应该把scanf函数和fscanf函数看作“模式匹配”函数。格式串表示的就是...scanf函数在读取输入时试图匹配的模式。如果输入和格式串不匹配,那么一旦发现不匹配函数就会返回。不匹配的输入字符将被“放回”留待以后读取

...scanf函数的格式串可能含有三种信息:

  • 转换说明...scanf函数格式串中的转换说明类似于...printf函数格式串中的转换说明。大多数转换说明(%[%c%n例外)会跳过输入项开始处的空白字符(3.2节)但是,转换说明不会跳过尾部的空白字符。如果输入含有·123¤,那么转换说明%d会读取·123,但是留下¤不读取。(这里使用·表示空格符,用¤表示换行符。)
  • 空白字符...scanf函数格式串中的一个或多个连续的空白字符与输入流中的零个或多个空白字符相匹配。
  • 非空白字符。除了%之外的非空白字符和输入流中的相同字符相匹配。

例如,格式串"ISBN %d-%d-%ld-%d"说明输入由下列这些内容构成:字母ISBN,可能有一些空白字符,一个整数,字符-,一个整数(前面可能有空白字符),字符-,一个长整数(前面可能有空白字符),字符-和一个整数(前面可能有空白字符)。


22.3.7 …scanf转换说明

用于...scanf函数的转换说明实际上比用于...printf函数的转换说明简单一些。...scanf函数的转换说明由字符%和跟随其后的下列选项(按照出现的顺序)构成。

  • 字符*(可选项)。字符*的出现意味着赋值屏蔽(assignment suppression):读入此数据项,但是不会把它赋值给对象。用*匹配的数据项不包含在...scanf函数返回的计数中。
  • 最大栏宽(可选项)。最大栏宽限制了输入项中的字符数量。如果达到了这个最大值,那么此数据项的转换将结束。转换开始处跳过的空白字符不进行统计。
  • 长度指定符(可选项)。长度指定符表明用于存储输入数据项的对象的类型与特定转换说明中的常见类型长度不一致。表22-11列出了每一个长度指定符、可以使用的转换说明以及两者相结合时的类型(表中没有给出的长度指定符和转换指定符的结合会引起未定义的行为)。

表22-11 用于...scanf函数的长度指定符

长度指定符 转换指定符 含义
hh① d、i、o、u、x、X、n signed char , unsigned char
h d、i、o、u、x、X、n short int , unsigned short int
l(ell) d、i、o、u、x、X、n long int , unsigned long int
l(ell) a、A、e、E、f、F、g、G double *
l(ell) c、s、[ wchar_t *
ll①(ell-ell) d、i、o、u、x、X、n long long int , unsigned long long int
j① d、i、o、u、x、X、n intmax_t , uintmax_t
z① d、i、o、u、x、X、n size_t *
t① d、i、o、u、x、X、n ptrdiff_t *
L a、A、e、E、f、F、g、G long double *

① 仅C99及之后的标准才有。

  • 转换指定符。转换指定符必须是表22-12中列出的某一种字符。

表22-12 用于...scanf函数的转换指定符

转换指定符 含义
d 匹配十进制整数,假设相应的实参是int *类型
i 匹配整数,假设相应的实参是int *类型。假定数是十进制形式的,除非它以0开头(说明是八进制形式),或者以0x0X开头(十六进制形式)
o 匹配八进制整数。假设相应的实参是unsigned int *类型
u 匹配十进制整数。假设相应的实参是unsigned int *类型
x、X 匹配十六进制整数。假设相应的实参是unsigned int *类型
a①、A①、e、E、f、F①、g、G 匹配浮点数。假设相应的实参是float *类型。在C99中,该数可以是无穷大或NaN
c 匹配n个字符,这里的n是最大栏宽。如果没有指定栏宽,那么就匹配一个字符。假设相应的实参是指向字符数组的指针(如果没有指定栏宽,就指向字符对象)。不在末尾添加空字符
s 匹配一串非空白字符,然后在末尾添加空字符。假设相应的实参是指向字符数组的指针
[ 匹配来自扫描集合的非空字符序列,然后在末尾添加空字符。假设相应的实参是指向字符数组的指针
p ...printf函数的输出格式匹配指针值。假设相应的实参是指向void*对象的指针
n 相应的实参必须指向int类型的对象。把到目前为止读入的字符数量存储到此对象中。没有输入会被吸收进去,而且...scanf函数的返回值也不会受到影响
% 匹配字符%

① 仅C99及之后的标准才有。

数值型数据项可以始终用符号(+-)作为开头。然而,说明符ouxX把数据项转换成无符号的形式,所以通常不用这些说明符来读取负数。

说明符[是说明符s更加复杂(且更加灵活)的版本。使用[的完整转换说明格式是%[集合]或者%[^集合],这里的集合可以是任意字符集。(但是,如果]是集合中的一个字符,那么它必须首先出现。)%[集合]匹配集合(即扫描集合)中的任意字符序列。%[^集合]匹配不在集合中的任意字符序列(换句话说,构成扫描集合的全部字符都不在集合中)。例如,%[abc]匹配的是只含有字母abc的任何字符串,而%[^abc]匹配的是不含有字母abc的任何字符串。

...scanf函数的许多转换指定符和中的数值转换函数(26.2节)有着紧密的联系。这些函数把字符串(如"-297")转换成与其等价的数值(-297)。例如,说明符d寻找可选的+号或-号,后边跟着一串十进制的数字。这样就与把字符串转换成十进制数的strtol函数所要求的格式完全一样了。表22-13展示了转换指定符和数值转换函数之间的对应关系。

表22-13 ...scanf转换指定符和数值转换函数之间的对应关系

转换指定符 字符串转换函数
d 10作为基数的strtol函数
i 0作为基数的strtol函数
o 8作为基数的strtoul函数
u 10作为基数的strtoul函数
x、X 16作为基数的strtoul函数
a、A、e、E、f、F、g、G strtod函数

请注意!!编写scanf函数的调用时需要十分小心。scanf格式串中无效的转换说明就像printf格式串中的无效转换说明一样糟糕,都会导致未定义的行为。


22.3.8 C99对…scanf转换说明的改变(C99)

C99开始的标准对scanffscanf的转换说明做了一些改变,但没有...printf函数那么多。

  • 增加了长度指定符。从C99开始增加了hhlljzt长度指定符,它们与...printf转换说明中的长度指定符相对应。
  • 增加了转换指定符。从C99开始增加了FaA转换指定符,提供这些转换指定符是为了与...printf相一致。...scanf函数把它们与eEfgG等同看待。
  • 具有读无穷数和NaN的能力。正如...printf函数可以输出无穷数和NaN一样,...scanf函数可以读这些值。为了能够正确读出,这些数的形式应该与...printf函数相同,忽略大小写(例如,INFinf都会被认为是无穷数)。
  • 支持宽字符...scanf函数能够读多字节字符,并在存储时将之转换为宽字符。%lc转换说明用于读出单个的多字节字符或者一系列多字节字符;%ls用于读取由多字节字符组成的字符串(在结尾添加空字符)。%l[集合]%l[^集合]转换说明也可以读取多字节字符串。

22.3.9 scanf示例

下面三个表格包含了scanf的调用示例。每个示例都把scanf函数应用于它右侧的输入字符。用高亮显示的字符会被调用吸收。调用后变量的值会出现在输入的右侧。

表22-14中的示例说明了把转换说明、空白字符以及非空白字符组合在一起的效果。在这三种情况下没有对j赋值,所以j的值在scanf调用前后保持不变。表22-15中的示例显示了赋值屏蔽和指定栏宽的效果。表22-16中的示例描述了更加深奥的转换指定符(即i[n)。

表22-14 scanf示例(第一组)

scanf函数的调用 输入 变量
n = scanf(“%d%d”, &i, &j); 12•,•34¤ n:1 i:12 j:不变
n = scanf(“%d,%d”, &i, &j); 12•,•34¤ n:1 i:12 j:不变
n = scanf(“%d ,%d”, &i, &j); 12•,•34¤ n:2 i:12 j:34
n = scanf(“%d, %d”, &i, &j); 12•,•34¤ n:1 i:12 j:不变

表22-15 scanf示例(第二组)

scanf函数的调用 输入 变量
n = scanf(“%*d%d”, &i); 12•34¤ n:1 i:34
n = scanf(“%*s%s”, str); My•Fair•Lady¤ n:1 str:“Fair”
n = scanf(“%1d%2d%3d”, &i, &j, &k); 12345¤ n:3 i:1 j:23 k:45
n = scanf(“%2d%2s%2d”, &i, str, &j); 123456¤ n:3 i:12 str:“34” j:56

表22-16 scanf示例(第三组)

scanf函数的调用 输入 变量
n = scanf(“%i%i%i”, &i, &j, &k); 12•012•0x12¤ n:3 i:12 j:10 k:18
n = scanf(“%[0123456789]”, str); 123abc¤ n:1 Str: “123”
n = scanf(“%[0123456789]”, str); abc123¤ n:0 str:不变
n = scanf(“%[^0123456789]”, str); abc123¤ n:1 Str: “abc”
n = scanf(“%*d%d%n”, &i, &j); 10•20•30¤ n:1 i:20 j:5

22.3.10 检测文件末尾和错误条件

void clearerr(FILE *stream);    
int feof(FILE *stream); 
int ferror(FILE *stream);

如果要求...scanf函数读入并存储n个数据项,那么希望它的返回值就是n。如果返回值小于n,那么一定是出错了。一共有三种可能情况:

  • 文件末尾。函数在完全匹配格式串之前遇到了文件末尾。
  • 读取错误。函数不能从流中读取字符。
  • 匹配失败。数据项的格式是错误的。例如,函数可能在搜索整数的第一个数字时遇到了一个字母。

但是如何知道遇到的是哪种情况呢?在许多情况下,这是无关紧要的,程序出问题了,可以把它舍弃。然而,有时候需要查明失败的原因。

每个流都有与之相关的两个指示器:错误指示器(error indicator)文件末尾指示器(end-of-file indicator),当打开流时会清除这些指示器。遇到文件末尾就设置文件末尾指示器,遇到读错误就设置错误指示器。(输出流上发生写错误时也会设置错误指示器。)匹配失败不会改变任何一个指示器。

一旦设置了错误指示器或者文件末尾指示器,它就会保持这种状态直到被显式地清除(可能通过clearerr函数的调用)。clearerr会同时清除文件末尾指示器和错误指示器

clearerr(fp);  /* clears eof and error indicators for fp */

//某些其他库函数因为副作用可以清除某种指示器或两种都可以清除,
//所以不需要经常使用clearerr函数。

我们可以调用feof函数和ferror函数来测试流的指示器,从而确定出先前在流上的操作失败的原因。如果为与fp相关的流设置了文件末尾指示器,那么feof(fp)函数调用就会返回非零值。如果设置了错误指示器,那么ferror(fp)函数的调用也会返回非零值。而其他情况下,这两个函数都会返回零。

scanf函数返回小于预期的值时,可以使用feof函数和ferror函数来确定原因。如果feof函数返回了非零的值,那么就说明已经到达了输入文件的末尾。如果ferror函数返回了非零的值,那么就表示在输入过程中产生了读错误。如果两个函数都没有返回非零值,那么一定是发生了匹配失败。不管问题是什么,scanf函数的返回值都会告诉我们在问题产生前所读入的数据项的数量。

为了明白feof函数和ferror函数可能的使用方法,现在来编写一个函数。此函数用来搜索文件中以整数起始的行。下面是预计的函数调用方式:

n = find_int("foo");

其中,"foo"是要搜索的文件的名字,函数返回找到的整数的值并将其赋给n。如果出现问题(文件无法打开或者发生读错误,再或者没有以整数起始的行),find_int函数将返回一个错误代码(分别是-1-2-3)。我们假设文件中没有以负整数起始的行。

int find_int(const char *filename) 
{ 
    FILE *fp = fopen(filename, "r"); 
    int n; 
    
    if (fp == NULL) 
        return1;   /* can’t open file */ 
    
    while (fscanf(fp, "%d", &n) != 1) { 
        if (ferror(fp)) { 
            fclose(fp); 
            return2;    /* input error */ 
        } 
        if (feof(fp)) { 
            fclose(fp); 
            return3;    /* integer not found */ 
        } 
        fscanf(fp, "%*[^\n]"); /* skips rest of line */ 
    } 
    fclose(fp); 
    return n; 
} 

while循环的控制表达式调用fscanf函数的目的是从文件中读取整数。如果尝试失败了(fscanf函数返回的值不为1),那么find_int函数就会调ferror函数和feof函数来了解是发生了读错误还是遇到了文件末尾。如果都不是,那么fscanf函数一定是由于匹配错误而失败的,因此find_int函数会跳过当前行的剩余字符并尝试下一行。请注意用转换说明%*[^\n]跳过全部字符直到下一个换行符为止的用法。(我们对扫描集合已有所了解,可以拿出来显摆一下了!)


22.4 字符的输入/输出

本节将讨论用于读和写单个字符的库函数。这些函数可以处理文本流和二进制流。

请注意!!本节中的函数把字符作为int类型而非char类型的值来处理。这样做的原因之一就是,输入函数是通过返回EOF来说明文件末尾(或错误)情况的,而EOF又是一个负的整型常量。


22.4.1 输出函数

int fputc(int c, FILE *stream); 
int putc(int c, FILE *stream); 
int putchar(int c); 

putchar函数向标准输出流stdout写一个字符:

putchar(ch);  /* writes ch to stdout */ 

fputc函数和putc函数是putchar函数向任意流写字符的更通用的版本:

fputc(ch, fp); /* writes ch to fp */ 
putc(ch, fp); /* writes ch to fp */

虽然putc函数和fputc函数做的工作相同,但是putc通常作为宏来实现(也有函数实现),而fputc函数则只作为函数实现。putchar本身通常也定义为宏:

#define putchar(c) putc((c), stdout)

标准库既提供putc又提供fputc,看起来很奇怪。但是,正如在14.3节看到的那样,宏有几个潜在的问题。C标准允许putc宏对stream参数多次求值,而fputc则不可以。虽然程序员通常偏好使用putc,因为它的速度较快,但fputc作为备选也是可用的。

如果出现了写错误,那么上述这3个函数都会为流设置错误指示器并且返回EOF。否则,它们都会返回写入的字符。


22.4.2 输入函数

int fgetc(FILE *stream); 
int getc(FILE *stream); 
int getchar(void); 
int ungetc(int c, FILE *stream);

getchar函数从标准输入流stdin中读入一个字符:

ch = getchar(); /* reads a character from stdin */

fgetc函数和getc函数从任意流中读入一个字符:

ch = fgetc(fp); /* reads a character from fp */ 
ch = getc(fp); /* reads a character from fp */ 

3个函数都把字符看作unsigned char类型的值(返回之前转换成int类型)。因此,它们不会返回EOF之外的负值。

getcfgetc之间的关系类似于putcfputc之间的关系。getc通常作为宏来实现(也有函数实现),而fgetc则只作为函数实现。getchar本身通常也定义为宏:

#define getchar() getc(stdin)

对于从文件中读取字符来说,程序员通常喜欢getc胜过fgetc。因为getc一般是宏的形式,所以它执行起来的速度较快。如果getc不合适,那么可以用fgetc作为备选。(标准允许getc宏对参数多次求值,这可能会有问题。)

如果出现问题,那么这3个函数的行为是一样的。如果遇到了文件末尾,那么这3个函数都会设置流的文件末尾指示器,并且返回EOF。如果产生了读错误,则它们都会设置流的错误指示器,并且返回EOF。为了区分这两种情况,可以调用feof函数或者ferror函数。

fgetc函数、getc函数和getchar函数最常见的用法之一就是从文件中逐个读入字符,直到遇到文件末尾。一般习惯使用下列while循环来实现此目的:

//惯用法
while ((ch = getc(fp)) != EOF) { 
    ... 
}

在从与fp相关的文件中读入字符并且把它存储到变量ch(它必须是int类型的)之中后,判定条件会把chEOF进行比较。如果ch不等于EOF,则表示还未到达文件末尾,就可以执行循环体。如果ch等于EOF,则循环终止。

请注意!!始终要把fgetcgetcgetchar函数的返回值存储在int类型的变量中,而不是char类型的变量中。把char类型变量与EOF进行比较可能会得到错误的结果。

还有另外一种字符输入函数,即ungetc函数。此函数把从流中读入的字符“放回”并清除流的文件末尾指示器。如果在输入过程中需要往前多看一个字符,那么这种能力可能会非常有效。比如,为了读入一系列数字,并且在遇到首个非数字时停止操作,可以写成

while (isdigit(ch = getc(fp))) { 
    ... 
} 
ungetc(ch, fp); /* pushes back last character read */ 

通过持续调用ungetc函数而放回的字符数量(不干涉读操作)依赖于实现和所含的流类型。只有第一次的ungetc函数调用保证会成功。调用文件定位函数(即fseekfsetposrewind)(22.7节)会导致放回的字符丢失。

ungetc返回要求放回的字符。如果试图放回EOF或者试图放回超过最大允许数量的字符数,则ungetc会返回EOF

22.4.2.1 程序——复制文件

下面的程序用来进行文件的复制操作。当程序执行时,会在命令行上指定原始文件名和新文件名。例如,为了把文件f1.c复制给文件f2.c,可以使用命令:

fcopy f1.c f2.c

如果命令行上的文件名不是两个,或者至少有一个文件无法打开,那么程序fcopy将产生出错消息。

/*
fcopy.c
--Copies a file
*/
#include  
#include  

int main(int argc, char *argv[]) 
{ 
    FILE *source_fp, *dest_fp; 
    int ch; 
    
    if (argc != 3) { 
        fprintf(stderr, "usage: fcopy source dest\n"); 
        exit(EXIT_FAILURE); 
    } 
    
    if ((source_fp = fopen(argv[1], "rb")) == NULL) { 
        fprintf(stderr, "Can't open %s\n", argv[1]); 
        exit(EXIT_FAILURE); 
    } 
    
    if ((dest_fp = fopen(argv[2], "wb")) == NULL) { 
        fprintf(stderr, "Can't open %s\n", argv[2]); 
        fclose(source_fp); 
        exit(EXIT_FAILURE); 
    } 
    
    while ((ch = getc(source_fp)) != EOF) 
        putc(ch, dest_fp); 
    
    fclose(source_fp); 
    fclose(dest_fp); 
    return 0; 
}

采用"rb""wb"作为文件模式,使fcopy程序既可以复制文本文件也可以复制二进制文件。如果用"r""w"来代替,那么程序将无法复制二进制文件。


22.5 行的输入/输出

下面将介绍读和写行的库函数。虽然这些函数也可有效地用于二进制的流,但是它们多数用于文本流


22.5.1 输出函数

int fputs(const char * restrict s, FILE * restrict stream); 
int puts(const char *s);

我们在13.3节已经见过puts函数,它是用来向标准输出流stdout写入字符串的:

puts("Hi, there!"); /* writes to stdout */

在写入字符串中的字符以后,puts函数总会添加一个换行符。

fputs函数是puts函数的更通用版本。此函数的第二个实参指明了输出要写入的流:

fputs("Hi, there!", fp);  /* writes to fp */

不同于puts函数,fputs函数不会自己写入换行符,除非字符串中本身含有换行符。

当出现写错误时,上面这两种函数都会返回EOF。否则,它们都会返回一个非负的数。


22.5.2 输入函数

char *fgets(char * restrict s, int n, FILE * restrict stream); 

13.3节中已经见过在新标准中废弃的gets函数了。

fgets函数是gets函数的更通用版本,它可以从任意流中读取信息。fgets函数也比gets函数更安全,因为它会限制将要存储的字符的数量。下面是使用fgets函数的方法,假设str是字符数组的名字:

fgets(str, sizeof(str), fp); /* reads a line from fp */

此调用将导致fgets函数逐个读入字符,直到遇到首个换行符时或者已经读入了sizeof(str)-1个字符时结束操作,这两种情况哪种先发生都可以。如果fgets函数读入了换行符,那么它会把换行符和其他字符一起存储。(因此,gets函数从来不存储换行符,而fgets函数有时会存储换行符。)

如果出现了读错误,或者是在存储任何字符之前达到了输入流的末尾,那么gets函数和fgets函数都会返回空指针。(通常,可以使用feof函数或ferror函数来确定出现的是哪种情况。)否则,两个函数都会返回自己的第一个实参(指向保存输入的数组的指针)。与预期一样,两个函数都会在字符串的末尾存储空字符。

现在已经学习了fgets函数,那么建议大家用fgets函数来代替gets函数。对于gets函数而言,接收数组的下标总有可能越界,所以只有在保证读入的字符串正好适合数组大小时使用gets函数才是安全的。在没有保证的时候(通常是没有的),使用fgets函数要安全得多。注意!!如果把stdin作为第三个实参进行传递,那么fgets函数就会从标准输入流中读取:

fgets(str, sizeof(str), stdin);

22.6 块的输入/输出

size_t fread(void * restrict ptr, 
             size_t size, size_t nmemb, 
             FILE * restrict stream); 
size_t fwrite(const void * restrict ptr, 
              size_t size, size_t nmemb, 
              FILE * restrict stream);

fread函数和fwrite函数允许程序在单步中读和写大的数据块。如果小心使用,fread函数和fwrite函数可以用于文本流,但是它们主要还是用于二进制的流

fwrite函数用来把内存中的数组复制给流。fwrite函数调用中第一个参数是数组的地址,第二个参数是每个数组元素的大小(以字节为单位),第三个参数是要写的元素数量,第四个参数是文件指针,此指针说明了要写的数据位置。例如,为了写整个数组a的内容,就可以使用下列fwirte函数调用:

fwrite(a, sizeof(a[0]), sizeof(a) / sizeof(a[0]), fp); 

没有规定必须写入整个数组,数组任何区间的内容都可以轻松地写入。fwrite函数返回实际写入的元素(不是字节)的数量。如果出现写入错误,那么此数就会小于第三个实参。

fread函数将从流读入数组的元素。fread函数的参数类似于fwrite函数的参数:数组的地址、每个元素的大小(以字节为单位)、要读的元素数量以及文件指针。为了把文件的内容读入数组a,可以使用下列fread函数调用:

n = fread(a, sizeof(a[0]), sizeof(a) / sizeof(a[0]), fp);

检查fread函数的返回值是非常重要的。此返回值说明了实际读的元素(不是字节)的数量。此数应该等于第三个参数,除非达到了输入文件末尾或者出现了错误。可以用feof函数和ferror函数来确定出问题的原因。

请注意!!不要把fread函数的第二个参数和第三个参数搞混了。思考下面这个fread函数调用:

fread(a, 1, 100, fp);

这里要求fread函数读入100个元素,且每个元素占1字节,所以它返回0~100范围内的某个值。下面的调用则要求fread函数读入一个有100字节的块:

fread(a, 100, 1, fp);

此情况中fread函数的返回值不是0就是1

当程序需要在终止之前把数据存储到文件中时,使用fwrite函数是非常方便的。以后程序(或者另外的程序)可以使用fread函数把数据读回内存中来。不考虑形式的话,数据不一定要是数组格式的。fread函数和fwrite函数都可以用于所有类型的变量,特别是可以用fread函数读结构或者用fwrite函数写结构。例如,为了把结构变量s写入文件,可以使用下列形式的fwrite函数调用:

fwrite(&s, sizeof(s), 1, fp);

请注意!!使用fwrite输出包含指针值的结构时需要小心。读回时不能保证这些值一定有效。


22.7 文件定位

int fgetpos(FILE * restrict stream, fpos_t * restrict pos); 
int fseek(FILE *stream, long int offset, int whence); 
int fsetpos(FILE *stream, const fpos_t *pos); 
long int ftell(FILE *stream); 
void rewind(FILE *stream);

每个流都有相关联的文件位置(file position)。打开文件时,会将文件位置设置在文件的起始处。(但如果文件按“追加”模式打开,初始的文件位置可以在文件起始处,也可以在文件末尾,这依赖于具体的实现。)然后,在执行读或写操作时,文件位置会自动推进,并且允许按照顺序贯穿整个文件。

虽然对许多应用程序来说顺序访问是很好的,但是某些程序需要具有在文件中跳跃的能力,即可以在这里访问一些数据,然后到别处访问其他数据。例如,如果文件包含一系列记录,我们可能希望直接跳到特定的记录处,并对其进行读或更新。通过提供5个函数来支持这种形式的访问,这些函数允许程序确定当前的文件位置或者改变文件的位置。

fseek函数改变与第一个参数(即文件指针)相关的文件位置。第三个参数说明新位置是根据文件的起始处、当前位置还是文件末尾来计算。为此定义了3种宏:

  • SEEK_SET:文件的起始处。
  • SEEK_CUR:文件的当前位置。
  • SEEK_END:文件的末尾处。

第二个参数是个(可能为负的)字节计数。例如,为了移动到文件的起始处,搜索的方向将为SEEK_SET,而且字节计数为0

fseek(fp, 0L, SEEK_SET);   /* moves to beginning of file */

为了移动到文件的末尾,搜索的方向应该是SEEK_END

fseek(fp, 0L, SEEK_END);   /* moves to end of file */

为了往回移动10个字节,搜索的方向应该是SEEK_CUR,并且字节计数为-10

fseek(fp, -10L, SEEK_CUR);  /* moves back 10 bytes */

注意!!字节计数是long int类型的,所以这里用0L-10L作为实参。(当然,用0-10也可以,因为参数会自动转换为正确的类型。)

通常情况下,fseek函数返回0。如果产生错误(例如,要求的位置不存在),那么fseek函数就会返回非零值。

顺便提一句,文件定位函数最适用于二进制流。C语言不禁止程序对文本流使用这些定位函数,但考虑到操作系统的差异,要小心使用fseek函数对流是文本的还是二进制的很敏感。对于文本流而言,要么offsetfseek的第二个参数)必须为0,要么whencefseek的第三个参数)必须是SEEK_SET,且offset的值通过前面的ftell函数调用获得。(换句话说,我们只可以利用fseek函数移动到文件的起始处或者文件的末尾处,或者返回前面访问过的位置。)对于二进制流而言,fseek函数不要求支持whenceSEEK_END的调用。

ftell函数以长整数返回当前文件位置。[如果发生错误,ftell函数会返回-1L,并且把错误码存储到errno(24.2节)中。]ftell可能会存储返回的值并且稍后将其提供给fseek函数调用,这也使返回前面的文件位置成为可能:

long file_pos; 
... 
file_pos = ftell(fp);    /* saves current position */ 
... 
fseek(fp, file_pos, SEEK_SET);  /* returns to old position */ 

如果fp是二进制流,那么ftell(fp)调用会以字节计数来返回当前文件位置,其中0表示文件的起始处。但是,如果fp是文本流,ftell(fp)返回的值不一定是字节计数,因此最好不要对ftell函数返回的值进行算术运算。例如,为了查看两个文件位置的距离而把ftell返回的值相减不是个好做法。

rewind函数会把文件位置设置在起始处。调用rewind(fp)几乎等价于fseek(fp, 0L, SEEK_SET),两者的差异是rewind函数不返回值,但会为fp清除错误指示器。

fseek函数和ftell函数都有一个问题:它们只能用于文件位置可以存储在长整数中的文件。为了用于非常大的文件,C语言提供了另外两个函数:fgetpos函数和fsetpos函数。这两个函数可以用于处理大型文件,因为它们用fpos_t类型的值来表示文件位置。fpos_t类型值不一定就是整数,比如,它可以是结构。

调用fgetpos(fp, &file_pos)会把与fp相关的文件位置存储到file_pos变量中。调用fsetpos(fp, &file_pos)会为fp设置文件的位置,此位置是存储在file_pos中的值。(此值必须通过前面的fgetpos调用获得。)如果fgetpos函数或者fsetpos函数调用失败,那么都会把错误码存储到errno中。当调用成功时,这两个函数都会返回0;否则,都会返回非零值。

下面是使用fgetpos函数和fsetpos函数保存文件位置并且稍后返回该位置的方法:

fpos_t file_pos; 
... 
fgetpos(fp, &file_pos);  /* saves current position */ 
... 
fsetpos(fp, &file_pos);  /* returns to old position */

22.7.1 程序——修改零件记录文件

下面这个程序打开包含part结构的二进制文件,把结构读到数组中,把每个结构的成员on_hand置为0,然后再把此结构写回到文件中。注意,程序用"rb+"模式打开文件,因此既可读又可写:

/*
invclear.c
--Modifies a file of part records by setting the quantity 
on hand to zero for all records
*/
#include  
#include  

#define NAME_LEN 25 
#define MAX_PARTS 100 

struct part { 
    int number; 
    char name[NAME_LEN+1]; 
    int on_hand; 
} inventory[MAX_PARTS]; 

int num_parts; 

int main(void) 
{ 
    FILE *fp; 
    int i; 
    
    if ((fp = fopen("inventory.dat", "rb+")) == NULL) { 
        fprintf(stderr,"Can’t open inventory file\n"); 
        exit(EXIT_FAILURE); 
    } 
    
    num_parts = fread(inventory, sizeof(struct part), 
                        MAX_PARTS, fp); 
    
    for (i = 0; i < num_parts; i++) 
        inventory[i].on_hand = 0; 
    
    rewind(fp);
    fwrite(inventory, sizeof(struct part), num_parts, fp); 
    fclose(fp); 
    
    return 0; 
} 

顺便说一下,这里调用rewind函数是很关键的。在调用完fread函数之后,文件位置是在文件的末尾。如果没有先调用rewind函数,就调用fwrite函数,那么fwrite函数将在文件末尾添加新数据,而不会覆盖旧数据。


22.8 字符串的输入/输出

本节里描述的函数有一点不同,因为它们与数据流或文件并没有什么关系。相反,它们允许我们使用字符串作为流读写数据sprintfsnprintf函数将按和写到数据流一样的方式写字符到字符串,sscanf函数从字符串中读出数据就像从数据流中读数据一样。这些函数非常类似于printfscanf函数,也都是非常有用的。sprintfsnprintf函数可以让我们使用printf的格式化能力,不需要真的往流中写入数据。类似地,sscanf函数也可以让我们使用scanf函数强大的模式匹配能力。下面将详细讲解sprintfsnprintfsscanf函数。

3个相似的函数(vsprintfvsnprintfvsscanf)也属于头,但这些函数依赖于在中声明的va_list类型。我们将推迟到26.1节讨论该头时再来介绍这3个函数。


22.8.1 输出函数

int sprintf(char * restrict s, const char * restrict format, ...); 
int snprintf(char *restrict s, size_t n, const char * restrict format, ...); //C99新增

sprintf函数类似于printf函数和fprintf函数,唯一的不同就是sprintf函数把输出写入(第一个实参指向的)字符数组而不是流中。sprintf函数的第二个参数是格式串,这与printf函数和fprintf函数所用的一样。例如,函数调用

sprintf(date, "%d/%d/%d", 9, 20, 2010); 

会把"9/20/2010"复制到date中。当完成向字符串写入的时候,sprintf函数会添加一个空字符,并且返回所存储字符的数量(不计空字符)。如果遇到错误(宽字符不能转换成有效的多字节字符),sprintf返回负值。

sprintf函数有着广泛的应用。例如,有些时候可能希望对输出数据进行格式化,但不是真的要把数据写出。这时就可以使用sprintf函数来实现格式化,然后把结果存储在字符串中,直到需要产生输出的时候再写出。sprintf函数还可以用于把数转换成字符格式。

snprintf函数与sprintf一样,但多了一个参数n。写入字符串的字符不会超过n-1,结尾的空字符不算;只要n不是0,就会有空字符。(我们也可以这样说:snprintf最多向字符串中写入n个字符,最后一个是空字符。)例如,函数调用

snprintf(name, 13, "%s, %s", "Einstein", "Albert");

会把"Einstein, Al"写入到name中。

如果没有长度限制,snprintf函数返回需要写入的字符数(不包括空字符)。如果出现编码错误,snprintf函数返回负值。为了查看snprintf函数是否有空间写入所有要求的字符,可以测试其返回值是否非负且小于n


22.8.2 输入函数

int sscanf(const char * restrict s, const char * restrict format, ... ); 

sscanf函数与scanf函数和fscanf函数都很类似,唯一的不同就是sscanf函数是从(第一个参数指向的)字符串而不是流中读取数据。sscanf函数的第二个参数是格式串,这与scanf函数和fscanf函数所用的一样。

sscanf函数对于从由其他输入函数读入的字符串中提取数据非常方便。例如,可以使用fgets函数来获取一行输入,然后把此行数据传递给sscanf函数进一步处理:

fgets(str, sizeof(str), stdin);  /* reads a line of input */ 
sscanf(str, "%d%d", &i, &j);   /* extracts two integers */ 

sscanf函数代替scanf函数或者fscanf函数的好处之一就是,可以按需多次检测输入行,而不再只是一次,这样使识别替换的输入格式和从错误中恢复都变得更加容易了。下面思考一下读取日期的问题。读取的日期既可以是月/日/年的格式,也可以是月-日-年的格式。假设str包含一行输入,那么可以按如下方法提取出月、日和年的信息:

if (sscanf(str, "%d /%d /%d", &month, &day, &year) == 3) 
    printf("Month: %d, day: %d, year: %d\n", month, day, year); 
else if (sscanf(str, "%d -%d -%d", &month, &day, &year) == 3) 
    printf("Month: %d, day: %d, year: %d\n", month, day, year); 
else 
    printf("Date not in the proper form\n");

scanf函数和fscanf函数一样,sscanf函数也返回成功读入并存储的数据项的数量。如果在找到第一个数据项之前到达了字符串的末尾(用空字符标记),那么sscanf函数会返回EOF


问与答

问1:如果我使用输入重定向或输出重定向,那么重定向的文件名会作为命令行参数显示出来吗?

答:不会。操作系统会把这些文件名从命令行中移走。假设用下列输入运行程序:

demo foo <in_file bar >out_file baz 

argc的值为4argv[0]将指向程序名,argv[1]会指向"foo"argv[2]会指向"bar"argv[3]会指向"baz"

问2:我一直认为行的末尾都是以换行符标记的,现在你说行末标记根据操作系统的不同而不同。如何解释这种差异呢?

答:C库函数使得每一行看起来都是以一个换行符结束的。不管输入文件有回车符、回行符,还是两者都有,getc等库函数都只会返回一个换行符。输出函数执行相反的操作。如果程序调用库函数向文件中输出换行符,函数会把该字符转换成恰当的行末标记。C语言的这种实现使得程序的可移植性更好,也更易编写。我们处理文本文件时不需要担心行的末尾到底是怎么表示的。注意,对以二进制模式打开的文件进行输入/输出操作时,不需要进行字符转换——回车符、回行符跟其他字符同等对待。

问3:我正打算编写一个需要在文件中存储数据的程序,该文件可供其他程序读取。就数据的存储格式而言,文本格式和二进制格式哪种更好呢?

答:这要看情况。如果数据全部是文本,那么用哪种格式存储没有太大的差异。然而,如果数据包含数,那么决定就比较困难一些了。

通常二进制格式更可取,因为此种格式的读和写都非常快。当存储到内存中时,数已经是二进制格式了,所以将它们复制给文件是非常容易的。用文本格式写数据相对就会慢许多,因为每个数必须要转换成字符格式(通常用fprintf函数)。以后读取文件同样要花费更多的时间,因为必须要把数从文本格式转换回二进制格式。此外,就像在22.1节看到的那样,以二进制格式存储数据常常能节省空间。

然而,二进制文件有两个缺点。一是很难阅读,这也就妨碍了调试过程;二是二进制文件通常无法从一个系统移植到另一个系统,因为不同类型的计算机存储数据的方式是不同的。比如,有些机器用2字节存储整数,而有些机器则用4字节来存储。字节顺序(大端/小端)也是一个问题。

问4:用于UNIX系统的C程序好像从不在模式字符串中使用字母b,即使待打开的文件是二进制格式也是如此。这是什么原因呢?

答:在UNIX系统中,文本文件和二进制文件具有完全相同的格式,所以不需要使用字母b。但是,UNIX程序员仍应该包含字母b,这样他们的程序将更容易移植到其他操作系统上。

问5:我已经看过调用fopen函数并且把字母t放在模式字符串中的程序了。字母t意味着什么呢?

答:C标准允许其他的字符在模式字符串中出现,但是它们要跟在rwab+的后边。有些编译器允许使用t来说明待打开的文件是文本模式而不是二进制模式。当然,无论如何文本模式都是默认的,所以字母t没有任何作用。在可能的情况下,最好避免使用字母t和其他不可移植的特性

问6:为什么要调用fclose函数来关闭文件呢?当程序终止时,所有打开的文件都会自动关闭,难道不是这样吗?

答:通常情况下是这样的,但如果调用abort函数(26.2节)来终止程序就不是了。即使在不用abort函数的时候,调用fclose函数仍有许多理由。首先,这样会减少打开文件的数量。操作系统对程序每次可以打开的文件数量有限制,而大规模的程序可能会与此种限制相冲突。(定义在中的宏FOPEN_MAX指定了可以同时打开的文件的最少数量。)其次,这样做使程序更易于理解和修改。通过寻找fclose函数,读者更容易确定不再使用此文件的位置。最后,这样做很安全。关闭文件可以确保正确地更新文件的内容和目录项。如果将来程序崩溃了,至少该文件不会受到影响。

问7:我正在编写的程序会提示用户输入文件的名字。我要设置多长的字符数组才可以存储这个文件名字呢?

答:这与使用的操作系统有关。好在你可以使用宏FILENAME_MAX(定义在中)来指定数组的大小。FILENAME_MAX是字符串的长度,这个字符串用于存储保证可以打开的最长的文件名。

问8fflush可以清除同时为读和写而打开的流吗?

答:根据C标准,当流(1)为输出打开,或者(2)为更新打开并且最后一个操作不是读时,调用fflush的结果才有定义。在其他所有情况下,调用fflush函数的结果是未定义的。当传递空指针给fflush函数时,它会清除所有满足(1)(2)的流。

问9:在...printf函数或...scanf函数调用中,格式串可以是变量吗?

答:当然。它可以是char *类型的任意表达式。这个性质使...printf函数和...scanf函数比我们想象的更加多样。请看下面这个来自KernighanRitchie所著的《C程序设计语言》一书的经典示例。此示例显示程序的命令行参数,以空格分隔:

while (--argc > 0)  
    printf((argc  >  1)  ?  "%s " : "%s", *++argv);

这里的格式串是表达式(argc > 1) ? "%s " : "%s",其结果是除了最后一个参数以外,对其他所有命令行参数都会使用"%s "

问10:除了clearerr函数,哪些库函数可以清除流的错误指示器和文件末尾指示器?

答:调用rewind函数可以清除这两种指示器,就好像打开或重新打开流一样;调用ungetc函数、fseek函数或者fsetpos函数仅可以清除文件末尾指示器。

问11:我无法使feof函数工作。这是因为即使到了文件末尾,它好像还是返回0。我做错了什么吗?

答:当前面的读操作失败时,feof函数只会返回一个非零值。在尝试读之前,不能使用feof函数来检查文件末尾。相反,你应该首先尝试读,然后检查来自输入函数的返回值。如果返回的值表明操作不成功,那么你可以随后使用feof函数来确定失败是不是因为到了文件末尾。换句话说,最好不要认为调用feof函数是检测文件末尾的方法,而应把它看作确认读取操作失败是因为到了文件末尾的方法

问12:我始终不明白为什么输入/输出库除了提供名为fputcfgetc的函数以外,还提供名为putcgetc的宏。依据21.1节的介绍,putcgetc已经有两种版本了(宏和函数)。如果需要真正的函数而不是宏,我们可以通过取消宏的定义来显示putc函数或getc函数。那么,为什么要有fputcfgetc存在呢?

答:这是历史原因造成的。在标准化以前,C语言没有规则要求用真正的函数在库中备份每个带参数的宏。putc函数和getc函数传统上只作为宏来实现,而fputc函数和fgetc函数则只作为函数来实现。

问13:把fgetc函数、getc函数或者getchar函数的返回值存储到char类型变量中会有什么问题?我不明白为什么判断char类型变量的值是否为EOF会得到错误的结果。

答:有两种情况可能导致该判定得出错误的结果。为了使下面的讨论更具体,这里假设使用二进制补码存储方式。

首先,假定char类型是无符号类型。(回忆一下,有些编译器把char作为有符号类型来处理,而有些编译器则把它看成无符号类型的。)现在假设getc函数返回EOF,把该返回值存储在名为chchar类型变量中。如果EOF表示-1(通常如此),那么ch的值将为255。把ch(无符号字符)与EOF(有符号整数)进行比较就要求把ch转换为有符号整数(在这个例子中是255)。因为255不等于-1,所以与EOF的比较失败了。

反之,现在假设char是有符号类型。如果getc函数从二进制流中读取了一个含有值255的字节,这样会产生什么情况呢?因为ch是有符号字符,所以把255存储在char类型变量中将为它赋值-1。如果判断ch是否等于EOF,则会(错误地)产生真结果。

问1422.4节描述的字符输入函数要求在读取用户输入之前看到回车键。如何编写能直接响应键盘输入的程序?

答:我们注意到,getcfgetcgetchar都是分配缓冲区的,这些函数在用户按下回车键时才开始读取输入。为了实时读取键盘输入(这对某些类型的程序很重要),需要使用适合你的操作系统的非标准库。例如,UNIX中的curses库通常提供这一功能。

问15:正在读取用户输入时,如何跳过当前输入行中剩下的全部字符呢?

答:一种可能是编写一个小函数来读入并且忽略第一个换行符之前的所有字符(包含换行符):

void skip_line(void) 
{ 
    while (getchar() != '\n') 
        ; 
}

另外一种可能是要求scanf函数跳过第一个换行符前的所有字符:

scanf("%*[^\n]");  /* skips characters up to new-line */ 

scanf函数将读取第一个换行符之前的所有字符,但是不会把它们存储下来(*表示赋值屏蔽)。使用scanf函数的唯一问题是它会留下换行符不读,所以可能需要单独丢弃换行符。

无论做什么,都不要调用fflush函数:

fflush(stdin); /* effect is undefined */

虽然某些实现允许使用fflush函数来“清洗”未读取的输入,但是这样做并不好。fflush函数是用来清洗输出流的。C标准规定fflush函数对输入流的效果是未定义的

问16:为什么把fread函数和fwrite函数用于文本流是不好的呢?

答:困难之一是,在某些操作系统中对文本文件执行写操作时,会把换行符变成一对字符(详细内容见22.1节)。我们必须考虑这种扩展,否则就很可能搞错数据的位置。例如,如果使用fwrite函数来写含有80个字符的块,因为换行符可能被扩展,所以有些块可能会占用多于80字节的空间。

问17:为什么有两套文件定位函数(即fseek/ftellfsetpos/fgetpos)呢?一套函数难道不够吗?

答:fseek函数和ftell函数作为C库的一部分已有些年头了,但它们有一个缺点:它们假定文件位置能够用long int类型的值表示。由于long int通常是32位的类型,当文件大小超过2147483647字节时,fseek函数和ftell函数可能无法使用。针对这个问题,创建C89标准时在中增加了fsetposfgetpos。这两个函数不要求把文件位置看作数,因此就没有long int的限制了。但是也不要认为必须使用fsetposfgetpos,如果你的实现支持64位的long int类型,即使对很大的文件也可以使用fseekftell

问18:为什么本章不讨论屏幕控制,即移动光标、改变屏幕上字符颜色等呢?

答:C语言没有提供用于屏幕控制的标准函数。标准只发布那些通过广泛的计算机和操作系统可以合理标准化的问题,而屏幕控制超出了这个范畴。在UNIX中解决这个问题的习惯做法是使用curses库,这个库支持不依赖终端方式的屏幕控制。

类似地,也没有标准函数可以用来构建带有图形用户界面的程序。不过,可以用C函数调用来访问操作系统中的窗口API(应用程序接口)


写在最后

本文是博主阅读《C语言程序设计:现代方法(第2版·修订版)》时所作笔记,日后会持续更新后续章节笔记。欢迎各位大佬阅读学习,如有疑问请及时联系指正,希望对各位有所帮助,Thank you very much!

你可能感兴趣的:(C语言,c语言,笔记,开发语言)