第二部分:标准输入流的安全性与成员函数
cin 是 istream 类的对象,用于从标准输入(通常是键盘)读取数据。由于其缓冲特性,使用时需要注意许多细节,否则容易导致程序行为异常。
3.1 cin 的缓冲机制与潜在问题
cin 是带缓冲的输入流。这意味着:
-
用户输入的内容并不会立即被程序处理,而是先存储在一个叫输入缓冲区的内存区域。
-
只有当按下 回车键(Enter) 时,这一整行数据才会被送入缓冲区,程序中的
cin或相关函数才开始从缓冲区中读取数据。 -
如果一次读取没有消耗完缓冲区的所有数据,剩余数据会保留,影响下一次读取。
主要问题:
-
类型不匹配 :如果程序期望一个整数
int,但用户输入了字母"abc",则提取失败,错误数据留在缓冲区。 -
输入内容过多 :如果一次读取(如
cin >> str)只读了一个单词,但用户输入了一整句,句子的剩余部分会留在缓冲区。 -
空白字符处理 :默认的
>>运算符会跳过前导空白字符 (空格、制表符、换行符),但在读取字符串时,遇到空白字符就会停止。这使得用cin >> string无法读取带空格的句子。
3.2 流状态(Stream State)
为了检测和处理上述错误,ios_base 类定义了一组流状态标志,用于指示流的当前状况。
cpp
// 在 ios_base 中定义的状态位 (通常通过 ios 类访问)
std::ios::goodbit // 一切正常,值为0
std::ios::eofbit // 到达文件末尾 (End-Of-File),对于cin可能是Ctrl+Z/D)
std::ios::failbit // 最近的IO操作失败,但流未损坏(例如类型不匹配)
std::ios::badbit // 流已损坏,发生了严重的IO错误(如磁盘满)
相关成员函数:
cpp
// 1. 获取当前状态字
iostate rdstate() const;
// 2. 便捷状态检查函数
bool good() const; // rdstate() == 0
bool eof() const; // 检查 eofbit 是否被设置
bool fail() const; // 检查 failbit 或 badbit 是否被设置
bool bad() const; // 检查 badbit 是否被设置
// 3. 清除/设置状态
void clear(iostate state = ios::goodbit);
clear() 函数非常重要 :当流进入 fail 或 bad 状态后,所有后续的IO操作都会被忽略,直到调用 clear() 将状态重置为 good。
3.3 处理错误输入的标准模式
当 cin >> variable 失败时,需要执行一个**"重置-清空-重试"**的循环。
cpp
#include <iostream>
#include <limits> // 用于 numeric_limits
int getValidInt() {
int value;
while (true) {
std::cout << "请输入一个整数: ";
std::cin >> value;
if (std::cin.good()) {
// 输入成功,跳出循环
break;
} else {
// 输入失败,处理错误
std::cout << "输入错误!请重新输入。\n";
// 1. 清除错误状态
std::cin.clear();
// 2. 清空输入缓冲区中所有剩余的错误数据,直到遇到换行符
// 方法一:使用 ignore 忽略缓冲区中最大数量的字符,直到遇到'\n'
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
// 方法二:简单忽略256个字符,对于一般输入足够
// std::cin.ignore(256, '\n');
}
}
// 清空缓冲区中可能多余的字符(比如在正确数字后输入的字符)
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
return value;
}
3.4 istream 的关键成员函数
为了解决 >> 运算符的局限,istream 提供了一系列更灵活的成员函数。
3.4.1 字符输入:get()
cpp
// 读取单个字符(包括空白字符)
istream& get(char& ch);
int get(); // 返回读取的字符,失败或EOF时返回EOF(-1)
示例:
cpp
char c1, c2;
std::cin.get(c1); // 读取一个字符,可能是空格
c2 = std::cin.get(); // 读取下一个字符
3.4.2 字符串输入:getline() 和 get()
cpp
// 读取一行字符串,直到遇到分隔符(默认'\n'),分隔符会被从缓冲区取出但**不存储**
istream& getline(char* s, streamsize count, char delim = '\n');
istream& getline(string& str, char delim = '\n'); // 用于std::string,更安全!
// get() 的字符串版本:读取直到遇到分隔符,但分隔符**留在缓冲区**
istream& get(char* s, streamsize count, char delim = '\n');
关键区别:
-
getline():读取并丢弃分隔符。 -
get():读取但不丢弃分隔符(留在缓冲区)。
示例:读取带空格的整行输入
cpp
#include <iostream>
#include <string>
int main() {
// 使用字符数组(C风格字符串),不安全,容易缓冲区溢出
char buffer[100];
std::cout << "请输入一句话 (C风格字符串): ";
std::cin.getline(buffer, 100); // 读取最多99个字符
std::cout << "你说的是: " << buffer << std::endl;
// 使用std::string,推荐!
std::string str;
std::cout << "请输入一句话 (std::string): ";
std::getline(std::cin, str); // 注意:这是全局函数,不是成员函数
std::cout << "你说的是: " << str << std::endl;
return 0;
}
3.4.3 其他实用成员函数
cpp
// 1. ignore(): 跳过(提取并丢弃)缓冲区中的字符
istream& ignore(streamsize count = 1, int delim = EOF);
// 示例:跳过直到换行符
std::cin.ignore(1000, '\n');
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // 跳过尽可能多
// 2. gcount(): 返回最近一次未格式化的输入操作(如get(), getline(), read())读取的字符数
streamsize gcount() const;
// 3. putback(): 将一个字符放回输入缓冲区
istream& putback(char c);
// 4. peek(): 查看下一个字符,但不从缓冲区取出
int peek();
示例:ignore() 和 gcount() 的使用
cpp
#include <iostream>
#include <limits>
int main() {
char buffer[10];
std::cout << "请输入超过9个字符的字符串: ";
std::cin.getline(buffer, 10);
if (std::cin.fail()) {
std::cout << "输入太长,只读取了: " << buffer << std::endl;
std::cout << "实际读取了 " << std::cin.gcount() << " 个字符" << std::endl;
// 清除错误状态并清空缓冲区剩余内容
std::cin.clear();
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
} else {
std::cout << "成功读取: " << buffer << std::endl;
}
return 0;
}
第三部分:ostream 成员函数与自定义类型的IO
4.1 ostream 的成员函数
cpp
// 1. put(): 输出单个字符
ostream& put(char c);
std::cout.put('A').put('B').put('\n');
// 2. write(): 输出原始字节序列(常用于二进制输出)
ostream& write(const char* s, streamsize n);
char data[] = "Hello";
std::cout.write(data, 5); // 输出"Hello"(不包括结尾的'\0')
// 3. flush(): 强制刷新输出缓冲区
ostream& flush();
std::cout << "请稍等..." << std::flush;
// 做一些耗时操作
std::cout << "完成!" << std::endl;
4.2 重载 << 和 >> 运算符
为了让自定义类型也能像内置类型一样使用流IO,我们需要重载这两个运算符。
基本模式:
cpp
// 输出运算符重载
ostream& operator<<(ostream& os, const MyClass& obj) {
// 将obj的成员以适当格式输出到os
os << obj.data1 << ", " << obj.data2;
return os; // 必须返回ostream&以支持链式调用
}
// 输入运算符重载
istream& operator>>(istream& is, MyClass& obj) {
// 从is读取数据并赋值给obj的成员
is >> obj.data1 >> obj.data2;
// 应该检查读取是否成功!
if (!is) {
obj = MyClass(); // 读取失败,恢复默认状态
}
return is;
}
示例:为复数类重载IO运算符
cpp
#include <iostream>
class Complex {
private:
double real, imag;
public:
Complex(double r = 0.0, double i = 0.0) : real(r), imag(i) {}
// 声明为友元,以便直接访问私有成员
friend std::ostream& operator<<(std::ostream& os, const Complex& c);
friend std::istream& operator>>(std::istream& is, Complex& c);
};
// 输出实现:格式为 (real, imag)
std::ostream& operator<<(std::ostream& os, const Complex& c) {
os << '(' << c.real << ", " << c.imag << ')';
return os;
}
// 输入实现:期望格式为 (real, imag)
std::istream& operator>>(std::istream& is, Complex& c) {
char ch;
is >> ch; // 应该读到 '('
if (ch == '(') {
is >> c.real >> ch; // 读取实部和逗号
if (ch == ',') {
is >> c.imag >> ch; // 读取虚部和 ')'
if (ch != ')') {
is.setstate(std::ios::failbit); // 格式错误
}
} else {
is.setstate(std::ios::failbit); // 格式错误
}
} else {
// 如果不是括号格式,将字符放回,尝试只读取一个实数(虚部为0)
is.putback(ch);
is >> c.real;
c.imag = 0.0;
}
return is;
}
int main() {
Complex c1(3, 4), c2;
std::cout << "c1 = " << c1 << std::endl; // 输出: c1 = (3, 4)
std::cout << "请输入复数(格式如 (5, 6)): ";
std::cin >> c2;
if (std::cin.good()) {
std::cout << "你输入的是: " << c2 << std::endl;
} else {
std::cout << "输入格式错误!" << std::endl;
std::cin.clear();
std::cin.ignore(1000, '\n');
}
return 0;
}
这部分的核心总结:
-
cin的缓冲区机制是许多输入问题的根源,必须理解。 -
流状态(
goodbit、failbit等) 是检测和处理输入错误的关键工具。 -
标准错误处理模式 :检查
fail()→clear()→ignore()→ 重试。 -
使用
getline()(特别是对于std::string) 来读取带空格的整行输入。 -
掌握
istream/ostream的成员函数(get()、put()、write()、ignore()等)。 -
通过重载
<<和>>运算符,可以让自定义类型无缝融入C++的流IO体系。