最近没钱租服务器了,好矛盾QAQ。用linux开游戏服务器的话不能用QQ机器人来搞事情(好想拿协议搞个linux的qq机器人啊,但我不会orz),但windows开服又太占内存了orz穷人不配。这次来学习一下C++ I/O流相关。
原帖地址: https://www.cnblogs.com/ranbom/
博主LeoRanbom
只在原帖地址的博客上发布,其他地方看到均为爬取。
如果觉得不错希望来点个赞。
简要说明
流可以看做为数据滑槽。来源和目的地随着流的方向不同而不同。如下为预定义的所有流
- cin——输入流,从“输入控制台”读取数据。
- cout——输出流,向“输出控制台”写入数据。
- cerr——非缓存的输出流,向“错误控制台”写入数据,“错误控制台”通常等同于”输出控制台“
- clog——cerr的缓冲版本。
缓冲是指,输入的数据会存放在缓冲区,然后以块的形式一次性发送。
非缓冲则是读入数据后,立刻将数据发送到目的地。
缓存在一次性写入较大数据时能够更快(比如文件),以达到提高性能的目的。
注意:随时可以用flush()方法来刷新缓冲区,要求缓冲区的流立刻将其中数据发送至目的地。
以上4个流还存在对应的宽字符版本:wcin,wcout,wcerr,wclog。
不过图形界面一般没有控制台,比如QT,它跟用户交互的方式是创建相应的对象,并且用对象的改变文本的方法来进行交互。所以要注意,不要再任何地方假定存在上述几个I/O流。
流中不仅包含普通数据,还包含称为当前位置(current position)的特殊数据。当前位置指的是流将要进行下一次读写操作的位置。
来源和目的地
在C++中,流可使用3个公共的来源和目的地:控制台、文件和字符串。流可应用于任何接收数据或生成数据的对象,因此可以编写基于流的网络类或者MIDI设备的流式访问类。
此节笔记内容主要记述控制台流,因为其他流往往和平台相关。
输出流
基本概念
输出流定义在ostream头文件中,往往直接用iostream。iostream声明了所有预定义的流实例。
cout <<返回一个流的引用。因此可以继续对同一个流使用<<运算符,以达到串联的目的。
可以解析C风格的转义字符,也可以使用std::endl换行。endl在换行之外,还会刷新缓存区,因此使用时应该小心,不然过多的刷新会导致性能降低。
其他方法
<<运算符是输出流中最常用的东西,但ostream头文件中可用看到还有其他一些有用的公有方法。
put()和write()
这两个方法是原始的输出方法。put()接收单个字符,write()则接收一个字符数组。
#include<iostream>
using namespace std;
int main() {
const char* arr = "hello world
";
cout.write(arr, strlen(arr));
cout.put('a');
}
输出为
hello world
a
flush()
向输出流写入数据是,流不一定会将数据立即写入目的地,大部分输出流都会进行缓冲(也就是积累数据)。在以下任意一种条件下,流将刷新(或写出)积累的数据:
- 遇到sentinel(如endl标记)时
- 流离开作用域被析构时
- 要求从对应的输入流输入数据是(即要求从cin输入时,cout会刷新)。后续学习文件流时会继续阐述这种对应方式是如何产生的。
- 流缓存满时。
- 显式要求流刷新缓存时。
显式要求流刷新缓存的方式是调用流的flush()方法。
int main() {
cout << "abc";
cout.flush();
cout << "def";
cout << endl;
}
不过这里运行后没有啥直观的感受。
处理输出错误
输出错误可能会出现在多种情况下,虽然一开始学习可能几乎不会遇到。比如说:试图打开一个不存在的文件;磁盘错误导致写入失败(磁盘已满)、
当一个流处于正常的可以状态时,它是“好的”。调用流的good()方法可以来判断这个流是否处于正常状态。
if (cout.good()) {
cout << "All good";
}
通过good()方法可方便地获得流的基本验证信息,但不能提供流不可用的原因。bad()方法则提供了稍多信息。如果bad()返回true,则意味着发生了致命错误(到达文件结尾这种算非致命错误。)。fail()方法则在最近一次操作失败是返回true,但未说明下一次操作是否也失败。例如,对输出流调用flush()后,来调用fail()来确保流仍然可以用。
流具有可以转换成bool类型的转换运算符!。它的结果与!fail()的返回结果相同,
即!cout 可以代替cout.fail()。
遇到文件结束标记时,good()和fail()都会返回false。其关系为good() == (!fail()&&!eof())。
同时可以要求流在发生故障时抛出异常。然后编写一个catch程序来捕捉ios_base::failure异常,然后对这个failure异常调用what()方法,获得错误的描述信息,调用code()方法来获得错误代码。(信息是否有用取决于所使用的标准库)
cout.exceptions(ios::failbit |ios::badbit|ios::eofbit);
try(
cout << "Hello World" << endl;
)catch (const ios_base::failure& ex){
cerr << "Caught exception: " << ex.what()
<< ", err code = " << ex.code() << endl;
}
通过clear方法可以重置流的错误状态——cout.clear();
注:控制台输出流错误检查不如文件输入输出流错误检查频繁。
输出操作算子
流的一项独特特性是,放入数据滑槽的内容并不仅限于数据,还可以识别操作算子(manipulator)。操作算子是能修改流行为的对象,而不是数据。
endl就是一个操作算子——封装了数据和行为。它要求流输出一个行结束序列,并且刷新缓存。
以下列举的操作算子大部分定义在ios 和iomanip标准头文件中。
- boolalpha 和noboolalpha:前者要求流将布尔值输出为true和false,后者则输出1和0.默认是noboolalpha
- hex,oct,dec:分别以十六进制、八进制、十进制输出数字
- setprecision:设置输出小数时的小数位数。(参数化的操作算子)
- setw:设置输出数值数据的字段宽度。同参数化。
- setfill:当数字宽度小于指定宽度时,用于填充的字符,参数为一个字符。
- showpoint和noshowpoint:对于不带小数部分的浮点数,强制流总是显示或不显示小数点。
- put_money:一个参数化的操作算子,向流写入一个格式化的货币值。
- put_time:一个参数化的操作算子,向流写入一个格式化的时间值。
- quoted:一个参数化的操作算子,把给定的字符串封装在引号中,并转义嵌入的引号。
上述操作算子对后续输出到流中的内容有效,直到重置操作算子为止。但setw仅对下一个输出有效。
在put_time时会用到localtime(),应该用安全的localtime_s(),linux中通常使用localtime_r()。
cout<<setprecision(2)可以转换成cout.precision(2)。
流式输入
类似于<<,可以通过>>从输入流读取数据,代码提供的变量会保存接收的值。>>会根据空白字符对输入的值进行标识化(如果想读入带空格的值,应该用get())。
通过cin可以立即刷新cout的缓存区。
处理输入错误
大部分和输入流有关的错误都发生在无数据可读的情况下,例如,可能到流尾(称为文件末尾,即使不是文件流)。查询输入流状态的最常见方法是在条件语句中访问输入流。例如,当cin保持在“良好”的状态时以下循环会持续进行:while(cin){...}
同时还能输入数据——while(cin >> ch){...}
还能像输出流一样用good(),bad(),fail()方法。
eof()方法在流到达尾部时返回true。与输出流类似,在遇到文件结束编辑室,good和fail都会返回false。
关系为 good() == (!fail()&&!eof())。
同时应该养成在读取数据后检查流状态的习惯,这样可以从异常输入中回复。
(clear()方法重置流)
Unix和linux中,用Control + D键入特殊字符来表示文件结束,而windows则是ctrl + Z。
其他方法
就像输出流一样,输入流也提供了一些方法,可以获得比>>更底层的访问功能。
get()
允许从流中读入原始输入数据。get()的最简单版本返回流中的下一个字符,其他版本一次读入多个字符。get()方法常用语避免>>的自动标志化。(也就是可以读入空格,多个单词)。
string readName(istream& stream)
{
string name;
while(stream){//or while(!stream.fail())
int next = stream.get();
if(!stream || next == std::char_traits<char>::eof())
break;
name += static_cast<char> (next);
}
return name;
}
这个函数的参数是对istream的非const引用。是因为它会改变流(主要是改变位置)。
还有就是这个函数内部get()方法的返回值是int型,而不是char型(看到这里我迷茫了几秒,这个假期我们C语言老师在补课班教其他人的时候问我scanf的原理,她当时说的是char型),原因是它会接收一些特殊的非字符值。比如std::char_traits
还有更常用的另一个版本的get(),它只接收一个字符的引用,并返回一个流的引用。
string readName(istream& stream){
string name;
char next;
while(stream.get(next)){
name += next;
}
return name;
}
unget()
大多数情况下,输入流是数据丢入滑槽,然后再塞入变量。但是unget却反过来了——将数据塞回滑槽。
它会让流回退一个位置,将读入的前一个字符放回流中。调用fail()方法可以查看unget()方法是否成功——如果当前位置已经是流的开头起始位置,那么就会失败。
noskipws操作算子告知流不要跳过空白字符,就像读取其他任何字符一样读取空白字符。
putback()
putback()和unget()一样,允许输入流反向移动1个字符,但是putback会将放回流中的字符接收为参数:
char ch1,ch2;
cin >> ch1;
cin.putback('e');
cin >> ch2;//ch2就会读入e字符
//'e' will be the next character read off the stream
peek()
通过peek()方法可以预览调用get()后返回的下一个值。——适用于预先查看一个值的场合。
getline()
从输入流中获得一行数据填充字符缓存区,数据量最多至指定大小。指定的大小中包括 字符,(即cin.getline(buffer, kBufferSize)最多读入kBufferSize-1个字符)。
有些版本的get()和getline()操作一样,区别在于get()会把换行序列留在输入流中。
还有一个用于C++字符串的std::getline()函数,它接收一个流引用、一个字符串引用和一个可选的分隔符作为参数。它的优点是不需要指定缓存区的大小。
string myString;
std::getline(cin, myString);
输入操作算子
- boolalpha和noboolalpha:前者字符串false会解析为布尔值false,后者0会解析为false,其他数都会解析为true。
- hex、oct、dec:分别以十六进制、八进制和十进制读入数字
- skipws和noskipws:告诉输入流在标记化时跳过或读入空白字符作为标记。默认skipws。
- ws:跳过流中当前位置的一串空白字符。
- get_money:参数化,读入格式化的货比值。
- get_time:参数化,读入格式化的时间值。
- quoted:参数化,读取封装在引号的字符串,并转义嵌入的引号
输入同样支持本地化。
对象的输入输出
重载<<和>>即可让其理解新的类型或者类。
(在类中定义一个output方法也可以,不过太笨拙了,毕竟无论啥方法都不如一个<<来的简便)
字符串流
将流语义用于字符串。GUI程序中,字符串流可能将文本显示在GUI元素中,而不是控制台或文件。同时字符串流也可以作为参数传递给不同的函数,维护当前读取位置。因为内建了标记化给你,它也非常适用于解析文本。
std::ostringstream类用于将数据写入字符串,std::istringstream则从字符串中读出数据。
它们两个都在sstream头文件中。
cout << "Enter tokens. Control + D(unix) or ctrl +Z(windows)to end" << endl;
ostringstream outStream;
while(cin){
string nextToken;
cout << "Next token: ";
cin >> nextToken;
if(!cin||nextToken == "done") break;
outStream << nextToken << " ";
}
cout << "The end result is: "<<outStream.str();
输入I love u done,则反馈为I love u。
同时可通过stream>>来给对象成员正确读入。
注:将对象转换为“扁平”类型(例如字符串)的过程通常称为编码(marshall),将对象保存至磁盘或通过网络发送时,编组操作非常有用!
相对于C++标准字符串,字符串流的优点是除了数据之外,这个对象还知道哪里进行下一次读或写的操作,也就是当前位置。还有就是支持操作算子和本地化。
文件流
文件本身很适合流,因为它除了数据外, 也要设计读写位置。std::ofstream和std::ifstream类在fstream头文件中。
输出文件流和其他输出流的主要区别在于:文件流的构造函数可以接收文件名以及打开文件的模式作为参数。
默认模式是写文件(ios_base::out),这种模式从文件开头写文件,改写任何已有的数据。给文件流构造函数的第二个参数指定常量ios_base:app。还可按追加模式打开输出文件流:
- ios_base:app——打开文件,在每一次写操作之前,移到文件末尾
- ios_base::ate——打开文件,打开之后立即移到文件末尾
- ios_base::binary——以二进制模式执行输入输出操作(相对于文本模式)
- ios_base::in——打开文件,从开头开始读取
- ios_base::out——打开文件,从开头开始写入,覆盖已有的数据。
- ios_base::trunc——打开文件,并删除(截断)任何已有的数据。
可以通过|来组合模式
ifstream类自动包含in模式,ofstream自动包含out模式。
它们两个析构函数会自动关闭底层文件,因此不需要像c一样调用close()方法。
文本模式与二进制模式
默认情况下文件流以 文本模式 打开。
文本模式下,会执行一些隐式转换。写入文件或读取的每一行都以 结束。但是,行结束符在文件中的编码与操作系统有关,win下,它是 。因此如果写入行以 结尾,底层实现会自动将其转换为 。同样,读取的时候 会自动转移回 。
通过seek()和tell()在文件中转移
所有的输入流和输出流都有seek()和tell()。
seek()允许移动到流的任意位置。输入流的seek()版本称为seekg(),输出流则为seekp()。而文件流既可以输入,也可以输出,所以要分别记住读位置和写位置,这称为双向IO。
seekg()和seekp()有两个重载版本,一个接收绝对位置,另一个接收 位置和偏移量。
位置类型为std::streampos,偏移量为std::streamoff。他们都以字节计数。
预定义有3个位置:
- ios_base::beg——表示流的开头
- ios_base::end——表示流的结尾
- ios_base::cur——表示流的当前位置
在2个参数的版本,整数会被隐式转换为streampos和streamoff类型。
可以通过tell()来查询流的当前位置。它同样有p和g两个版本。
将流链接在一起
输入输出流建立链接,实现“访问时刷新”的功能。从输入流请求数据时,链接的输出流也会自动刷新。对于互相依赖的文件流来说 特别有用,但它适用于所有流。
通过tie()方法完成流链接,若要将输出流链接至输入流,则对输入流调用tie()方法,传入输出流的地址。传入nullpttr即解除链接。
flush()方法在ostream基类定义,所以可以将输出流链接到另一个输出流,来达成2个文件同步的目的——写入一个文件,发送给另一个文件的缓存数据都会被刷新。
(cin和cout,cerr和cout都存在链接。clog则不链接。它们的宽版本也相应地有链接)[之前玩游戏有想搞跨服同步,这块的知识应该可以用上√]
双向I/O
双向流时iostream的子类,iostream则是istream和ostream的子类(多重继承)。所以双向流可以用<<,>>和输入输出流的方法。
fstream提供了双向文件流适用于需要替换文件中数据的应用程序。但为了实时保存,应该建一个映射。但如果数据集过于庞大,无法全部保存在内存中。但如果使用iostream,则不需要这样做,可以直接扫描文件,找到记录。然后以追加模式打开输出文件,从而添加新的记录。
不过只有在数据大小固定时,才能用它正常工作。
总结
此次学习最重要的内容是流的概念。因为不同系统或者其他软件可能有自己的文件访问或者IO库。掌握流或类流库的思想才是重要的。