1. EOF(end of file)
大家都知道流(文本流,标准输入流)结束时会返回EOF,那么EOF到底是什么呢?
在stdio.h中我们可以看到相关声明:
#define EOF (-1)
很明显,EOF是一个宏,被定义为-1。
到这里,其实是有些疑问的。这个-1代表的是每个流的结束处都有一个-1吗?还是别的东西呢?
如果说是前者,即流的结束处都有一个-1的字符,对于文本文件其实是可以实现的,因为文本文件对应的编码都是正值,因此可以正确识别结尾。但是对于二进制文件呢?如何识别-1,似乎看起来不太可能!
其实在Linux系统之中,EOF根本不是一个字符,而是当系统读取到文件结尾,所返回的一个信号值(也就是-1)。系统通过比较文件的长度来确定文件的结尾。
知道了这一点,我们来看一下Linux下关于fegtc()函数的声明:
int fgetc(FILE *stream);fgetc() reads the next character from stream and returns it as an unsigned char cast to an int, or EOF on end of file or error.
翻译一下,fegtc()会从读取流中的下一个字符,并返回一个由unsigned char提升而来的int类型值,或者在遇到文件结束或读取出错时返回EOF(即-1)。
因此,fgetc()的下面这种使用方式是错误的。
while(fgetc(fp) != EOF){do something;}//当fgect(fp)返回EOF时,无法确定是否到达文件末尾。
因此C语言提供了feof()函数来进一步判断是否到达文件末尾,因此我们可以这样写:
int c;while(!feof(fp)){c = fgetc(fp);do something;}/*但是这样也是存在错误的!因为fgetc()会在读取到结尾之后再向后读的时候feof()才会返回一个非0值,因此上面的代码会使得具体逻辑执行n+1次。*/
最安全的写法是下面这种:
int c = fgetc(fp);while(c != EOF){do something;c = fgetc(fp);}if (feof(fp)){printf("End of file.\n");}else {printf("Something went wrong.\n");}
此外EOF在表示标准输入(fp=0,sdtin)时候需要注意下:
Linux中,在新的一行的开头,按下Ctrl-D,就代表EOF(如果在一行的中间按下Ctrl-D,则表示输出"标准输入"的缓存区,所以这时必须按两次Ctrl-D);Windows中,新的一行的开始按下Ctrl-Z表示EOF。此外,需要注意Linux和Windows的输入分别是采用非阻塞式和阻塞式,也就是说Linux中你按下Ctrl+D后,程序立即响应,将键盘输入的东西放到输入缓冲区中(同时忽略输入的这个Ctrl+D),而Windows下则是你按了回车以后它才去看你输入有没有Ctrl+Z的。那么,如果真的想输入Ctrl-D怎么办?这时必须先按下Ctrl-V,然后就可以输入Ctrl-D,系统就不会认为这是EOF信号。Ctrl-V表示按"字面含义"解读下一个输入,要是想按"字面含义"输入Ctrl-V,连续输入两次就行了。
上面说了EOF的实现方式,以及使用有关返回值为EOF的函数时的注意事项。下面来说一下fegtc()函数是如何区分文件中的-1(这样说是不太严谨的)和文件结束标志返回的EOF值。
如果你读取的文件本身是一个二进制文件,由于fgetc()读取时候是一个字节一个字节读的,即使是-1(十六进制补码为0xffffffff),读取时候从低位开始读0xff,发生整形提升变为0x000000ff != -1,整个而过程如下:
-1 二进制为:1111 1111 1111 1111 1111 1111 1111 1111fgetc(fp) 以unsigned char格式读取因此读取的数据是低8位 即 1111 1111然后fgetc(fp)会将unsigned char进行整形提升 变为:0000 0000 0000 0000 0000 0000 1111 1111这代表二进制255 int c = fgetc(fp) = 255 != -1 然后继续执行fgetc() 读取-1的第二个字节....第四个字节。结合代码感受一下!
int main() {FILE* fp = nullptr;int a = -1;if ((fp = fopen(fileName, "wb")) == NULL){return -1;}//将a以二进制形式写入,fp所指的文件fwrite(&a, sizeof(int), 1, fp);fclose(fp);if ((fp = fopen(fileName, "rb")) == NULL){return -1;}int b;//以二进制格式读取fp所指的文件的内容int readcnt = fread(&b, sizeof(int), 1, fp);fclose(fp);printf("%d\n", b);FILE* fp = fopen(fileName, "rb");int c;//以fgetc()方式读取二进制文件,注意此时文件只有-1while ((c = fgetc(fp)) != EOF) {cout << c << endl;}fclose(fp);return 0;}
结果如下:
如果你读取的是一个文本文件,-1在进行保存是是按照"-",“1”,进行保存的,同理读取时候也是按照字节进行读取“-”,“1”,他们的值都会被从unsigned char转为 int 不存在会和-1冲突的情况。
到这里为止,我们已经说明白了EOF到底是个什么东西,以及文件中的-1和EOF是如何区分的。但是我再强调一点,上面的解释是基于fgetc()函数的。不同的读文件方式是由不同的判断标准的,比如用二进制方式读取,Linux读取到文件末尾后同样会返回一个信号,但是C标准库在处理这个信号是可能会采取不同的措施,比如,fread()函数返回值小于要读取的个数时就代表到达末尾。
然后我们再来看一个著名的bug:
char c;while((c = fgetc(stdin)) != EOF)putchar(c);
这段代码的执行结果如何?答案是不确定的,要看具体的编译器对于char的规定是unsigned char还是signed char。
fgetc()----> unsigned char ---- int
c = fgetc() -------> int ----- char
c != EOF --------> char ----- int
因此最终循环是否能跳出,主要判断的是 (int)0xff ?= (EOF = 0xffffffff)呢? 所以,这个问题的本质就是看char类型到底是什么,因为它决定了发生整形提升时的规则。
最后,再说一点,char是字符类型,不是特指ascii字符类型,因此不要对signed char可以表示负数而有什么疑惑。
2.fegets gets getc getchar
先来看一下这几个函数的具体定义和描述信息:
#include <stdio.h>int fgetc(FILE *stream);char *fgets(char *s, int size, FILE *stream);int getc(FILE *stream);int getchar(void);char *gets(char *s);fgetc() reads the next character from stream and returns it as an unsigned char cast to an int, or EOF on end of file or error.getc() is equivalent to fgetc() except that it may be implemented as a macro which evaluates stream more than once.getchar() is equivalent to getc(stdin).fgets() reads in at most one less than size characters from stream and stores them into the buffer pointed to by s. Reading stops after an EOF or a newline. If a newline is read, it is stored into the buffer. A terminating null byte ('\0') is stored after the last character in the buffer.
fgetc()我们已经说过了,不再赘述。getc()他和fgetc()一样,除了getc()是通过宏来实现的。
主要区别在于:
1.getc()是用宏定义的,因此不能作为函数参数进行传递。
2.其次就是宏的参数是不能有副作用的,有副作用的表达式,指的是表达式执行后,会改变表达式中某些变量的值。比如++i*++i。(有关内容不懂的话,请去查阅宏相关知识。)
3.考虑到函数调用过程需要改变堆栈信息,而宏调用不需要,因此getc效率可能比fgetc要高一些。
gets()和fgets()
Never use this function.gets() reads a line from stdin into the buffer pointed to by s until either a terminating newline or EOF, which it replaces with a null byte ('\0'). No check for buffer overrun is performed (see BUGS below).
这是linux的man手册中关于gets的一段描述,可以看出linux建议我们永远不要使用这个函数。
这两个函数都是以EOF或换行符结尾的,fgets需要我们指定缓冲区和读取的字节数目,而gets只需要指定具体的流。下面来感受一下他们的区别:
int main() {char buffer[7] = {};fgets(buffer, 7, stdin);printf("%s\n", buffer);return 0;}
输入123456789 9个字节超过了缓冲区大小,截取前六个字节,因为这个函数说明中说了会默认留一个字节添加"\0"。
没有超过指定字节数,且除了\0"还有多余空间,会把换行符也添加进去。这里有时候需要注意下。
接下来看gets
int main() {char buffer[7] = {};gets_s(buffer);printf("%s\n", buffer);return 0;}
超过7字节数(不要忘记\0),程序崩溃。
getchar(),这个函数就相当于getc(stdin),即只能从标准输入读取。