C++的 IO 流
目录
1. C语言的输入输出
C语言中我们用到的最频繁的输入输出方式就是scanf ()与printf()。 scanf(): 从标准输入设备(键盘)读取数据,并将值存放在变量中。printf(): 将指定的文字/字符串输出到标准输出设备(屏幕)。注意宽度输出和精度输出控制。C语言借助了相应的缓冲区来进行输入与输出。
对输入输出缓冲区的理解:
- 可以屏蔽掉低级I/O的实现,低级I/O的实现依赖操作系统本身内核的实现,所以如果能够屏蔽这部分的差异,可以很容易写出可移植的程序。
- 可以使用这部分的内容实现“行”读取的行为,对于计算机而言是没有“行”这个概念,有了这部分,就可以定义“行”的概念,然后解析缓冲区的内容,返回一个“行”。
2. 流是什么
“流”即是流动的意思,是物质从一处向另一处流动的过程,是对一种有序连续且具有方向性的数据( 其单位可以是bit,byte,packet )的抽象描述。
C++流是指信息从外部输入设备(如键盘)向计算机内部(如内存)输入和从内存向外部输出设备(显示器)输出的过程。这种输入输出的过程被形象的比喻为“流”。
它的特性是:有序连续、具有方向性
为了实现这种流动,C++定义了I/O标准类库,这些每个类都称为流/流类,用以完成某方面的功能
3. C++IO流
C++系统实现了一个庞大的类库,其中ios为基类,其他类都是直接或间接派生自ios类

3.1 C++标准IO流
C++标准库提供了4个全局流对象cin、cout、cerr、clog,使用cout进行标准输出,即数据从内存流向控制台(显示器)。使用cin进行标准输入即数据通过键盘输入到程序中,同时C++标准库还提供了cerr用来进行标准错误的输出,以及clog进行日志的输出,从上图可以看出,cout、cerr、clog是ostream类的三个不同的对象,因此这三个对象现在基本没有区别,只是应用场景不同。
在使用时候必须要包含文件并引入std标准命名空间。注意:
- cin为缓冲流。键盘输入的数据保存在缓冲区中,当要提取时,是从缓冲区中拿。如果一次输入过多,会留在那儿慢慢用,如果输入错了,必须在回车之前修改,如果回车键按下就无法挽回了。只有把输入缓冲区中的数据取完后,才要求输入新的数据。
- 输入的数据类型必须与要提取的数据类型一致,否则出错。出错只是在流的状态字state中对位置位(置1),程序继续。
- 空格和回车都可以作为数据之间的分格符,所以多个数据可以在一行输入,也可以分行输入。但如果是字符型和字符串,则空格(ASCII码为32)无法用cin输入,字符串中也不能有空格。回车符也无法读入。
- cin 和cout 可以直接输入和输出内置类型数据,原因:标准库已经将所有内置类型的输入和输出全部重载了,例如:

- 对于自定义类型,如果要支持cin和cout的标准输入输出,需要对<<和>>进行重载。
在线 OJ 中的输入和输出:
// 单个元素循环输入
while(cin>>a)
{
// ...
}
// 多个元素循环输入
while(c>>a>>b>>c)
{
// ...
}
// 整行接收
while(cin>>str)
{
// ...
}
ctrl + c 暴力结束程序,ctrl + z + \n 可以结束 while(cin>>s)
int main()
{
string str;
//istream& operator>>(istream& is, string& str);
while (cin >> str)//库中的string自定义类型
{
cout << str << endl;
}
return 0;
}
cin >> str 就是 operator>>(cin, str)
上面代码会调用,是 string 的流插入运算符重载,

那么下面方法也可以实现
int main()
{
string str;
while (operator>>(cin,str))
{
cout << str << endl;
}
return 0;
}
为什么 operator>>(cin,str) 返回的 istream类型可以给while判断,这里发生了隐式类型转换。
类型转换
- 内置类型之间,相近类型可以转换
- 没有关联的类型,可以通过构造函数转换,实现内置类型 -> 自定义类型 或 自定义类型-> 自定义类型
- operator type()
这里我们介绍一下 operator type() ,可以实现自定义类型 -> 内置类型的转换。while(cin>>str) 就是这个方法。
仿函数是这样写的:
class A{
bool operator()
{}
};
要把 istream 类型转换为 bool 类型,C++11库中提供了重载类型转换这样的方法。

class A{
operator bool()
{
//条件 ctrl + z + \n 返回false
return true;
}
};
int main()
{
//bool 整形 指针
//istream 可以隐式类型转换为bool
A a;
bool i = a;
cout << i << endl;
return 0;
}
同理把 A 换成 isteam 就可以实现,括号内不需要有参数。
sync_with_stdio 是 C++ 标准库中 std::ios_base 类的静态成员函数,用于同步 C++ 的输入输出流(cin、cout、cerr、clog、wcin 等)与 C 标准 I / O 函数(printf、scanf、getchar、puts 等)。
主要作用
- 默认情况下(sync_with_stdio(true)):C++ 流与 C 标准 I / O 共享同一个底层缓冲区,cin / cout 和 scanf / printf 可以混合使用,且顺序符合预期(不会出现数据错乱)。
- 关闭同步(sync_with_stdio(false)):C++ 流将拥有独立的缓冲区,不再与 C 标准 I / O 同步。这样能大幅提升 cin / cout 的性能,但不能混用 printf / scanf 等 C 风格 I / O,否则可能导致未定义行为。
-
开同步(默认):安全但慢,允许混用
cin/cout与scanf/printf。 -
关同步:快但危险,不能混用两种 I/O 风格。
int main()
{
int i = 0;
int j = 0;
cin >> i;
scanf("%d", &j); //兼容
//调用 sync_with_stdio(false) 可以取消c与c++的兼容性,速度会变快
cin.sync_with_stdio(false);
cout.sync_with_stdio(false);
cout << i << endl;
cout << j << endl;
return 0;
}
3.2 C++文件IO流
C++根据文件内容的数据格式分为二进制文件和文本文件。采用文件流对象操作文件的一般步
骤:
- 定义一个文件流对象 ifstream ifile(只输入用) ofstream ofile(只输出用) fstream iofile(既输入又输出用)
- 使用文件流对象的成员函数打开一个磁盘文件,使得文件流对象和磁盘文件之间建立联系
- 使用提取和插入运算符对文件进行读写操作,或使用成员函数进行读写
- 关闭文件

这里箭头指的是继承关系,可以看出这里有一个菱形继承,所以 fstream 有 ifstream 和 ofstream 的功能,stringstream 有 istringstream 和 ostringstream 的功能。
C++ 中 fprintf 和 fscanf只能针对内置类型,>> 能更好的应对内置类型和自定义类型。
我们使用 Data 类 和 ServerInfo 来介绍如何写入和读出文件。
class Date
{
public:
Date(int y= 1, int m = 1, int d = 1)
:_year(y)
, _month(m)
, _day(d) {
};
friend ostream& operator<<(ostream& out, const Date& date);
friend istream& operator>>(istream& in, Date& data);
private:
int _year;
int _month;
int _day;
};
//ofs << winfo._date
ostream& operator<<(ostream& out,const Date& date)
{
out << date._year << " " << date._month << " " << date._day;
return out;
}
istream& operator>>(istream& in, Date& date)
{
in >> date._year >> date._month >> date._day;
return in;
}
struct ServerInfo
{
//char _address[32];
string _address;//会有问题,后面讲解
double _x ;
Date _date;
};
out 和 in 是相对内存来说的,所以ofstream就是写入文件,ifstream就是读出方法,下面是参数定义:

第一个参数是文件名,第二个参数是写入读取方式,在父类中定义的,由于继承关系,子类也能调用,这些方式分别使用不同的二进制,所以可以按位或到一起。 binary 二进制方式。

ate (at end) | 打开后立即将文件位置指针定位到末尾 | - 初始读写位置在文件尾 - 可以通过 seek 函数重新定位到文件中间或开头- 不影响文件原有内容 |
app (append) | 每次输出操作前自动将位置指针移到末尾 | - 所有写入都追加在文件末尾 - 通常无法通过 seek 改变写入位置(标准规定写入前总是移到末尾)- 读取操作可能受限(取决于是否同时指定 in) |
trunc (truncate) | 打开文件时清空其原有内容 | - 若文件已存在,其内容被丢弃,长度变为 0 - 通常与 out 结合使用(如 out | trunc)- 不影响位置指针的初始位置(一般是开头) |
3.2.1 二进制读写
class BinIO
{
public:
BinIO(const char* filename = "info.bin")
:_filename(filename) {
}
//winfo是文件内容
void Write(const ServerInfo& winfo)
{
//创建一个写入对象,ofstream 是一个类,可以不用主动调用close
ofstream ofs(_filename,ofstream::out|ofstream::binary);
//因为需要的是char*类型的指针 写入
ofs.write((char*)&winfo, sizeof(winfo));
}
void Read(ServerInfo& rinfo)
{
ifstream ifs(_filename, ifstream::in | ifstream::binary);//读对象
ifs.read((char*) & rinfo, sizeof(rinfo));//读
}
private:
string _filename;
};
读写操作:
写
int main()
{
ServerInfo winfo = { "192.0.0.1",12.13,{2022,4,10} };
BinIO bin;
bin.Write(winfo);
return 0;
}
二进制方式写数据,读写文件使用容器需要注意:string VS2022 下有buffer,短的可以存,如果string内容过长,会在堆区开辟空间,会再存一个地址, 不同进程中这个main 执行完string中的_ptr 释放, 之后其他进程再读就会找不到内容,是野指针,发生了浅拷贝,同一进程不会有问题。
读
int main()
{
ServerInfo rinfo;
BinIO bin;
bin.Read(rinfo);
cout << rinfo._address << endl;
cout << rinfo._date << endl;
cout << rinfo._x << endl;
return 0;
}
3.2.2 文本读写
文本读写优点是都是字符串可以看懂,但缺点是需要转换,C++提供了转为为字符串的函数:

但一个个调用还是太麻烦了,C++已经提供了流插入提取可以直接使用。
class TxtIO
{
public:
TxtIO(const char* filename = "info.txt")
:_filename(filename) {
}
void Write(const ServerInfo& winfo)
{
// ofstream::out 不用写了,因为有缺省值,//创建一个写入对象 ,不用加二进制
ofstream ofs(_filename);
//会调用 自定义类型 的operator << 因为 ofstream 继承 ostream
ofs << winfo._address << endl;//通过流插入提取
ofs << winfo._date << endl;
ofs << winfo._x << endl;
}
void Read(ServerInfo& rinfo)
{
// ifstream::in 读对象
ifstream ifs(_filename);
//流提取默认 空格 换行 是 分割 , 有也有getline获取,定义分割符
ifs >> rinfo._address;
ifs >> rinfo._date;
ifs >> rinfo._x;
}
private:
string _filename;
};
我们可以直接使用提供的流方式读写,但是要注意空格和换行是分隔符。
4.stringstream简单介绍
在C语言中,如果想要将一个整形变量的数据转化为字符串格式,如何去做?
- 使用 itoa() 函数
- 使用 sprintf() 函数
但是两个函数在转化时,都得需要先给出保存结果的空间,那空间要给多大呢,就不太好界定,而且转化格式不匹配时,可能还会得到错误的结果甚至程序崩溃。
int main()
{
int n = 123456789;
char s1[32];
_itoa(n, s1, 10);
char s2[32];
sprintf(s2, "%d", n);
char s3[32];
sprintf(s3, "%f", n);//格式不匹配
return 0;
}
下图是ostringstream,istringstream也大致相同:

在C++中,可以使用stringstream类对象来避开此问题。
在程序中如果想要使用stringstream,必须要包含头文件 <sstream>。在该头文件下,标准库三个类:istringstream、ostringstream 和 stringstream,分别用来进行流的输入、输出和输入输出操作,本文主要介绍stringstream。
stringstream主要可以用来:
- 将数值类型数据格式化为字符串
- 字符串拼接
- 序列化和反序列化结构数据
序列化:信息转换为字符串。反序列化:字符串转换为信息
struct ChatInfo
{
string _name;
int _id;
Date _date;
string _msg;
};
stringstream 有 istringstream 和 ostringstream的功能
int main()
{
ChatInfo winfo = { "张三",2321,{2026,5,9},"晚上一起看电影" };
stringstream oss;
oss << winfo._name << endl;
oss << winfo._id << endl;
oss << winfo._date << endl;
oss << winfo._msg << endl;
string str = oss.str();
cout << str << endl;
ChatInfo rinfo;
stringstream iss(str);
iss >> rinfo._name >> rinfo._id >> rinfo._date >> rinfo._msg;
cout << "-------------------------------------------------------"
<< endl;
cout << "姓名:" << rinfo._name << "(" << rinfo._id << ") ";
cout << rinfo._date << endl;
cout << rinfo._name << ":> " << rinfo._msg << endl;
cout << "-------------------------------------------------------"
<< endl;
return 0;
}

注意:
- stringstream实际是在其底层维护了一个string类型的对象用来保存结果。
- 多次数据类型转化时,一定要用clear()来清空,才能正确转化,但clear()不会将stringstream底层的string对象清空。
- 可以使用s. str("")方法将底层string对象设置为""空字符串。
- 可以使用s.str()将让stringstream返回其底层的string对象。
- stringstream使用string类对象代替字符数组,可以避免缓冲区溢出的危险,而且其会对参数类型进行推演,不需要格式化控制,也不会出现格式化失败的风险,因此使用更方便,更安全
#include <sstream>
#include <iostream>
using namespace std;
int main() {
stringstream ss;
int num;
// 1. 第一次转换:将字符串 "123" 转为 int
ss << "123";
ss >> num; // 读取成功,num = 123
cout << "第一次 num: " << num << endl;
// 2. 第一次转换后,流指针已经到了末尾,触发了 eofbit(状态变坏)
// 此时如果直接塞入新数据,下面这行是无效的!
ss << "456"; // ❌ 因为流状态是坏的,这行写入失败!
// 3. 很多人会立刻调用 clear() 来恢复状态
ss.clear(); // ✅ 现在状态恢复正常(goodbit)
ss >> num; // 再次尝试读取,猜猜 num 是多少?
cout << "第二次 num: " << num << endl;
// 输出结果:第二次 num: 123 (震惊!因为底层还存着旧的 "123456"!)
return 0;
}
本篇结束!
更多推荐
所有评论(0)