C/C++IO流详解

1. C语言的输入与输出

1.1 基本输入输出函数

scanf() 函数,标准输入

// 基本用法
int num;
float f;
char str[100];
scanf("%d %f %s", &num, &f, str);

// 宽度控制
char name[11];
scanf("%10s", name);  // 最多读取10个字符

// 格式控制
int a, b;
scanf("%d,%d", &a, &b);  // 输入格式必须为 "10,20"
scanf("%d @ %d", &a, &b);  // 输入格式必须为 "10 @ 20"

占位符(也叫格式符,printf相同,printf还提供特定格式,下面讲解):

%d - 整数
%f - 浮点数
%c - 字符
%s - 字符串
%lf - 双精度浮点数
%x - 十六进制整数

printf() 函数详解

// 简单格式控制
int num = 123;
double f = 3.14159;
printf("整数: %d, 浮点数: %.3f\n", num, f);

// 更复杂的格式控制
printf("%-10s: %08d\n", "ID", 123);    // "ID       : 00000123"
printf("价格: $%6.2f\n", 99.9);        // "价格: $ 99.90"

运行结果:

在这里插入图片描述

printf常用格式控制

%-10s - 左对齐,宽度10
%10s - 右对齐,宽度10
%08d - 宽度8,不足补0
%.3f - 保留3位小数
%x - 十六进制输出
%p - 指针地址输出

1.2 缓冲区深入理解

缓冲区的类型

全缓冲:文件操作通常使用,缓冲区满时刷新

行缓冲:标准输入输出使用,遇到换行符时刷新

无缓冲:标准错误输出使用,立即输出

缓冲区的作用

当程序在进行IO(外设与内存间数据传输)的时候,会调用一些系统调用,具有一定的开销。

举个例子:当我们在进行寄快递的时候,先将快递交给快递站,快递站不会立即将我们的快递发出,而是等待,等待更多的人来寄快递,等到快递站的快递到达一定的数量,才会统一装车,统一运出去。这里的快递站就相当于一个缓冲区,我们如果每个人都让快递站给我们单独运送包裹,那对于整个快递系统来说,费时费力,几乎不可能实现。

我们在编写程序的时候,我们输出的数据不会从内存直接输出到外设(显示器,文件等),输入数据也同理,不会直接从外设输入(键盘,文件),而是有一个缓冲区,将内容先输入/输出在缓冲区里,输出缓冲区在等到具有一定的数据的时候就会刷新,即将数据输出。而输入缓冲区则是将输入的数据缓存起来,等待我们去拿

#include <stdio.h>

int main() {
    int a, b;
    
    // 示例1:缓冲区中的数据会保留
    printf("请输入两个整数: ");
    scanf("%d", &a);
    // 如果输入 "100 200",则200会留在缓冲区中
    scanf("%d", &b);  // 直接从缓冲区读取200
    
    // 示例2:清空输入缓冲区
    int c;
    char ch;
    scanf("%d", &c);
    while((ch = getchar()) != '\n' && ch != EOF);  // 每次读取一个字节,直到读取到结束符EOF
    
    return 0;
}

缓冲区想要了解更细致,可以参考我的操作系统文章:Linux文件系统理解1,其中的第5部分为缓冲区理解

2. 流是什么

C++的IO流系统构建了一套完整的面向对象输入输出框架,以"流"这一抽象概念为核心,重新定义了数据在不同设备间的传输方式。与C语言中基于函数的scanf和printf不同,C++通过流类体系将输入输出操作封装为更加安全、灵活的面向对象的操作

在标准IO流方面,C++提供了cin、cout、cerr和clog四个全局流对象,分别对应标准输入、标准输出、标准错误输出和日志输出。这些流对象通过运算符重载技术,不仅支持内置数据类型的直接读写,还能通过自定义重载>>和<<运算符来扩展对用户定义类型的支持。

文件IO流通过ifstream、ofstream和fstream等类,为文件操作提供了统一而便捷的接口。无论是文本文件还是二进制文件,都能通过相似的流操作语法进行读写,同时支持多种文件打开模式和错误状态检测。

字符串IO流,特别是stringstream类,解决了C语言中数值与字符串转换的安全性问题。它通过内存中的字符串缓冲区,提供了类型安全的格式化能力,避免了传统sprintf函数可能引发的缓冲区溢出风险,同时简化了复杂数据结构的序列化和反序列化过程。

3. C++IO流

3.1 IO流类库完整体系

C++通过面向对象的方式实现了一套完整的io体系,类图如下(图片来自网站:cpluscplus.com):

在这里插入图片描述

对于上图:

istream,ifstream,istringstream都是输入流

ostream,ofstream,ostringstream都是输出流

iostream,fstream,stringstream都是输入输出流,即具有输出流功能,也有输入流功能

输入输出状态表示

在这里插入图片描述

在ios_base类中设计了流状态,类似于位图结构,使用其中特定的比特位表示某一个状态,如上图所示。

表示 good() eof() fail() bad() rdstate()
无错误(零值iostate) true false false false goodbit
输入操作达到文件末尾 false true false false eofbit
I/O操作中的逻辑错误 false false true false failbit
I/O操作中的读/写错误 false false true truet badbit

failbit与badbit区别为,faibit发生的错误可以挽回,该流对象清空流状态还可以继续使用,badbit即致命错误,不可挽回

rdstate()表示获取对象错误标志,没有错误就返回goodbit

good()定义类似以下代码

bool basic_ios::good() const {
  return rdstate() == goodbit;
}

输入Ctrl+z,是文件结束的标志符,流对象读到之后会讲eofbit设置为true,表示读到了文件结尾

当使用流对象进行一次IO后,该对象流状态被设置成相应的状态。

int main() {
    int number;
    int cnt = 10;

    while(true)
    {
        std::cout << "请输入一个整数: ";
        std::cin >> number;

        if (std::cin.good()) {
            std::cout << "流状态正常" << std::endl;
        }

        if (std::cin.eof()) {
            std::cout << "到达文件末尾,退出程序" << std::endl;
            exit(0);
        }

        if (std::cin.fail()) {
            std::cout << "非致命错误发生" << std::endl;

            std::cin.clear();  // 清除错误状态
            std::cin.ignore(1000, '\n');  // 忽略错误输入
            std::cout << "输入无效,已忽略"  << std::endl;
        }

        if (std::cin.bad()) {
            std::cout << "致命错误发生,退出程序" << std::endl;
            exit(-1);

        }

        std::cout << std::endl;
    }

    return 0;

}

在上面的程序中依次输入

1 2 3 a b x ctrl+z

运行结果如下:

在这里插入图片描述

当输入为整形的时候则正常,输入为其他类型的时候就会错误。

全局流对象详细说明

cin (标准输入)

#include <iostream>
#include <limits>

int main() {
    int age;
    std::string name;
    
    // 基本的输入
    std::cout << "请输入姓名和年龄: ";
    std::cin >> name >> age;
    
    // 错误处理
    if(std::cin.fail()) {
        std::cin.clear();  // 清除错误状态
        std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');  // 清空缓冲区
        std::cout << "输入错误,请重新输入: ";
        std::cin >> name >> age;
    }
    
    return 0;
}

cout (标准输出)

#include <iostream>
#include <iomanip>

int main() {
    int num = 255;
    double pi = 3.1415926535;
    
    // 格式化输出
    std::cout << "十进制: " << num << std::endl;
    std::cout << "十六进制: " << std::hex << num << std::endl;
    std::cout << "八进制: " << std::oct << num << std::endl;
    
    // 控制精度
    std::cout << "PI: " << std::setprecision(4) << pi << std::endl;
    
    // 控制宽度和对齐
    std::cout << std::setw(10) << std::left << "姓名" 
              << std::setw(5) << std::right << "年龄" << std::endl;
    std::cout << std::setw(10) << std::left << "张三" 
              << std::setw(5) << std::right << 25 << std::endl;
    
    return 0;
}

cerr 和 clog 的区别

#include <iostream>
#include <fstream>

int main() {
    // cerr - 无缓冲,立即输出,用于错误信息
    std::cerr << "这是一个错误信息" << std::endl;
    
    // clog - 有缓冲,用于日志信息
    std::clog << "这是一个日志信息" << std::endl;
    
    // 重定向示例
    std::ofstream logFile("log.txt");
    std::streambuf* originalClogBuffer = std::clog.rdbuf();
    std::clog.rdbuf(logFile.rdbuf());  // 重定向clog到文件
    
    std::clog << "这条日志会写入文件" << std::endl;
    
    // 恢复原来的缓冲区
    std::clog.rdbuf(originalClogBuffer);
    
    return 0;
}

3.2 自定义类型重载流插入/提取

重载流插入,作为成员函数重载时,this占据第一个参数,对象就会成为第一个操作数,d << cout ,所以不可以重载为成员函数,只能重载为全局的,达到 cout<<d的效果,但是访问不了私有成员,可以用友元声明。

要支持连续插入 cout<<d1<<d2,就需要增加返回值 (cout<<d1)<<d2, cout<<d1返回一个cout

cin与cout同理

#include <iostream>
using namespace std;
class Date
{
    friend bool operator==(const Date& d1, const Date& d2); //声明该函数为Date的友元函数,负责在全局无法访问Date的private私有成员
    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)
    {
    }
    // >运算符重载
    bool operator>(const Date& d)
    {
        if (_year > d._year) return true;
        else if (_year == d._year)
        {
            if (_month > d._month)
                return true;
            if (_month == d._month && _day > d._day)
                return true;
        }
        return false;
    }
private:
    //声明成员变量
    int _year;
    int _month;
    int _day;
};

bool operator==(const Date& d1, const Date& d2)
{
    return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day;
}

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

istream& operator>>(istream& in, Date& d)
{
    in >> d._year >> d._month >> d._day;
    return in;
}
int main()
{
    Date d1;

    cout << "请输入一个日期, 空格隔开:" << endl;
    cin >> d1;   //流插入调用
    cout << d1 << endl;  //流提取调用


    return 0;
}

在这里插入图片描述

3.3 文件IO流fstream应用

文件IO的时候,与标准IO基本相同,只不过需要我们带上文件路径和打开方式来进行打开文件

定义文件流对象

ifstream fin(“filename”, mod);

ofstream fout(“filename”, mod);

这里的mod对应文件流对象的几种模式,与C语言类似,下列表格详细列出

模式标志 适用流类型 作用描述 文件不存在时 文件存在时 常见组合与备注
std::ios::in ifstream, fstream 以读取方式打开文件。 打开失败 打开文件,读指针在开头。 ifstream 的默认模式。
std::ios::out ofstream, fstream 以写入方式打开文件。 创建新文件 清空文件内容。 ofstream 的默认模式。常与 trunc 组合。
std::ios::app ofstream, fstream 追加模式。所有写入都追加到文件末尾。 创建新文件 打开文件,不清空,写指针在末尾。 与 out 同时使用。ate 和 app 的区别在于 app 每次写操作前都会定位到末尾。
std::ios::ate ifstream, ofstream, fstream 打开文件后,立即将读/写指针定位到文件末尾。 创建新文件 打开文件,不清空,指针移到末尾。 初始位置在末尾,但之后可以自由移动指针进行读写。
std::ios::trunc ofstream, fstream 如果文件已存在,则截断它(清空所有内容)。 创建新文件 清空文件内容。 如果只指定 out 而未指定 app/ate/in,则默认包含 trunc。
std::ios::binary 所有流类型 二进制模式打开文件,禁止字符转换。 - - 与任何模式组合使用。在Windows上尤为重要,可避免 \n 被转换为 \r\n。

这些模式标志定义在 std::ios_base 类中,可以在创建文件流对象(ifstream, ofstream, fstream)时,通过构造函数或调用 open() 成员函数时使用。它们可以通过位或操作 | 进行组合。

常用组合示例

以下是一些常见的模式组合及其实际效果:

组合方式 等效简写/说明 行为
std::ios::in | std::ios::out fstream 的常见默认(需手动指定) 打开文件进行读写。文件必须存在,否则失败。不会清空。
std::ios::out | std::ios::trunc 仅 std::ios::out 创建新文件或清空现有文件以进行写入。
std::ios::out | std::ios::app - 创建新文件或打开现有文件,所有写入都追加到末尾。
std::ios::in | std::ios::out | std::ios::trunc - 创建新文件或清空现有文件以进行读写。
std::ios::in | std::ios::out | std::ios::app - 打开文件进行读写。读指针在开头,但每次写入前都会将写指针移到末尾
std::ios::in | std::ios::binary - 以二进制模式打开文件进行读取。

一些文件对象定义的时候会有默认的参数,默认行为如下:

ifstream: 默认为 in。

ofstream: 默认为 out(隐含 trunc,即会清空文件)。

fstream: 无默认模式,必须显式指定。

app vs ate

app(Append): 是一种操作约束,所有写入必须在末尾进行。

ate(At End): 是一个初始位置,打开文件后指针在末尾,但之后可以随意移动指针到文件任何位置进行读写。

二进制模式: 在处理非文本文件(如图片、视频)或需要精确控制换行符时,务必使用 binary 模式。

代码示例:

#include <iostream>
#include <fstream>
using namespace std;
int main()
{
	ofstream fcout("data.txt", ios::out);  //定义ofstream对象,与cout相同,只不过cout是库里定义好
	fcout << "this is a file message, three number list: " << endl; //
	fcout << 20 <<" " << 22 << " " << 24 << endl;
	fcout.close();//文件流对象在销毁的时候也会默认关闭文件,一些场景下也可以不手动关闭文件
	
	char  message[64];
	int num1, num2, num3;
	
	ifstream fcin("data.txt",ios::in);
	fcin.getline(message, sizeof(message));
	fcin >> num1 >> num2 >> num3;
	fcin.close();


	cout << message << num1 << " " << num2 << " " << num3 << endl;
    
    
    ofstream fcout("data.txt", ios::out | ios::app);  //追加形式打开
	fcout << "this is second file message, three number list: " << endl; //
	fcout << 30 <<" " << 40 << " " << 50 << endl;
	fcout.close();


	return 0;
}

在这里插入图片描述

文件data.txt内容:

在这里插入图片描述

3.4 字符串IOstringstream 应用

与文件IO流基本相同,但是在定义stringIO对象的时候,传入的是string对象(ostream会默认生成一个string对象,不传入参数也可以后续获取默认生成的),并非文件路径,之前从文件中进行读取/输出数据,现在在字符串中。

#include <sstream>  // 包含字符串流头文件
#include <string>

using namespace std;

int main() {
    // 将各种数据类型转换为字符串
    cout << "数据类型转字符串" << endl;

    ostringstream oss;  // 输出字符串流,将数据输出到字符串,用于构建字符串

    int age = 25;
    double salary = 75000.50;
    string name = "张三";

    // 像使用 cout 一样将数据写入字符串流
    oss << "姓名: " << name << ", 年龄: " << age << ", 薪资: " << salary;

    // 获取构建好的完整字符串
    string result = oss.str();
    cout << result << endl << endl;


    //从字符串解析数据 
    cout << "字符串解析数据" << endl;

    string data = "100 98.5 你好";
    istringstream iss(data);  // 输入字符串流,用于解析字符串

    int score1;
    double score2;
    string text;

    // 像使用 cin 一样从字符串流读取数据
    iss >> score1 >> score2 >> text;

    cout << "整数: " << score1 << endl;
    cout << "浮点数: " << score2 << endl;
    cout << "字符串: " << text << endl << endl;


    return 0;
}

运行结果:
在这里插入图片描述

4、C++IO接口汇总

4.1 通用 I/O 接口(适用于所有流类型)

基本状态检查接口

接口 说明
good() 流状态正常,可进行I/O操作
eof() 已到达文件/流末尾
fail() 最近的操作失败(但流未完全损坏)
bad() 流已损坏,无法继续使用
clear() 清除错误状态标志
rdstate() 返回当前流状态

格式化输入/输出接口

接口 说明
<< 操作符 格式化输出(插入器)
>> 操作符 格式化输入(提取器)
setf(), unsetf() 设置/清除格式标志
precision() 设置浮点数精度
width() 设置字段宽度
fill() 设置填充字符

定位接口

接口 说明
tellg() 获取输入流当前位置
tellp() 获取输出流当前位置
seekg(pos) 设置输入流位置
seekp(pos) 设置输出流位置
seekg(off, dir) 设置输入流相对位置
seekp(off, dir) 设置输出流相对位置

4.2 文件 I/O 特殊接口

文件流特有接口(fstream, ifstream, ofstream)

接口 说明
open(filename, mode) 打开文件
close() 关闭文件
is_open() 检查文件是否成功打开
构造函数接受文件名 创建时直接打开文件

文件打开模式

模式 说明
ios::in 以读方式打开
ios::out 以写方式打开
ios::app 追加模式
ios::ate 打开后定位到末尾
ios::trunc 截断文件
ios::binary 二进制模式

4.3 字符串 I/O 特殊接口

接口 说明
str() 获取底层字符串
str(string) 设置底层字符串内容
构造函数接受字符串 创建时初始化内容

4.4 特殊 I/O 接口

非格式化 I/O 接口

接口 说明 适用流
get() 读取单个字符 输入流
getline() 读取一行 输入流
put(char) 写入单个字符 输出流
read(char*, size) 读取二进制数据 输入流
write(char*, size) 写入二进制数据 输出流
gcount() 返回上次读取的字符数 输入流

缓冲区操作接口

接口 说明
flush() 刷新输出缓冲区
sync() 同步输入缓冲区
peek() 查看下一个字符但不提取
putback(char) 将字符放回输入流
unget() 回退一个字符
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐