C++ 中 cin.getline() 的刷新问题
当您想要从 C++ 的输入流中删除无关字符时,通常是因为您混合了格式化和未格式化的输入方法。格式化的方法将在流中留下换行符,未格式化的方法将使用它并成功终止,但完全无法执行您想要的操作。
#include <iostream>
int main() {
std::cout<<"Enter the letter A: ";
std::cin.get();
std::cout<<"Enter the letter B: ";
std::cin.get();
std::cout<<"Too late, you can't type anymore\n";
}
通常这个问题源于另一个问题,即如何在程序终止之前暂停程序。仅当流中没有剩余字符时,使用 cin.get() 才有效。当你抛出一个 cin>> foo; 的那一刻在代码中,解决方案突然失败了。您需要清除流中的所有剩余字符,然后它才能再次工作。
那么如何解决这个问题呢?好消息是,除非您想挑剔,否则它就像循环一样简单:
C++ 语法(切换纯文本)
#include <istream>
void ignore_line ( std::istream& in ) {
char ch;
while ( in.get ( ch ) && ch != '\n' );
}
该循环只是读取字符,直到读取到文件结尾或换行符。通常假设 C++ 中的交互式输入是面向行的,并且保证在读取换行符后有一个干净的缓冲区。虽然这不是真的(输入不必是面向行的),但它的传播范围足够广,我们可以出于本线程的目的假设它。
那么这种方法有什么问题呢?没有什么。事实上,这已经是最好的了,除非你想深入挖掘并解决微妙的问题。但在我们研究问题之前,这里有一个替代解决方案,它以不同的方式完成相同的事情:
#include <ios>
#include <istream>
#include <limits>
void ignore_line ( std::istream& in ) {
in.ignore ( std::numeric_limits<std::streamsize>::max(), '\n' );
}
std::istream 的忽略成员函数将读取并丢弃最多 N 个字符或直到分隔符。在上面的例子中,N用streamsize数据类型的最大值表示,分隔符是换行符。它同样适用于较大的值(80 很常见): C++ 语法 (切换纯文本) in.ignore ( 80, '\n' );但是,streamsize 数据类型更有可能准确表示流正在使用的缓冲区的大小,并且它更有可能始终有效。这是我推荐的解决方案。
那么这有什么问题呢?有两个值得注意的问题。第一个很容易修复,这是因为 istream 不太灵活。 istream 实际上是 basic_istream 的 typedef。如果你想要一个宽流与ignore_line一起工作,那么你可以使用istream。所以技巧是使用 basic_istream 代替:
#include <ios>
#include <istream>
#include <limits>
template <typename CharT>
void ignore_line ( std::basic_istream<CharT>& in ) {
in.ignore ( std::numeric_limits<std::streamsize>::max(), in.widen ( '\n' ) );
}
现在ignore_line是一个模板函数,它将从第一个参数派生出流包含的字符类型。您可以传递任何派生自 basic_istream 或专用于 basic_istream 的流,问题就消失了。最好在文字上使用 Widen,而不是仅仅使用 '\n',以便在必要时将其正确转换为更宽的类型。好,易于。
第二个问题更难。困难得多。这更困难,因为标准 iostream 似乎因缺乏可移植性或不需要的功能而随时阻碍您的前进。事实上,要彻底解决问题是不可能的。问题是行为根据流的内容而不同。例如:
#include <iostream>
#include <ios>
#include <istream>
#include <limits>
template <typename CharT>
void ignore_line ( std::basic_istream<CharT>& in ) {
in.ignore ( std::numeric_limits<std::streamsize>::max(), in.widen ( '\n' ) );
}
int main() {
std::cout<<"First input: ";
std::cin.get();
std::cout<<"Clearing cin.\n";
std::cin.clear();
ignore_line ( std::cin );
std::cout<<"All done.\n";
}
运行该程序三次:
输入:“asdf” 输出:程序完成,无需您输入任何内容。
输入:只需按 Enter 输出:程序等待您再次按 Enter。
输入:信号 EOF 输出:程序等待您再次按 Enter 键。
问题是流是空的。如果立即按 Enter 键,则会在流中放置一个换行符并由 cin.get 使用。与 EOF 信号类似。此时流中已没有任何内容,并且 cin.ignore 会停止一切,直到您输入更多字符。这是因为 cin.ignore 是阻塞读取。如果没有什么可读的,它就会等待。
我们希望它不会阻止这三种情况中的任何一种。好消息是 iostream 库支持一些可能的解决方案。坏消息是这些都是死胡同。这里有两个常见的:
同步成员函数 istream 类支持一个名为sync 的成员函数。为什么它具有这样的功能尚有争议,因为没有人能就它应该做什么达成一致。甚至 Bjarne Stroustrup 本人也错误地指出它会丢弃流中的所有字符:
#include <iostream>
int main() {
std::cout<<"First input: ";
std::cin.get();
std::cout<<"Clearing cin.\n";
std::cin.clear();
std::cin.sync();
std::cout<<"All done.\n";
}
当它起作用时,效果非常好。坏消息是 C++ 标准不需要同步来执行诸如丢弃无关字符之类的操作。该解决方案是不可移植的。
in_avail 成员函数 下一步是查看 istream 的流缓冲区的 in_avail 成员函数。乍一看,这个成员函数会告诉你流中有多少个字符,如果它返回 0,你就可以避免调用ignore:
#include <iostream>
#include <ios>
#include <istream>
#include <limits>
template <typename CharT>
void ignore_line ( std::basic_istream<CharT>& in ) {
if ( in.rdbuf()->in_avail() > 0 )
in.ignore ( std::numeric_limits<std::streamsize>::max(), in.widen ( '\n' ) );
}
int main() {
std::cout<<"First input: ";
std::cin.get();
std::cout<<"Clearing cin.\n";
std::cin.clear();
ignore_line ( std::cin );
std::cout<<"All done.\n";
}
与同步一样,当它起作用时,效果很好。但该标准再次提出了一个障碍,即不需要 in_avail 来为您提供流中字符的准确表示。事实上,一些流行的实现有一个严格遵守的 in_avail ,它总是返回 0。不是很有用。现在我们必须发挥创意。
putback 成员函数
#include <iostream>
#include <ios>
#include <istream>
#include <limits>
template <typename CharT>
void ignore_line
( std::basic_istream<CharT>& in ) {
if ( !in.putback ( in.widen ( '\n' ) ) )
in.ignore ( std::numeric_limits<std::streamsize>::max(), in.widen ( '\n' ) ); else
in.ignore(); }
int main()
{ std::cout<<"First input: ";
std::cin.get();
std::cout<<"Clearing cin.\n";
std::cin.clear();
ignore_line ( std::cin );
std::cout<<"All done.\n";
}
这看起来非常有希望,因为乍一看,您似乎可以尝试推回换行符。如果操作失败,则最后读取的字符不是换行符,您可以随意调用忽略而不阻塞。如果操作成功,换行符又回来了,您可以通过单个字符忽略将其删除。
可悲的是,它不起作用。不需要 putback 就可以预见地完成这些操作,这就提出了为什么它可用的问题。
但回溯实际上让我们接近了一个在大多数情况下似乎足够可行的解决方案。我们可以通过使用流缓冲区的 sungetc 成员函数来保证最后读取的字符被放回,而不是依赖 putback 来判断失败与否。诀窍是取消最后一个字符,然后再次读取它并针对换行符进行测试:
#include <iostream>
#include <ios>
#include <istream>
#include <limits>
template <typename CharT>
void ignore_line ( std::basic_istream<CharT>& in ) {
if ( in.rdbuf()->sungetc() != std::char_traits<CharT>::eof()
&& in.get() != in.widen ( '\n' ) ) {
in.ignore ( std::numeric_limits<std::streamsize>::max(), in.widen ( '\n' ) );
}
}
int main() {
std::cout<<"First input: ";
std::cin.get();
std::cout<<"Clearing cin.\n";
std::cin.clear();
ignore_line ( std::cin );
std::cout<<"All done.\n";
}
我们使用 sungetc 而不是 istream 的 unget 的原因是因为 unget 返回流,但 sungetc 返回的是被推回的字符,或者 EOF。这样我们就可以更容易地判断函数是否失败。
如果 sungetc 失败,则以下情况之一为真:
1) 流处于错误状态。 2) 没有需要删除的字符。 3) 流不支持取消获取字符。
如果 sungetc 成功,总会有一个字符需要读取并针对换行符进行测试。如果该字符与换行符匹配,则最后读取的字符也是换行符,我们不需要调用忽略。如果字符不匹配,则尚未读取整行,我们可以安全地调用忽略而不阻塞。
如果流处于错误状态,则调用代码必须处理该情况。如果没有需要删除的字符,那么这正是该解决方案旨在正确处理的问题。但是,如果流不支持取消获取字符,那就是一个问题。 ignore_line函数总是无法丢弃字符,因此对于那些不支持取消获取字符的实现,我们可以添加一个强制忽略的标志。有时了解有多少个字符被忽略也很有用,所以我们也添加它,我们就有了最终的解决方案:
#include <ios>
#include <istream>
#include <limits>
template <typename CharT>
std::streamsize ignore_line ( std::basic_istream<CharT>& in, bool always_discard = false ) {
std::streamsize nread = 0;
if ( always_discard || ( in.rdbuf()->sungetc() != std::char_traits<CharT>::eof()
&& in.get() != in.widen ( '\n' ) ) )
{
// The stream is good, and we haven't
// read a full line yet, so clear it out
in.ignore ( std::numeric_limits<std::streamsize>::max(), in.widen ( '\n' ) );
nread = in.gcount(); }
return nread;
}
为了更好地衡量,我还将包括一个调用ignore_line的操纵器以及一个使用ignore_line来暂停程序的操纵器。这样,未清洗的群众就可以停止使用系统(“暂停”);和 getch();:
class ignoreline {
bool _always_discard;
mutable std::streamsize _nread;
public:
ignoreline ( bool always_discard = false )
: _always_discard ( always_discard ), _nread ( 0 ) {}
std::streamsize gcount() const { return _nread;
}
template <typename CharT>
friend std::basic_istream<CharT>& operator>> ( std::basic_istream<CharT>& in, const ignoreline& manip )
{
manip._nread = ignore_line ( in, manip._always_discard );
return in;
}
};
class pause {
ignoreline _ignore;
public:
pause ( bool always_discard = false ) : _ignore ( always_discard ) {}
std::streamsize gcount()
const { return _ignore.gcount();
}
template <typename CharT>
friend std::basic_istream<CharT>& operator>> ( std::basic_istream<CharT>& in, const pause& manip )
{
if ( !( in>> manip._ignore ) )
in.clear();
std::cout<<"Press Enter to continue . . .";
return in.ignore();
}
};
现在,所有三种情况的行为都相同:
int main()
{ std::cout<<"First input: ";
std::cin.get();
std::cout<<"Clearing cin.\n";
std::cin>> ignoreline();
std::cout<<"All done.\n";
std::cin>> pause();
}
这个故事的寓意是:事情从来没有看起来那么简单,编写可移植的代码来完成你想要的事情是极其困难的,而且 iostream 库是一团糟。
注意:如果您是初学者,请忘记一切,只需了解存在冲洗问题并使用 cin