C++PrimerPlus 第五章 循环和关系表达式 - 5.5 循环和文本输入
5.5 循环和文本输入
知道循环的工作原理后,来看一看循环完成的一项最常见、最重要的任务:逐字符地读取来自文件或键盘的文本。例如,读者可能想编写一个能够计算输入中的字符数、行数和字数的程序。传统上,C++和C语言一样,也使用while循环来完成这类任务。下面介绍这是如何完成的。即使熟悉C语言,也不要太快地浏览本节和下一节。尽管C++中的while循环与C语言中的while循环一样,但C++的I/O工具不同,这使得C++循环看起来与C语言循环有些不同。事实上,cin对象支持3种不同模式的单字符输入,请用户接口各不相同。下面介绍如何在while循环中使用这三种模式。
5.5.1 使用原始的cin进行输入
如果程序要使用循环来读取来自键盘的文本输入,则必须有办法知道何时停止读取。如何知道这一点呢?一种办法是选择某个特殊字符——有时被称为哨兵字符(sentinel character),将其作为停止标记。即在屏幕上显示读取的字符。按下键盘上的键不能自动将字符显示到屏幕上,程序必须通过回显输入字符来完成这项工作。通常,这种任务由操作系统处理。运行完毕后,该程序将报告处理的总字符数。程序清单5.16列出了该程序的代码。
程序清单5.16 textin1.cpp
//textin1.cpp -- reading chars with a while loop
#include<iostream>
int main()
{
using namespace std;
char ch;
int count = 0; //use basic input
cout << "Enter characters; enter # to quit: \n";
cin >> ch; //get a character
while (ch != '#') //test the character
{
cout << ch; //echo the character
++count; //count the character
cin >> ch; //get the next character
}
cout << endl << count << " characters read\n";
return 0;
}
下面是该程序的运行情况:
Enter characters; enter # to quit:
see ken run#really fast
seekenrun
9 characters read
程序说明
请注意该程序的结构。该程序在循环之前读取第一个输入字符,这样循环可以测试第一个字符。这很重要,因为第一个字符可能是#。由于textin1.cpp使用的是入口条件循环,因此在这种情况下,能够正确地跳过整个循环。由于前面已经将变量count设置为0,因此count的值也是正确的。
如果读取的第一个字符不是#,则程序进入该循环,显示字符,增加计数,然后读取下一个字符。最后一步极为重要,没有这一步,循环将反复处理第一个输入字符,一直进行下去。有了这一步后,程序就可以处理到下一个字符。
注意,该循环设计遵循了前面指出的几条指导原则。结束循环的条件是最后读取的一个字符是#。该条件是通过在循环之前读取一个字符进行初始化的,而通过循环体结尾读取下一个字符进行更新。
上面的做法合情合理。但为什么程序在输出时省略了空格呢?原因在cin。读取char值时,与读取其他基本类型一样,cin将忽略空格和换行符。因此输入中的空格没有被回显,也没有被包括在计数内。
更为复杂的是,发送给cin的输入被缓冲。这意味着只有在用户按下回车键后,他输入的内容才会被发送给程序。这就是在运行该程序时,可以在#后面输入字符的原因。按下回车键后,整个字符序列将被发送给程序,但程序在遇到#字符后将结束对输入的处理。
5.5.2 使用cin.get(char)进行补救
通常,逐个字符读取输入的程序需要检查每个字符,包括空格、制表符和换行符。cin所属的istream类(在iostream中定义)中包含一个能够满足这种要求的成员函数。具体地说,成员函数cin.get(ch)读取输入中的下一个字符(即使它是空格),并将其赋给变量ch。使用这个函数调用替换cin>>ch,可以修补程序清单5.16的问题。程序清单5.17列出了修改后的代码。
程序清单5.17 textin2.cpp
//textin2.cpp -- using cin.get(char)
#include<iostream>
int main()
{
using namespace std;
char ch;
int count = 0;
cout << "Enter characters; enter # to quit: \n";
cin.get(ch); //use the cin.get(ch) function
while (ch != '#')
{
cout << ch;
++count;
cin.get(ch); //use it again
}
cout << endl << count << " characters read\n";
return 0;
}
下面是该程序的运行情况:
Enter characters; enter # to quit:
Did you use a #2 pencil?
Did you use a
14 characters read
现在,该程序回显了每个字符,并将全部字符计算在内,其中包括空格。输入仍被缓冲,因此输入的字符个数仍可能比最终到达程序的要多。
如果熟悉C语言,可能以为这个程序存在严重的错误!cin.get(ch)调用将一个值放在ch变量中,这意味着将修改该变量的值。在C语言中,要修改变量的值,必须将变量的地址传递给函数。但程序清单5.17调用cin.get()时,传递的是ch,而不是&ch。在C语言中,这样的代码无效,但在C++中有效,只要函数将参数声明为引用即可。引用是C++在C语言的基础上新增的一种类型。头文件iostream将cin.get(ch)的参数声明为引用类型,因此该函数可以修改其参数的值。我们将在第8章中详细介绍。同时,C语言行家可以松一口气了——通常,在C++中传递的参数的工作方式与在C语言中相同。然而,cin.get(ch)不是这样。
5.5.3 使用哪一个cin.get()
在第4章的程序清单4.5中,使用了这样的代码?
char name[ArSize];
...
cout << “Enter your name:\n”;
cin.get(name, ArSize).get();
最后一行相当于两个连续的函数调用:
cin.get(name, ArSize);
cin.get();
cin.get()的一个版本接受两个参数:数组名(字符串(char* 类型)的地址)和ArSize(int类型的整数)。(记住,数组名是其第一个元素的地址,因此字符数组名的类型为char*。)接下来,程序使用了不接受任何参数的cin.get()。而最近,我们这样使用过cin.get():
char ch;
cin.get(ch);
这里cin.get接受一个char参数。
看到这里,熟悉C语言的读者将再次感到兴奋或困惑。在C语言中,如果函数接受char指针和int参数,则使用该函数时,不能只传递一个参数(类型不同)。但在C++中,可以这样做,因为该语言支持被称为函数重载的OOP特性。函数重载允许创建多个同名函数,条件是它们的参数列表不同。例如,如果在C++中使用cin.get(name, ArSize),则编译器将找到使用char*和int作为参数的cin.get()版本;如果使用cin.get(ch),则编译器将使用接受一个char参数的版本;如果没有提供参数,则编译器将使用不接受任何参数的cin.get()版本。函数重载允许对多个相关的函数使用相同的名称,这些函数以不同方式或针对不同类型执行相同的基本任务。第8章将讨论该主题。另外,通过使用istream类中的get()示例,读者将逐渐习惯函数重载。为区分不同的函数版本,我们在引用它们时提供参数列表。因此,cin.get()指的是不接受任何参数的版本,而cin.get(char)则指的是接受一个参数的版本。
5.5.4 文件尾条件
程序清单5.17表明,使用诸如#等符号来表示输入结束很难令人满意,因为这样的符号可能就是合法输入的组成部分,其他符号(如@和%)也如此。如果输入来自于文件,则可以使用一种功能更强大的技术——检测文件尾(EOF)。C++输入工具和操作系统协同工作,来检测文件尾并将这种信息告知程序。
乍一看,读取文件中的信息似乎同cin和键盘输入没什么关系,但其实存在两个相关的地方。首先,很多操作系统(包括Unix、Linux和Windows命令提示符模式)都支持重定向,允许用文件替换键盘输入。例如,假设在Windows中有一个名为gofish.exe的可执行程序和一个名为fishtale的文本文件,则可以在命令提示符模式下输入下面的命令:
gofish <fishtale
这样,程序将从fishtale文件(而不是键盘)获取输入。<符号是Unix和Windows命令提示符模式的重定向运算符。
其次,很多操作系统都允许通过键盘来模拟文件尾条件。在Unix中,可以在行首按下Ctrl+D来实现;在Windows命令提示符模式下,可以在任意位置按Ctrl+Z和Enter。有些C++实现支持类似的行为,即使底层操作系统并不支持。键盘输入的EOF概念时间上是命令行环境遗留下来的。然而,用于Mac的Symantec C++模拟了UNIX,将Ctrl+D视为仿真的EOF。Metrowerks Codewarrior能够在Macintosh和Windows环境下识别Ctrl+Z。用于PC的Microsoft Visual C++、Borland C++ 5.5和GNU C++都能够识别行首的Ctrl+Z,但用户必须随后按下回车键。总之,很多PC编程环境都将Ctrl+Z视为模拟的EOF,但具体细节(必须在行首还是可以在任何位置,是否必须按下回车键等)各不相同。
如果编程环境能够检测EOF,可以在类似于程序清单5.17的程序中使用重定向的文件,也可以使用键盘输入,并在键盘输入中模拟EOF。这一点似乎很有用,因此我们来看看究竟如何做。
检测到EOF后,cin将两位(eofbit和failbit)都设置为1。可以通过成员函数eof()来查看eofbit是否被设置;如果检测到EOF,则cin.eof()将返回bool值true,否则返回false。同样,如果eofbit或failbit被设置为1.则fail()成员函数返回true,否则返回false。注意,eof()和fail()方法报告最近读取的结果;也就是说,它们在事后报告,而不是预先报告。因此应将cin.eof()或cin.fail()测试放在读取后,程序清单5.18中的设计体现了这一点。它使用的是fail(),而不是eof(),因此前者可用于更多的实现中。
程序清单5.18 textin3.cpp
//textin3.cpp -- reading chars to end of file
#include<iostream>
int main()
{
using namespace std;
char ch;
int count = 0;
cin.get(ch); //attempt to read a char
while (cin.fail() == false) //test for EOF
{
cout << ch; //echo character
++count;
cin.get(ch); //attempt to read another char
}
cout << endl << count << " characters read\n";
return 0;
}
下面是该程序的运行情况:
The green bird sings in the winter.<ENTER>
The green bird sings in the winter.
Yes, but the crow flies in the dawn.<ENTER>
Yes, but the crow flies in the dawn.
<CTRL>+<Z><ENTER>
73 characters read
这里在Windows 7系统上运行该程序,因此可以按下Ctrl+Z和回车键来模拟EOF条件。请注意,在Unix和类Unix(包括Linux和Cygwin)系统中,用户应按Ctrl+Z组合键将程序挂起,而命令fg恢复执行程序。
通过使用重定向,可以用该程序来显示文本文件,并报告它包含的字符数。下面,我们在Unix系统运行该程序,并对一个两行的文件进行读取、回显和计算字数($是Unix提示符):
$ textin3 < stuff
I am a Unix file. I am proud
to be a Unix file.
48 characters read
$
5.5.4.1 EOF结束输入
前面指出过,cin方法检测到EOF时,将设置cin对象中一个指示EOF条件的标记。设置这个标记后,cin将不读取输入,再次调用cin也不管用。对于文件输入,这是有道理的,因为程序不应读取超出文件尾的内容。然而,对于键盘输入,有可能模拟EOF来结束循环,但稍后要读取其他输入。cin.clear()方法可能清除EOF标记,使输入继续进行。这将在第17章详细介绍。不过要记住的是,在有些系统中,按Ctrl+Z实际上将结束输入和输出,而cin.clear()将无法恢复输入和输出。
5.5.4.2 常见的字符输入做法
每次读取一个字符,直到遇到EOF的输入循环的基本设计如下:
cin.get(ch); //attempt to read a char
while(cin.fail() == false) //test for EOF
{
... //do stuff
cin.get(ch); //attempt to read another char
}
可以在上述代码中使用一些简捷方式。第6章将介绍的!运算符可以将true切换为false或将false切换为true。可以使用此运算符将上述while测试改写成这样:
while (!cin.fail()) //while input has not failed
方法cin.get(char)的返回值是一个cin对象。然而,istream类提供了一个可以将istream对象(如cin)转换为bool值的函数;当cin出现在需要bool值的地方(如在while循环的测试条件中)时,该转换函数将被调用。另外,如果最后一次读取成功了,则转换得到的bool值为true;否则为false。这意味着可以将上述while测试改写为这样:
while(cin) //while input is successful
这比!cin.fail()或!cin.eof()更通用,因为它可以检测到其他失败原因,如磁盘故障。
最后,由于cin.get(char)的返回值为cin,因此可以将循环精简成这种格式:
while (cin.get(ch)) //while input is successful
{
... //do stuff
}
这样,cin.get(char)只被调用一次,而不是两次:循环前一次、循环结束一次。为判断循环测试条件,程序必须首先调用cin.get(ch)。如果成功,则将值放入ch中。然后,程序获得函数调用的返回值,即cin。接下来,程序对cin进行bool转换,如果输入成功,则结果为true,否则为false。三条指导原则(确定结束条件、对条件进行初始化以及更新条件)全部被放在循环测试条件中。
5.5.5 另一个cin.get()版本
“怀旧”的C语言用户可能喜欢C语言中的字符I/O函数——getchar()和putchar(),它们仍然适用,只要像在C语言中那样包含头文件stdio.h(或新的cstdio)即可。也可以使用istream和ostream类中类似功能的成员函数,来看看这种方式。
不接受任何参数的cin.get()成员函数返回输入中的下一个字符。也就是说,可以这样使用它:
ch = cin.get();
该函数的工作方式与C语言中的getchar()相似,将字符编码作为int值返回;而cin.get(ch)返回一个对象,而不是读取的字符。同样,可以使用cout.put()函数(参见第3章)来显示字符:
cout.put(ch);
该函数的工作方式类似C语言中的putchar(),只不过其参数类型为char,而不是int。
为成功地使用cin.get(),需要知道其如何处理EOF条件。当该函数到达EOF时,将没有可返回地字符。相反,cin.get()将返回一个用符号常量EOF表示地特殊值。该常量是在头文件iostream中定义的。EOF值必须不同于任何有效的字符值,以便程序不会将EOF与常规字符混淆。通常,EOF被定义为值-1,因为没有ASCII码为-1的字符,但并不需要知道实际的值,而只需在程序中使用EOF即可。例如,程序清单5.18的核心是这样:
char ch;
cin.get(ch);
while (cin.fail() == false) //test for EOF
{
cout << ch;
++count;
cin.get(ch);
}
可以使用int ch,并用cin.get()代替cin.get(char),用cout.put()代替cout,用EOF测试代替cin.fail()测试:
int ch; //for compatibility with EOF value
ch = cin.get();
while (ch != EOF)
{
cout.put(ch); //cout.put(char(ch)) for some implementations
++count;
ch = cin.get();
}
如果ch是一个字符,则循环将显式它。如果ch为EOF,则循环将结束。
除了当前所做的修改外,关于使用cin.get()还有一个微妙而重要的问题。由于EOF表示的不是有效字符编码,因此可能不与char类型兼容。例如,在有些系统中,char类型是没有符号的,因此char变量不可能为EOF值(-1)。由于这种原因,如果使用cin.get()(没有参数)并测试EOF,则必须将返回值赋给int变量,而不是char变量。另外,如果将ch的类型声明为int,而不是char,则必须在显示ch时将其强制转换为char类型。
程序清单5.19将程序清单5.18进行了修改,使用了cin.get()方法。它还通过将字符输入与while循环测试合并在一起,使代码更为简洁。
程序清单5.19 textin4.cpp
//textin4.cpp -- reading chars with cin.get()
#include<iostream>
int main()
{
using namespace std;
int ch; //should be int, not char
int count = 0;
while ((ch = cin.fail()) != EOF) //test for end-of-file
{
cout.put(char(ch));
++count;
}
cout << endl << count << " characters read\n";
return 0;
}
下面是该程序的运行情况:
The sullen mackerel sulks in the shadowy shallows.<ENTER>
The sullen mackerel sulks in the shadowy shallows.
Yes, but the blue bird of happiness harbors secrets.<ENTER>
Yes, but the blue bird of happiness harbors secrets.
<CTRL>+<Z><ENTER>
104 characters read
下面分析一下循环条件:
while ((ch = cin.get()) != EOF)
子表达式ch = cin.get()两端的括号导致程序首先计算该表达式。为此,程序必须首先调用cin.get()函数,然后将该函数的返回值赋给ch。由于赋值语句的值为左操作数的值,因此整个子表达式变为ch的值。如果这个值是EOF,则循环将结束,否则继续。该测试条件中所有的括号都是必不可少的。如果省略其中的一些括号:
while (ch = cin.get() != EOF)
由于!=运算符的优先级高于=,因此程序将首先对cin.get()的返回值和EOF进行比较。比较的结果为false或true,而这些bool值将被转换为0或1,并本质赋给ch。
另一方面,使用cin.get(ch)(有一个参数)进行输入时,将不会导致任何类型方面的问题。前面讲过,cin.get(char)函数在到达EOF时,不会将一个特殊值赋给ch。事实上,在这种情况下,它不会将任何值赋给ch。ch不会被用来存储非char值。下表总结了cin.get(char)和cin.get()之间的差别。
那么应使用cin.get()还是cin.get(char)呢?使用字符参数的版本更符合对象方式,因为其返回值是istream对象。这意味着可以将它们拼接起来。例如,下面的代码将输入中的下一个字符读入到ch1中,并将接下来的一个字符读入到ch2中:
cin.get(ch1).get(ch2);
这是可行的,因为函数调用cin.get(ch1)返回一个cin对象,然后便可以通过该对象调用get(ch2)。
get()的主要用途是能够将stdio.h的getchar()和putchar()函数转换为iostream的cin.get()和cout.put()方法。只要用头文件iostream替换stdio.h,并用作用相似的方法替换所有的getchar()和putchar()即可。(如果旧的代码使用int变量进行输入,而所用的实现包含put()的多个原型,则必须做进一步的调整。)