前言
众所周知,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.
参见文章:
http://blog.csdn.net/rebel_321/article/details/4927464
当然,不能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 |
多个 |
无 |
无 |
无 |