C++的流简介和简单使用

C++的流简介和简单使用

C语言的输入与输出

C 语言中用到的最频繁的输入输出方式就是 scanfprintf

scanf :从标准输入设备 ( 键盘 ) 读取数据,并将值存放在变量中。标准输入设备对应程序中的标准输入流 stdin

printf :将指定的字符串输出到标准输出设备。标准输出设备对应程序中的标准输出流 stdout

这个标准输出设备在 Windows 叫控制台窗口 (Console Window)或命令提示符窗口 (Command Prompt),在 Linux 叫终端 (Terminal)且部分场景也叫终端模拟器控制台

使用 scanfprintf 时要注意宽度输出和精度输出控制。

C 语言借助了相应的缓冲区来进行输入与输出。如下图所示,数据从标准输入流代表的 A 进入缓冲区,当代码生成的进程需要时,会从缓冲区获取,若缓冲区没有数据则进程会阻塞在此进行等待;若用户按下组合键 Ctrl 加 C 或 Z ,或 A 从一开始就是一个指定的文件,文件只有一个 EOF ,则程序会停止读取。

对输入输出缓冲区的理解:

1.可以屏蔽掉低级 IO 的实现。简单来说就是同一个代码,放在不同的操作系统都能运行,不必关注底层的 IO 是如何实现的。低级 IO 的实现依赖操作系统本身内核的实现,所以如果能够屏蔽这部分的差异,可以很容易写出可移植的程序。

2.可以使用这部分的内容实现 "行" 读取的行为,对于计算机而言是没有 "行" 这个概念,有了这部分,就可以定义 "行" 的概念,然后解析缓冲区的内容,返回一个 "行" 。

几乎所有的编程语言,最终都会转换成计算机中的进程运行。因此对于 IO ,可以肯定的是这些编程语言都有封装有相关的系统调用,因此学会其中一个,便能通过同样的思路去理解其他编程语言的 IO 操作。

C++的流

"流" 即是流动的意思,是物质从一处向另一处流动的过程,是对一种有序连续且具有方向性的数据( 其单位可以是 bit, byte, packet )的抽象描述。计算机里的物质绝大多数情况下指的是数据,因为计算机里的流的本质是数据在不同设备之间流动

C++ 的流是指信息从外部输入设备 (如键盘)向计算机内部 (如内存)输入 ,和从内存向外部输出设备 (显示器)输出的过程 。这种输入输出的过程被形象的比喻为 "流"。它的特性 是:有序连续具有方向性

为了实现这种流动,C++ 定义了 IO 标准类库,这些每个类都称为流或流类,用以完成某方面的功能。其中 ios 为基类,其他类都是直接或间接派生自 ios 类。同时 IO 类库是使用菱形虚拟继承实现

设计菱形继承是为了让 ios 类和 ios_base 类同时具有 istream 类和 ostream 类的功能。

下图来自 Input/Output - C++ Reference

实际上这个库和 C 语言的底层实现没有区别 ,原因是它们都使用同样的系统调用,都和操作系统进行直接交互。或者说这里的类库是将系统调用用 C++ 的方式封装。

在基本熟悉操作系统相关的编程和文件体系后,只需掌握用法即可,除非需要扩展流库、开发编译器和维护标准库,但这些很明显不是一般的开发者学习的东西。

当然,这个地方是 C++ 的难点之一,涉及了虚继承 + 多重继承 + 模板 + 缓冲区管理 + 格式化 + 本地化等,难度源于组件繁多、职责纠缠、设计历史包袱重,说是历史遗留的 "大便" 也不为过。所以这里的方向是先介绍如何使用,再另外开一篇结合操作系统原理进行详细讲解。

C++标准IO流

C++标准库提供了 4 个全局流对象 cincoutcerrclog,使用 cout 进行标准输出,即数据从内存流向控制台 (显示器) 。

使用 cin 进行标准输入即数据通过键盘输入到程序中 ,同时 C++ 标准库还提供了 cerr 用来进行标准错误的输出 ,以及 clog 进行日志的输出

从上图可以看出,coutcerrclogostream 类的三个不同的对象,因此这三个对象现在基本没有区别,只是应用场景不同。

这 4 个实现 IO 功能的类在使用时候必须要包含文件并引入 std 标准命名空间。

C 语言的 scanfprintfperror ,和 C++ 的 cincoutcerr 的使用场景类似,它们的底层都封装有所在操作系统的系统调用。

cin和cout使用举例

cin为缓冲流。键盘输入的数据保存在缓冲区中 ,当要提取时从缓冲区中拿 。使用 cin 获取从标准输入流 stdin 得到的数据,需要借助函数 operator>>

cpp 复制代码
#include <cstdio>
#include <iostream>
using namespace std;

int main() {
    int x = 3;
    cin >> x;
    cout << x << '\n';
    cin.operator>>(x);                     // >>重载运算符,2种方式等同
    (cout.operator<<(x)).operator<<('\n'); // 等价于cout<<x<<10
    (cout.operator<<(endl)).operator<<(x) << endl;
    return 0;
}

operator<< 没有 operator<<(char) 的情况,所以 operator<<('\n') 选择的函数是 opeartor<<(int) ,所以 (cout.operator<<(x)).operator<<('\n'); 输出和 cout<<x<<10 相同。

若使用 cin 输入时输入错了,必须在回车之前修改,否则回车键按下就无法挽回了。只有把输入缓冲区中的数据取完后,才要求输入新的数据。

输入的数据类型必须与要提取的数据类型一致 ,否则可能会出错。例如这里在 Linux 的 gcc、 Windows 的 gcc 和 VS 的 MSVC 下输入 2 段由英文非数字字符组成的由空格隔开的字符串, x 均被 0 覆盖,但其他编译器生成的程序不确定,这取决于编译器的行为。

空格和回车都可以作为数据之间的分格符 ,所以多个数据可以在一行输入,也可以分行输入。但如果是字符型和字符串,则空格(ASCII码为32)无法用 cin 输入,字符串中也不能有空格。回车符也无法读入。

流插入<<和流提取>>支持自定义类型

cincout 可以直接输入和输出内置类型数据 ,因为标准库已经将所有内置类型的输入和输出全部重载了。这里参考流提取 istream::operator>> - C++ Reference 和流插入 ostream::operator<< - C++ Reference

cpp 复制代码
// arithmetic types (1)	即内置类型
istream& operator>> (bool& val);
istream& operator>> (short& val);
istream& operator>> (unsigned short& val);
istream& operator>> (int& val);
istream& operator>> (unsigned int& val);
istream& operator>> (long& val);
istream& operator>> (unsigned long& val);
istream& operator>> (float& val);
istream& operator>> (double& val);
istream& operator>> (long double& val);
istream& operator>> (void*& val);
// stream buffers (2) 即ios类
istream& operator>> (streambuf* sb );
// manipulators (3)	
istream& operator>> (istream& (*pf)(istream&));
istream& operator>> (ios& (*pf)(ios&));
istream& operator>> (ios_base& (*pf)(ios_base&));
// C++11 新增
// arithmetic types (1)
istream& operator>> (long long& val);
istream& operator>> (unsigned long long& val);

// arithmetic types (1)
ostream& operator<< (bool val);
ostream& operator<< (short val);
ostream& operator<< (unsigned short val);
ostream& operator<< (int val);
ostream& operator<< (unsigned int val);
ostream& operator<< (long val);
ostream& operator<< (unsigned long val);
ostream& operator<< (float val);
ostream& operator<< (double val);
ostream& operator<< (long double val);
ostream& operator<< (void* val);
// stream buffers (2)	
ostream& operator<< (streambuf* sb );
// manipulators (3)	
ostream& operator<< (ostream& (*pf)(ostream&));
ostream& operator<< (ios& (*pf)(ios&));
ostream& operator<< (ios_base& (*pf)(ios_base&));
// C++11新增
ostream& operator<< (long long val);
ostream& operator<< (unsigned long long val);

<<>> 的实现还有一个好处:对于自定义类型,如果要支持 cincout 的标准输入输出,需要对 <<>> 进行重载,且自定义类型需要将 >>>> 设置为友元或成员变量设置为公有。

cpp 复制代码
#include <iostream>
#include <cstdio>
using namespace std;

class A {
private:
	friend istream& operator>>(istream&,A&);
	friend ostream& operator<<(ostream&, A&);
	int x;
	int y;
};
istream& operator>>(istream& in, A& aim) {
	(in >> aim.x).operator>>(aim.y);
	return in;
}
ostream& operator<<(ostream& out, A& aim) {
	(out.operator<<(aim.x)) << ' ' << aim.y;
	return out;
}
int main() {
	A a;
	cin >> a;
	cout << a;
	return 0;
}

cin连续输入和重载类型转换

部分在线 OJ 中的输入和输出,对于 IO 类型的算法,需要循环输入。例如这个输入模式:

cpp 复制代码
int main(){
    string st;
    while(cin>>st){
        // 算法逻辑
    }
    return 0;
}

连续输入时,Windows 在输入ctrl + + + Z时结束,而 Linux 则是 ctrl + + + D

也可以使用ctrl + + + C,但在 Linux 使用这个的本质是发送导致进程终止的信号给操作系统,会强行终止当前进程,需要评估使用环境。

istream 类型对象转换为逻辑条件判断值。这里参考 istream::operator>> - C++ Referenceios::operator bool - C++ Reference

cpp 复制代码
istream& operator>> (int& val);
explicit operator bool() const;

使用 while(cin>>i) 去流中提取对象数据时,调用的是 operator>> ,返回值是 istream 类型的对象,理论上无法直接通过对象判断 bool值,但这里却可以做逻辑判断。

这是因为自 C++11 起, istream 类提供了向 bool 类型的隐式类型转换。简单来说就是 istream 内内置了 operator bool 成员函数,例如类 A 内置了 operator bool ,也可当成 bool 类来使用。

cpp 复制代码
#include <cstdio>
#include <iostream>
using namespace std;

class A {
public:
    A(int _x = 0) : x(_x) {}
    operator bool() {
        return x == 0 ? false : true;
    }

private:
    int x;
};

int main() {
    A a, b(1);
    if (a) {
        cout << "xxx.\n";
    } else if (b) { // 调用它
        cout << "helloworld\n";
    } else {
        cout << "what up!\n";
    }
    return 0;
}

所以之前的代码的等价关系:

cpp 复制代码
while(cin>>st){}\\...
// 等价于
while ((cin >> st).operator bool()) { ... }  // C++11 起

即在这个场景下,istream 对象又调用了operator booloperator bool 调用时如果接收流失败,或者有结束标志 EOF ,则返回 false。而旧标准 C++98 则使用

operator void*() ,返回非空指针(通常为流对象自身地址)表示 true,返回空指针表示 false

类似的重载类型转换还有 operator intoperator double ,以及 operator class nameoperator classname

operator class name 用于没在本类前声明过的类,operator classname 用于在本类前声明过的类。

但这些符号的滥用可能会使隐式类型转换造成歧义,因此推荐使用 explicit 关键字阻止隐式转换。

文件IO流ifstream和ofstream

C++ 根据文件内容的数据格式分为二进制文件文本文件。采用文件流对象操作文件的一般步骤:

  1. 定义一个文件流对象。

    • ifstream ifile
    • ofstream ofile
    • fstream iofile
  2. 使用文件流对象的成员函数打开一个磁盘文件,使得文件流对象和磁盘文件之间建立联系。

  3. 使用提取和插入运算符对文件进行读写操作,或使用成员函数进行读写。

  4. 关闭文件。

这和 C 语言的 fopen 打开文件,各种文件操作结束后,使用 fclose 的流程,以及 Linux 的 open 系统调用打开文件,各种文件操作结束后,close 关闭文件的流程基本相似。这里整理一张表格,对比之下即可快速上手 istream 类和 ostream 类。

操作 C(fopen/fclose) Linux系统调用(open/close) C++ (文件流)
对象类型 FILE* int(文件描述符) ifstream, ofstream, fstream 对象
打开文件 fopen("file.txt", "r") open("file.txt", O_RDONLY) ifstream fin("file.txt")或 fin.open("file.txt")
错误处理 检查返回值是否为 NULL(毕竟是结构体指针) 检查返回值是否为 -1 检查流对象状态,如if(!fin)或fin.is_open()
读写操作 fprintf, fscanf, fread, fwrite 等 read(), write()等 文本: 插入<<和提取>>运算符, 二进制: read(), write()方法
关闭文件 fclose(file_ptr) close(fd) 主动调用fin.close()或由析构函数自动关闭
随机访问 fseek, ftell lseek seekg() (读), seekp() (写), tellg(), tellp()
头文件/库 stdio.h / cstdio fcntl.h, unistd.h 等 fstream

简单使用ifstream

这里简单使用 ifstream 对象读取文件。实际上 cin 本身就是 istream 对象,但它代表的是键盘,因此可以模仿着使用。

cpp 复制代码
#include <fstream>
#include <iostream>
#include <string>
using std::ifstream;
using std::ofstream;
using std::string;

int main() {
    // 按只读的方式打开文件
    ifstream test("test.txt"); // 也可以ifstream test;test.open("test.txt");
    // if (!test) { // 这里和if (!test.is_open())二选一即可
    if (!test.is_open()) {
        std::cout << "文件test.txt不存在\n";
        return 0;
    }
    string st;
    test >> st; // 欲读取hello, world,奈何不读空格
    std::cout << st << '\n';

    // 达到和C语言的rewind一样的效果
    test.clear();                 // 习惯性清除状态位,保证后续操作不受影响
    test.seekg(0, std::ios::beg); // 相当于 rewind

    std::getline(test, st); // 直接读取整行
    std::cout << st << '\n';
    return 0;
}

若文件 test.txt 的内容为 "Hello, world! " ,则代码输出

复制代码
Hello,
Hello, world.

其中 istream 的成员函数 istream::seekggetline 参考:

cpp 复制代码
istream& seekg (streampos pos);
istream& seekg (streamoff off, ios_base::seekdir way);

// C++98
istream& getline (istream& is, string& str, char delim);
istream& getline (istream& is, string& str);
// C++11 新增移动构造
istream& getline (istream&& is, string& str, char delim);
istream& getline (istream&& is, string& str);

代码中的 test.seekg(0, std::ios::beg)0 是从文件开始处偏移的字节数,std::ios::beg 表示从文件头开始计算。这和 fseek(fp, 0, SEEK_SET) 功能相同。

代码中的test.clear():虽然不是每次都需要,但当流之前因为读到文件尾(EOF)或发生格式错误而进入失败状态时,必须先调用 clear() 清除状态,否则 seekg 可能会被忽略。

代码中的getline:从 istream 中读取一行字符串。一般常用 getline(cin,st) 从键盘输入到缓冲区的数据中读取一行。

简单使用ofstream

这里简单使用 ofstream 对象向文件写入。写入分完全覆盖和尾部追加。

完全写入的代码示例:

cpp 复制代码
#include <fstream>
#include <iostream>
#include <string>
using std::ifstream;
using std::ofstream;
using std::string;
string st = "what up!\n";

void _f1(ofstream &test) {
    test << st;
    test.close(); // 主动关闭文件,更换打开模式
}

void f1() {
    ofstream test("test.txt");
    _f1(test);
    test.open("test.txt", std::ios::app); // 以追加方式重新打开
    test << st;                           // 这时文件内可以看到2行what up!
}

void f2() {
    ofstream test;
    test.open("test.txt",
              ofstream::out | ofstream::trunc); // 先清空,再写入
    test << st;
}

int main() {
    // f1(); // f1()之后文件内能看到2行what up!
    f2(); // 先清空,再写入
    return 0;
}

这里的 app 表示以追加的方式打开文件。使用时类似 Linux 的 open 系统调用,将一堆标识符进行或运算代表文件属性。

这里对常用的属性列举表格进行描述:

文件打开模式 说明
std::ios::in 为输入(读)而打开。
std::ios::out 为输出(写)而打开。
std::ios::app 所有写入都追加到文件末尾。
std::ios::binary 以二进制模式打开,用于非文本数据。
std::ios::ate 打开后立即将读写位置移到文件末尾。
std::ios::trunc 如果文件已存在,先清空其内容再打开。

处理非文本数据时必须使用 readwrite 成员函数,并配合 std::ios::binary 打开模式。

stringstream

在C语言中,如果想要将一个整形变量的数据转化为字符串格式,如何去做?

  1. 使用 itoa() 函数。原型 char* itoa(int value,char* str,int base);

  2. 使用C语言自带的 sprintf() 函数。原型 int sprintf(char* str, const char* format,...);

但是这 2 个函数在转化时,都得需要先给出保存结果的空间,空间给多大不好界定,而且转化格式不匹配时,可能还会得到错误的结果甚至程序崩溃。

在 C++ 中,可以使用 stringstream 类对象来避开此问题。

在程序中如果想要使用 stringstream,必须要包含头文件 sstream 。在该头文件下,标准库三个类:istringstreamostringstreamstringstream,分别用来进行流的输入、输出和输入输出操作,这里主要介绍stringstream

若观察 ios_base 类的菱形虚拟继承组件,可发现 stringstream 是最后一层。

stringstream主要可以用来:

  1. 将数值类型数据格式化为字符串
  2. 字符串拼接
  3. 序列化和反序列化结构数据

数值格式化

stringstream 实际是在其底层维护了一个string类型的对象用来保存结果。所以多次数据类型转化时,一定要用clear()来清空,才能正确转化,但 clear()不会将stringstream底层的string对象清空,因此还需要使用s. str("")方法将底层string对象设置为 ""空字符串。

此外可以使用s.str()将让stringstream返回其底层的string对象。

stringstream 其会对参数类型进行推演,不需要格式化控制,也不会出现格式化失败的风险。

将数值类型数据格式化为字符串参考:

cpp 复制代码
#include <iostream>
#include <sstream>
#include <string>
using std::cout;
using std::endl;
using std::string;
using std::stringstream;

void f1() {
    int a = 12345678;
    string sa;
    // 将一个整型变量a转化为字符串,存储到string类对象中
    stringstream s;
    s << a;
    s >> sa;
    cout << sa << endl;
}

void f2() {
    // f1()的逻辑
    int a = 12345678;
    string sa;
    stringstream s;
    s << a;
    s >> sa;
    s.str("");
    // 将stringstream底层管理string对象设置成 "",避免结果堆积
    s.clear();
    // 注意多次转换时,必须使用clear将上次转换状态清空掉
    // 清空s, 不清空会转化失败
    double d = 12.34;
    s << d;
    s >> sa;
    cout << sa << endl; // 不做处理
    string sValue;
    sValue = s.str();
    // str()方法:返回stringsteam中管理的string类型
    cout << sValue << endl;
}

void f3() {
    int a = 12345678;
    string sa, sa2;
    stringstream s;
    s << a;
    s >> sa;
    cout << sa << endl; // 用完后不做处理
    // s.str("");
    // s.clear();
    double d = 1145.14;
    s << d;
    s >> sa2;
    cout << sa2 << endl; // 读取不到信息
    string sValue;
    sValue = s.str();
    cout << sValue << endl;
}

void f4() {
    int a = 12345678;
    string sa;
    stringstream s;
    s << a;
    s >> sa;
    cout << sa << endl;
    // s.str("");
    s.clear(); // 只清空
    double d = 1145.14;
    s << d;
    s >> sa;
    cout << sa << endl;
    string sValue;
    sValue = s.str();
    // 这里可以看到很明显的堆积
    cout << sValue << endl;
}

int main() {
    // f1(); // 简单的数字转换成字符串
    // f2(); // 多次转换
    // f3(); // 演示不处理stringstream的情况
    f4(); // 演示不使用空字符串覆盖,只清空的情况
    return 0;
}

4 个子程序分别输出:

复制代码
// f1
12345678
// f2
12.34
12.34
// f3
12345678

12345678
// f4
12345678
1145.14
123456781145.14

字符串拼接

cpp 复制代码
#include <iostream>
#include <sstream>
#include <string>
using std::cout;
using std::endl;
using std::string;
using std::stringstream;

int main() {
    stringstream sstream;
    // 将多个字符串放入 sstream 中
    sstream << "Hello," << " " << "world";
    sstream << ".\n";
    cout << "resoult: " << sstream.str() << endl;
    // 清空 sstream
    sstream.str("");
    sstream << "what up!";
    cout << "resoult: " << sstream.str() << endl;
    return 0;
}

输出:

复制代码
resoult: Hello, world.

resoult: what up!

这里第 1 次拼接有加 \n ,第 2 次没有。所以第 2 次输出没有换行。

序列化和反序列化

简单理解就是把各种信息转换成字符串 的过程叫序列化把字符串里面的信息提取出来再转换成内存的数据结构 ,这个过程叫做反序列化

实践当中的序列化和反序列化更多的是用于网络当中。网络可以将多个计算机抽象成一个对象,这些对象需要建立连接进行数据通信,通信的格式是 JSON 数据。JSON 数据由大量键值对组成,将这些键值对翻译成程序可处理的信息叫反序列化,反之将程序内的数据转换成 JSON 数据叫序列化,JSON 格式数据尽管由多个键值对组成,但整体还是一个字符串。

一般情况下 C++ 标准库都不带 JSON 库,都是用的第三方库,使用时还需要安装。详细见网络编程部分。

相关推荐
乐观勇敢坚强的老彭1 小时前
C++ STL 常用容器的速查表
java·c++·算法
北冥you鱼1 小时前
深入解析 Go 中 sync.YAML.Unmarshal:如何将 YAML 数据填充到 Config 结构体
开发语言·算法·golang
goldbin_zhang_xu1 小时前
我用 Go 重写了一个 OpenClaw 框架:这就是 GoClaw
开发语言·后端·golang·agent框架·goclaw·双循环机制
lingran__1 小时前
C++ STL map 与 set 底层剖析与模拟实现万字详解|基于红黑树,复刻 SGI-STL 泛型复用架构
开发语言·c++·stl·set·map·泛型编程·sgi-stl
野生技术架构师1 小时前
Multi-agent的新趋势,从Agent Team到Agent Swarm
开发语言·人工智能
Bonnie_12151 小时前
10-深入理解ConcurrentHashMap(JDK1.8)
java·开发语言
宿6742 小时前
vue3-指令和事件处理
开发语言·前端·javascript
星轨初途2 小时前
LeetCode 热题 100——day11 滑动窗口最大值
数据结构·c++·算法·leetcode·职场和发展
重生之小比特2 小时前
【初阶C++】内存管理
c++·内容运营