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

  • 相关阅读:
    CSS浮动(float、clear)通俗讲解
    JAVA 类的加载
    数据库操作 delete和truncate的区别
    正则表达式 匹配相同数字
    Oracle EBS OM 取消订单
    Oracle EBS OM 取消订单行
    Oracle EBS OM 已存在的OM订单增加物料
    Oracle EBS OM 创建订单
    Oracle EBS INV 创建物料搬运单头
    Oracle EBS INV 创建物料搬运单
  • 原文地址:https://www.cnblogs.com/ranbom/p/12777581.html
Copyright © 2020-2023  润新知