C++学习(c++17)——IO流

最近没钱租服务器了,好矛盾QAQ。用linux开游戏服务器的话不能用QQ机器人来搞事情(好想拿协议搞个linux的qq机器人啊,但我不会orz),但windows开服又太占内存了orz穷人不配。这次来学习一下C++ I/O流相关。



LeoRanbom的博客

原帖地址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\n";
    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::eof()。

还有更常用的另一个版本的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()

从输入流中获得一行数据填充字符缓存区,数据量最多至指定大小。指定的大小中包括\0字符,(即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 << "\t";
}
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()方法。

文本模式与二进制模式

默认情况下文件流以 文本模式 打开。

文本模式下,会执行一些隐式转换。写入文件或读取的每一行都以\n结束。但是,行结束符在文件中的编码与操作系统有关,win下,它是\r\n。因此如果写入行以\n结尾,底层实现会自动将其转换为\r\n。同样,读取的时候\r\n会自动转移回\n。

通过seek()和tell()在文件中转移

所有的输入流和输出流都有seek()和tell()。

seek()允许移动到流的任意位置。输入流的seek()版本称为seekg(),输出流则为seekp()。而文件流既可以输入,也可以输出,所以要分别记住读位置和写位置,这称为双向IO。

seekg()和seekp()有两个重载版本,一个接收绝对位置,另一个接收 位置和偏移量。

位置类型为std::streampos,偏移量为std::streamoff。他们都以字节计数。

预定义有3个位置:

  1. ios_base::beg——表示流的开头
  2. ios_base::end——表示流的结尾
  3. 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库。掌握流或类流库的思想才是重要的。

posted @ 2020-04-26 10:28  LeoRanbom  阅读(382)  评论(1编辑  收藏  举报