流的继承

流的四种状态

IO 操作与生俱来的一个问题是可能会发生错误,一些错误是可以恢复的,另一些是不可以的。在C++ 标准库中,用 iostate 来表示流的状态,不同的编译器 iostate 的实现可能不一样,不过都有四种状态:

  • badbit 表示发生系统级的错误,如不可恢复的读写错误。通常情况下一旦 badbit 被置位,流就无法再使用了。

  • failbit 表示发生可恢复的错误,如期望读取一个int数值,却读出一个字符串等错误。这种问题通常是可以修改的,流还可以继续使用。

  • eofbit表示到达流结尾位置, 流在正常输入输出的情况下结束,会被置为eofbit状态。

  • goodbit 表示流处于有效状态。流在有效状态下,才能正常使用。如果 badbit 、 failbit 和 eofbit 任何一个被置位,则流无法正常使用。

这四种状态都定义在类 ios_base 中,作为其数据成员存在。在 GNU GCC7.4 的源码中,流状态的实现

如下:

通过流的状态函数实现

bool good() const      //流是goodbit状态,返回true,否则返回false
bool bad() const       //流是badbit状态,返回true,否则返回false
bool fail() const      //流是failbit状态,返回true,否则返回false
bool eof() const       //流是eofbit状态,返回true,否则返回false

标准输入输出流

cin

istream 类定义了一个全局输入流对象,即 cin , 代表的是标准输入,它从标准输入设备(键盘)获取数据,程序中的变量通过流提取符 “>>”(输入流符号) 从流中提取数据。

流提取符 “>>” 从流中提取数据时通常跳过输入流中的空格、 tab 键、换行符等空白字符。只有在输入完数据再按回车键后,该行数据才被送入键盘缓冲区,形成输入流,提取运算符 “>>” 才能从中提取数据。

int num;
cin >> num;
if (cin) {
    // 输入成功,num 包含有效数据
    cout << "输入的数字是: " << num << endl;
} else {
    // 输入失败,可能是类型不匹配或其他错误
    cout << "输入错误!" << endl;
    // 清除错误状态,以便后续输入
    cin.clear();
    // 忽略缓冲区中的错误数据
    cin.ignore(numeric_limits<streamsize>::max(), '\n');
}

if (cin) 等价于 if (cin.good()),当且仅当 goodbit 被设置(其他标志都未设置)时,条件才为 true

istream 类重载了 布尔转换运算符(C++11 后为 explicit operator bool() const),这使得 cin 对象可以被隐式转换为布尔值,用于判断流的状态:

  • 当流状态正常(goodbit 有效)时,转换为 true
  • 当流状态异常(如 failbitbadbit 或 eofbit 被设置)时,转换为 false

这就是为什么可以写出 if (cin) 或 while (cin >> x) 这样的代码 —— 本质上是在检查 cin 转换后的布尔值,判断输入操作是否成功。

自定义类的布尔转换

#include <iostream>
using namespace std;

class Counter {
private:
    int count;
public:
    Counter(int c = 0) : count(c) {}
    
    // 显式布尔转换:当 count > 0 时视为"真"
    explicit operator bool() const {
        return count > 0;
    }
};

int main() {
    Counter c1(5);
    Counter c2(0);
    
    if (c1) {  // 等价于 if (c1.operator bool())
        cout << "c1 有效(count > 0)" << endl;
    }
    
    if (!c2) {
        cout << "c2 无效(count = 0)" << endl;
    }
    
    return 0;
}

缓冲机制

缓冲机制分为三种类型:全缓冲、行缓冲和不带缓冲

全缓冲:在这种情况下,当填满缓冲区后才进行实际 I/O 操作。全缓冲的典型代表是对磁盘文件的读写。

行缓冲:在这种情况下,当在输入和输出中遇到换行符时,执行真正的 I/O 操作。这时,我们输入的字符先存放在缓冲区,等按下回车键换行时才进行实际的 I/O 操作。典型代表是cin,输出到终端的cout。

不带缓冲:也就是不进行缓冲,有多少数据就刷新多少。标准错误输出 cerr是典型代表,这使得出错信息可以直接尽快地显示出来。

cout

ostream 类定义了全局输出流对象 cout,即标准输出,在缓冲区刷新时将数据输出到终端。

如下几种情况会导致输出缓冲区内容被刷新:

1.遇到endl刷新

void test(){
    for(int i = 0; i < 5; ++i){
        cout << 'a' ;
    }
        cout << endl ;
    sleep(3);
}

立即输出5个a

2.缓冲区满刷新

void test(){
    for(int i = 0; i < 1025; ++i){
        cout << 'a';
    }
    sleep(3);
}

马上输出了1024个a,等待3秒后输出了最后一个a

3,程序结束时刷新

void test(){
    for(int i = 0; i < 5; ++i){
        cout << 'a' ;
    }
    sleep(3);
}

等待3秒,输出5个a

endl 是 C++ 标准库中的一个操纵符(manipulator),本质上是一个函数模板,定义大致如下:

template<class CharT, class Traits>
std::basic_ostream<CharT, Traits>& endl(std::basic_ostream<CharT, Traits>& os) {
    os.put(os.widen('\n')); // 插入换行符
    os.flush(); // 刷新输出缓冲区
    return os;
}

它的作用有两个:

  1. 向输出流中插入一个换行符(\n),相当于在屏幕上换行;
  2. 强制刷新输出流的缓冲区(flush()),确保数据立即显示在屏幕上(避免数据暂存在缓冲区中不输出)。

文件流

所谓“文件”,一般指存储在外部介质上数据的集合。一批数据是以文件的形式存放在外部介质上的。操作系统是以文件为单位对数据进行管理的。要向外部介质上存储数据也必须先建立一个文件(以文件名标识),才能向它输出数据。外存文件包括磁盘文件、光盘文件和U盘文件。目前使用最广泛的是磁盘文件。

文件流是以外存文件为输入输出对象的数据流。

文件输入流是从外存文件流向内存的数据,文件输出流是从内存流向外存文件的数据。每一个文件流都有一个内存缓冲区与之对应。文件流本身不是文件,而只是以文件为输入输出对象的流。若要对磁盘文件输入输出,就必须通过文件流来实现。

C++ 对文件进行操作的流类型有三个:

ifstream(文件输入流)

ofstream(文件输出流)

fstream (文件输入输出流)

文件输入输出流

根据不同的情况,对文件的读写操作,可以采用不同的文件打开模式。文件模式在 GNU GCC7.4 源码实现中,是用一个叫做 openmode 的枚举类型定义的,它位于 ios_base 类中。文件模式一共有六种,它们分别是:

in : 输入,文件将允许做读操作;如果文件不存在,打开失败

out : 输出,文件将允许做写操作;如果文件不存在,则直接创建一个

app : 追加,写入将始终发生在文件的末尾,强制所有写入都发生在文件末尾

ate : 末尾,立即定位到文件的末尾,但是后续可以自由移动指针到其他位置进行读写

trunc : 截断,如果打开的文件存在,其内容将被丢弃,其大小被截断为零

binary : 二进制,读取或写入文件的数据为二进制形式

//使用键盘的录入功能,录入5个数字,
//并且存入文件中(输出),随后读取文件的数据(输入),将之前录入的数据显示出来
//此时同时具有读和写两个功能
//两种方案:
//1.使用文件输入输出流对象,也就是fstream
//2.分别使用文件输入流和文件输出流对象,也就是同时使用ifstream和ofstream
void test(){
    //对于fstream来说,如果使用它进行写操作,那么需要自行手动创建需要写出的
    //文件,不会自动创建该文件
    fstream fs("digit.txt");
    int number;
    for(int i = 0; i < 5; ++i){
        //使用流提取符号从标准输入流缓存区中提取数据到number
        cin >> number;
        //fs从number中读取数据,进入到文件输出流缓冲区中
        fs << number << " ";
    }
    //因为fs后续还需要使用,所以我们不要去close
    //写完之后文件的游标实际上是位于最尾端
    fs.seekg(0);
    fs.flush();
    int number2;
    for(int i = 0; i < 5; ++i){
        fs >> number2;
        cout << number2 << " ";
    }
    cout << endl;
}
//cin >> number >> number2;
void test2(){
    fstream fs("3.txt");
    //下面两种方式都是将文件的游标设置为文件的起始位置
    //如果我们需要设置文件的末尾,那么使用哪种方式方便???第二种
    //fs.seekg(0);
    //fs.seekg(0, std::ios_base::beg);
    
    fs.seekg(12);
    //fs.seekg(0, std::ios_base::end);
    cout << fs.tellg() << endl;//获取文件的游标
}

void test3(){
    ofstream ofs("digit2.txt");
    int number;
    for(int i = 0; i < 5; ++i){
        //从标准输入流缓冲区中提取数字,一共提取5次
        //分隔符默认情况下是空格换行制表符
        cin >> number;
        //ofs文件输出流对象会将number里面的值写入到缓冲区,随后刷新到文件
        ofs << number << " ";
    }
    //将文件输出流对象关闭,此时会触发刷新操作
    ofs.close();
    ifstream ifs("digit2.txt");
    if(!ifs){
        cout << "the file is not found" << endl;
        return;
    }
    for(int i = 0; i < 5; ++i){
        //文件输入流将缓冲区里面的数据提取到number变量中
        //默认的分隔符也是空格、换行、制表符
        ifs >> number;
        //再将number里面的值交给标注的输出流缓冲区,随后输出到屏幕的终端
        cout << number << " ";
    }
    cout << endl;
}

文件输入流

文件输入流对象的创建

他们的构造函数形式都很类似:

ifstream();
explicit ifstream(const char* filename, openmode mode = ios_base::in);
explicit ifstream(const string & filename, openmode mode = ios_base::in);

ofstream();
explicit ofstream(const char* filename, openmode mode = ios_base::out);
explicit ofstream(const string & filename, openmode mode = ios_base::out);

fstream();
explicit fstream(const char* filename, openmode mode = ios_base::in|out);
explicit fstream(const string & filename, openmode mode = ios_base::in|out);

构造函数中的explicit关键字表示禁止隐式类型转换。

我们可以将输入流对象的创建分为两类:

  1. 可以使用无参构造创建ifstream对象,再使用open函数将这个文件输入流对象与文件绑定(若文件不存在,则文件输入流进入failbit状态);

  2. 也可以使用有参构造创建ifstream对象,在创建时就将流对象与文件绑定,后续操作这个流对象就可以对文件进行相应操作。

1.使用<<进行读取

#include <iostream>
#include <string>
#include <fstream>
using std::cout;
using std::endl;
using std::cerr;
using std::ifstream;
using std::string;

void test(){
    ifstream ifs("temp.txt");
    if(!ifs.good()){
        cerr << "ifstream is not good" << endl;
        return;
    }

    string word;
    while(ifs >> word){
        cout << word;
    }
    cout << endl;
    ifs.close();
}

int main()
{
    test();
    return 0;
}

2.使用getline进行按行读取

使用<string>提供的getline方法,工作中更常用

基本语法

#include <string>
#include <iostream>  // 用于输入流

// 从输入流 is 中读取一行到字符串 str,终止符默认为 '\n'
std::istream& getline(std::istream& is, std::string& str);

// 自定义终止符:读到 delim 时停止(delim 不会被存入 str)
std::istream& getline(std::istream& is, std::string& str, char delim);

核心特点

  1. 读取范围:从输入流中读取字符,直到遇到换行符 '\n'(默认)或自定义终止符,换行符 / 终止符不会被存入字符串
  2. 处理空格:会读取并保留输入中的空格(与 cin >> str 不同,cin >> 遇到空格就停止)。
  3. 覆盖性:每次调用会清空原字符串 str,再存入新读取的内容。
  4. 返回值:返回输入流对象 is,可用于判断读取是否成功(如结合 if 语句)。

示例:从文件中读取每一行并打印到屏幕

void test(){
    ifstream ifs("1_buffer.cpp");
    if(!ifs.good()){
        cerr << "ifstream is not good" << endl;
        return;
    }

    string word;
    while(getline(ifs, word)){
        cout << word << endl;
    }
    cout << endl;
    ifs.close();
}

示例:判断读取是否成功

#include <iostream>
#include <string>

int main() {
    std::string line;
    std::cout << "请输入内容(Ctrl+Z 结束):" << std::endl;
    
    // 循环读取,直到输入结束(如 Ctrl+Z)
    while (std::getline(std::cin, line)) {
        std::cout << "读取到:" << line << std::endl;
    }
    
    std::cout << "输入结束" << std::endl;
    return 0;
}
特性 std::getline(cin, str) cin >> str
终止条件 遇到换行符 '\n' 遇到空格、制表符、换行符等空白字符
是否保留空格 是(完整读取一行,包括空格) 否(遇到空格就停止)
处理输入流中的换行 读取并丢弃换行符(不存入 str) 留下换行符在输入流中

3.read函数

#include <fstream>  // 用于文件流
#include <string>

// 从输入流 is 中读取 count 字节到 buffer
std::istream& read(char* buffer, std::streamsize count);

从文件读取数据到 std::string

#include <iostream>
#include <fstream>
#include <string>

int main() {
    // 打开文件(二进制模式,避免文本模式的转义处理)
    std::ifstream file("data.bin", std::ios::binary);
    if (!file.is_open()) {
        std::cerr << "无法打开文件" << std::endl;
        return 1;
    }

    // 确定文件大小(用于分配缓冲区)
    file.seekg(0, std::ios::end);  // 移动到文件末尾
    std::streamsize file_size = file.tellg();  // 获取当前位置(即文件大小)
    file.seekg(0, std::ios::beg);  // 回到文件开头

    // 准备 string 作为缓冲区(预分配空间)
    std::string str;
    str.resize(file_size);  // 分配足够的空间

    // 用 read 读取数据到 string 的底层缓冲区
    file.read(&str[0], file_size);  // 或 str.data()(C++11 后)
    //不能用 c_str():返回 const char*,不允许通过它写入数据,会导致编译错误。

    if (file) {
        std::cout << "成功读取 " << file.gcount() << " 字节" << std::endl;
        // 此时 str 中存储了读取的数据
    } else {
        std::cerr << "读取失败,实际读取 " << file.gcount() << " 字节" << std::endl;
    }

    return 0;
}

ifstream 默认以文本模式打开文件,而 read() 是二进制读取方式。

文件输出流

文件输出流对象的创建

模式常量 含义
std::ios::out 写入模式(默认,若文件不存在则创建,若存在则清空原有内容)
std::ios::app 追加模式(写入的数据添加到文件末尾,不覆盖原有内容)
std::ios::trunc 截断模式(打开文件时清空原有内容,与 out 配合使用,默认已包含)
std::ios::binary 二进制模式(以二进制而非文本方式处理数据,适合非文本文件如图片、视频)
std::ios::in 读取模式(与输出流配合时,可同时读写,但需注意文件是否存在)
#include <fstream>
void test0(){
    ofstream ofs;
    ofs.open("test1.cc");
    
    ofstream ofs2("test2.cc");
    
    string filename = "test3.cc";
    ofstream ofs3(filename);
}

推测一下,如果文件输出流对象绑定的文件不存在,可以吗?

—— 可以,如果文件不存在,就创建出来

创建文件输出流对象的核心是关联文件名和指定打开模式,两种创建方式(默认构造 + open() 或直接构造)均可,根据场景选择。使用时务必检查文件是否打开成功,并根据需求选择合适的打开模式(如写入、追加、二进制等)。

通过<<运算符写内容

string filename = "test3.cc";
ofstream ofs3(filename);

string line("hello,world!\n");
ofs << line; 

ofs.close();

通过write函数写内容

char buff[100] = "hello,world!";
ofs.write(buff,strlen(buff));

文件的游标

在 C++ 文件流操作中,“游标”(也称为文件位置指示器文件指针)是一个标记,用于指示下一次读写操作开始的位置。通过控制游标位置,可以实现对文件的随机访问(而非仅能顺序读写)。

1. 游标相关的核心函数

C++ 的文件流类(ifstreamofstreamfstream)提供了以下函数控制游标位置:

函数 功能描述
seekg(pos) 移动输入流(读操作)的游标到指定位置(pos 为字节偏移量)
seekp(pos) 移动输出流(写操作)的游标到指定位置
tellg() 返回输入流当前游标的位置(距离文件开头的字节数)
tellp() 返回输出流当前游标的位置

 

  • 对于可读可写的流(fstream),seekg 和 seekp 通常作用于同一个游标(可认为读写共用一个位置指示器)。
  • 偏移量的参考点(起点)可通过第二个参数指定(默认是文件开头)。

2. 游标位置的参考点(起始位置)

通过函数的第二个参数指定游标移动的参考基准,取值来自 std::ios 命名空间:

参考点常量 含义
std::ios::beg 从文件开头开始计算偏移量(默认值)
std::ios::cur 从当前游标位置开始计算偏移量
std::ios::end 从文件末尾开始计算偏移量(偏移量可负,如 -5 表示文件末尾前 5 字节)
#include <fstream>
#include <iostream>
#include <string>

int main() {
    std::ifstream file("example.txt");
    if (!file) {
        std::cerr << "无法打开文件" << std::endl;
        return 1;
    }

    // 获取初始游标位置(默认在文件开头,应为 0)
    std::streampos pos = file.tellg();
    std::cout << "初始位置:" << pos << " 字节" << std::endl;  // 输出:0

    // 移动游标到文件第 5 字节(从开头算)
    file.seekg(5, std::ios::beg);
    std::cout << "移动后位置:" << file.tellg() << " 字节" << std::endl;  // 输出:5

    // 读取从第 5 字节开始的内容
    std::string content;
    getline(file, content);
    std::cout << "从位置 5 开始的内容:" << content << std::endl;

    return 0;
}
void test(){
    //统计输出流文件的长度,使用tellp
    ofstream ofs("tellp.txt");
    ofs << "hello,world,hello,python,hello,c++";
    ofs.flush();
    cout << ofs.tellp();
}

字符串输入输出流

字符串I/O是内存中的字符串对象与字符串输入输出流对象之间做内容传输的数据流,通常用来做格式转换。

C++ 对字符串进行操作的流类型有三个:

istringstream (字符串输入流)

ostringstream (字符串输出流)

stringstream (字符串输入输出流)

字符串输入流

它们的构造函数形式都很类似:

istringstream(): istringstream(ios_base::in) { }
explicit istringstream(openmode mode = ios_base::in);
explicit istringstream(const string& str, openmode mode = ios_base::in);

ostringstream(): ostringstream(ios_base::out) { }
explicit ostringstream(openmode mode = ios_base::out);
explicit ostringstream(const string& str, openmode mode = ios_base::out);

stringstream(): stringstream(in|out) { }
explicit stringstream(openmode mode = ios_base::in|ios_base::out);
explicit stringstream(const string& str, openmode mode = ios_base::in|ios_base::out);

将字符串的内容传输给字符串输入流对象,再通过这个对象进行字符串的处理(解析)

创建字符串输入流对象时传入c++字符串,字符串的内容就被保存在了输出流对象的缓冲区中。之后可以通过输入流运算符将字符串内容输出给不同的变量,起到了字符串分隔的作用。

将字符串s的内容传给了两个int型数据

//ip 8080
//port 47.115.220.165
void test2(){
    //读取配置文件
    //先构建一个文件输入流对象,读取文件的数据
    ifstream ifs("aliyun.conf");
    if(!ifs){
        cout << "the file is not found" << endl;
        return;
    }
    //使用getline读取一行到一个string对中
    string line;
    while(getline(ifs, line)){
       istringstream iss(line);
       string key,value;
       iss >> key >> value;
       cout << key << "=" << value << endl;
    }
}

字符串输出流

通常的用途就是将各种类型的数据转换成字符串类型

void test0(){
    int num = 123, num2 = 456;
    ostringstream oss;
    //把所有的内容都传给了字符串输出流对象
    oss << "num = " << num << " , num2 = " << num2 << endl;
    cout << oss.str() << endl;
}

将字符串、int型数据、字符串、int型数据统统传给了字符串输出流对象,存在其缓冲区中,利用它的str函数,全部转为string类型并完成拼接。

有一种更好用的函数to_string

std::to_string 针对不同的基本数据类型提供了重载版本,常见原型如下:

#include <string>

// 转换整数类型
std::string to_string(int value);
std::string to_string(long value);
std::string to_string(long long value);
std::string to_string(unsigned value);
std::string to_string(unsigned long value);
std::string to_string(unsigned long long value);

// 转换浮点类型
std::string to_string(float value);
std::string to_string(double value);
std::string to_string(long double value);

std::to_string 的使用非常直观:传入基本类型的变量或常量,返回对应的 std::string 对象。

#include <iostream>
#include <string>

int main() {
    // 整数转换
    int num = 123;
    std::string str_int = std::to_string(num);
    std::cout << "整数转换:" << str_int << "(类型:" << typeid(str_int).name() << ")" << std::endl;

    // 浮点数转换
    double pi = 3.14159;
    std::string str_double = std::to_string(pi);
    std::cout << "浮点数转换:" << str_double << std::endl;

    // 无符号整数转换
    unsigned long long big_num = 1234567890ULL;
    std::string str_ull = std::to_string(big_num);
    std::cout << "大整数转换:" << str_ull << std::endl;

    return 0;
}
  • std::to_string 优势:代码简洁,无需手动管理流对象,适合简单转换。
  • stringstream 优势:支持自定义格式(如 setprecision 控制小数位数),支持更多类型转换。

 

 

Logo

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

更多推荐