【C++】编程实例:循环与文本输入问题

任务描述: 逐字符地读取来自文件或键盘的文本

使用原始的cin进行输入

分析: 如果程序要使用循环来读取来自键盘的文本输入,则必须有办法知道何时停止读取。

如何知道这一点呢?

一种方法是选择某个特殊字符—有时被称为哨兵字符(sentinel character),将其作为停止标记。例如,在遇到#字符时停止读取输入。该程序计算读取的字符数,并回显这些字符,即在屏幕上显示读取的字符。按下键盘上的键不能自动将字符显示到屏幕上,程序必须通过回显输入字符来完成这项工作。通常,这种任务由操作系统处理。运行完毕后,该程序将报告处理的总字符数。

#include <iostream>
#include <cstring>
using namespace std;

int main() {
	char ch;
	int count = 0;
	cout << "Enter characters;enter # to quit:\n";
	cin >> ch;
	while (ch != '#') {
		cout << ch;
		++count;
		cin >> ch;
	}
	cout << endl << count << "characters read\n";
	return 0;
}

运行结果:

Enter characters;enter # to quit:
May your pain clean your eyes # this character
Mayyourpaincleanyoureyes
24characters read

--------------------------------
Process exited after 122.7 seconds with return value 0
请按任意键继续. . .

请仔细观察运行结果:

上面的做法合情合理。但为什么程序在输出时省略了空格呢?

原因出在cin。读取char值时,与读取其他基本类型一样,cin将忽略空格和换行符。因此输入中的空格没有被回显,也没有被包括在计数内。

更为复杂的是,发送给cin的输入被缓冲。这意味着只有在用户按下回车键后,他输入的内容才会被发送给程序。这就是在运行该程序时,可以在#后面输入字符的原因。按下回车键后,整个字符序列将被发送给程序,但程序在遇到#字符后将结束对输入的处理。

使用cin.get(char)进行补救

通常,逐个字符读取输入的程序应该需要检查每个字符,包括空格、制表符和换行符。

cin所属的istream类(在iostream中定义)中包含一个能够满足这种要求的成员函数。成员函数cin.get(ch)读取输入中的下一个字符(即使它是空格),并将其赋给变量ch。使用这个函数调用替换cin>>ch,可以修补这个漏洞。

#include <iostream>
#include <cstring>
using namespace std;

int main() {
	char ch;
	int count = 0;
	cout << "Enter characters;enter # to quit:\n";
	cin.get(ch);
	while (ch != '#') {
		cout << ch;
		++count;
		cin.get(ch);
	}
	cout << endl << count << "characters read\n";
	return 0;
}

运行结果:

Enter characters;enter # to quit:
May your pain clean your eyes # this character
May your pain clean your eyes
30characters read

--------------------------------
Process exited after 331.4 seconds with return value 0
请按任意键继续. . .

文件尾条件

使用诸如#等符号来表示输入结束很难令人满意,因为这样的符号可能就是合法输入的组成部分,其他符号(如@
和%)也如此。如果输入来自于文件,则可以使用一种功能更强大的技术—检测文件尾(EOF)。C++输入工具和操作系统协同工作,来检测文件尾并将这种信息告知程序。

乍一看,读取文件中的信息似乎同cin和键盘输入没什么关系,但其实存在两个相关的地方。首先,很多操作系统都支持重定向,允许用文件替换键盘输入。例如,假设在Windows中有一个名为gofish.exe的可执行程序和一个名为fishtale的文本文件,则可以在命令提示符模式下输入下面的命令:

gofish <fishtable

程序将从fishtale文件(而不是键盘)获取输入。< 符号是Unix和Windows命令提示符模式的重定向运算符。

其次,很多操作系统都允许通过键盘来模拟文件尾条件。在Unix中,可以在行首按下Ctrl+D来实现;在Windows命令提示符模式下,可以在任意位置按Ctrl+Z和Enter。

键盘输入的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。这一点似乎很有用,因此我们来看看究竟如何做。

  1. 检测到EOF后,cin将两位(eofbit和failbit)都设置为1。
  2. 通过成员函数eof( )来查看eofbit是否被设置:
    • 如果检测到EOF,则cin.eof( )将返回bool值true,否则返回false。
    • 同样,如果eofbit或failbit被设置为1,则fail( )成员函数返回true,否则返回false。

什么是重定向?

修改标准输入和标准输出关联的工具。

  • 输出到文件,而不是显示器
  • 允许用文件替换键盘输入
  • Windows允许通过键盘模拟文件尾:Ctrl+Z

注意,eof( )和fail( )方法报告最近读取的结果;也就是说,它们在事后报告,而不是预先报告。因此应将cin.eof( )或cin.fail( )测试放在读取后。下面的程序体现了这一点。它使用的是fail( ),而不是eof( ),因为前者可用于更多的实现中。

#include <iostream>
#include <cstring>
using namespace std;

int main() {
	char ch;
	int count = 0;
	cout << "Enter characters;enter # to quit:\n";
	cin.get(ch);
	while (cin.fail() == false) {
		cout << ch;
		++count;
		cin.get(ch);
	}
	cout << endl << count << "characters read\n";
	return 0;
}

运行结果如下:

Enter characters;enter # to quit:
May your pain clean your eyes.<ENTER>
May your pain clean your eyes.
tes<ENTER>
tes
^Z<ctrl+Z>
<ENTER>

35characters read

--------------------------------
Process exited after 21.74 seconds with return value 0
请按任意键继续. . .

通过使用重定向,可以用该程序来显示文本文件,并报告它包含的字符数。下面,我们在Windows系统运行该程序,并对一个四行的文件进行读取、回显和计算字数:

D:\Program Files (x86)\Dev-Cpp\code>test2.exe < stuff.txt
Enter characters;enter # to quit:
我是一个
txt文档。
我有
四行。
30characters read

结合输入

前面指出过,cin方法检测到EOF时,将设置cin对象中一个指示EOF条件的标记。设置这个标记后,cin将不读取输入,再次调用cin也不管用。对于文件输入,这是有道理的,因为程序不应读取超出文件尾的内容。然而,对于键盘输入,有可能使用模拟EOF来结束循环,但稍后要读取其他输入。cin.clear( )方法可能清除EOF标记,使输入继续进行。这将在第17章详细介绍。不过要记住的是,在有些系统中,按Ctrl+Z实际上将结束输入和输出,而cin.clear( )将无法恢复输入和输出。

如何精简程序

方法cin.get(char)的返回值是一个cin对象。然而,istream类提供了一个可以将istream对象(如cin)转换为bool值的函数;当cin出现在需要bool值的地方(如在while循环的测试条件中)时,该转换函数将被调用。另外,如果最后一次读取成功了,则转换得到的bool值为true;否则为false。

这意味着可以将上述while测试改写为这样:

#include <iostream>
#include <cstring>
using namespace std;

int main() {
	char ch;
	int count = 0;
	cout << "Enter characters;enter # to quit:\n";
	cin.get(ch);
	while (cin) {//注意到了吗?cin可以被转换成一个布尔值
		cout << ch;
		++count;
		cin.get(ch);
	}
	cout << endl << count << "characters read\n";
	return 0;
}

这比! cin.fail( )或!cin.eof( )更通用,因为它可以检测到其他失败原因,如磁盘故障。最后,由于cin.get(char)的返回值为cin,因此可以将循环精简成这种格式:

while (cin.get(ch)) {//注意到了吗?cin.get()返回的就是一个cin对象
		cout << ch;
		++count;
	}

cin.put()

cin.put(ch);

最初,put( )成员只有一个原型—put(char)。可以传递一个int参数给它,该参数将被强制转换为char。C++标准还要求只有一个原型。然而,有些C++实现都提供了3个原型:put(char)、put(signed char)和put(unsigned char)。在这些实现中,给put( )传递一个int参数将导致错误消息,因为转换int的方式不止一种。使用显式强制类型转换的原型(如cin.put(char(ch)))可使用int参数。

cin.get( )将返回一个用符号常量EOF表示的特殊值。该常量是在头文件iostream中定义的。EOF值必须不同于任何有效的字符值,以便程序不会将EOF与常规字符混淆。通常,EOF被定义为值−1,因为没有ASCII码为−1的字符,但并不需要知道实际的值,而只需在程序中使用EOF即可。

可以使用int ch,并用cin.get( )代替cin.get(char),用cout.put( )代替cout,用EOF测试代替cin.fail( )测试:

int ch;
ch = cin.get();
while(ch != EOF){
    cout.put(ch);
    ++count;
    ch = cin.get();
}

如果ch是一个字符,则循环将显示它。如果ch为EOF,则循环将结束。

需要知道的是,EOF不表示输入中的字符,而是指出没有字符。

除了当前所做的修改外,关于使用cin.get( )还有一个微妙而重要的问题。由于EOF表示的不是有效字符编码,因此可能不与char类型兼容。例如,在有些系统中,char类型是没有符号的,因此char变量不可能为EOF值(−1)。由于这种原因,如果使用cin.get( )(没有参数)并测试EOF,则必须将返回值赋给int变量,而不是char变量。另外,如果将ch的类型声明为int,而不是char,则必须在显示ch时将其强制转换为char类型。

那么如何针对以上问题修改代码呢?

int ch;
int count = 0;
while((ch = cin.get()) != EOF){
    cout.put(char(ch));
    ++count;
}

分析一下循环条件:

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(ch)与cin.get()的差异:

属性 cin.get(ch) cin = cin.get()
传递输入字符的方式 赋给参数ch 将函数返回值赋给ch
用于字符输入时函数的返回值 istream对象(执行bool转换后为true) int类型的字符编码
到达EOF时函数的返回值 istream对象(执行bool转换后为false) EOF

那么应使用cin.get( )还是cin.get(char)呢?使用字符参数的版本更符合对象方式,因为其返回值是istream对象。这意味着可以将它们拼接起来。例如,下面的代码将输入中的下一个字符读入到ch1中,并将接下来的一个字符读入到ch2中:

cin.get(ch1).get(ch2);

这是可行的,因为函数调用cin.get(ch1)返回一个cin对象,然后便可以通过该对象调用get(ch2)。

猜你喜欢

转载自blog.csdn.net/weixin_43717839/article/details/129084782