前言
一、输入流与清空缓冲区
1. 为什么要清空缓冲区
2. 如何清空缓冲区
(1) while(getchar()!='\n') --- C/C++
(2) cin.ignore() --- C++
(3) cin.sync() --- C++
二、包含空格或换行符的字符串完整读取实现
1. 逐字符读取并记录
2. 逐字符串读取并记录
在处理输入流时,清空缓冲区是一个常见的操作,尤其是在读取字符串时。当我们使用输入流读取字符或字符串后,输入缓冲区中可能会保留一些未读取的字符或符号,这些未读取的内容可能会对后续的输入操作产生干扰。因此,我们需要清空缓冲区,确保输入流中只包含我们需要的内容。本篇博客将介绍为什么要清空缓冲区以及如何进行清空操作。此外,还将探讨如何完整读取包含空格或换行符的字符串,并提供了两种具体可行的实现方法。
首先我们给出一个简单案例,希望接收一串密码和用户性别,以判断用户是否可以成功登录,先来看C风格代码(scanf 向下对应):
// scanf - getchar
void test1()
{
char ch = 0;
char password[20];
printf("请输入密码:\n");
scanf("%s", password);
printf("请输入性别: Y(男) X(女)\n");
ch = getchar();
if( ch == 'Y')
{
printf("男性性别已记录\n");
}
else if(ch == 'X')
{
printf("女性性别已记录\n");
}
else
{
printf("输入有误!\n");
}
}
试着运行:
可以发现我们在输入密码后还未输入性别选项,该部分就已经自己跳过,究竟怎么一回事呢?
当键盘输入字符串时,并非直接赋值给变量,而是由缓存区作为中继器,键盘输入先存入缓冲区,再由缓冲区传给变量,实际接收的字符串结尾包含'\n'换行符,password会从缓冲区拿取'\n'或' '空格字符之前的字符串,所以实际输入最后包含的'\n'换行符直接被接收性别的 getchar() 读取,正如我们看到的该选项被直接跳过。
于是我们改变输入操作的语句,将getchar()换为scanf观察是否存在同样的问题:
printf("请输入性别: Y(男) X(女)\n");
scanf("%c", &ch);
继续运行:
不出所料地发现了同样的问题,scanf同样在缓冲区直接读取。
不正规地混用C与C++,我们使用 cin 做后续衔接接收字符传给字符变量ch,改变输入操作的语句:
printf("请输入性别: Y(男) X(女)\n");
cin >> ch;
试着运行:
发现结果理想,并未出现直接读取缓冲区的换行符,而是成功接收了cin输入流传给变量ch的值,为什么cin可以在未清空缓冲区的情况下实现呢?
在C++中,cin对象在读取输入时会自动忽略前导空白字符(包括换行符)。
这么看来C++中cin对象好哇塞,其实我们的输入对它而言同样哇塞,因为我们输入的字符串是完美的中间没有特殊字符(比如空格)的字符串,当我们换个输入样例看看:
可以说cin在这里好用,但是没有到完全脱离缓冲区的好用,所以清空缓冲区在后续有输入要求时同样必要!
接着给出同样作用的C++风格实现代码:
// cin - getchar
void test2()
{
char ch = 0;
string s;
cout << "请输入密码:" << endl;
cin >> s;
cout << "请输入性别: Y(男) X(女)" << endl;
ch = getchar();
if (ch == 'Y')
{
cout << "男性性别已记录" << endl;
}
else if(ch == 'X')
{
cout << "女性性别已记录" << endl;
}
else
{
cout << "输入有误!" << endl;
}
}
运行结果:
同样地将 getchar() 换为 cin 看看效果:
cout << "请输入性别: Y(男) X(女)" << endl;
cin >> ch;
运行结果:
通过对比C与C++风格代码,后续缓冲区的清空问题将首次接收输入的scanf转换为cin同样存在,人为清空缓冲区的必要性显著。
示例代码:
void test3()
{
string name;
cout << "请输入账号:" << endl;
cin >> name;
// 方法1
int c;
while((c = getchar()) != EOF && c != '\n'){} // 未读到文件结尾前提下,清空换行符前的所有字符
string password;
cout << "请输入密码:" << endl;
cin >> password;
cout << "请确认信息:" << endl;
cout << "账号:" << name << endl
<< "密码:" << password << endl;
}
试着运行:
可以看到我们输入账号后通过getchar()空读取清空缓冲区,不影响密码的正确输入。
给出示例:
void test3()
{
string name;
cout << "请输入账号:" << endl;
cin >> name;
// 方法2
cin.ignore(numeric_limits::max(), '\n'); // 用于忽略输入缓冲区中的指定数量的字符
string password;
cout << "请输入密码:" << endl;
cin >> password;
cout << "请确认信息:" << endl;
cout << "账号:" << name << endl
<< "密码:" << password << endl;
}
注意 istream.ignore() 是输入流中的重载函数,如果 ignore() 内部没有参数时,默认清空缓冲区的一个字符,当然需要清空缓冲区就需要传入最大清理字符数,同时可以设定当遇到什么字符就停止清除缓冲区,用法如下:
总结一下就是:
从输入流中提取并丢弃字符,直到碰到参数delim并包含delim(delim也会被丢弃)
从输入流中提取并丢弃字符,直到遇到下列三种情况
1.提取的字符达到了参数count指定的数量
2.在输入序列中遇到文件结束(EOF)
3.输入序列中的下一个字符为参数delim指定的字符(这个字符会被提取并丢弃)
试着运行:
可以看到同样完美地清空缓冲区,保证了密码的正确输入。
仅仅需要将cin.ignore()语句换为cin.sync()即可,有如下代码:
// 方法3
cin.sync(); // 清空缓冲区所有字符
测试运行:
发现网上所说的 cin.sync() 方法在这里并不能实现清空缓冲区,究竟为什么?
在某些情况下,使用 cin.sync() 函数可能会失效。这可能是由于输入缓冲区中仍然存在其他字符或换行符导致的。
我们试着换一个输入案例(中间不包含空格等字符):
发现并没有跳过输入密码的语句,说明cin.sync()确实清理掉了最后输入的'\n'换行符,不妨展开讨论一下 cin.sync() 到底什么情况下有效:
// 测试cin.sync()
void test4()
{
cout << "请输入字符:" << endl;
char c;
c = getchar();
cin.clear();
cin.sync();
string s;
cout << "请输入字符串:" << endl;
cin >> s;
cout << "c = " << c << endl;
cout << "s = " << s << endl;
}
以上使用cin.clear()的目的是排除cin.fail()的影响使得cin.sync()失效的可能,因为在输入错误的情况下,在流中fail()结果为1( 状态值无效) ,执行cin.sync(); 清空流是无效的,输入流中的数据依旧在(从错误开始那个,包括错误那个字符仍然在流中)。
运行结果:
可以看出除去结尾的'\n'符外,不包含其他特殊字符,这里 cin.sync() 依然存在未能成功清除缓冲区的问题。
当我们将 cin.clear() 和 cin.sync() 语句换为 cin.ignore() 语句时:
这个结果堪称完美。
所以综上而言,cin.sync() 在清除缓冲区的用途远不如 cin.ignore() ,所以我更倾向于选择在清空或特定清除缓冲区时选用方法1、2。
出于读取单个字符的效率考虑,此处使用 getchar() 而不使用scanf,逐字符读取代码如下:
// 包含空格的字符串完整读取实现
void test5() // 逐字符读取并记录
{
char c;
string s;
cout << "请逐字符输入: (按 ctrl+z 表示结束)" << endl;
while ((c = getchar()) != EOF && c != '\n')
//while ((c = getchar()) != EOF) // 读取记录包含换行符在内的输入字符
{
getchar(); // 清除掉用于确认输入的'\n'换行符
s += c;
}
cout << "字符串如下:" << endl << s << endl;
}
测试运行:
可以看到所有字符包括空格都被完整记录,当然具体使用需要根据模板做出相应修改和填充。
首先我们给出一种利用cin输入流对象结合拼接字符串的方法:
void test6() // 逐字符串读取并记录
{
string temp_s;
string s;
while (cin >> temp_s)
{
s += (temp_s + '\n'); // 读取记录包含换行符在内的输入字符
//s += temp_s;
cin.ignore(numeric_limits::max(), '\n');
}
s.pop_back(); // 去除结尾用于转新行,以传入EOF结束标识符为目的的'\n'换行符
cout << "字符串如下:" << endl << s << endl;
}
测试运行:
可以看到我们成功记录了每次输入子字符串后的回车符,但是有个大问题:给单个字符串即这里的 temp_s 赋值时,“fd i*”字符串中间有空格,所以“fd”后面的子字符串未被读取。原因正如上面所言cin对象在读取输入时会自动忽略前导空白字符(包括换行符) ,那有没有其他办法可以解决子字符串中包含空格字符或其他特殊字符的问题呢?
接下来给出一种利用 getline() 函数实现的办法:
void test7()
{
string temp_s;
string s;
while(getline(cin,temp_s))
{
//s += temp_s;
s += (temp_s + '\n'); // 读取记录包含换行符在内的输入字符
}
s.pop_back(); // 去除结尾用于转新行,以传入EOF结束标识符为目的的'\n'换行符
cout << "字符串如下:" << endl << s << endl;
}
测试输入:
主打一个完美还原,成功读入了包含空格的子字符串,同时每个字符串最后的换行符也得到了还原。
所以,通过上面案例看来 getline() 函数相比 cin 对象可以更好地接收各类字符串,but提一句别忘了加string头文件,iostream标准库不包含这个字符串函数操作。
总结
本篇博客介绍了清空缓冲区的目的和常见的清空方法。清空缓冲区是为了确保输入流中只包含我们需要的内容,避免未读取的字符或符号对后续的输入操作产生干扰。我们可以使用 while(getchar()!='\n')
、cin.ignore()
或 cin.sync()
来清空缓冲区。另外,我们还介绍了两种实现方法来完整读取包含空格或换行符的字符串,分别是逐字符读取并记录和逐字符串读取并记录。通过掌握这些技巧,我们能够更好地处理输入流,并编写出更健壮的程序。希望本篇博客对您有所帮助,如果您对这些话题感兴趣,请继续阅读后续的具体内容。