C++ 的 IO 流

💬 :如果你在阅读过程中有任何疑问或想要进一步探讨的内容,欢迎在评论区畅所欲言!我们一起学习、共同成长~!

👍 :如果你觉得这篇文章还不错,不妨顺手点个赞、加入收藏,并分享给更多的朋友噢~!


1. C 语言的输入输出

1.1 常用输入输出函数

C 语言中最常用的输入输出方式是 scanf() 与 printf() 

  • scanf():从标准输入流(通常是键盘)读取格式化数据,按照指定的格式将输入数据存储到对应的变量中。
  • printf():将格式化的数据输出到标准输出流(通常是显示器)。使用时需注意宽度输出和精度输出控制。

1.2 输入输出缓冲区

C 语言借助相应的缓冲区来进行输入与输出,输入输出缓冲区作用如下:

  • 屏蔽低级 I/O 的实现:低级 I/O 的实现依赖操作系统本身内核,屏蔽这部分差异可使程序更具可移植性。
  • 实现 “行” 读取行为:计算机本身没有 “行” 的概念,通过缓冲区可定义 “行”,并解析其内容返回 “行”。


2. 流是什么

“流” 是对有序连续且具有方向性的数据的抽象描述。

C++ 流指信息从外部输入设备(如键盘)向计算机内部(如内存)输入,以及从内存向外部输出设备(显示器)输出的过程。

C++ 流的特性:

  • 有序连续:数据按顺序流动。
  • 有方向性:分为输入流和输出流。
  • 基于 I/O 标准类库:通过类对象实现输入输出操作。


3. C++  IO 流

C++ 中构建了一个体系庞大的类库,在这个类库的继承体系里,ios类是基类,其他所有相关类均直接或间接派生自ios类。

3.1 C++ 标准 IO 流

3.1.1 全局流对象

C++ 标准库提供了 4 个全局流对象:cincoutcerrclog

  • cin:标准输入流(键盘输入到程序),属于istream类。
  • cout:标准输出流(程序输出到显示器),属于ostream类。
  • cerr:标准错误输出流(无缓冲,直接输出)。
  • clog:标准日志输出流(有缓冲)。

coutcerrclogostream类的三个不同对象,应用场景不同,但基本功能类似。使用时必须包含头文件<iostream>,并引入std标准命名空间。

3.1.2 关键细节

3.1.2.1 缓冲机制
  • cin为缓冲流,输入数据先存入缓冲区,提取时从缓冲区读取。
  • 输入错误会设置流状态字state,但程序继续执行。
3.1.2.2 数据类型匹配
  • 输入类型需与提取类型一致,否则出错(如给int变量输入字符将出错)。
3.1.2.3 分隔符处理
  • 空格和回车可作为数据分隔符,字符型和字符串无法读取空格或回车。
3.1.2.4 内置类型支持
  • 标准库已重载<<>>运算符,可直接输入输出内置类型(如intdouble)。
3.1.2.5 自定义类型支持
  • 需重载<<>>运算符才能使用cin/cout
3.1.2.6 在线 OJ 输入输出

<1> IO类型的算法,一般都要循环输入:

(1)单个元素循环输入

适用场景:读取多组单一类型数据(如多组整数)。

​
int num;
while (cin >> num) 
{  // 输入整数,遇EOF结束
    cout << "输入的数:" << num << endl;
}

(2)多个元素循环输入

适用场景:读取每行包含多个数据的输入(如每行输入 “姓名 年龄 分数”)。

string name;
int age;
double score;
while (cin >> name >> age >> score) 
{  // 按顺序读取多个元素
    cout << name << " " << age << " " << score << endl;
}

(3)非整行字符串输入(不含空格)

适用场景:读取以空格 / 换行分隔的单词(如多个独立字符串)。

string word;
while (cin >> word) 
{  // 读取单个单词,遇空格/换行/EOF结束
    cout << "单词:" << word << endl;
}

(4)整行字符串输入(含空格)

适用场景:读取完整句子或段落(如 “Hello, world!”)。

string line;
while (getline(cin, line)) 
{  // 读取整行,包括空格,遇EOF或空行结束
    if (line.empty()) break;  // 可选:跳过空行
    cout << "整行内容:" << line << endl;
}

<2> 输出必须与题目要求完全一致,包括空格、换行符、标点符号等,多一个或少一个字符均可能导致错误

<3> Windows 系统的 VS 系列编译器中,通过输入 Ctrl + Z  再按下 Enter ,就可发送 EOF(文件结束符) 信号,快捷终止输入循环。

3.1.2.7 流对象的逻辑判断
  • istream 对象能直接用于条件判断。若输入成功,则继续执行相关操作;若输入失败,则停止执行相关操作。
  • 自定义类型可通过重载 operator bool() 函数来添加额外的逻辑判断。这些额外逻辑判断通常用于设置业务层面的结束条件。

istream 对象可作为逻辑条件值,是因为该对象会调用 operator bool 函数。

当流提取成功时,operator bool返回true;当遇到文件结束符(EOF,如键盘输入Ctrl+Z)、提取类型不匹配(如给int变量输入字符)或流被关闭时,operator bool返回false,循环终止。

示例:

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

class Date 
{
    // 声明友元函数以访问私有成员
    friend ostream& operator<<(ostream& out, const Date& d);
    friend istream& operator>>(istream& in, Date& d);

public:
    Date(int year = 1, int month = 1, int day = 1)
        : _year(year)
        , _month(month)
        , _day(day) {}

    // 类型转换运算符:定义Date对象的逻辑判断条件
    explicit operator bool() const 
    {
        // 假设输入_year为0时,返回false
        return _year != 0;
    }

private:
    int _year;
    int _month;
    int _day;
};

// 重载流提取运算符:从输入流读取Date对象
istream& operator>>(istream& in, Date& d) 
{
    in >> d._year >> d._month >> d._day; 
    return in; // 返回输入流对象,支持链式调用(如cin >> d1 >> d2)
}

// 重载流插入运算符:向输出流写入Date对象
ostream& operator<<(ostream& out, const Date& d) 
{
    out << d._year << "-" << d._month << "-" << d._day; 
    return out; // 返回输出流对象,支持链式调用(如cout << d << endl)
}

int main() 
{
    // 输出内置类型(自动调用标准库重载的<<运算符)
    int i = 1;
    double j = 2.2;
    cout << "内置类型输出:" << endl;
    cout << "整数i = " << i << endl;
    cout << "浮点数j = " << j << endl;

    // 输出自定义类型Date(调用重载的<<运算符)
    Date d(2022, 4, 10); 
    cout << "\n初始化的日期:" << d << endl;

    // 循环输入Date对象,直到输入年份为0时结束
    cout << "\n请输入日期(格式:年 月 日,输入0 0 0结束):" << endl;
    while (cin >> d) 
    { 
        // 用operator>>返回的istream对象调用operator bool
        // 输入成功后,检查Date对象的逻辑状态(_year是否为0)
        if (!d) 
        { 
            cout << "检测到结束标志(年份为0),退出程序。" << endl;
            break;
        }
        cout << "输入的日期为:" << d << endl; 
        cout << "请继续输入(或输入0 0 0结束):" << endl;
    }

    return 0;
}

3.2 C++ 文件 IO 流

根据数据的组织形式,数据文件可分为二进制文件和文本文件。

  • 数据在内存中以二进制形式存储,若不加转换输出到外存文件中,就是二进制文件;
  • 若要求在外存上以 ASCII 码形式存储,则需在存储前转换,以 ASCII 字符形式存储的文件就是文本文件。

采用文件流对象操作文件的一般步骤:

  • (1)定义文件流对象
类名用途定义语法示例
ifstream只读取文件

1.直接构造并打开文件:

ifstream 对象名("文件名",模式);
2. 默认构造后打开文件:
ifstream 对象名;
对象名.open("文件名", 模式);

若未显式指定模式将默认ios_base::in

std::ifstream fin("data.txt");
std::ifstream fin;
fin.open("data.bin", std::ios_base::binary);
// 或
std::ifstream fin;
fin.open("data.bin", std::ios_base::in | std::ios_base::binary);

ofstream只写入文件

1. 直接构造并打开文件(覆盖写入):
ofstream 对象名("文件名",模式);
2.默认构造后打开文件:
ofstream 对象名;
对象名.open("文件名", 模式);

若未显式指定模式将默认ios_base::out

类似 ifstream
fstream读写文件fstream 对象名("文件名", 模式);
模式必须包含 ios_base::in | ios_base::out )
std::fstream file("data.txt", std::ios_base::in | std::ios_base::out);
// 追加读写:
std::fstream file("data.txt", std::ios_base::in | std::ios_base::out | std::ios_base::app);

注意ifstream / ofstream / fstream 均需包含头文件 <fstream> ;ifstream 、ofstream 、fstream 、ios_base 要展开命名空间或使用 std:: 前缀。

  • (2)打开文件
    • 成员函数:open(const char* filename, ios_base::mode)
    • 常用打开模式:
      • ios_base::in:输入模式(读文件)。
      • ios_base::out:输出模式(写文件,覆盖原有内容)。
      • ios_base::app:追加模式(写文件,内容追加到末尾)。
      • ios_base::binary:二进制模式(默认为文本模式)。
  • (3)读写文件
    • 文本读写:使用<<>>运算符(需重载自定义类型运算符)。
    • 二进制读写:使用write()和read()成员函数(按字节操作)。
  • (4)关闭文件:调用close()成员函数。

3.2.1 模拟服务器配置信息的读写

#include <iostream>       
#include <fstream>        
#include <string>         
using namespace std;      

class Date 
{
public:
    Date(int year = 1, int month = 1, int day = 1)
        : _year(year), _month(month), _day(day) {}

    friend ostream& operator<<(ostream& out, const Date& d);
    friend istream& operator>>(istream& in, Date& d);

private:
    int _year;   
    int _month;  
    int _day;    
};

ostream& operator<<(ostream& out, const Date& d) 
{
    out << d._year << "-" << d._month << "-" << d._day;
    return out;
}

istream& operator>>(istream& in, Date& d) 
{
    char separator1, separator2; // 用于读取日期中的分隔符(如'-')
    in >> d._year >> separator1 >> d._month >> separator2 >> d._day;
    return in;
}

// 服务器信息结构体
struct ServerInfo 
{
    char _address[32];  // 地址(字符数组)
    int _port;          // 端口号
    Date _date;         // 日期(已重载IO运算符)
};

// 配置管理类
class ConfigManager 
{
public:
    // 构造函数:初始化文件名
    explicit ConfigManager(const char* filename) : _filename(filename) {}

    // 二进制写入文件
    void WriteBin(const ServerInfo& info) 
    {
        ofstream ofs(_filename, ios_base::out | ios_base::binary); 
        if (!ofs.is_open()) 
        { // 检查文件是否打开成功
            cerr << "Error: Failed to open file for writing (binary)." << endl;
            return;
        }
        // 写入结构体数据(需保证结构体无虚函数、无动态成员,否则需自定义序列化逻辑)
        ofs.write(reinterpret_cast<const char*>(&info), sizeof(info));
        ofs.close();
    }

    // 二进制读取文件
    void ReadBin(ServerInfo& info) 
    {
        ifstream ifs(_filename, ios_base::in | ios_base::binary);
        if (!ifs.is_open()) 
        { 
            cerr << "Error: Failed to open file for reading (binary)." << endl;
            return;
        }
        // 读取结构体数据
        ifs.read(reinterpret_cast<char*>(&info), sizeof(info));
        ifs.close();
    }

    // 文本写入文件
    void WriteText(const ServerInfo& info) 
    {
        ofstream ofs(_filename);
        if (!ofs.is_open()) 
        { 
            cerr << "Error: Failed to open file for writing (text)." << endl;
            return;
        }
        // 使用流插入运算符写入数据(需保证Date类已重载<<)
        ofs << info._address << " " << info._port << " " << info._date << endl;
        ofs.close();
    }

    // 文本读取文件
    void ReadText(ServerInfo& info) 
    {
        ifstream ifs(_filename);
        if (!ifs.is_open()) 
        { 
            cerr << "Error: Failed to open file for reading (text)." << endl;
            return;
        }
        // 使用流提取运算符读取数据(需保证Date类已重载>>)
        ifs >> info._address >> info._port >> info._date;
        ifs.close();
    }

private:
    string _filename; // 配置文件名
};

int main() 
{
    // 初始化服务器信息
    ServerInfo winfo = 
    {
        "192.0.0.1",    // 地址
        80,             // 端口号
        {2022, 4, 10}   // 日期(使用Date构造函数初始化)
    };

    // 创建配置管理器对象(二进制和文本文件)
    ConfigManager cf_bin("test.bin");
    ConfigManager cf_text("test.txt");

    // ---------------------- 二进制操作 ----------------------
    // 写入二进制文件
    cf_bin.WriteBin(winfo);
    cout << "二进制文件写入完成。" << endl;

    // 读取二进制文件
    ServerInfo rbinfo;
    cf_bin.ReadBin(rbinfo);
    cout << "二进制读取结果:" << endl;
    cout << "地址:" << rbinfo._address << endl;
    cout << "端口:" << rbinfo._port << endl;
    cout << "日期:" << rbinfo._date << endl;
    cout << endl;

    // ---------------------- 文本操作 ----------------------
    // 写入文本文件
    cf_text.WriteText(winfo);
    cout << "文本文件写入完成。" << endl;

    // 读取文本文件
    ServerInfo rtinfo;
    cf_text.ReadText(rtinfo);
    cout << "文本读取结果:" << endl;
    cout << "地址:" << rtinfo._address << endl;
    cout << "端口:" << rtinfo._port << endl;
    cout << "日期:" << rtinfo._date << endl;

    return 0;
}


4. 简单介绍 stringstream

传统C语言方法(如 sprintf / itoa )在进行类型转换与字符串操作时,存在缓冲区溢出风险且需手动管理内存空间,尤其在处理结构体数据的序列化与反序列化时不够安全便捷。

stringstream 通过流式操作实现自动类型推导,并利用安全的string缓冲区管理,有效简化了类型转换与字符串拼接过程,规避了上述风险,更适用于序列化场景。

4.1 头文件与类定义

  • 头文件:#include <sstream>
  • 相关类:
    • istringstream:从字符串读取数据(输入流)。
    • ostringstream:向字符串写入数据(输出流)。
    • stringstream:双向操作(读写字符串)。

4.2 核心功能

4.2.1 数值类型转字符串

  • 避免itoa()sprintf()的缓冲区溢出问题,自动推导类型。

示例代码:

#include <iostream>       
#include <sstream>        
#include <string>         // 包含string类型头文件

using namespace std;      

int main()
{
    int a = 12345678;     
    string sa;            // 定义用于存储转换结果的string对象
    stringstream s;       // 定义stringstream对象,用于数据转换

    // 第一次转换:将整数a转换为字符串
    s << a;               // 向stringstream中插入int类型数据(自动格式化)
    s >> sa;              // 从stringstream中提取数据到string对象sa
    cout << "整数转字符串结果:" << sa << endl;  

    // ---------------------- 关键操作:清空流状态和底层缓冲区 ----------------------
    s.clear();            // 重置流状态标志(清除可能的badbit状态)
    s.str("");            // 清空stringstream底层维护的string对象,避免残留数据影响下次转换
    // ----------------------------------------------------------------------------

    // 第二次转换:将双精度浮点数d转换为字符串
    double d = 12.34;     
    s << d;               // 向stringstream中插入double类型数据(自动格式化)
    s >> sa;              // 从stringstream中提取数据到string对象sa
    cout << "浮点数转字符串结果:" << sa << endl;  

    return 0;
}

4.2.2 字符串拼接

  • 方便合并多个字符串或变量。

示例代码:

#include <iostream>      
#include <sstream>        
#include <string>         

using namespace std;      

int main() 
{
    // 创建stringstream对象用于字符串操作
    stringstream sstream;

    // 向流中插入多个字符串进行拼接
    sstream << "Hello" << " " << "World," << " 你好!";

    // 从stringstream中提取拼接后的完整字符串
    string result = sstream.str();

    cout << "拼接结果:" << result << endl;  

    return 0;
}

4.2.3 序列化与反序列化结构数据

  • 将结构体数据转为字符串(如网络传输),或从字符串解析回结构体。

示例代码:

#include <iostream>
#include <sstream>
#include <string>
using namespace std;

class Date 
{
public:
    Date(int year = 1, int month = 1, int day = 1)
        : _year(year), _month(month), _day(day) {}

    friend istream& operator>>(istream& in, Date& d);
    friend ostream& operator<<(ostream& out, const Date& d);

private:
    int _year;
    int _month;
    int _day;
};

istream& operator>>(istream& in, Date& d) 
{
    char separator1, separator2; // 用于处理日期格式中的分隔符(如YYYY-MM-DD)
    in >> d._year >> separator1 >> d._month >> separator2 >> d._day;
    return in;
}

ostream& operator<<(ostream& out, const Date& d) 
{
    out << d._year << "-" << d._month << "-" << d._day; 
    return out;
}

struct ChatInfo 
{
    string _name;
    int _id;
    Date _date; // 假设已重载<<和>>
    string _msg;
};

int main() 
{
    // 序列化:结构体转字符串
    ChatInfo winfo = 
    {
        "张三",
        135246,
        {2022, 4, 10}, // 使用Date构造函数初始化日期
        "晚上一起看电影吧"
    };

    ostringstream oss;
    // 按顺序输出结构体成员,用空格分隔(需注意字符串中的空格会导致反序列化问题)
    oss << winfo._name << " "
        << winfo._id << " "
        << winfo._date << " "
        << winfo._msg; // 假设_msg中不含空格,否则需特殊处理(如转义或使用其他分隔符)

    string str = oss.str();
    cout << "序列化字符串:" << str << endl;

    // 反序列化:字符串转结构体
    ChatInfo rInfo;
    istringstream iss(str);
    // 按顺序读取数据,与序列化顺序严格一致
    iss >> rInfo._name >> rInfo._id >> rInfo._date >> rInfo._msg;

    // 检查反序列化是否成功(处理可能的读取错误)
    if (iss.fail()) 
    {
        cerr << "反序列化失败:输入格式错误" << endl;
        return 1;
    }

    cout << "\n反序列化结果:" << endl;
    cout << "姓名:" << rInfo._name << "(" << rInfo._id << ")" << endl;
    cout << "时间:" << rInfo._date << endl;
    cout << "消息:" << rInfo._msg << endl;

    return 0;
}

4.3 注意事项

  1. 状态与缓冲区管理
    • clear():重置流状态(如badbit),但不清空底层字符串。
    • str(""):清空底层string对象,避免多次转换时数据累积。
  2. 安全性
    • 使用string代替字符数组,避免缓冲区溢出。
    • 自动类型推导,无需手动格式化,减少错误风险。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值