C++字符输入方式初探——为什么不能eof

前言

众所周知,C++的字符处理,尤其是输入输出,各种“读到文件结束”十分复杂。本文简单研究一下C++的各种字符输入方式。

由于C++提供了各种eof方法以供判断文件结尾,这就让人产生一种写类似这样代码的冲动:

while(eof){
    读入什么东西;
}

而它在绝大多数情况下都是不能正常工作的。如果你改成这样:


while(true){
    读入什么东西;
    if(eof) break;
}

就能正常运行了,但是看上去一点也不不优美酷炫装逼吊炸天。


本文的目的就是对C++中各种主流读入方式,研究第一种方法为什么不行。

注意:本文所有测试都是在Windows环境下进行的。

C风格

C++中,字符输入输出有两种风格:一种是继承于C的,cstdio库中包含的各函数,另一种是C++风格的,iostream/fstream。

在cplusplus.com中,可以查到cstdio的如下成员函数:

getchar();fgetc();getc();
gets();fgets();
scanf();fscanf();
fread();

各种getchar

这里有三个函数:getchar,fgetc和getc。其中,fgetc和getc等价,它们都相当于给定了输入文件指针的getchar。

getchar函数的返回值是一个int:如果它成功读取字符,这个int就等于读取字符的ASCII码,否则就是EOF。这个EOF是编译器用宏定义的一个常数,也就是-1. “读取不成功”包含你能想到的各种情况:比如读完了文件的最后一个字符,或文件不存在,或文件为空。

如果用getchar读取文件,能否用eof(注意这是小eof,那个大EOF是一个宏定义的常量)判断文件是否结束呢?答案是不行。不妨做个实验:

    freopen("input.in","r",stdin);
    while(!feof(stdin)){
        cout<<(int)getchar()<

这个feof是cstdio中的eof函数(判断文件是否结束),在这里一定不能用cin.eof(),它俩根本不是一个系统的。


这个原因是为什么呢?在cstdio中,判断eof的方式是设一个标志(end of file indicator),这个标志平时是0,而当你用任何方式试图去读“文件结尾再下一位”的时候,它就会变成一个正值,表明文件结束。但是,如果一直用getchar,那么它站在文件末尾的时候并不知道下一位还有没有东西,那就需要进行一次尝试,所以就读进来了一个-1.

参见文章:
当然,不能eof也不是什么大事:用-1判就行了嘛。本来getchar返回-1就是为这个目的而设计的。

在C++读入界,getchar就像一个香港记者:跑得比谁都快,(因此称霸OI选手的快速读入模板),但读来读去,不做任何格式分析,too simple。想判空格?想判换行?客官您自己写去吧。

举个栗子:

    freopen("input.in","r",stdin);
    char c;
    while((c=getchar())!=-1){
        cout<

如此就可把一个文件里的所有字符从头到尾,原封不动地输出出来。


gets和fgets

不同于getchar/fgetc,fgets和gets有显著区别。

gets

gets的函数原型是:

char * gets ( char * str );

它的作用是:读取字符数组直到遇见一个回车('\n'),或读到了文件末尾。如果读取成功,它会把读取的东西放进str数组中(不包括最后可能的换行符,并在str的末尾添加一个'\0'),并返回str的指针。如果读取失败,str数组不变,并返回一个空指针(NULL)。如果这一行有回车,它就相应读入一个长度为0的字符数组。


那么,gets能不能用eof判文件结束呢?答案是:不太行。用下面的代码试一试:

    freopen("input.in","r",stdin);
    char s[100];
    char *s1;
    while(!feof(stdin)){
        s1=gets(s);
        printf("%d ",s1);
        cout<

你会发现:如果你的文件最后一行不是回车,那么它能正常工作。否则不行。


这个锅是这么产生的:当fgets读了最后一行,看到了回车,并发现后面没有的时候,就会设置eof标志。而这一行的读入是否成功呢?如果这一行有字符,它就认为成功,否则(即最后一行是回车)失败。如果读取失败,那它就不会改变str的值——这就导致看上去,倒数第二行被读了两次。

当然,eof不靠谱,用靠谱的方法就是了——看看gets的返回值是不是NULL即可。如果你是一名经验丰富的膜法师,不妨用这个函数,无他,代码短尔。

fgets

fgets和原型是:

char * fgets ( char * str, int num, FILE * stream );

它比gets多了两个参数:num代表最多允许读取的位数,stream是文件指针。


fgets还有一个重大的不同:它会把回车(如果有的话)放在读入的字符数组后面。这就会造成一种尴尬的局面:最后一行的后面没有回车(如果它能被成功读入的话)。其他都和gets一样。

scanf和fscanf

这两个都是大家喜闻乐见的函数。fscanf就是scanf加一个文件指针,这里按下不表。

scanf的用法比较多样,这里主要关注两种:用它读"%s"(字符数组)和"%c"(字符)。

字符数组

scanf("%s",str); 这条命令意味着:一直读字符,直到遇到空格/tab/回车/文件结束。它返回一个整数:如果读取成功,就返回读取的字符数组个数(这里只打了一个%s,所以至多是1),如果读取失败,就返回常量EOF(也就是-1)。如果想用eof读到文件结尾,就会和gets遇到同样的问题:如果文件的最后一个字符是空格/tab/回车之一,最后一次读取就会挂掉。

不用eof,用返回-1这个标准判断就能搞定了,大家都说好。

字符

scanf("%c",&ch);这条命令等价于:ch=getchar(); 自然,试图用eof判结尾也会遇到getchar中出现的问题。

换言之,scanf一个字符和getchar的功能完全一样——而且它比getchar跑得慢。

那这个用法和咸鱼有什么区别呢?

完全没有区别,它就是咸鱼。

呃……其实还有点区别。你可以指定读几个字符,并且把传过去的&ch换成一个字符数组。这个用法比较生僻,就略过了。

fread

fread的函数原型是:

size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );

它的作用是一下把一整个文件(stream)读进数组ptr里。size是要读入的每个字符的大小(以byte记),而count是总共至多读多少个字符。它的返回值是一个非负整数:成功读入的字符个数。


这个函数一般不用,用法和getchar类似,好处也是跑得比谁都快,不过得预先开一个数组。举个栗子:

    FILE *fin = fopen("input.in","r");
    char s[100]={0};
    cout<

C++风格

至于C++风格,就是大家喜闻乐见的,iostream和fstream全家桶。

首先明确一点:iostream和fstream都是继承istream类而来。换个容易理解的说法:cin和fin其实是一个东西。下面我们全部用cin为例。

研究一下istream类,其中能读东西的大概是这些函数:
cin>>yituodongxi;//一坨东西
cin.get();
cin.getline();
cin.read();


重载运算符

istream类型重载了>>(右移)运算符,使得你能够顺滑地干类似cin>>a;这样的事情。我们在这里讨论两种情况:读一个字符,和读一堆字符。

读一个

C++实现istream类型的方法比较复杂,它包装了一个streambuf类型,从而让它同时能读字符/字符串/字符数组。具体的操作细节不用关心,如果cin>>ch;(ch是char型),它做的事情是:读第一个非空格/tab/回车/文件结束的字符。

它和getchar都是“读一个字符”的方法,而如果妄图用cin.eof()判文件结尾,getchar的问题在这里同样会出现。更为坑爹的一点是:如果读不成,它会直接让你的ch保持原样,而非像getchar那样,给一个-1供人凭吊。在表面上看来,就是最后一个字符会重复读两遍。

举个栗子:

    freopen("input.in","r",stdin);
    char ch;
    while(!cin.eof()){
        cin>>ch;
        cout<<(int)ch<<" "<

读一堆

如果你cin的是一个字符串/字符数组,那它的表现和scanf一个字符数组是类似的。如果文件的最后一个字符是空格/tab/回车之一,while(eof)就会失败。

get

cin.get()重载了好多种,不过都和之前我们研究过的函数等价:

int get();//这货就是getchar
istream& get (char& c);//这货相当于scanf("%c"),但读取失败会返回NULL
istream& get (char* s, streamsize n);//这货相当于scanf("%s"),同样读取失败返回NULL
istream& get (char* s, streamsize n, char delim);//和楼上类似但可以自定义分隔符
istream& get (streambuf& sb);//能兼容上面几种
istream& get (streambuf& sb, char delim);//和楼上类似但可以自定义分隔符

getline

cin.getline()有两种:

istream& getline (char* s, streamsize n );
istream& getline (char* s, streamsize n, char delim );//能自定义分隔符

它其实和gets一样(但你得指定最大读取长度n),试图用while(eof)时遇到的问题也和gets一样:如果文件的最后一个字符是回车,就会挂掉。

举个栗子:

    freopen("input.in","r",stdin);
    char ch[100];
    while(!cin.eof()){
        cin.getline(ch,100);
        cout<

read

其实就是fread的C++版本啦。毫无保留地把所有字符读进来,从头读到尾。不过用法简洁一些,省去了sizeof(char)。

举个栗子:

    freopen("input.in","r",stdin);
    char str[100];
    cin.read(str,100);
    cout<

总结

为什么while(eof)这种写法不能正常运行呢?简而言之,分为两种:读单个字符的方法,原因是在“无法预知未来”;读一串字符的方法,原因是“无法定义失败”。好像突然就哲学起来了……(逃

我们总结出如下这张表格,作为参考:

函数名 读取个数 分隔符 eof什么时候失败 读取失败的特征
getchar/fgetc/getc 一个 永远 返回-1
gets/fgets 多个 回车('\n') 文件末尾是分隔符 返回NULL
scanf/fscanf "%c" 一个 永远 返回-1
scanf/fscanf "%s" 多个 空格/tab/回车 文件末尾是分隔符 返回-1
fread 多个
cin>>ch 一个 空格/tab/回车 永远 返回NULL
cin>>str 多个 空格/tab/回车 文件末尾是分隔符 返回NULL
cin.get() 一个 永远 返回-1
cin.get(ch) 一个 永远 返回NULL
cin.get(str) 多个 空格/tab/回车 文件末尾是分隔符 返回NULL
cin.getline 多个 回车('\n') 文件末尾是分隔符 返回NULL
cin.read 多个

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注