C++流类库 标准输入流的安全性与成员函数 ostream 成员函数与自定义类型的IO

第二部分:标准输入流的安全性与成员函数

cinistream 类的对象,用于从标准输入(通常是键盘)读取数据。由于其缓冲特性,使用时需要注意许多细节,否则容易导致程序行为异常。

3.1 cin 的缓冲机制与潜在问题

cin带缓冲的输入流。这意味着:

  1. 用户输入的内容并不会立即被程序处理,而是先存储在一个叫输入缓冲区的内存区域。

  2. 只有当按下 回车键(Enter) 时,这一整行数据才会被送入缓冲区,程序中的 cin 或相关函数才开始从缓冲区中读取数据。

  3. 如果一次读取没有消耗完缓冲区的所有数据,剩余数据会保留,影响下一次读取。

主要问题:

  1. 类型不匹配 :如果程序期望一个整数 int,但用户输入了字母 "abc",则提取失败,错误数据留在缓冲区。

  2. 输入内容过多 :如果一次读取(如 cin >> str)只读了一个单词,但用户输入了一整句,句子的剩余部分会留在缓冲区。

  3. 空白字符处理 :默认的 >> 运算符会跳过前导空白字符 (空格、制表符、换行符),但在读取字符串时,遇到空白字符就会停止。这使得用 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() 函数非常重要 :当流进入 failbad 状态后,所有后续的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;
}

这部分的核心总结:

  1. cin 的缓冲区机制是许多输入问题的根源,必须理解。

  2. 流状态(goodbitfailbit等) 是检测和处理输入错误的关键工具。

  3. 标准错误处理模式 :检查 fail()clear()ignore() → 重试。

  4. 使用 getline()(特别是对于 std::string 来读取带空格的整行输入。

  5. 掌握 istream/ostream 的成员函数(get()put()write()ignore()等)。

  6. 通过重载 <<>> 运算符,可以让自定义类型无缝融入C++的流IO体系。

相关推荐
扣脚大汉在网络1 小时前
关于一句话木马
开发语言·网络安全
暗然而日章1 小时前
C++基础:Stanford CS106L学习笔记 5 内存与指针
c++·笔记·学习
学习路上_write1 小时前
FREERTOS_定时器——创建和基本使用
c语言·开发语言·c++·stm32·嵌入式硬件
学技术的大胜嗷1 小时前
如何在 VSCode 中高效开发和调试 C++ 程序:面向用过 Visual Studio 的小白
c++·vscode·visual studio
Sheffi661 小时前
ARC 的自动释放机制与 autoreleasepool 深度解析
macos·objective-c·cocoa
ExiFengs1 小时前
使用Java 8函数式编程优雅处理多层嵌套数据
java·开发语言·python
liu****1 小时前
10.指针详解(六)
c语言·开发语言·数据结构·c++·算法
美味小鱼1 小时前
DupFinder:一个用 Rust 编写的高性能重复文件查找工具
开发语言·后端·rust
VBA63371 小时前
数组与字典解决方案第三十二讲:数组的拆分和维数转换
开发语言