zoukankan      html  css  js  c++  java
  • 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
    ";
        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()

    从输入流中获得一行数据填充字符缓存区,数据量最多至指定大小。指定的大小中包括字符,(即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个位置:

    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库。掌握流或类流库的思想才是重要的。

  • 相关阅读:
    java线程读取文件,可以同时读写 202006031002
    JS,JQuery bug202005282020
    js,jquery缩小加载的图片202005131907
    spring boot/spring cloud + mybatis + mysql bug 20200513
    html2020042901,table元素之间的间距
    ie8-ie11浏览器bug2020042801
    css的bug2020042801
    xml读取解析bug20200423
    Poi读取word(doc)文档的文本或图片
    NOI2020专题
  • 原文地址:https://www.cnblogs.com/ranbom/p/12777581.html
Copyright © 2011-2022 走看看