1. EOF宏,C语言EOF宏详解2. gets和fgets函数及其区别,C语言gets和fgets函数详解3. puts和fputs函数及其区别,C语言puts和fputs函数详解4. feof和ferror函数,C语言feof和ferror函数详解5. setbuf与setvbuf函数,C语言setbuf与setvbuf函数详解6. fseek、ftell和rewind函数,C语言fseek、ftell和rewind函数详解7. fread和fwrite函数,C语言fread和fwrite函数详解8. C语言文本流和二进制流9. C语言文件的打开和关闭,C语言文件操作10. C语言格式化输出11. C语言格式化输入12. C语言文件随机访问fseek()和ftell()函数
EOF 是 End Of File 的缩写,在 C 语言标准库中的定义如下:
#define EOF (-1)
迄今为止,关于 EOF 作用的观点各异。大多数程序员认为“文件中有一个 EOF 字符,用于表示文件的结尾”。但实际上,这个观点并不正确(或者说并不完整),在文件所包含的数据中,并没有什么文件结束符。从 EOF 宏的定义中可以看出,EOF 宏的值为 -1,属于 int 类型的数据,在 32 位系统中,可以表示为 0xFFFFFFFF。由此可见,EOF 并不是一个字符,也不是文件中实际存在的内容。那么,为什么会有这样的观点存在呢?
其实原因很简单,因为对一些数据读取函数(如 fgetc 与 getc 函数)而言,如果读到文件末尾(也可以理解为“如果不能从文件中读取”,即文件已经读完或者文件读取出错),则返回一个整数(-1),这就是所谓的 EOF。因此,EOF 宏不但能够表示读文件到了结尾这一状态(这种状态可以用 feof() 来检测),还能表示 I/O 操作中的读、写错误(通常可以用 ferror() 来检测)以及其他一些关联操作的错误状态。
看下面这段示例代码:
- int main(void)
- {
- FILE *fp=NULL;
- int c;
- fp=fopen("myfile.txt","r");
- if(fp == NULL)
- {
- printf("不能够访问该文件.\n");
- exit(1);
- }
- while((c=fgetc(fp)) != EOF)
- {
- printf("%x\n", c);
- }
- fclose(fp);
- fp=NULL;
- }
对于 fgetc(或者 getc)函数,它返回一个 int 类型的数据。在正常情况下,fgetc(或者 getc)函数以 unsigned char 的方式读取文件流,并扩张为一个整数返回。换言之,fgetc(或 getc)函数从文件流中读取一个字节,并加上 24 个 0,成为一个小于 256 的整数,然后返回。
对于上面的示例代码,在正常读取的情况下,fgetc 函数返回的整数均小于 256(即 0x0~0xFF)。因此,就算读到了字符 0xFF,由于变量 c 被定义为 int 型,实际上这里的 c 等于 0x000000FF,而不是等于 EOF(即 0xFFFFFFFF),当然也不会误判为文件结尾。也就是说,即使是上面的示例代码遇到字符 0xFF,while 循环也不会结束,因为 0xFF 会被转化 0x000000FF,而不是 0xFFFFFFFF(EOF)。
既然如此,如果这里把 c 定义为 char 类型,那么其结果又将会怎样呢?如下面的示例代码所示:
- char c;
- fp=fopen("myfile.txt","r");
- if(fp == NULL)
- {
- printf("不能够访问该文件.\n");
- exit(1);
- }
- while((c=fgetc(fp)) != EOF)
- {
- printf("%x\n", c);
- }
因为文本文件中存储的是 ASCII 码,而 ASCII 码中 FF 代表空值(blank),所以如果读文件返回了 0xFF,也就说明已经到了文本文件的结尾处。也就是说,在语句“while((c=fgetc(fp))!=EOF)”中,当读取的字符为 0xFF 时,子语句“c=fgetc(fp)”中的“fgetc(fp)”的值由 0x000000FF 转换为 char 类型(即 c 等于 0xFF);而在执行子语句“c!=EOF”时,字符与整数比较,c 被转换为 0xFFFFFFFF,条件成立,遇到空格字符时就退出。由此可见,如果是二进制文件,其中可能会包含许多 0xFF,因此不能把读到 EOF 作为文件结束的条件,而此时只能使用 feof() 函数。
再假如,这里又将 c 定义为 unsigned char 类型,结果会与上面的 char 类型相同吗?如下面的示例代码所示:
- unsigned char c;
- fp=fopen("myfile.txt","r");
- if(fp == NULL)
- {
- printf("不能够访问该文件.\n");
- exit(1);
- }
- while((c=fgetc(fp))!= EOF)
- {
- printf("%x\n", c);
- }
在上面的“while((c=fgetc(fp))!=EOF)”语句中,就算是语句“fgetc(fp)”返回的结果为 -1(即 0xFFFFFFFF),但通过语句“c=fgetc(fp)”对其强制转换 unsigned char 类型,即 c 等于 0xFF。而在执行子语句“c!=EOF”时,c 被转换成 0x000000FF,永远也不可能等于 0xFFFFFFFF,因此表达式“c!=EOF”将永远成立。
由此可见,只有将 c 定义成 int 类型的变量,才能够与 fgetc 函数返回类型一致。
每当讨论 gets 函数时,大家不由自主地就会想起 1988 年的“互联网蠕虫”,它在 UNIX 操作系统的 finger 后台程序中使用一个 gets 调用作为它的攻击方式之一。很显然,对蠕虫病毒的实现来说, gets 函数的功劳不可小视。不仅如此,GCC 也不推荐使用gets和puts函数。
那么,究竟是什么原因导致 gets 函数这么不招人待见呢?
我们知道,对于 gets 函数,它的任务是从 stdin 流中读取字符串,直至接收到换行符或 EOF 时停止,并将读取的结果存放在 buffer 指针所指向的字符数组中。这里需要注意的是,换行符不作为读取串的内容,读取的换行符被转换为 null('\0') 值,并由此来结束字符串。即换行符会被丢弃,然后在末尾添加 null('\0') 字符。其函数的原型如下:
- char* gets(char* buffer);
如果读入成功,则返回与参数 buffer 相同的指针;如果读入过程中遇到 EOF 或发生错误,返回 NULL 指针。因此,在遇到返回值为 NULL 的情况,要用 ferror 或 feof 函数检查是发生错误还是遇到 EOF。
函数 gets 可以无限读取,不会判断上限,所以程序员应该确保 buffer 的空间足够大,以便在执行读操作时不发生溢出。也就是说,gets 函数并不检查缓冲区 buffer 的空间大小,事实上它也无法检查缓冲区的空间。
如果函数的调用者提供了一个指向堆栈的指针,并且 gets 函数读入的字符数量超过了缓冲区的空间(即发生溢出),gets 函数会将多出来的字符继续写入堆栈中,这样就覆盖了堆栈中原来的内容,破坏一个或多个不相关变量的值。如下面的示例代码所示:
- int main(void)
- {
- char buffer[11];
- gets(buffer);
- printf("输出: %s\n",buffer);
- return 0;
- }
示例代码的运行结果为:
aaa
输出: aaa
根据运行结果,当用户在键盘上输入的字符个数大于缓冲区 buffer 的最大界限时,gets 函数也不会对其进行任何检查,因此我们可以将恶意代码多出来的数据写入堆栈。由此可见,gets 函数是极其不安全的,可能成为病毒的入口,因为 gets 函数没有限制输入的字符串长度。所以我们应该使用 fgets 函数来替换 gets 函数,实际上这也是大多程序员所推荐的做法。
相对于 gets 函数,fgets 函数最大的改进就是能够读取指定大小的数据,从而避免 gets 函数从 stdin 接收字符串而不检查它所复制的缓冲区空间大小导致的缓存溢出问题。当然,fgets 函数主要是为文件 I/O 而设计的(注意,不能用 fgets 函数读取二进制文件,因为 fgets 函数会把二进制文件当成文本文件来处理,这势必会产生乱码等不必要的麻烦)。其中,fgets 函数的原型如下:
- char *fgets(char *buf, int bufsize, FILE *stream);
该函数的第二个参数 bufsize 用来指示最大读入字符数。如果这个参数值为 n,那么 fgets 函数就会读取最多 n-1 个字符或者读完一个换行符为止,在这两者之中,最先满足的那个条件用于结束输入。
与 gets 函数不同的是,如果 fgets 函数读到换行符,就会把它存储到字符串中,而不是像 gets 函数那样丢弃它。即给定参数 n,fgets 函数只能读取 n-1 个字符(包括换行符)。如果有一行超过 n-1 个字符,那么 fgets 函数将返回一个不完整的行(只读取该行的前 n-1 个字符)。但是,缓冲区总是以 null('\0') 字符结尾,对 fgets 函数的下一次调用会继续读取该行。
也就是说,每次调用时,fgets 函数都会把缓冲区的最后一个字符设为 null('\0'),这意味着最后一个字符不能用来存放需要的数据。所以如果某一行含有 size 个字符(包括换行符),要想把这行读入缓冲区,要把参数 n 设为 size+1,即多留一个位置存储 null('\0')。
最后,它还需要第 3 个参数来说明读取哪个文件。如果是从键盘上读入数据,可以使用 stdin 作为该参数,如下面的代码所示:
- int main(void)
- {
- char buffer[11];
- fgets(buffer,11,stdin);
- printf("输出: %s\n",buffer);
- return 0;
- }
对于上面的示例代码,如果输入的字符串小于或等于 10 个字符,那么程序将完整地输出结果;如果输入的字符串大于 10 个字符,那么程序将截断输入的字符串,最后只输出前 10 个字符。示例代码运行结果为:
aaaaaaaaaaaaaaaa
输出: aaaaaaaaaa
除此之外,C99 还提供了 fgets 函数的宽字符版本 fgetws 函数,其函数的一般原型如下面的代码所示:
- wchar_t *fgetws(wchar_t * restrict s, int n, FILE * restrict stream);
该函数的功能与 fgets 函数一样。
与 gets 函数一样,对于 puts 函数,同样建议使用 fputs 函数来代替 puts 函数。如下面的示例代码所示:
- int main(void)
- {
- char buffer[11];
- fgets(buffer,11,stdin);
- fputs(buffer,stdout);
- return 0;
- }
int main(void) {char buffer[11];fgets(buffer,11,stdin);fputs(buffer,stdout);return 0; }
其中,puts 函数的原型如下所示:
int puts(const char *str);
我们知道,puts 函数主要用于向标准输出设备(屏幕)写入字符串并换行,即自动写一个换行符('\n')到标准输出。理论上,该函数的作用与“printf("%s\n",str);”语句相同。但是,puts 函数只能输出字符串,不能进行相关的格式变换。与此同时,它需要遇到 null('\0') 字符才停止输出。因此,非字符串或无 null('\0') 字符的字符数组最好不要使用该函数打印,否则无法正常结束。如下面的代码所示:
- int main(void)
- {
- char str[] = {'H','E','L','L','O'};
- puts(str);
- return 0;
- }
int main(void) {char str[] = {'H','E','L','L','O'};puts(str);return 0; }
在上面的示例代码中,因为字符数组 str 在结尾处缺少一个 null('\0') 字符(也就是说它不是一个严格意义上的字符串)。因此,在调用 puts 函数的时候,程序将不知道什么时候停止输出,从而导致输出结果未定义。运行结果如下图所示:
图 1 示例代码的运行结果(Microsoft Visual Studio 2010)
正确的做法是应该在字符数组 str 的结尾处添加一个 null('\0') 字符,如下面的示例代码所示:
- char str[] = {'H','E','L','L','O','\0'};
char str[] = {'H','E','L','L','O','\0'};
fputs 函数的函数原型如下所示:
int fputs(const char *str, FILE *stream);
相对于 puts 函数,fputs 函数用来向指定的文件写入一个字符串(不换行)。当然,也可以使用 stdout 作为参数进行输出显示(它同样需要遇到 null('\0') 字符才停止输出),如下面的代码所示:
- int main(void)
- {
- char str[] = {'H','E','L','L','O','\0'};
- fputs(str,stdout);
- return 0;
- }
int main(void) {char str[] = {'H','E','L','L','O','\0'};fputs(str,stdout);return 0; }
其运行结果如下图所示:
、
当然,fputs 函数主要用于对指定文件进行写入操作,如下面的示例代码所示:
- int main(void)
- {
- FILE *fp=NULL;
- fp=fopen("myfile.txt","wb");
- if(fp == NULL)
- {
- printf("不能够访问该文件.\n");
- exit(1);
- }
- fputs("this is a test", fp);
- fclose(fp);
- fp=NULL;
- return 0;
- }
int main(void) {FILE *fp=NULL;fp=fopen("myfile.txt","wb");if(fp == NULL){printf("不能够访问该文件.\n");exit(1);}fputs("this is a test", fp);fclose(fp);fp=NULL;return 0; }
运行上面的示例代码,文件“myfile.txt”会被写入一行“this is a test”字符串。
与 fgetws 一样,C99 同样也提供了 fputs 函数的宽字符版本 fputws,其函数的一般原型如下面的代码所示:
int fputws(const wchar_t * restrict s, FILE * restrict stream);
正如前面所讲,fgetc(或者getc)函数返回 EOF 并不一定就表示文件结束,读取文件出错时也会返回 EOF。即 EOF 宏不但能够表示读到了文件结尾这一状态,而且还能表示 I/O 操作中的读、写错误以及其他一些关联操作的错误状态。很显然,仅凭返回 EOF(-1) 就认为文件结束显然是不正确的。
也正因为如此,我们需要使用 feof 函数来替换 EOF 宏检测文件是否结束。当然,在用 feof 函数检测文件是否结束的同时,也需要使用 ferror 函数来检测文件读取操作是否出错,当 ferror 函数返回为真时就表示有错误发生。在实际的程序中,应该每执行一次文件操作,就用 ferror 函数检测是否出错。
其中,文件结束检测函数 feof 的一般原型如下:
int feof(FILE *fp);
值得注意的是,函数 feof 只用于检测流文件,当文件内部位置指针指向文件结束时,并未立即置位 FILE 结构中的文件结束标记,只有再执行一次读文件操作,才会置位结束标志,此后调用 feof 才会返回为真。看下面的示例代码:
- int main(void)
- {
- FILE *fp=NULL;
- char c;
- fp=fopen("myfile.txt","r");
- if(fp == NULL)
- {
- printf("不能够访问该文件.\n");
- exit(1);
- }
- while(!feof(fp))
- {
- c = fgetc(fp);
- printf("%c:\t%x\n",c,c);
- }
- fclose(fp);
- fp=NULL;
- }
int main(void) {FILE *fp=NULL;char c;fp=fopen("myfile.txt","r");if(fp == NULL){printf("不能够访问该文件.\n");exit(1);}while(!feof(fp)){c = fgetc(fp);printf("%c:\t%x\n",c,c);}fclose(fp);fp=NULL; }
这里假设“myfile.txt”文件中存储的是“ABCDEF”,从表面上看,该示例代码的输出结果应该是“ABCDEF”。但实际情况并非如此,你会发现最终输出结果会多输出一个结束字符EOF(这里的 EOF 是 fgetc 函数的返回值,并不是文件中存在的 EOF),运行结果如图 1 所示。
图 1 示例代码的运行结果(Microsoft Visual Studio 2010)
因此,为了解决上述情况,需要在“while(!feof(fp))”循环语句中加以判断,如下面的代码所示:
- int main(void)
- {
- FILE *fp=NULL;
- char c;
- fp=fopen("myfile.txt","r");
- if(fp == NULL)
- {
- printf("不能够访问该文件.\n");
- exit(1);
- }
- while(!feof(fp))
- {
- c=fgetc(fp);
- if(c!=-1)
- {
- printf("%c:\t%x\n",c,c);
- }
- }
- fclose(fp);
- fp=NULL;
- }
int main(void) {FILE *fp=NULL;char c;fp=fopen("myfile.txt","r");if(fp == NULL){printf("不能够访问该文件.\n");exit(1);}while(!feof(fp)){c=fgetc(fp);if(c!=-1){printf("%c:\t%x\n",c,c);}}fclose(fp);fp=NULL; }
当然,也可以采用下面的这种方式进行判断:
- while(true)
- {
- c=fgetc(fp);
- if(feof(fp))
- {
- break;
- }
- printf("%c:\t%x\n",c,c);
- }
while(true) {c=fgetc(fp);if(feof(fp)){break;}printf("%c:\t%x\n",c,c); }
或者采用如下形式:
- c = fgetc(fp);
- while(!feof(fp))
- {
- printf("%c:\t%x\n",c,c);
- c = fgetc(fp);
- }
c = fgetc(fp); while(!feof(fp)) {printf("%c:\t%x\n",c,c);c = fgetc(fp); }
不论采用上述 3 种方式的哪一种,都能够得到如图 2 所示的正确结果。
图 2 示例代码的运行结果(Microsoft Visual Studio 2010)
正如上面所阐述的,在使用 feof 函数检测文件是否结束的同时,还需要使用 ferror 函数来检测文件读取操作是否出错,当 ferror 函数返回为真时就表示有错误发生。如下面的示例代码所示:
- while(!feof(fp))
- {
- if(ferror(fp))
- {
- perror("error");
- break;
- }
- c=fgetc(fp);
- if(c!=-1)
- {
- printf("%c:\t%x\n",c,c);
- }
- }
while(!feof(fp)) {if(ferror(fp)){perror("error");break;}c=fgetc(fp);if(c!=-1){printf("%c:\t%x\n",c,c);} }
除此之外,最后还需要调用 clearerr 函数来清除文件出错标志和文件结束标志,将其置为 0。如下面的示例代码所示:
- if(ferror(fp))
- {
- clearerr(fp);
- /*************/
- }
在讨论 setvbuf 与 setbuf 函数之前,先来看如下一段示例代码:
- int main(void)
- {
- FILE* fp=NULL;
- int fd;
- const char *f1="testfprintf.log";
- const char *f2="testwrite.log";
- fp = fopen(f1, "wb");
- if(fp == NULL)
- {
- return -1;
- }
- fd = open(f2, O_WRONLY|O_CREAT|O_EXCL, 0666);
- if(fd < 0)
- {
- return -1;
- }
- while(1)
- {
- fprintf(fp, "fprintf------|\n");
- write(fd, "write|\n", sizeof("write|\n"));
- sleep(1);
- }
- return 0;
- }
在上面的示例代码中,使用 fprintf 函数对文件 testfprintf.log 执行写入操作,使用 write 函数对文件 testwrite.log 执行写入操作。这里需要注意的是,因为 fprintf 函数会缓冲 4096 字节的数据,只有当达到这么多字节的数据时才会进行实际的磁盘写入。
因此,运行上面的示例程序,然后实时查看 testfprintf.log 文件与 testwrite.log 文件,会发现 testfprintf.log 文件不会被实时写入,只有当写入的数据的大小为 4096 字节的倍数的时候才会被写入;而 write 函数则不同,因为它不进行任何缓冲(直接写入磁盘),所以文件 testwrite.log 不断有数据写入,运行结果如图 1 所示。
图 1 示例代码的运行结果
在上面的示例中不难发现,通过提供缓冲区可以尽可能减少 read 和 write 调用的次数,从而降低执行 I/O 的时间。在 C 语言中,标准 I/O 库提供了 3 种类型的缓冲。
1) 全缓冲
在进行 I/O 操作时,只有当 I/O 缓冲区被填满时,才进行实际的 I/O 操作。对于驻留在磁盘上的文件,通常就是由标准 I/O 库来实施全缓冲的。在一个流上执行第一次 I/O 操作时,相关标准 I/O 函数通常调用 malloc 来获得需要使用的缓冲区。
在默认情况下,全缓冲的缓冲区可以由标准 I/O 例程自动刷新。当然,也可以通过调用 fflush 函数来强制刷新一个数据流。但需要特别注意的是,在标准 I/O 库方面,flush 函数意味着将缓冲区中的内容写到磁盘上;而在终端驱动程序方面,flush 函数则表示丢弃已存储在缓冲区中的数据。
2) 行缓冲
在这种情况下,只有当在输入和输出中遇到换行符时,才执行实际的 I/O 作。当然,因为标准 I/O 库用来收集每一行的缓冲区的长度是固定的,所以只要填满了缓冲区,即使还没有写一个换行符,也必须进行 I/O 操作。
很显然,它允许我们一次输出一个字符(如 fputc 函数),但只有在写完一行之后才进行实际 I/O 操作。当流涉及一个终端时,通常使用行缓冲。例如,使用最频繁的 printf 函数就是采用行缓冲,所以感觉不出缓冲的存在。
3) 不带缓冲
标准 IO 库不对字符进行缓冲存储。在一般情况下,标准错误流 stderr 通常也是不带缓冲的。
相对于这些系统默认的情况,也可以通过调用标准库函数 setbuf 和 setvbuf 来更改缓冲类型。函数 setbuf 和 setvbuf 将使得在打开文件后用户可以建立自己的文件缓冲区,而不使用由 fopen 函数打开文件所设定的默认缓冲区。函数 setbuf 和 setvbuf 的一般函数原型如下所示:
void setbuf(FILE *fp, char *buf);
int setvbuf(FILE *fp, char *buf, int mode, size_t size);
使用 setbuf 与 setvbuf 函数指定文件的缓冲区一定要在文件读写之前。一旦用户自己指定了文件的缓冲区,文件的读写就要在用户指定的缓冲区中进行,而不在系统默认指定的缓冲区中进行。
对于 setbuf 函数,当指定参数 buf 为 null 时,setbuf 函数将使得文件 I/O 不带缓冲。如下面的示例代码所示:
- setbuf(fp, NULL);
对 setvbuf 函数来说,由于 setbuf 函数没有返回值,因此也无法确定 setbuf 函数的调用是否成功。在实际使用中,应该尽量使用 setvbuf 来替换 setbuf 函数,以验证流被成功地更改。如下面的示例代码所示:
- if (setvbuf(file, buf, buf ? _IOFBF : _IONBF, BUFSIZ) != 0)
- {
- }
对 setvbuf 函数,则由 malloc 函数来分配缓冲区,参数 size 指明了缓冲区的长度(必须大于 0),而参数 mode 则表示缓冲的类型,取值如下所示:
- _IOFBF,全缓冲。
- _IOLBF,行缓冲。
- _IONBF,不缓冲,此时忽略 buf 与 size 参数的值,直接读写文件,不再经过文件缓冲区缓冲。
对于文件的读写方式,C 语言不仅支持简单地顺序读写方式,还支持随机读写(即只要求读写文件中某一指定的部分)。对顺序读写方式来说,随机读写方式需要将文件内部的位置指针移动到需要读写的位置再进行读写,这通常也被称为文件的定位。
对于文件的定位,可以通过 rewind、fseek 与 ftell 函数来完成。
其中,rewind 函数用于将文件内部的位置指针重新指向一个流(数据流或者文件)的起始位置。这里需要注意的是,这里的“指针”表示的不是文件指针,而是文件内部的位置指针。即随着对文件的读写,文件的位置指针(指向当前读写字节)向后移动。而文件指针指向整个文件,如果不重新赋值,文件指针不会发生改变。
rewind 函数的一般原型如下所示:
void rewind(FILE *fp);
从上面的函数原型可以看出,rewind 并没有返回值,因此也无法做安全性检查。如下面的示例代码所示:
- FILE *fp=NULL;
- fp=fopen("Test.txt","r");
- if(fp==NULL)
- {
- }
- rewind(fp);
在上面的示例代码中,由于 rewind 函数没有返回值,所以我们很难判断“rewind(fp)”是否执行成功。因此,应该尽量使用 fseek 来替换 rewind 函数,从而以验证流已经成功地回绕。如下面的示例代码所示:
- if (fseek(fp, 0L, SEEK_SET) != 0)
- {
- }
相对于 rewind 函数而言,fseek 函数的功能更加强大,它用来设定文件的当前读写位置,从而可以实现以任意顺序访问文件的不同位置,以实现文件的随机访问。其函数的一般原型如下所示:
int fseek(FILE *fp,long offset,int from);
如果该函数执行成功,fp 将指向以 from 为基准,偏移 offset 个字节的位置,函数的返回值为 0;如果该函数执行失败(比如 offset 超过文件自身大小),则不改变 fp 指向的位置,函数的返回值为 -1,并设置 errno 的值,可以用 perror 函数来输出错误信息。
对于 fseek 函数中的参数:第一个参数 fp 为文件指针;第二个参数 offset 为偏移量,它表示要移动的字节数,整数表示正向偏移,负数表示负向偏移;第三个参数 from 表示设定从文件的哪里开始偏移,取值范围如表 1 所示。
起始点 | 表不符号 | 数字表示 |
---|---|---|
文件首 | SEEK_SET | 0 |
当前位置 | SEEK_CUR | 1 |
文件末尾 | SEEK_END | 2 |
由表 1 可知:
- SEEK_SET 表示从文件起始位置增加 offset 个偏移量为新的读写位置;
- SEEK_CUR 表示从目前的读写位置增加 offset 个偏移量为新的读写位置;
- SEEK_END 表示将读写位置指向文件尾后,再增加 offset 个偏移量为新的读写位置。
当 from 值为 SEEK_CUR 或 SEEK_END 时,参数 offset 允许出现负值。如下面的示例代码所示:
- /*将读写位置移动到离文件开头100字节处*/
- fseek(fp,100L,0);
- /*将读写位置移动到离文件当前位置100字节处*/
- fseek(fp,100L,1);
- /*将读写位置退回到离文件结尾100字节处*/
- fseek(fp,-100L,2);
- /*将读写位置移动到文件的起始位置*/
- fseek(fp,0L,SEEK_SET);
- /*将读写位置移动到文件尾*/
- fseek(fp,0L,SEEK_END);
不难发现,上面的语句“(void)fseek(fp,0L,SEEK_SET);”的作用实际上等同于 rewind 函数。与此同时,在使用 fseek 函数时,还应该注意如下 3 点。
- 首先,调用 fseek 函数的文件指针 fp 应该指向已经打开的文件,否则将会出现错误。
- 其次,fseek 函数一般用于二进制文件,当然也可以用于文本文件。需要特别注意的是,当 fseek 函数用于文本文件操作时,一定要注意回车换行的情况。因为在一般浏览工具(如 UltraEdit)中,回车换行被视为两个字符 0x0D 和 0x0A,但真实的文件读写和定位却按照一个字符 0x0A 进行处理。因此,在碰到此类问题时,可以考虑将文件整个读入内存,然后在内存中手工插入 0x0D的方法,这样可以达到较好的处理效果。
- 最后,fseek 函数只返回执行的结果是否成功,并不返回文件的读写位置。因此,你可以使用 ftell 函数来取得当前文件的读写位置。
ftell 函数的原型为:
long ftell(FILE *fp);
该函数用于得到文件位置指针当前位置相对于文件首的偏移字节数。在随机方式存取文件时,由于文件位置频繁前后移动,程序不容易确定文件的当前位置。在使用 fseek 函数后,再调用函数 ftell 就能非常容易地确定文件的当前位置。如下面的示例代码所示:
- long getfilelength(FILE *fp)
- {
- long curpos=0L;
- long length=0L;
- curpos = ftell(fp);
- fseek(fp, 0L, SEEK_END);
- length = ftell(fp);
- fseek(fp, curpos, SEEK_SET);
- return length;
- }
fread 与 fwrite 函数的原型如下面的代码所示:
size_t fread(void *buf, size_t size, size_t count, FILE *fp);
size_t fwrite(const void * buf, size_t size, size_t count, FILE *fp);
- 参数 size 是指单个元素的大小(其单位是字节而不是位,例如,读取一个 int 型数据就是 4 字节);
- 参数 count 指出要读或写的元素个数,这些元素在 buf 所指的内存空间中连续存放,共占“size*count”个字节。
即 fread 函数从文件 fp 中读出“size*count”个字节保存到 buf 中,而 fwrite 把 buf 中的“size*count”个字节写到文件 fp 中。最后,函数 fread 和 fwrite 的返回值为读或写的记录数,成功时返回的记录数等于 count 参数,出错或读到文件末尾时返回的记录数小于 count,也可能返回 0。
需要注意的是,尽管 fread 和 fwrite 函数可以对数据进行成块读写,但并不是说一次想读写多少数据就能全部读写多少数据,毕竟缓存有限,而且不同的操作系统的缓存大小也可能不一样。同时,许多程序员还认为函数的参数 (size、count) 与位置对齐有关,甚至认为语句“fwrite(ptr,1,1024,fp)”的执行效率会比“fwrite(ptr,1024,1,fp)”高。实际情况并非如此,如在 glibc-2.17 库中对 fwrite 函数的实现如下:
- _IO_size_t _IO_fwrite (const void *buf, _IO_size_t size, _IO_size_t count, _IO_FILE *fp)
- {
- _IO_size_t request = size * count;
- _IO_size_t written = 0;
- CHECK_FILE (fp, 0);
- if (request == 0)
- return 0;
- _IO_acquire_lock (fp);
- if (_IO_vtable_offset (fp) != 0 || _IO_fwide (fp, -1) == -1)
- written = _IO_sputn (fp, (const char *0 buf, request);
- _IO_release_lock (fp);
- if (written == request)
- return count;
- else if (written == EOF)
- return 0;
- else
- return written / size;
- }
_IO_size_t _IO_fwrite (const void *buf, _IO_size_t size, _IO_size_t count, _IO_FILE *fp) {_IO_size_t request = size * count;_IO_size_t written = 0;CHECK_FILE (fp, 0);if (request == 0)return 0;_IO_acquire_lock (fp);if (_IO_vtable_offset (fp) != 0 || _IO_fwide (fp, -1) == -1)written = _IO_sputn (fp, (const char *0 buf, request);_IO_release_lock (fp);if (written == request)return count;else if (written == EOF)return 0;elsereturn written / size; }
首先,在把参数 size 与 count 传进函数之后,第一步就是通过语句“_IO_size_t request=size*count;”来计算“size*count”,所以这两个参数与什么位置对齐根本没有半点关系。
其次,在函数返回时,如果整个写入成功(“written==request”),就返回 count;如果遇到 EOF(“written==EOF”),就返回 0;否则返回“written/size”。由此可见,函数返回的是成功写入的块数,而不是字节数(除非 size 为 1),这样做有许多好处。例如,在写入多个结构体时,返回值能告诉你成功写入的结构体的个数。当然,这样看来,前面的“fwrite(ptr,1,1024,fp)”与“fwrite(ptr,1024,1,fp)”语句还是有所差别的。但是,如果调用者只关心是否全部写入成功,那么就完全没必要纠结于语句“fwrite(ptr,1,1024,fp)”与“fwrite(ptr,1024,1,fp)”之间的差别了。
对于 fread 函数,其道理与 fwrite 函数完全一样,如下面的函数源代码所示:
- _IO_size_t _IO_fread (void *buf,_IO_size_t size,_IO_size_t count,_IO_FILE *fp)
- {
- _IO_size_t bytes_requested = size * count;
- _IO_size_t bytes_read;
- CHECK_FILE (fp, 0);
- if (bytes_requested == 0)
- return 0;
- _IO_acquire_lock (fp);
- bytes_read = _IO_sgetn (fp, (char *) buf, bytes_requested);
- _IO_release_lock (fp);
- return bytes_requested == bytes_read ? count : bytes_read / size;
- }
_IO_size_t _IO_fread (void *buf,_IO_size_t size,_IO_size_t count,_IO_FILE *fp) {_IO_size_t bytes_requested = size * count;_IO_size_t bytes_read;CHECK_FILE (fp, 0);if (bytes_requested == 0)return 0;_IO_acquire_lock (fp);bytes_read = _IO_sgetn (fp, (char *) buf, bytes_requested);_IO_release_lock (fp);return bytes_requested == bytes_read ? count : bytes_read / size; }
通过使用函数 fopen()(或 tmpfile())打开一个文件就会建立一个新的流,直到调用函数 fclose(),该流才会结束。C 语言将文件的管理工作交由运行环境(也就是运行程序的系统)。因此,流是一个传输通道,利用该通道,数据可以从运行环境流入程序中,或从程序流向运行环境。C 语言对设备(例如控制台)的处理机制与对文件的机制是一样的。
每个流都有一个锁机制,当多个线程访问同一个流时,I/O 库函数利用该锁以保证同步性。当对一个流执行读写操作,或者查询和移动流文件位置指示器时,所有流 I/O 函数会首先获得独占访问该流的权限。一旦操作完成,流的权限就会被再次释放,以供其他线程访问。独占流访问权限可以防止“数据竞争”和并发的 I/O 操作。
文本流
文本流用来传输文本中的字符,这里的文本被分割成许多行。文本行都包含一个字符序列,并以换行符作为该序列结尾。文本行也可以为空,也就是说只包含一个换行符。文本最后一行可以有,也可以没有用作结尾的换行符,这由 C 语言的实现版本决定。C 程序中文本的内部表示方式都是一样的,不受程序运行时所在系统的影响。在给定系统上进行文字输入和输出操作,涉及删除、增加或改变给定的字符。例如,在不是 Unix 内核的操作系统上,在读取文本文件时,行尾修饰符通常必须转换成换行符。
对 Windows 系统而言,行尾修饰符由两个控制字符组成:\r(回车)和 \n(换行)。类似,在 Windows 平台上,文本流中的控制字符 ^Z(字符码 26),用来表示文本流的结束。
程序员通常不必担心这些系统平台间必要的适应性,因为标准库的 I/O 函数会自动完成转换。然而,如果想确保调用输入函数会生成与之前调用输出函数时同样的文本,那么文本中除了可打印字符以外,只能包含换行符和水平制表符。而且,最后一行应该以一个换行符作为结束,并且任何一行都不能在换行符之前出现空格。
二进制流
二进制流是字节序列,它们不作修改直接传送。也就是说,当操作二进制流时,I/O 函数不会翻译任何控制字符。通过二进制流方式写入文件的数据,在同一个系统中,可以原封不动地读取出来。然而,在有些实现版本中,会在流尾端加上一些值为 0 的字节。二进制流通常用于编写二进制数据(例如,数据库记录),而不将它转换为文本。如果程序通过二进制流来读取一个文本文件的内容,那么程序中出现的文字就是其存储时的形式,包括所在系统使用的全部控制字符。
在常见的 Unix 系统上,文本流和二进制流之间没有差别。
每个用于打开文件的函数(也就是 fopen()、freopen()和 tmpfile())都会返回一个指向 FILE 对象的指针,该 FILE 对象包含与被打开文件相关联的流。一旦打开了文件,就可以调用函数传递数据并对流进行处理。这些函数都把指向 FILE 对象的指针(通常称为 FILE 指针)作为它们的参数之一。FILE 指针指定了正在进行操作的流。
I/O 链接库也包含了用于操作文件系统的函数,这些函数把文件名作为它们的参数之一。使用这些函数不需要事先打开文件。它们包括:
(1) 函数 remove()删除一个文件(或者空目录)。该字符串参数是文件名。如果文件具有多个名称,那么 remove()只会删除所指定的名称,而非删除文件本身。该文件数据还可以通过别的方式来获取,但是不能通过已删除的文件名访问。
(2) 函数 rename()改变一个文件(或目录)的名称。该函数的两个字符串参数依次为旧文件名和新文件名。函数 remove()和 rename()的返回值类型都是 int,成功时都会返回 0,失败时都会返回非 0值,下面的语句将 songs.dat 重命名为 mysong.dat:
- if ( rename( "songs.dat", "mysongs.dat" ) != 0 )
- fprintf( stderr, "Error renaming \"songs.dat\".\n );
if ( rename( "songs.dat", "mysongs.dat" ) != 0 )fprintf( stderr, "Error renaming \"songs.dat\".\n );
导致函数 rename()失败的原因包括:使用旧文件名的文件不存在;程序获取文件的权限不够;或者文件已经被打开。至于具体何种格式的文件名才是合法的,这是由实现版本决定。
无论是新文件或已有文件,首先必须打开该文件,才可以向文件中写入数据,或者修改其中的内容。打开一个文件时,必须指定访问模式(access mode),以表明计划对该文件进行的是读、写或读写结合等操作。当使用完该文件后,必须关闭它以释放资源。
打开文件
标准库提供函数 fopen()用以打开文件(在特殊情况下,还可以使用函数 freopen()和 tmpfile()来打开文件):- FILE *fopen( const char * restrict filename,
- const char * restrict mode );
FILE *fopen( const char * restrict filename,const char * restrict mode );
字符串 filename 向该函数传入所需打开的文件的名称。该文件名字符串也可以包含目录信息,但必须保证字符串长度不得超过宏 FILENAME_MAX 中指定的最大长度。函数的第二个参数 mode 也是一个字符串,用来指定文件访问模式。函数 freopen()会把文件与一个新的流关联起来。
- FILE *freopen(const char * restrict filename,
- const char * restrict mode,
- FILE * restrict stream );
FILE *freopen(const char * restrict filename,const char * restrict mode,FILE * restrict stream );
该函数将一个流重新定向。与 fopen()类似,freopen()也会用指定的访问模式打开指定的文件。但不同的是,freopen()不会建立新的流,而是将文件与已有的流关联,已有的流通过该函数的第三个参数指定。之前与该流关联的文件会被关闭。freopen()常被用来重新定向到标准流 stdin、stdout 和 stderr。
- FILE *tmpfile( void );
FILE *tmpfile( void );
函数 tmpfile()会建立一个新的临时文件,其文件名与所有已有文件名都不一样,然后打开该文件,进行二进制数据的读写操作(类似于函数 fopen()采用“wb+”访问模式)。如果该程序正常地结束,该文件会被自动删除。
所有三个打开文件的函数 fopen()、freopen()和 tmpfile(),都会返回一个指针。如果成功,该指针就指向已打开的流,如果失败,该指针就为空指针。
如果一个文件打开用于写操作,程序应赋予其独立访问权限以防止其他程序同时对该文件进行写操作。传统的标准函数并不能确保独立文件访问权限,但是 C11 新增的三个新“安全”函数 fopen_s()、freopen_s()和 tmpfile_s(),在操作系统支持的前提下,可以提供独立访问权限。
访问模式
函数 fopen()和 freopen()的第二个参数指定了文件的访问模式,访问模式决定了流所许可的输入和输出操作。对访问模式字符串的许可值有严格的限制。该字符串的第一个字符只能为三种形式:r(表示“read”)、w(表示“write”)或者 a(表示“append”)。在最简单情况下,该字符串只包含一个字符。模式字符串还可以包含 + 和 b(如果两者同时具有,次序是没有关系的,+b 效果等同于 b+)。
模式字符串中的加号(+)表示读写操作都可以进行。然而,程序不可以在读操作和写操作之间立即作切换。在写操作之后,必须调用函数 fflush()或者定位函数(fseek()、fsetpos()或 rewind()),然后才可以执行读操作。在读操作之后,必须调用定位函数,然后才可以执行写操作。
模式字符串中的 b 表示文件以二进制模式打开。也就是说,与该文件关联的流是二进制流。如果模式字符串中没有 b,新建立的流就是字符串流。
当模式字符串以 r 开始时,该文件必须已经存在于文件系统中。当模式字符串以 w 开始时,如果文件不存在,则会建立一个新文件;如果文件存在,该文件当前内容会被清除,因为在“write”模式中,函数 fopen()将文件长度设置为 0。
C11 新增一个功能,在操作系统支持的前提下,允许在独立写操作模式下打开文件。可以在以 w 起始的模式字符串中使用后缀 x,例如 wx 或 w+bx,以指定独立访问权限。如果文件已经存在或者不能被创建,则文件打开函数执行失败(返回空指针)。否则,将创建文件并以独立访问权限打开它。
当模式字符串以 a 开始时,如果文件不存在,则也会建立一个新文件。如果文件存在,该文件当前内容会被保留,因为所有新写入的内容都会从文件尾端添加。下面是一个简单的示例:
- #include <stdio.h>
- #include <stdbool.h>
- _Bool isReadWriteable( const char *filename )
- {
- FILE *fp = fopen( filename, "r+" ); // 打开一个文件以用于读写
- if ( fp != NULL ) // fopen()是否执行成功
- {
- fclose(fp); // 成功:关闭文件,没有错误需要处理
- return true;
- }
- else // 失败
- return false;
- }
#include <stdio.h> #include <stdbool.h> _Bool isReadWriteable( const char *filename ) {FILE *fp = fopen( filename, "r+" ); // 打开一个文件以用于读写if ( fp != NULL ) // fopen()是否执行成功{fclose(fp); // 成功:关闭文件,没有错误需要处理return true;}else // 失败return false; }
上例也展示了如何利用函数 fclose()关闭一个文件。
关闭文件
关闭文件时需要使用函数 fclose(),该函数的原型是:- int fclose( FILE *fp );
int fclose( FILE *fp );
该函数把缓冲区内存在的所有数据保存到文件中,关闭文件,释放所有用于该流输入输出缓冲区的内存。函数 fclose()返回 0 表示成功,返回 EOF 表示产生错误。
当程序退出时,所有打开的文件都会自动关闭。尽管如此,还是应该在完成文件处理后,主动关闭文件。否则,一旦遇到非正常的程序终止,就可能会丢失数据。而且,一个程序可以同时打开的文件数量是有限的,数量上限小于等于常量 FOPEN_MAX 的值。
printf()函数系列
printf()函数以及多种它的相关函数都能够提供数据的格式化输出功能,它们通过使用格式化字符串(format string)作为函数参数来指定具体格式。然而,不同的函数具有不同的输出目的,以及对所需输出数据的访问方法。下面的 printf()函数系列可用于处理字节导向流:- int printf(const char*restrict format,...);
int printf(const char*restrict format,...);
- int fprintf(FILE*restrict fp,const char*restrict format,...);
int fprintf(FILE*restrict fp,const char*restrict format,...);
- int sprintf(char*restrict buf,
- const char*restrict format,...);
int sprintf(char*restrict buf, const char*restrict format,...);
在上述函数原型中出现的省略号(...),表示还可有更多参数,但这些参数是可选的。还有一些 printf()函数系列需要一个指针参数,以指向一个参数列表,而不是在函数调用时直接接收数量可变的参数。这些函数的名称都以一个 v 开始,表示“variable argument list”(可变参数列表)的意思:
- int vprintf( const char * restrictformat, va_list argptr );
- int vfprintf( FILE * restrict fp, const char * restrict format,
- va_list argptr );
- int vsprintf( char * restrict buf, const char * restrict format,
- va_list argptr );
- int vsnprintf( char * restrict buffer, size_t n,
- const char * restrict format, va_list argptr );
int vprintf( const char * restrictformat, va_list argptr ); int vfprintf( FILE * restrict fp, const char * restrict format,va_list argptr ); int vsprintf( char * restrict buf, const char * restrict format,va_list argptr ); int vsnprintf( char * restrict buffer, size_t n,const char * restrict format, va_list argptr );
如果想使用支持可变参数列表的函数,除了头文件 stdio.h 以外,还必须包含头文件 stdarg.h。
上述函数都有相应的宽字符导向流版本。针对宽字符的 printf()函数名称中包括字符串 wprintf 而不是 pintf,例如,vfwprintf()和 swprintf()等。但有一个例外:没有 snwprintf()函数。而是采用 snprintf()对应到 swprintf(),该函数采用一个参数来指定最大输出长度。
C11 标准为这些函数都提供了一个新的“安全”的版本。这些对应的新函数均以后缀 _s(例如,fprintf_s())。新函数测试它们接收的所有指针参数是否为空指针。
格式化字符串
格式化字符串是每个 printf()系列函数都具有的一个参数。格式化字符串定义了数据的输出格式,并包含了一些普通字符和转换说明(conversion specification)。每个转换说明都定义了函数该如何将可选参数转换并格式化,以供输出。printf()函数将格式化字符串写入到输出,使用对应可选参数的格式化值来替代转换说明。转换说明以百分号 % 开始,并以一个字母结尾,这称为转换修饰符(conversion specifier)。(为了在输出中表示 %,需要一个特殊的转换修饰符:%%。printf()将该符号转换成一个单独的百分号。)
转换说明的语法以转换修饰符作为结尾。在本文中,我们将使用这两个术语来讨论调用函数 printf()和 scanf()时所使用的格式化字符串。
转换修饰符决定了转换的类型,并且必须符合对应的可选参数。如下例所示:
- int score = 120;
- char player[ ] = "Mary";
- printf( "%s has %d points.\n", player, score );
int score = 120; char player[ ] = "Mary"; printf( "%s has %d points.\n", player, score );
在调用 printf()时所使用的格式化字符串包含两个转换说明:%s 和 %d。对应的两个可选参数也分别被指定:一个字符串,匹配转换修饰符 s(表示“string”),以及一个 int 数值,匹配转换修饰符 d(表示“decimal”)。示例中的函数调用,会在标准输出设备中写入下面的字符串:
Mary has 120 points.
所有的转换说明(但 %% 是例外)都具有下面的通用格式:
%[标记][字段宽度][.精度][长度修饰符]修饰符
方括号内的这部分语法都是可选的,但是若要使用它们,就必须遵循上述次序。下面一节会详细解释每个参数类型合法的转换说明。所有转换说明都可包含“字段宽度”(field width)。然而,并非所有的转换类型都有“精度”(precision)这个选项,对不同的修饰符来说,精度意义是不一样的。
字段宽度
进行格式化的表格输出时,字段宽度选项非常有用。如果包括该选项,字段宽度必须是正的十进制整数(或者是一个星号,下面会介绍)。字段宽度指定对应的数据项所输出的最少字符数量。默认情况下,字段中的被转换数据为右对齐(right-justified),左边多的位置用空格填补。如果标记包含减号(-),则为左对齐(left-justified),超出的字段宽度采用空格向右填补。下面的例子先输出一行位置编号,然后展示字段宽度选项对输出的作用:
- printf("1234567890123456\n"); // 字符位置
- printf( "%-10s %s\n", "Player", "Score" ); // 表头
- printf( "%-10s %4d\n", "John", 120 ); // 字段宽度:10;4
- printf( "%-10s %4d\n", "Mary", 77 );
printf("1234567890123456\n"); // 字符位置 printf( "%-10s %s\n", "Player", "Score" ); // 表头 printf( "%-10s %4d\n", "John", 120 ); // 字段宽度:10;4 printf( "%-10s %4d\n", "Mary", 77 );
上述语句会生成一个简单表格:
1234567890123456
Player Score
John 120
Mary 77
如果输出转换的结果比所指定的宽度具有更多的字符,那么字段会做必要的扩充,以输出完整的数据。
如果字段是右对齐的,可以采用 0 而非空格填充。要实现这样的效果,在转换说明标记中包括一个 0(指数字零)。下面的例子以 mm-dd-yyyy 的格式输出日期:
- int month = 5, day = 1, year = 1987;
- printf( "Date of birth: %02d-%02d-%04d\n", month, day, year );
int month = 5, day = 1, year = 1987; printf( "Date of birth: %02d-%02d-%04d\n", month, day, year );
该 printf()调用会产生下面的输出:
Date of birth: 05-01-1987
也可以使用一个变量来指定字段宽度。要实现这样的效果,采用一个星号(*)作为转换说明中的字段宽度,并在 printf()调用时包括一个额外的函数参数。该参数必须具有 int 类型,并且出现在需输出的参数之前。如下例所示:
- char str[ ] = "Variable field width";
- int width = 30;
- printf( "%-*s!\n", width, str );
char str[ ] = "Variable field width"; int width = 30; printf( "%-*s!\n", width, str );
上例中的 printf 语句在字段靠左边位置输出字符串 str,并且字段宽度由变量 width 决定。结果如下:
Variable field width !
请注意输出的最后有一个感叹号(!)。感叹号之前的一大段空格并非 str[] 在初始化时被赋值的内容。这些空格而是 printf 语句根据我们的要求为该字符串指定 30 个字符宽度而自动填充的。
输出字符和字符串
printf()中针对字符串的转换修饰符是 s,正如前面代码中所示。针对字符的修饰符是 c(表示 char)。它们总结如表 1 所示。修饰符 | 参数类型 | 表示 |
c | int | 一个单独的字符 |
s | char 指针 | 该指针参数所指向的字符串 |
下面的例子在一个队员名单中各成员之间输出一个分隔字符:
- char *team[ ] = { "Vivian", "Tim", "Frank", "Sally" };
- char separator = ';';
- for ( int i = 0; i < sizeof(team)/sizeof(char *); ++i )
- printf( "%10s%c ", team[i], separator );
- putchar( '\n' );
char *team[ ] = { "Vivian", "Tim", "Frank", "Sally" }; char separator = ';'; for ( int i = 0; i < sizeof(team)/sizeof(char *); ++i )printf( "%10s%c ", team[i], separator ); putchar( '\n' );
用转换说明 %c 表示的参数,可以拥有比 int 还小的类型(例如 char)。整数提升会自动地将该类型参数转换成 int。然后函数 printf()将该 int 参数转换为 unsigned char,并输出对应的字符。
对于字符串输出来说,可以指定能被输出的最多字符数量。这时用到转换说明的精度选项,精度表示为一个点后接一个十进制整数。如下例所示:
- char msg[] = "Every solution breeds new problems.";
- printf( "%.14s\n", msg ); // 精度:14
- printf( "%20.14s\n", msg ); // 字段宽度是20;精度是14
- printf( "%.8s\n", msg+6 ); // 从字符串msg的第7个字符起输出字符串,精度为8
char msg[] = "Every solution breeds new problems."; printf( "%.14s\n", msg ); // 精度:14 printf( "%20.14s\n", msg ); // 字段宽度是20;精度是14 printf( "%.8s\n", msg+6 ); // 从字符串msg的第7个字符起输出字符串,精度为8
上述语句会产生下面的输出结果:
Every solution
Every solution
solution
输出整数
函数 printf()可以把整数值转换为十进制、八进位或十六进制表示。表 2 列出了用于格式化输出整数的转换修饰符。修饰符 | 参数类型 | 表示 |
d,i | int | 十进制 |
u | unsigned int | 十进制 |
o | unsigned int | 八进位 |
x | unsigned int | 十六进制,用小写的 a、b、c、d、e、f |
X | unsigned int | 十六进制,用大写的 A、B、C、D、E、F |
下面的例子展示同一个整数的不同转换方式:
- printf( "%4d %4o %4x %4X\n", 63, 63, 63, 63 );
printf( "%4d %4o %4x %4X\n", 63, 63, 63, 63 );
该 printf()调用会产生下面的输出:
63 77 3f 3F
修饰符 u、o、x 与 X 把对应的参数解释为无符号整数。如果参数类型是 int,并且其值是负数,则转换后输出的是对应参数按照无符号整数解释时其位模式下的正数值:
- printf( "%d %u %X\n", -1, -1, -1 );
printf( "%d %u %X\n", -1, -1, -1 );
如果 int 为 32 位宽,那么该语句会产生下面的输出:
-1 4294967295 FFFFFFFF
因为参数会受到整数提升的影响,同样的转换修饰符可以被用来格式化 short 和 unsigned short 参数。对于类型是 long 或 unsigned long 的参数,必须在 d、i、u、o、x 和 X 修饰符前面加上长度修饰符 l(小写的 L)。类似地,如果参数是 long long 或 unsigned long long 类型,则其长度修饰符是 ll(两个小写 L)。如下例所示:
- long bignumber = 100000L;
- unsigned long long hugenumber = 100000ULL * 1000000ULL;
- printf( "%ld %llX\n", bignumber, hugenumber );
long bignumber = 100000L; unsigned long long hugenumber = 100000ULL * 1000000ULL; printf( "%ld %llX\n", bignumber, hugenumber );
上述语句产生下面的输出:
100000 2540BE400
输出浮点数
表 3 列出了函数 printf()用来格式化输出浮点数的转换修饰符。修饰符 | 参数类型 | 表示 |
f | double | 十进制浮点数 |
e、E | double | 指数表示法,十进制 |
g、G | double | 浮点数或指数表示法,选择其中较短者 |
a、A | double | 指数表示法,十六进制 |
最常用的修饰符是 f 和 e(或 E)。下面的例子展示了它们的用法:
- double x = 12.34;
- printf( "%f %e %E\n", x, x, x );
double x = 12.34; printf( "%f %e %E\n", x, x, x );
该 printf()调用将产生下面的输出:
12.340000 1.234000e+01 1.234000E+01
在指数表示法中出现的 e 是大写还是小写,取决于函数转换修饰符中所用 e 的大小写。而且,如上例所示,默认输出显示精度为 6 位小数。转换修饰符中的精度选项可修改这个默认设置:
- double value = 8.765;
- printf( "Value: %.2f\n", value ); // 精度为2:表示输出为2位小数
- printf( "Integer value:\n"
- " Rounded: %5.0f\n" // 字段宽度为5;精度为0
- " Truncated: %5d\n", value, (int)value );
double value = 8.765; printf( "Value: %.2f\n", value ); // 精度为2:表示输出为2位小数 printf( "Integer value:\n"" Rounded: %5.0f\n" // 字段宽度为5;精度为0" Truncated: %5d\n", value, (int)value );
该 printf()调用会产生下面的输出:
Value: 8.77
Integer value:
Rounded: 9
Truncated: 8
正如上例所示,printf()会将浮点数按向上或向下取近似值,以便于输出。如果指定精度为0,那么小数点本身则会被省略。如果仅仅想把小数部分直接去掉,而不是取近似值,直接将它转换为整数类型可达到目的。
上述修饰符也可以配合 float 参数使用,因为 float 参数会自动地被提升为 double。但是,如想输出类型为 long double 的参数,必须在转换修饰符之前插入长度修饰符 L,如下例所示:
- #include <math.h>
- long double xxl = expl(1000);
- printf( "e to the power of 1000 is %.2Le\n", xxl );
当从一个格式化数据源中读取数据时,C 语言提供了 scanf()函数系列。与 printf()函数一样,scanf()函数需要一个格式化字符串作为其参数,以控制 I/O 格式与程序内部数据之间的转换。本文介绍在 scanf()和 printf()函数中使用格式化字符串和转换修饰符的差异。
scanf()函数系列
各种 scanf()函数处理输入源字符的方式都是相同的。不同的是这些函数所针对的数据源种类,以及它们接收参数的方式。下面的 scanf()函数针对字节导向流:
- int scanf(const char*restrict format,...);
int scanf(const char*restrict format,...);
从标准输入流 stdin 中读取数据。
- int fscanf(FILE*restrict fp,const char*restrict format,...);
int fscanf(FILE*restrict fp,const char*restrict format,...);
从 fp 所引用的输入流中读取数据。
- int sscanf(const char*restrict src,const char*restrict format,...);
int sscanf(const char*restrict src,const char*restrict format,...);
从 src 指向的 char 数组中读取数据。
省略号表示还有更多的可选参数。可选参数是指向变量的指针,scanf()函数将转换结果存储在这些变量中。
类似于 printf()函数,scanf()函数系列也包含变体版本。变体版本将一个指针作为参数,指向一个参数列表,而不是在函数调用时直接接收数量可变的参数。
这些变体版本的函数名称以字母 v 开头,表示“variable argument list”(可变参数列表):例如,vscanf()、vfscanf()和 vsscanf()。如果想使用支持可变参数列表的函数,除了头文件 stdio.h 以外,还必须包含头文件 stdarg.h。
这些函数都具有相应的针对宽字符导向流的版本。宽字符函数名称中包含 wscanf 而不是 scanf,例如 wscanf()和 vfwscanf()。
C11 标准为这些函数都提供了一个新的“安全”的版本。这些对应的新函数均以后缀 _s(如 fscanf_s())。新函数测试在读入一个字符串到数组之前,是否超出了数组边界。
格式化字符串
scanf()函数的格式化字符串包含普通字符和转换说明,转换说明定义了如何解释以及转换读入的字符序列。scanf()函数所使用的大多数转换修饰符都与 printf()函数所定义的一样。然而,scanf()函数的转换说明没有标记和精度选项。针对 scanf()函数转换说明的通用语法如下所示:
%[*][字段宽度][长度修饰符]修饰符
对于格式化字符串中的每个转换说明,从输入源读入的字符的数量与转换方式都会与转换修饰符一致。结果会存储在对应指针参数所指向的对象中。如下例所示:
- int age = 0;
- char name[64] = "";
- printf( "Please enter your first name and your age:\n" );
- scanf( "%s%d", name, &age );
int age = 0; char name[64] = ""; printf( "Please enter your first name and your age:\n" ); scanf( "%s%d", name, &age );
假设用户在提示符下输入如下内容:
- Bob 27\n
Bob 27\n
调用 scanf()函数,会将字符串 Bob 写进 char 数组 name 中,然后将 27 写进 int 变量 age 中。
所有的转换说明,除了具有修饰符 c 的情况以外,都会忽略前面的空白字符(whitespace character)。在上例中,用户可以在第一个词 Bob 前,或者在 Bob 与 27 之间,放置任意多个空格、制表符或换行符,这些操作均不影响结果。
针对给定的转换说明,当 scanf()读到任何空白字符时,或者任何无法以该转换说明解释的字符时,读取序列字符的操作将会终止。无法被解释的字符会被放回到输入流中,下一个转换说明将从该字符开始。在前述例子中,假设用户输入如下:
- Bob 27years\n
Bob 27years\n
在读取到字符 y 时,它不可能是十进制数值的一部分,针对转换说明 %d,scanf()会停止读取对应的字符。在调用该函数后,字符 years\n 会继续留在输入流的缓冲区中。
如果在忽略所有空白符之后,scanf()还是找不到符合当前转换说明的字符,则生成错误,scanf()函数终止处理输入。下面将介绍如何捕获这类错误。
通常,在调用 scanf()函数时,格式化字符串只包含转换说明。如果不是,那么格式化字符串中除转换说明与空白符以外的其他所有字符,必须与输入源对应位置的字符完全一致。否则 scanf()函数就会终止处理,并将不匹配的字符放回到输入流中。
格式化字符串中所出现的一个或多个连续空白符,必须符合输入流中连续空格的数量。换句话说,对于格式化字符串中出现的所有空白符,scanf()会读取并略过数据源中的所有空白字符,直到读入第一个非空白符。在理解这一点后,请判断下面的 scanf()调用方式有什么问题。
- scanf( "%s%d\n", name, &age ); // 有什么问题
scanf( "%s%d\n", name, &age ); // 有什么问题
假设用户输入下面这一行字符:
- Bob 27\n
Bob 27\n
本例中,scanf()在读入换行符后不会返回,而是继续读取更多输入,直到出现非空白字符出现。
有时候,需要读取并略过符合给定转换说明的字符序列,不存储结果。可以在转换说明中采用 %* 来达到前述效果。对于具有星号的转换说明,不要包括对应的指针参数。
scanf()函数的返回值是成功存储数据项的数量。如果一切执行顺利,返回值就是转换说明的数量(但不计包含星号的转换说明)。如果发生读取错误或在转换数据项前就到达了输入源尾部,则 scanf()函数会返回值 EOF。如下例所示:
- if ( scanf( "%s%d", name, &age ) < 2 )
- fprintf( stderr, "Bad input.\n" );
- else
- { /* ...测试存储的值... */ }
if ( scanf( "%s%d", name, &age ) < 2 )fprintf( stderr, "Bad input.\n" ); else { /* ...测试存储的值... */ }
字段宽度
字段宽度是十进制整型正数,它指定了对于给定的转换说明,scanf()所读取字符的最大数量。对于字符串输入来说,字段宽度可以防止缓冲区出现溢出情况:
- char city[32];
- printf( "Your city: ");
- if ( scanf( "%31s", city ) < 1 ) // 不要读入超过31个字符
- fprintf( stderr, "Error reading from standard input.\ n" );
- else
- /* ... */
char city[32]; printf( "Your city: "); if ( scanf( "%31s", city ) < 1 ) // 不要读入超过31个字符fprintf( stderr, "Error reading from standard input.\ n" ); else /* ... */
printf()会输出超过指定字段宽度的字符,但 scanf()不同于 printf(),转换修饰符 s 不会读入超过指定字段宽度的字符到缓冲区。
读取字符和字符串
转换说明 %c 和 %1c 都会从输入流中读取一个字符,包括空白符。通过指定字段宽度,可以读取数量等于字段宽度的字符,包括空白符,只要没有遇到输入流的结束。当采用这种方式读取多个字符时,对应的指针参数必须指向一个空间足够大的 char 数组,以存储下所有读到的字符。
使用转换修饰符 c 的 scanf()函数,不会在读入字符序列的尾部加上字符串终止符。例如:
- scanf( "%*5c" );
scanf( "%*5c" );
该 scanf()调用会读取并丢弃输入源紧接着的 5 个字符。
转换说明 %s 总是读取恰好一个词,遇到空白符时结束读取。如果想读取整行文本,可以使用函数 fgets()。
下面的示例逐词地读取文本文件的内容。假设文件指针 fp 关联了一个文本文件,并且该文件已打开,以用于读取:
- char word[128];
- while ( fscanf( fp, "%127s", word ) == 1 )
- {
- /* ...处理读到的词... */
- }
char word[128]; while ( fscanf( fp, "%127s", word ) == 1 ) {/* ...处理读到的词... */ }
除了转换修饰符 s 以外,也可以使用“扫描集”(scanset)修饰符来读取字符串,它由方括号所包含的一串无序字符组成([scanset])。scanf()函数接着读取所有字符,然后将它们存储为一个字符串(带有字符串终止符),直到遇到不匹配扫描集中任一字符时才停止。例如:
- char strNumber[32];
- scanf( "%[0123456789]", strNumber );
char strNumber[32]; scanf( "%[0123456789]", strNumber );
如果用户输入 345X67,那么 scanf()会把 345\0 字符串存储到数组 strNumber 中。字符 X 以及后续字符则仍然留在输入缓冲区中。
逆向使用转换扫描集(也就是说,除扫描集中的字符外,其他都符合),做法是在扫描集的左括号后面加上一个插入号(^)。下面的 scanf()调用读取所有字符(包括空白符),直到句子结束的标点符号,然后再读入标点符号本身:
- char ch, sentence[512];
- scanf( "%511[^.!?]%c", sentence, &ch );
char ch, sentence[512]; scanf( "%511[^.!?]%c", sentence, &ch );
下面的 scanf()调用读取并丢弃所有字符,一直到当前行结束:
- scanf( "%*[^\n]%*c" );
scanf( "%*[^\n]%*c" );
读取整数
类似 printf()函数,scanf()函数为整数提供了下面的转换修饰符:d、i、u、o、x 和 X。它们允许读入十进制、八进位与十六进制表示法,并转换为 int 或 unsigned int 变量。如下例所示:
- // 读入一个非负的十进制整数
- unsigned int value = 0;
- if ( scanf( "%u", &value ) < 1 )
- fprintf( stderr, "Unable to read an integer.\n" );
- else
- /* ... */
// 读入一个非负的十进制整数 unsigned int value = 0; if ( scanf( "%u", &value ) < 1 )fprintf( stderr, "Unable to read an integer.\n" ); else/* ... */
对于 scanf()函数内的修饰符 i,读入数字的基数(进制)并非预先定义好的。基数是由读入的数字字符序列的前缀符号所决定的,这些符号的表示方式与 C 源代码中整数常量相同。
如果字符序列不是以 0 开始,那么它会被解释为十进制数字。如果以 0 开始,并且第二个字符不是 x 或 X,那么该序列会被解释为八进位数字。如果以 0x 或 0X 开始,则以十六进制数字读入。
如果想把所读取的整数赋值给一个 short、char、long 或 long long 变量(或者它们所对应的无符号类型),必须在转换修饰符之前插入一个长度修饰符:h 表示 short,hh 表示 char,l 表示 long,ll 表示 long long。在下面的示例中,FILE 指针 fp 指向一个打开用于读取的文件:
- unsigned long position = 0;
- if (fscanf( fp, "%lX", &position) < 1 ) // 读取一个十六进制整数
- /* ... 处理错误:无法读入数字... */
unsigned long position = 0; if (fscanf( fp, "%lX", &position) < 1 ) // 读取一个十六进制整数/* ... 处理错误:无法读入数字... */
读取浮点数
当处理浮点数时,scanf()函数使用与 printf()相同的转换修饰符:f、e、E、g 和 G。而且,C99 新增了修饰符 a 和 A。所有这些修饰符以同样的方式解释读取的字符序列。可以被解释成浮点数的字符序列,与 C 语言中的有效浮点常量是一样的。scanf()也可以转换整数,并将它们存储在浮点变量中。
所有这些修饰符将数字转换成 float 类型浮点值。如果想将它们转换并存储成 double 或 long double,必须插入一个长度修饰符:double 使用 l(小写L),long double 则使用 L。如下例所示:
- float x = 0.0F;
- double xx = 0.0;
- // 读取两个浮点数:将一个转换为float,另一个转换为double
- if ( scanf( "%f %lf", &x, &xx ) < 2 )
- /* ... */
float x = 0.0F; double xx = 0.0; // 读取两个浮点数:将一个转换为float,另一个转换为double if ( scanf( "%f %lf", &x, &xx ) < 2 )/* ... */
如果该 scanf()调用接收到的输入序列是 12.37\n,那么会将 12.3 存储在到 float 变量 x 中,而 7.0 存储到 double 变量 xx 中。
文件随机访问是指在某个文件内直接读写任何给定位置数据的能力。通过获取与设定文件位置指示符可以实现这一功能,文件位置指示符指定了文件中的当前访问位置,该文件与一个给定的流关联。
获取当前文件位置
下面的函数返回当前文件的访问位置。当需要标记文件中的位置,以便以后返回到该位置时,可以使用下面的函数。
- long ftell(FILE*fp);
long ftell(FILE*fp);
ftell()返回 fp 流的文件位置。对一个二进制流来说,它与该位置之前的字符数量是相同的,也就是当前字符位置距离文件头部的偏差。当发生错误时,ftell()返回 -1。
- int fgetpos(FILE*restrict fp,fpos_t*restrict ppos);
int fgetpos(FILE*restrict fp,fpos_t*restrict ppos);
fgetpos()将 fp 流的文件位置指示符写入 ppos 所引用的对象,该对象类型为 fpos_t。如果 fp 是一个宽字符导向流,那么 fgetpos()所存储的指示符也会包含流当前的转换状态。当发生错误时,fgetpos()返回非 0 值;当执行成功时,返回 0。
下面的示例记录文件 messages.txt 中以 # 字符开头的所有行的位置:
- #define ARRAY_LEN 1000
- long arrPos[ARRAY_LEN] = { 0L };
- FILE *fp = fopen( "messages.txt", "r" );
- if ( fp != NULL)
- {
- int i = 0, c1 = '\n', c2;
- while ( i < ARRAY_LEN && ( c2 = getc(fp) ) != EOF )
- {
- if ( c1 == '\n' && c2 == '#' )
- arrPos[i++] = ftell( fp ) - 1;
- c1 = c2;
- }
- /* ... */
- }
#define ARRAY_LEN 1000 long arrPos[ARRAY_LEN] = { 0L }; FILE *fp = fopen( "messages.txt", "r" ); if ( fp != NULL) {int i = 0, c1 = '\n', c2;while ( i < ARRAY_LEN && ( c2 = getc(fp) ) != EOF ){if ( c1 == '\n' && c2 == '#' )arrPos[i++] = ftell( fp ) - 1;c1 = c2;}/* ... */ }
设置文件访问位置
下面的函数修改文件位置指示符。
- int fsetpos(FILE*fp,const fpos_t*ppos);
int fsetpos(FILE*fp,const fpos_t*ppos);
将文件位置指示符和转换状态设置成 ppos 所引用对象中存储的值。ppos 所引用对象内的这些值必须通过调用函数 fgetpos()才能获得。如果成功,fsetpos()返回 0,并清除该流的 EOF 标记。如果发生错误,则返回非 0 值。
- int fseek(FILE*fp,long offset,int origin);
int fseek(FILE*fp,long offset,int origin);
将文件位置指示符设置为以参数 origin 作为参考点,offset 作为偏差。三种可能的参考点均被定义为宏值,参数 offset 指定位置只可能是相对这三种参考点中的一种。
表 1 列出了这些宏,以及在 ANSI C 定义它们之前,曾用于 origin 的传统取值。这些 offset 值可以是负的,但是,最终结果所获得的文件位置必须大于等于 0。
宏名称 | origin的传统取值 | 偏差相对于的参考点 |
SEEK_SET | 0 | 文件开头 |
SEEK_CUR | 1 | 当前文件位置 |
SEEK_END | 2 | 文件结尾 |
当处理文本流时(在可区分文本流和二进制流的系统上),应该使用通过调用函数 ftell()获得的值作为 offset 参数,并且让 origin 的值为 SEEK_SET。
函数 ftell()与 fseek()、fgetpos()与 fsetpos()并非互相兼容的,因为 fgetpos()和 fsetpos()用来指示文件位置的 fpos_t 对象,可以不是算术类型。
如果成功的话,fseek()会清除流的 EOF 标记并返回 0。非 0 的返回值表示发生错误。函数 rewind()将文件位置指示符设置成文件开头,并清除流的 EOF 与错误标记:
- void rewind( FILE *fp );
void rewind( FILE *fp );
如果不考虑对错误标记的影响,那么调用 rewind(fp)等同于:
- (void)fseek( fp, 0L, SEEK_SET )
(void)fseek( fp, 0L, SEEK_SET )
如果该文件已被以读写模式打开,那么在成功调用 fseek()、fsetpos()或 rewind()之后,就可以进行读写操作。
下面的例子使用一个索引表来存储文件中记录的位置。这个方法允许直接地访问需要被更新的记录。
- // setNewName():在索引表中找关键字,并且更新文件中关键字所对应的记录
- // 包含这些记录的文件,必须以“读写模式”打开;也就是采用模式字符串"r+b"
- // 参数:—指向被打开数据文件的指针;—关键字;—新名称
- // 返回值:指向更新记录的指针,当未找到时,返回NULL
- // ---------------------------------------------------------------
- #include <stdio.h>
- #include <string.h>
- #include "Record.h" // 定义类型Record_t, IndexEntry_t:
- // typedef struct { long key; char name[32];
- // /* ... */ } Record_t;
- // typedef struct { long key, pos; } IndexEntry_t;
- extern IndexEntry_t indexTab[]; // 索引表
- extern int indexLen; // 表条目的数量
- Record_t *setNewName( FILE *fp, long key, const char *newname )
- {
- static Record_t record;
- int i;
- for ( i = 0; i < indexLen; ++i )
- {
- if ( key == indexTab[i].key )
- break; // 找到指定的键
- }
- if ( i == indexLen )
- return NULL; // 没有找到
- // 将文件位置设定到该记录:
- if (fseek( fp, indexTab[i].pos, SEEK_SET ) != 0 )
- return NULL; // 定位失败
- // 读取记录:
- if ( fread( &record, sizeof(Record_t), 1, fp ) != 1 )
- return NULL; // 读取错误
- if ( key != record.key ) // 测试键值
- return NULL;
- else
- { // 更新记录
- size_t size = sizeof(record.name);
- strncpy( record.name, newname, size-1 );
- record.name[size-1] = '\0';
- if ( fseek( fp, indexTab[i].pos, SEEK_SET ) != 0 )
- return NULL; // 设定文件位置出错
- if ( fwrite( &record, sizeof(Record_t), 1, fp ) != 1 )
- return NULL; // 写入文件出错
- return &record;
- }
- }
// setNewName():在索引表中找关键字,并且更新文件中关键字所对应的记录 // 包含这些记录的文件,必须以“读写模式”打开;也就是采用模式字符串"r+b" // 参数:—指向被打开数据文件的指针;—关键字;—新名称 // 返回值:指向更新记录的指针,当未找到时,返回NULL // --------------------------------------------------------------- #include <stdio.h> #include <string.h> #include "Record.h" // 定义类型Record_t, IndexEntry_t:// typedef struct { long key; char name[32];// /* ... */ } Record_t;// typedef struct { long key, pos; } IndexEntry_t;extern IndexEntry_t indexTab[]; // 索引表 extern int indexLen; // 表条目的数量Record_t *setNewName( FILE *fp, long key, const char *newname ) {static Record_t record;int i;for ( i = 0; i < indexLen; ++i ){if ( key == indexTab[i].key )break; // 找到指定的键}if ( i == indexLen )return NULL; // 没有找到// 将文件位置设定到该记录:if (fseek( fp, indexTab[i].pos, SEEK_SET ) != 0 )return NULL; // 定位失败// 读取记录:if ( fread( &record, sizeof(Record_t), 1, fp ) != 1 )return NULL; // 读取错误if ( key != record.key ) // 测试键值return NULL;else{ // 更新记录size_t size = sizeof(record.name);strncpy( record.name, newname, size-1 );record.name[size-1] = '\0';if ( fseek( fp, indexTab[i].pos, SEEK_SET ) != 0 )return NULL; // 设定文件位置出错if ( fwrite( &record, sizeof(Record_t), 1, fp ) != 1 )return NULL; // 写入文件出错return &record;} }
在写操作之前的第二个 fseek()调用,可以用下面代码替换,以相对于之前的位置,移动文件指针:
- if (fseek( fp, -(long)sizeof(Record_t), SEEK_CUR ) != 0 )
- return NULL; // 设定文件位置出错