C++基础与深度解析 | 输入与输出 | 文件与内存操作 | 流的状态、定位与同步

文章目录

一、IOStream概述

  • IOStream 采用流式 I/O 而非记录 I/O ,但可以在此基础上引入结构信息。

    C++中的IOStream库确实采用了流式I/O,而不是记录I/O。流式I/O是一种连续的、基于字符的I/O方式,它允许数据以一种连续的流的形式进行读写。这种方式与记录I/O相比,更加灵活,因为数据可以以任何顺序被读取或写入,而不需要事先知道数据的确切结构。

    在C++中,流式I/O是通过iostream库实现的,它提供了一系列的类和函数来处理输入和输出。这些类包括iostreamistreamostreamstringstream等,它们都继承自ios_base类,提供了丰富的功能来处理流。

    虽然流式I/O是连续的,但是C++的IOStream库也允许引入结构信息,以便于处理更复杂的数据结构。这可以通过以下几种方式实现:

    1. 操作符重载 :通过重载输入(>>)和输出(<<)操作符,可以定义自定义数据类型的输入输出方式。
    2. 格式化输出 :使用iostream库中的格式化操作,如setwsetprecision等,可以控制输出的格式。
    3. 序列化:对于复杂的数据结构,如类和对象,可以通过序列化的方式将其转换为可以流式传输的格式。这通常涉及到将数据结构分解为基本数据类型,然后逐个写入流中。
    4. 模板:使用模板,可以创建通用的I/O操作,这些操作可以适用于不同的数据类型。
    5. iostream操纵符 :C++标准库提供了一些操纵符,如endlsetw等,它们可以改变流的状态或输出格式。
    6. 自定义流缓冲区:通过自定义流缓冲区,可以控制流的读写行为,引入更多的结构信息。
  • IOStream所处理的两个主要问题

    • 表示形式的变化:使用格式化 / 解析在数据的内部表示与字符序列间进行转换

      C++的IOStream库提供了格式化和解析的机制,使得数据可以在其内部表示(如整数、浮点数、对象等)和字符序列(如字符串、字符数组等)之间进行转换。

      • 格式化

        指的是将数据转换为一种特定的字符序列格式,以便于显示或存储。例如,一个整数可以被格式化为一个带有特定宽度和填充字符的字符串。

      • 解析

        指的是格式化的逆过程,它涉及将字符序列转换回其内部表示。例如,从字符串中解析出一个整数或浮点数。

      C++的IOStream库提供了多种格式化和解析的功能,包括但不限于:

      • 使用iostream中的操纵符,如setwsetprecisionfixedscientific等,来控制数值的显示格式。
      • 使用iostream中的格式化标志,如showbaseshowpointuppercase等,来改变数值的显示方式。
      • 通过重载输入和输出操作符(operator>>operator<<),为自定义类型提供格式化和解析的功能。
    • 与外部设备的通信:针对不同的外部设备(终端、文件、内存)引入不同的处理逻辑

      C++的IOStream库抽象了与不同外部设备通信的细节,使得程序可以使用统一的接口来处理各种输入输出设备,如终端、文件、内存等。

      • 在C++中,任何可以进行输入输出的设备都可以被视为一个流。iostream库定义了几种基本的流类型,包括:

        • std::cin:标准输入流,通常关联到键盘输入。
        • std::cout:标准输出流,通常关联到屏幕输出。
        • std::cerr:标准错误流,用于输出错误信息,也关联到屏幕。
        • std::clog:标准日志流,用于输出日志信息。
      • 流的派生类

        除了基本的流类型,C++还提供了一些派生类来处理特定的设备,如:

        • std::basic_istreamstd::basic_ostreamstd::basic_iostream:这些类用于终端的输入、输出和双向操作

        • std::ofstreamstd::ifstreamstd::fstream:这些类分别用于文件的输出、输入和双向操作。

        • std::stringstreamstd::istringstreamstd::ostringstream:这些类用于基于字符串的流操作,可以视为内存中的文件。

      • 缓冲区

        IOStream库使用缓冲区来提高输入输出的效率。对于外部设备,如文件,缓冲区可以减少实际的I/O操作次数。对于终端设备,缓冲区可以提供行缓冲的功能。

  • IOStream处理输入输出所涉及的操作

    • 格式化 / 解析

      • 格式化:将数据转换为用户可读的格式。例如,将整数转换为字符串,或者将浮点数格式化为具有特定小数位数的字符串。
      • 解析:将格式化后的字符串转换回原始数据类型。例如,从字符串中解析出一个整数或浮点数。
    • 缓存

      • IOStream库使用缓存来提高I/O操作的效率。数据首先被写入到缓存中,然后在适当的时机被刷新到实际的I/O设备上。
      • 缓存可以是行缓冲(每次遇到换行符或缓冲区满时刷新)或全缓冲(缓冲区满时刷新)。
    • 编码转换

      • 在处理字符数据时,IOStream库可以进行编码转换。例如,可以在读取或写入文件时,将本地编码的数据转换为UTF-8编码。

        在 C++ 中,使用宽字符(wchar_t)可以更容易地处理 Unicode 字符。你可以先将本地编码转换为宽字符,然后再转换为 UTF-8。

        UTF-8 编码是一种可变长度的编码方式,它可以用来表示 Unicode 字符集中的任何字符。UTF-8 的特点是它可以使用1到4个字节来表示一个字符。

        示例:将宽字符字符串转换为 UTF-8 编码

        c++ 复制代码
        #include <iostream>
        #include <string>
        #include <locale>
        #include <codecvt>
        
        std::string wchar_to_utf8(const std::wstring& wstr) {
            std::wstring_convert<std::codecvt_utf8<wchar_t>> converter;
            return converter.to_bytes(wstr);
        }
        
        int main() {
            std::locale::global(std::locale(""));
            std::wstring wide_str = L"你好,世界!";
            std::string utf8_str = wchar_to_utf8(wide_str);
        
            std::cout << "UTF-8 Encoded String: " << utf8_str << std::endl;
            return 0;
        }

        上述代码使用了 C++11 引入的 std::wstring_convertstd::codecvt_utf8 来转换宽字符到 UTF-8 编码的字符串。这些功能在 C++17 中被标记为弃用,因此在未来可能会被移除。

      • 编码转换通常由特定的本地化设置或由用户定义的转换机制来控制。

    • 传输

      • 传输指的是数据在程序内部和外部设备之间的移动。
      • IOStream库提供了一系列的操作符(如<<>>)和函数来传输基本数据类型,以及用户定义类型的数据。
    • 错误处理

      • IOStream库提供了错误状态标志,可以用来检查I/O操作是否成功,以及是否有错误发生。
      • 错误状态可以通过ios_base::failbitios_base::badbitios_base::eofbit等标志来检查。
    • 定位

      IOStream库允许对流进行定位操作,例如,可以移动到文件的特定位置,或者回退到流中的先前位置。

    • 操作符

      C++提供了一系列的操纵符,如endl(插入换行并刷新缓冲区)、setw(设置字段宽度)、setprecision(设置数值的精度)等,用于控制I/O操作的格式。

    • 本地化

      IOStream库支持本地化,允许程序根据用户的区域设置来调整I/O操作的行为,例如日期和时间的格式化。

    • 模板

      IOStream库大量使用了模板,使得它可以与各种数据类型一起工作,包括用户定义的类型。

  • 采用模板来封装字符特性,采用继承来封装设备特性

    • C++ 的 I/O 库使用模板来封装不同的字符类型和编码特性。这是通过模板类 std::basic_ios 及其派生类实现的,这些类定义了 I/O 操作的基本属性和行为。如:

      std::basic_ios 模板接受一个模板参数,该参数指定了字符类型:

      c++ 复制代码
      template <class CharT, class Traits = char_traits<CharT>>
      class basic_ios : public ios_base {
          // ...
      };
      ...
      ...
      ...
      template<
          class CharT,
          class Traits = std::char_traits<CharT>
      > class basic_ifstream : public std::basic_istream<CharT, Traits> {
          // ...
      };
      • CharT 是字符类型,可以是 charwchar_tchar16_tchar32_t,分别对应不同的字符集和编码。
      • Traits 是一个特性类,通常使用默认参数 char_traits<CharT>,它定义了字符操作的特性,如比较和赋值。

      通过这种方式,C++ I/O 库可以支持多种字符类型和编码,而不需要为每种类型编写特定的代码。因此,常用的类型实际上是类模板实例化的结果。

    • C++ I/O 库使用继承来封装不同类型的 I/O 设备的特性。std::basic_ios 是一个基类,它定义了所有 I/O 流的公共接口和行为。然后,针对不同类型的 I/O 设备(如文件、字符串、控制台等),有特定的派生类:

      • std::basic_istream:输入流,从设备读取数据。
      • std::basic_ostream:输出流,向设备写入数据。
      • std::basic_iostream:输入输出流,同时支持读取和写入。

      这些类进一步派生自 std::basic_ios,并添加了与输入输出操作相关的特定功能:

      c++ 复制代码
      template <class CharT, class Traits>
      class basic_istream : public basic_ios<CharT, Traits> {
          // 输入流特有的操作
      };
      
      template <class CharT, class Traits>
      class basic_ostream : public basic_ios<CharT, Traits> {
          // 输出流特有的操作
      };
      
      template <class CharT, class Traits>
      class basic_iostream : public basic_istream<CharT, Traits>,
                             public basic_ostream<CharT, Traits> {
          // 同时支持输入和输出
      };

      通过继承,C++ I/O 库能够以一种层次化的方式组织代码,使得特定类型的 I/O 设备可以重用公共的 I/O 功能,同时添加自己的特定功能。

二、输入与输出

输入与输出分为格式化与非格式化两类。这两类提供了不同的方式来处理数据的输入和输出。

  • 非格式化 I/O :不涉及数据表示形式的变化(省略掉格式化/解析操作,使用类中函数完成非格式化I/O)

    非格式化 I/O是指直接读写数据的二进制形式,不涉及任何数据格式的转换。这通常用于底层的文件操作,例如读写图片、音频文件或其他二进制数据。

    • 常用输入函数: get / read / getline / gcount

      • std::istream::get()

        用于读取一个字符,不包括换行符

        c++ 复制代码
        char ch;
        std::cin.get(ch);
      • std::istream::read()

        用于读取一定数量的字符到缓冲区

        c++ 复制代码
        char buffer[10];
        std::cin.read(&buffer[0], sizeof(buffer));
      • std::istream::getline()

        用于读取一行文本直到换行符,不包括换行符

        c++ 复制代码
        std::string line;
        std::cin.getline(&line[0], line.max_size());
      • std::istream::gcount()

        返回最后一次非格式化输入操作读取的字符数

        c++ 复制代码
        int count = std::cin.gcount();
    • 常用输出函数: put / write

      • std::ostream::put()

        用于写入一个字符

        c++ 复制代码
        std::cout.put('A');
      • std::ostream::write()

        用于写入一定数量的字符到输出流

        c++ 复制代码
        const char* buffer = "Hello, World!";
        std::cout.write(buffer, sizeof(buffer) - 1);
  • 格式化I/O:使用移位操作符(重载了移位操作符)来进行的输入 (>>) 与输出 (<<)

    • C++ 通过操作符重载以支持内建数据类型的格式化 I/O。如

      c++ 复制代码
      #include <iostream>
      
      int main() {
          int number = 42;
          double pi = 3.14159;
          std::cout << "The number is: " << number << " and the value of PI is: " << pi << std::endl;
          // 输出: The number is: 42 and the value of PI is: 3.14159
      
          return 0;
      }

      numberpi 被自动转换为字符串,并按照默认格式输出

    • 程序员可通过重载操作符以支持自定义类型的格式化 I/O,通常在你的自定义类或结构体中完成。如:

      c++ 复制代码
      #include <iostream>
      
      class Point {
      public:
          double x, y;
      
          // 自定义输出操作符
          friend std::ostream& operator<<(std::ostream& os, const Point& p) {
              os << "Point(" << p.x << ", " << p.y << ")";
              return os;
          }
      
          // 自定义输入操作符
          friend std::istream& operator>>(std::istream& is, Point& p) {
              is >> p.x >> p.y;
              return is;
          }
      };
      
      int main() {
          Point p;
          std::cout << "Enter the coordinates of the point: \n";
          std::cin >> p;
          std::cout << "You entered the point: " << p << std::endl;
      
          return 0;
      }

      运行:

      c++ 复制代码
      Enter the coordinates of the point: 
      1 2
      You entered the point: Point(1, 2)
  • 格式控制

    在 C++ 中,格式控制是格式化输入输出(I/O)的一个重要方面,它允许开发者精确地指定数据如何在输出流中显示。在 C++ 中,I/O 流的格式控制可以通过多种方式实现,包括位掩码(bitmasks)、字符类型以及可以自由设定的值(如宽度)。这些格式化参数可以通过成员函数设置,并且通常用于控制输出的格式。

    • 位掩码类型格式化参数

      位掩码类型参数用于设置或清除流的特定属性。这些属性是 std::ios_base 类的枚举值,可以作为参数传递给流操作。以下是一些常用的位掩码类型格式化参数:

      • std::ios_base::showpos:显示正数的正号。当设置此标志时,正数前面会显示加号(+)。
      • std::ios_base::uppercase:对于特定类型的输出,使用大写形式(例如,科学计数法中的 'E')。
      • std::ios_base::fixed:对于浮点数,使用定点格式而不是科学计数法。
      • std::ios_base::scientific:对于浮点数,使用科学计数法。
    • 字符类型

      字符类型参数通常用于设置填充字符,这是通过 std::setfill 函数实现的:

      c++ 复制代码
      std::setfill(char ch);
    • 取值相对随意的格式化参数

      std::setw 函数允许你设置下一个输出项的宽度,这是一个可以自由设定的值:

      c++ 复制代码
      std::setw(int n);

      这个宽度值是相对随意的,你可以根据需要设置输出的宽度。

      注意:width 方法的特殊性:触发后被重置

      std::setw 的一个重要特性是,一旦执行了输出操作,流的宽度设置就会被重置为0。这意味着如果你想要连续输出多个项目,并且每个项目都需要特定的宽度,你必须在每个项目之前重新调用 std::setw 来设置宽度。

    示例

    c++ 复制代码
    #include <iostream>
    #include <iomanip> // 必须包含这个头文件来使用格式化功能
    
    int main() {
        int number = 42;
        double pi = 3.14159;
        char fillChar = '*';
    
        // 使用位掩码设置显示正数的正号
        std::cout << std::showpos << number << " is positive" << std::endl;
    
        // 使用字符类型设置填充字符
        std::cout << std::setw(20) << std::setfill(fillChar) << "Fill with " << fillChar << std::endl;
    
        // 使用取值相对随意的参数设置宽度
        std::cout << std::setw(10) << std::fixed << std::setprecision(2) << pi << std::endl;
    
        return 0;
    }
    • std::showpos 是一个位掩码类型参数,用于显示正数的正号。
    • std::setfill(fillChar) 是一个字符类型参数,用于设置填充字符。
    • std::setw(20)std::setprecision(2) 是取值相对随意的参数,分别用于设置输出的宽度和浮点数的小数位数。

    运行:

    c++ 复制代码
    +42 is positive
    **********Fill with *
    *****+3.14

C++ 中的操纵符(manipulators)是用来简化格式化参数设置的一组特殊的函数或对象。它们允许开发者以一种更直观和更易读的方式来控制输入输出流的格式。操纵符可以触发实际的插入(插入操作符 <<)与提取(提取操作符 >>)操作,并且可以链式使用,以对数据流进行连续的格式化设置。操纵符可以大大简化格式化参数的设置。例如,而不是使用多个 std::ios_base 风格的设置函数。

操纵符可以分为以下几类

  1. 数值操纵符:用于控制数值的显示格式,如小数点后的位数、使用定点或科学计数法等。
    • std::setw
    • std::setprecision
    • std::fixed
    • std::scientific
  2. 字符与字符串操纵符:用于控制字符和字符串的显示,如设置填充字符。
    • std::setfill
  3. 布尔操纵符:用于控制布尔值的显示。
    • std::boolalpha(将布尔值显示为 "true" 或 "false" 而不是 "1" 或 "0")
  4. 宽字符与宽字符串操纵符:用于控制宽字符和宽字符串的显示。
    • std::setw(对于宽字符)
  5. 格式化标志操纵符:用于设置流的格式化标志,如对齐方式、显示正号等。
    • std::showpos
    • std::noshowpos
    • std::left
    • std::right
    • std::internal
  6. 输入操纵符:用于控制输入流的行为。
    • std::skipws(跳过输入流中的空白字符)
    • std::noskipws(不跳过输入流中的空白字符)
c++ 复制代码
#include <iostream>
#include <iomanip>

int main() {
    double value = 12345.6789;

    // 使用操纵符设置小数点后保留两位,并且使用定点表示法
    std::cout << std::fixed << std::setprecision(2) << value << std::endl;

    // 使用操纵符设置宽度和填充字符
    std::cout << std::setw(15) << std::setfill('0') << value << std::endl;

    // 使用操纵符设置左对齐,并显示正号
    std::cout << std::left << std::showpos << value << std::endl;

    return 0;
}

在 C++ 中,输入操作通常使用提取操作符 >> 来完成。与格式化输出相比,输入操作通常对格式的限制更为宽松,这意味着输入操作能够接受多种格式的数据。然而,这种灵活性也带来了一些需要注意的地方,尤其是在提取 C 风格字符串时。

  • 提取操作符 >>

    提取操作符 >> 用于从输入流中读取数据,并将其存储在指定的变量中。对于不同类型的数据,>> 操作符的行为会有所不同:

    • 对于整数和浮点数,>> 会读取字符直到遇到非数字字符(例如,空格、制表符或换行符)。
    • 对于字符串,>> 的行为取决于使用的类型(std::string 或 C 风格字符串)。
  • 提取 C 风格字符串的注意事项

    当使用 >> 操作符提取 C 风格字符串(字符数组)时,需要特别小心内存越界的问题。这是因为 C 风格字符串通常需要一个额外的空字符('\0')来标记字符串的结束,而 >> 操作符不会自动添加这个空字符,也不会检查目标数组的大小。

    例如:

    C++ 复制代码
    #include <iostream>
    #include <cstdio> // 用于 printf
    
    int main() {
        char cStr[10]; // 定义一个大小为10的字符数组
    
        std::cout << "Enter a string: ";
        std::cin >> cStr; // 从 cin 提取字符串
    
        // 打印字符串,演示越界风险
        printf("Received string: %s\n", cStr);
    
        return 0;
    }

    如果用户输入的字符串长度超过数组 cStr 的大小(不包括空字符),就会发生内存越界,可能导致不可预测的行为,如程序崩溃或安全漏洞。

    安全提取C风格字符串

    为了避免内存越界,可以采取以下措施:

    1. 限制输入长度:

      使用 std::cin.getline() 方法,它可以限制读取的字符数,从而避免越界。

      c++ 复制代码
      std::cin.getline(cStr, sizeof(cStr));

      使用std::setw()操作符来限制输入长度

      c++ 复制代码
      std::cin >> std::setw(10) >> cStr;
    2. 使用 std::string:考虑使用 std::string 类型代替 C 风格字符串,因为它会自动管理内存,并提供更多安全特性。

三、文件与内存操作

1.文件操作

在 C++ 中,文件输入输出通常通过文件流对象来实现,这些对象基于模板类 std::basic_ifstreamstd::basic_ofstreamstd::basic_fstream。这些类分别用于只读、只写和读写两种模式的文件操作。需定义头文件#include <fstream>

  • std::basic_ifstream :基于 std::istream,用于文件输入。

    c++ 复制代码
    template<
        class CharT,
        class Traits = std::char_traits<CharT>
    > class basic_ifstream : public std::basic_istream<CharT, Traits>
    c++ 复制代码
    std::ifstream	std::basic_ifstream<char>
    std::wifstream	std::basic_ifstream<wchar_t>
  • std::basic_ofstream :基于 std::ostream,用于文件输出。

    c++ 复制代码
    template<
        class CharT,
        class Traits = std::char_traits<CharT>
    > class basic_ofstream : public std::basic_ostream<CharT, Traits>
    c++ 复制代码
    std::ofstream	std::basic_ofstream<char>
    std::wofstream	std::basic_ofstream<wchar_t>
  • std::basic_fstream :基于 std::iostream,同时支持文件的输入和输出。

    c++ 复制代码
    template<
        class CharT,
        class Traits = std::char_traits<CharT>
    > class basic_fstream : public std::basic_iostream<CharT, Traits>
    c++ 复制代码
    std::fstream	std::basic_fstream<char>
    std::wfstream	std::basic_fstream<wchar_t>

这些类模板接受两个参数:

  • CharT:字符类型,通常为 char,表示用于文件的字符类型。
  • traits:特性类,通常为 char_traits<CharT>,定义了字符序列的操作。

文件流的状态

文件流对象可以处于以下两种状态之一:

  • 打开:文件流对象已经与一个文件成功关联
  • 关闭:文件流对象没有与任何文件关联,或者已经关闭了与文件的关联

一旦文件流被打开,它不能被再次打开,直到它被关闭。尝试打开一个已经打开的文件流将导致运行时错误。只有当文件流处于打开状态时,才能进行输入输出操作。

示例:我们首先尝试打开一个文件用于写入,然后写入一些数据并关闭文件。接着,我们尝试打开同一个文件用于读取,并读取其内容直到文件末尾。

c++ 复制代码
#include <iostream>
#include <fstream>
#include <string>

int main() {
    // 创建 ofstream 对象,用于写入文件
    std::ofstream outfile("example.txt");
    if (!outfile.is_open()) {
        std::cerr << "Unable to open file for writing!" << std::endl;
        return 1;
    }

    // 写入数据到文件
    //先写到缓存区中,std::endl的作用是插入换行符并刷新输出缓存区
    //如果没有std::endl,则close()函数会先刷新缓存区再关闭文件
    outfile << "Hello, World!" << std::endl;  

    // 关闭文件
    outfile.close();

    // 创建 ifstream 对象,用于从文件读取
    std::ifstream infile("example.txt");
    if (!infile.is_open()) {
        std::cerr << "Unable to open file for reading!" << std::endl;
        return 1;
    }

    // 读取文件内容
    std::string line;
    while (std::getline(infile, line)) {
        std::cout << line << std::endl;
    }

    // 文件会在流对象被销毁时自动关闭,但显式关闭是一个好的习惯
    infile.close();

    return 0;
}

注意事项

  • 使用 is_open() 成员函数来检查文件是否成功打开。
  • 使用 close() 成员函数来关闭文件流。虽然文件流对象在销毁时会自动关闭文件,但显式关闭文件是一个好习惯。
  • 检查文件操作的返回值,以确保操作成功。
  • 对于大型文件或频繁的读写操作,考虑使用缓冲区来提高性能。

文件流的打开模式

标记名 作用
std::ios::in 以输入模式打开文件(读文件)
std::ios::out 以输出模式打开文件(写文件),如果文件存在,其内容会被截断。
std::ios::ate 打开文件并立即将文件指针定位到文件末尾。通常与 std::ios::instd::ios::out 结合使用
std::ios::app 以追加模式打开文件。写操作会在文件末尾添加内容,而不是覆盖现有内容
std::ios::trunc 如果文件已存在,先截断文件内容到0长度,然后打开文件进行写操作
std::ios::binary 以二进制模式打开文件,禁止任何系统特定的字符转换
  • 每种文件流都有缺省的打开方式

    每种文件流类型的缺省打开方式如下:

    • std::ifstream:默认为 std::ios::in

    • std::ofstream:默认为 std::ios::out | std::ios::trunc

      即使没有显式指定,使用默认的 out 模式打开文件时,其效果与显式使用 out | trunc(输出模式和截断模式的组合)相同。这意味着如果文件已存在,它的内容将被清空。

    • std::fstream:默认为 std::ios::in | std::ios::out

  • 注意 ateapp 的异同

    • std::ios::ate:打开文件后,文件指针会立即移动到文件末尾。这种方式通常用于确定文件大小或在文件末尾进行操作,但并不常用于普通的读写操作。
    • std::ios::app:用于追加模式。每次写操作都会在文件末尾添加内容,不会覆盖现有数据。std::ios::app 也常用于日志文件的写入
    • std::ios::ate 通常与 std::ios::instd::ios::out 结合使用,而 std::ios::app 则与 std::ios::out 结合使用
  • std::ios::binary 能禁止系统特定的转换

    使用 std::ios::binary 打开模式可以禁止系统对换行符等进行特定转换,确保读写的是原始的二进制数据。这对于非文本文件(如图片、音频等)的读写非常重要

  • 避免意义不明确的流使用方式(如 ifstream + out )

    • 避免同时使用输入和输出模式打开文件

      例如,使用 std::ios::in | std::ios::outstd::ifstreamstd::ios::out | std::ios::appstd::ofstream,因为这种行为在不同平台上可能有不同的含义,可能会导致不可预测的结果。

    • 不要在未明确指定模式的情况下打开文件流,因为这可能导致混淆和错误。

  • 合法的打开方式组合

    打开方式 效果 加结尾模式标记 加二进制模式标记
    in 只读方式打开文件 初始文件位置位于文件末尾 禁止系统转换
    out out | trunc 如果文件存在,将长度截断为0,否则建立文件供写入 初始文件位置位于文件末尾 禁止系统转换
    out | app 打开或建立文件,仅供文件末尾写入 初始文件位置位于文件末尾 禁止系统转换
    in | out 打开文件供更新使用(支持读写) 初始文件位置位于文件末尾 禁止系统转换
    in | out | trunc 如果文件存在,将长度截断为0;否则打开文件供更新使用(支持读写) 初始文件位置位于文件末尾 禁止系统转换

示例:

c++ 复制代码
#include <iostream>
#include <fstream>

int main() {
    // 创建 ofstream 对象,以追加模式打开文件
    std::ofstream outfile("example.txt", std::ios::out | std::ios::app);
    if (!outfile.is_open()) {
        std::cerr << "Unable to open file for appending!" << std::endl;
        return 1;
    }

    // 追加内容到文件
    outfile << "Appended text." << std::endl;

    // 创建 ifstream 对象,以二进制读取模式打开文件
    std::ifstream infile("example.txt", std::ios::in | std::ios::binary);
    if (!infile.is_open()) {
        std::cerr << "Unable to open file for reading!" << std::endl;
        return 1;
    }

    // 读取文件内容
    char buffer[255];
    infile.read(buffer, sizeof(buffer));
    std::cout << "File content: " << buffer << std::endl;

    return 0;
}

首先以追加模式打开 outfile,然后写入一些文本。接着,我们以二进制读取模式打开 infile 并读取文件内容。

2.内存操作

在 C++ 中,内存流提供了一种使用内存(而不是文件系统)作为输入输出目标的手段。内存流基于 std::basic_istringstreamstd::basic_ostringstreamstd::basic_stringstream 类,分别用于内存中的输入、输出和双向操作。这些类都使用 std::string 作为默认的字符容器。需定义头文件#include <sstream>

  • std::basic_istringstream :基于 std::istream,用于从内存中读取数据。

    c++ 复制代码
    template<
        class CharT,
        class Traits = std::char_traits<CharT>,
        class Allocator = std::allocator<CharT>
    > class basic_istringstream : public basic_istream<CharT, Traits>;
    c++ 复制代码
    std::istringstream	std::basic_istringstream<char>
    std::wistringstream	std::basic_istringstream<wchar_t>
  • std::basic_ostringstream :基于 std::ostream,用于向内存中写入数据。

    c++ 复制代码
    template<
        class CharT,
        class Traits = std::char_traits<CharT>,
        class Allocator = std::allocator<CharT>
    > class basic_ostringstream : public basic_ostream<CharT, Traits>;
    c++ 复制代码
    std::ostringstream	std::basic_ostringstream<char>
    std::wostringstream	std::basic_ostringstream<wchar_t>
  • std::basic_stringstream :基于 std::iostream,用于内存中的双向数据流。

    c++ 复制代码
    template<
        class CharT,
        class Traits = std::char_traits<CharT>,
        class Allocator = std::allocator<CharT>
    > class basic_stringstream : public basic_iostream<CharT, Traits>;
    c++ 复制代码
    std::stringstream	std::basic_stringstream<char>
    std::wstringstream	std::basic_stringstream<wchar_t>

内存流打开模式

内存流的打开模式与文件流类似,受以下模式影响:

  • std::ios::in:输入模式。
  • std::ios::out:输出模式,如果用于读取,将清空内存流中的内容。
  • std::ios::ate :到达末尾,通常与 std::ios::in 结合使用,用于从流的末尾开始操作。
  • std::ios::app:追加模式,写操作会在流的末尾添加内容。

每种文件流类型的缺省打开方式如下:

  • std::istringstream:默认为 std::ios::in

  • std::ostringstream:默认为 std::ios::out | std::ios::trunc

  • std::stringstream:默认为 std::ios::in | std::ios::out

示例:

c++ 复制代码
#include <iostream>
#include <sstream>
 
int main()
{
    // default constructor (input/output stream)
    std::stringstream buf1;
    buf1 << 7;
    int n = 0;
    buf1 >> n;
    std::cout << "buf1 = " << buf1.str() << " n = " << n << '\n';
 
    // input stream
    std::istringstream inbuf("-10");
    inbuf >> n;
    std::cout << "n = " << n << '\n';
 
    // output stream in append mode (C++11)
    std::ostringstream buf2("test", std::ios_base::ate);
    buf2 << '1';
    std::cout << buf2.str() << '\n';
}

运行结果:

c++ 复制代码
buf1 = 7 n = 7
n = -10
test1

使用 str() 方法获取底层所对应的内存:

内存流对象提供了 str() 成员函数,用于获取或设置底层 std::string 对象。当你想要获取内存流中的内容时,可以使用 str() 方法。

c++ 复制代码
std::basic_string<CharT, Traits, Allocator> str() const;

示例:

c++ 复制代码
#include <sstream>
#include <iostream>

int main() {
    std::stringstream ss;
    ss << "Sample text";
    std::string s = ss.str(); // 获取内存流中的字符串
    std::cout << s << std::endl;

    return 0;
}

注意:避免使用 str().c_str() 的形式获取 C 风格字符串,因为这会返回底层 std::string 对象的临时副本的 C 风格字符串,一旦临时副本被销毁,得到的指针将指向无效的内存区域。

基于字符串流的字符串拼接优化操作

基于字符串的流可以用于优化字符串拼接操作,尤其是在循环中。传统的字符串拼接可能会因为多次复制字符串内容而导致性能问题,而使用内存流可以避免这种情况,因为它直接操作底层的 std::string 对象,从而提高效率。

c++ 复制代码
#include <sstream>
#include <string>
#include <vector>

std::string concatenate(const std::vector<std::string>& vec) {
    std::ostringstream oss;
    for (const auto& s : vec) {
        oss << s;
    }
    return oss.str();
}

concatenate 函数使用 std::ostringstream 来拼接 std::vector 中的字符串,避免了直接字符串拼接带来的性能消耗。

四、流的状态、定位与同步

1.流的状态

在 C++ 中,流的状态是通过 std::ios 类的成员函数来管理和查询的。流的状态表示流在执行输入输出操作时遇到的不同情况。以下是一些主要的流状态标志(iostate):

c++ 复制代码
typedef /*implementation defined*/ iostate;
static constexpr iostate goodbit = 0;
static constexpr iostate badbit  = /* implementation defined */
static constexpr iostate failbit = /* implementation defined */
static constexpr iostate eofbit  = /* implementation defined */
  • failbit :输入/输出操作失败(格式化或提取错误)
    • 当输入操作遇到无法识别的输入数据,或者输出操作遇到错误时,将设置 failbit
    • 一旦 failbit 被设置,流就进入了失败状态,后续的操作通常会失败。
  • badbit :不可恢复的流错误
    • 当发生严重的流错误,如底层文件系统错误或硬件故障时,将设置 badbit
    • 设置 badbit 通常意味着流遇到了无法恢复的错误。
  • eofbit :关联的输入序列已抵达文件尾
    • 当输入操作到达文件或流的末尾时,将设置 eofbit
    • eofbit 表示没有更多的数据可供输入。
  • goodbit :无错误
    • 如果流的状态良好,没有错误标志被设置,可以认为流处于 goodbit 状态。

对于 ios_base::iostate 标志的所有可能组合的值:

检测流的状态

在 C++ 中,检测流的状态是通过检查流对象的内部状态标志来完成的。这些状态标志反映了流在执行输入输出操作时的不同情况。以下是用于检测流状态的方法:

  • good()

    如果流没有遇到任何错误(即没有设置任何错误标志),good() 返回 true。换句话说,如果流处于"良好"状态(既没有 failbit 也没有 badbit 也没有 eofbit),good() 将返回 true

  • fail()

    如果流设置了 failbit(表示输入操作失败,如无法解析输入数据),fail() 返回 true。这个方法通常在尝试从流中读取数据后被调用,以检查操作是否成功。

  • bad()

    如果流设置了 badbit(表示发生了严重的错误,如底层硬件错误),bad() 返回 true。一旦 badbit 被设置,流就处于损坏状态,通常无法恢复。

  • eof()

    如果流设置了 eofbit(表示已经到达文件末尾或流末尾),eof() 返回 true。这个方法通常在尝试从输入流中读取数据后被调用,以检查是否还有更多数据。

示例:

c++ 复制代码
#include <iostream>
#include <fstream>

int main() {
    std::ifstream file("example.txt");
    if (!file.good()) {
        std::cerr << "Stream in bad state." << std::endl;
        return 1;
    }

    // 进行文件操作...
    char ch;
    while (file.get(ch)) {
        // 处理字符 ch
    }
	//检查了流的不同状态,并根据状态执行了相应的操作
    if (file.eof()) {
        std::cout << "End of file reached." << std::endl;
    }

    if (file.fail()) {
        std::cerr << "Input operation failed." << std::endl;
    }

    if (file.bad()) {
        std::cerr << "Stream has encountered a fatal error." << std::endl;
    }

    return 0;
}

流对象本身可以被转换为 bool 类型,这种转换基于流的状态。如果流处于失败或损坏状态,转换结果为 false;否则为 true。这允许流对象直接在条件语句中使用:

c++ 复制代码
std::ifstream file("example.txt");
if (file) { //检查 std::ifstream 对象是否可以进行 I/O 操作
    // 文件成功打开,可以安全地进行 I/O 操作
} else {
    // 文件打开失败
}

注意区分 fail 与 eof:

  • 可能会被同时设置,但二者含意不同

    流的状态标志 failbiteofbit 可以同时被设置。例如,在文件读取操作中,如果遇到文件末尾并且之前有读取失败,两个标志都可能被触发。

  • 转换为 bool 值时不会考虑 eof

    • failbit 被设置时,流对象的布尔转换(即隐式转换为 bool 类型)将失败,导致表达式如 if (stream) 评估为 false
    • failbit 不同,eofbit 不会影响流对象的布尔上下文转换。即使 eofbit 被设置,流对象仍然可以隐式地转换为 true,因为到达文件末尾并不表示错误发生。因此,不会考虑eof(仅表示到达了流的末尾)。流对象的布尔转换不会考虑 eofbit,只会在 failbitbadbit 被设置时失败。

通常来说,只要流处于某种错误状态时,插入 / 提取操作就不会生效

例如,对于双向流来说,当提取操作处于eofbit状态,此时插入操作也不会生效。

复位流:通常来说,不会调用流状态的复位

当流处于错误状态时,通常需要采取一些措施来处理错误:

  • 检查流状态:在执行插入或提取操作之前和之后,检查流的状态以确定操作是否成功。
  • 清除错误标志:使用 clear() 成员函数清除流的错误标志(如 failbit),使流恢复到可操作状态。
  • 关闭和重新打开流:对于某些错误,可能需要关闭并重新打开流来恢复。

在 C++ 中,流状态可以通过 clear()setstate() 函数进行复位或修改:

  • clear():设置流的状态为具体的值(缺省为 goodbit )

    • clear() 函数用于清除流的错误标志。默认情况下,它会将流的状态设置为 goodbit,表示流处于良好状态,没有任何错误标志被设置。
    • 也可以在 clear() 函数中传递一个错误标志作为参数,来设置流的状态为特定的错误状态。
    c++ 复制代码
    std::ifstream file;
    // ...
    file.clear();  // 清除错误标志,流状态设置为 goodbit
    file.clear(std::ios::eofbit);  // 将流状态设置为 eofbit
  • setstate():将某个状态附加到现有的流状态上

    • setstate() 函数用于将某个特定的错误标志附加到流的当前状态上。它会设置流的状态为当前状态与传递给它的错误标志的组合。
    • 这通常用于在检测到特定错误时,手动设置流的错误状态。
    c++ 复制代码
    std::ifstream file;
    // ...
    file.setstate(std::ios::failbit);  // 附加 failbit 到流状态

示例:

c++ 复制代码
#include <iostream>
#include <fstream>
#include <ios>

int main() {
    std::ifstream file("example.txt");
    if (!file) {
        std::cerr << "Error opening file." << std::endl;
        return 1;
    }

    // 假设读取过程中遇到错误
    char ch;
    if (file >> ch) {
        // 成功读取
    } else {
        std::cerr << "Read error encountered." << std::endl;
        // 清除 failbit,尝试继续读取
        file.clear(); 
    }

    // 如果需要将流状态设置为特定的错误状态
    // file.setstate(std::ios::eofbit);

    return 0;
}

捕获流异常:exceptions方法:对于流来说,通常不会使用异常处理的方法。

在 C++ 中,流对象提供了 exceptions() 方法,用于设置和捕获流的异常。通过 exceptions() 方法,你可以指定在流遇到特定错误时抛出的异常类型。

  • 使用 std::ios_base::iostate 枚举值来设置流的异常掩码。
  • 可以在 try 块中执行 I/O 操作,并在 catch 块中捕获由流错误触发的异常。std::ios_base::failure 是一个由 I/O 库抛出的异常类型,它提供了错误描述。
c++ 复制代码
#include <iostream>
#include <fstream>
#include <ios>

int main() {
    std::ifstream file("example.txt");

    // 设置流的异常掩码,当 failbit 被设置时抛出 std::ios_base::failure 异常
    file.exceptions(std::ios_base::failbit);

    try {
        int data;
        file >> data;  // 尝试读取数据
    } catch (const std::ios_base::failure& e) {
        std::cerr << "I/O failure: " << e.what() << std::endl;
    }

    // 可以再次调用 exceptions() 来改变异常掩码
    file.exceptions(std::ios_base::eofbit | std::ios_base::failbit);  // 设置多个掩码

    return 0;
}

2.流的定位

获取流的位置

在 C++ 中,可以通过 tellg()tellp() 函数获取输入流和输出流的位置(pos_type 类型)。这两个函数分别用于获取与流对象关联的当前读取位置和写入位置。

  • tellg() :从输入流中获取当前的位置。它返回一个 pos_type 类型的值,表示从流的开始到当前读取位置的偏移量。

    c++ 复制代码
    #include <iostream>
    #include <sstream>
    #include <string>
     
    int main()
    {
        std::string str = "Hello, world";
        std::istringstream in(str);
        std::string word;
        in >> word;
        std::cout << "After reading the word \"" << word
                  << "\" tellg() returns " << in.tellg() << '\n';
    }

    运行结果:

    c++ 复制代码
    After reading the word "Hello," tellg() returns 6

    为什么没有读取world,因为标准输入默认情况下会在遇到空白字符时停止读取。这里的空白字符包括空格、制表符、换行符等。

  • tellp() :从输出流中获取当前的位置。同样,它返回一个 pos_type 类型的值,表示从流的开始到当前写入位置的偏移量。

    pos_type 是一个定义在 std::ios_base 中的类型,通常用作流位置的指标

    c++ 复制代码
    #include <iostream>
    #include <sstream>
    int main()
    {
        std::ostringstream s;
        std::cout << s.tellp() << '\n';
        s << 'h';
        std::cout << s.tellp() << '\n';
        s << "ello, world ";
        std::cout << s.tellp() << '\n';
        s << 3.14 << '\n';
        std::cout << s.tellp() << '\n' << s.str();
    }

    运行结果:

    0
    1
    13
    18
    hello, world 3.14
    
  • tellg()tellp() 可能会失败,这通常发生在以下情况:

    • 流没有与有效的输入/输出设备关联。
    • 流处于错误状态。
    • 流的缓冲区没有正确地跟踪其位置。

    tellg()tellp() 失败时,它们将返回 pos_type(-1)。这是一个约定,用来表示位置获取失败。

注意事项

  • 在使用 tellg()tellp() 后,你应该检查流的状态以确保位置获取成功。
  • 对于某些类型的流(如某些类型的字符串流或内存流),tellg()tellp() 可能总是返回有效的结果。
  • 对于文件流,如果未打开文件或文件被外部程序修改,tellg()tellp() 可能会失败。

设置流的位置

在 C++ 中,seekg()seekp() 函数用于设置输入流和输出流的位置。这些函数允许你移动与流相关联的文件指针或字符串指针,以便在流中前进或后退到特定位置。

  • seekg() :用于输入流,如 std::ifstreamstd::istringstream
  • seekp() :用于输出流,如 std::ofstreamstd::ostringstream

需要注意:如果指向已经输入的位置再进行输入,此时是覆盖操作而不是插入操作。

c++ 复制代码
#include <iostream>
#include <sstream>
#include <string>
 
int main()
{
    std::string str = "Hello, world";
    std::istringstream in(str);
    std::string word1, word2;
 
    in >> word1;
    in.seekg(0); // rewind
    in >> word2;
 
    std::cout << "word1 = " << word1 << '\n'
              << "word2 = " << word2 << '\n';
}

运行结果:

c++ 复制代码
word1 = Hello,
word2 = Hello,

这两个方法分别有两个重载版本:

c++ 复制代码
basic_istream& seekg( pos_type pos ); //设置绝对位置
basic_istream& seekg( off_type off, std::ios_base::seekdir dir ); //设置相对位置
  • 设置绝对位置 :接受一个 pos_type 参数,表示从流的开始位置(ios_base::beg)起的字节偏移量。

    c++ 复制代码
    std::streampos pos = // ...;
    inStream.seekg(pos);
    outStream.seekp(pos);
  • 设置相对位置 :接受一个 off_type 参数(通常是 std::streamoff 类型)和一个 ios_base::seekdir 枚举值,表示相对移动的参考点。

    • ios_base::beg:从流的开始位置起设置偏移量。
    • ios_base::cur:从当前流位置起设置偏移量。
    • ios_base::end:从流的结束位置起设置偏移量。
    c++ 复制代码
    std::streamoff offset = // ...;
    inStream.seekg(offset, std::ios_base::beg);
    outStream.seekp(offset, std::ios_base::beg);

3.流的同步

在 C++ 中,流的同步确保了数据在输出流中被正确地写入目标设备(如屏幕、文件等),并且在输入流中从源设备正确地读取数据。

流同步过程的详细步骤:

  • 数据写入缓冲区

    当数据被写入到输出流(如 std::cout 或文件流)时,数据首先被放入一个内部缓冲区中。

  • 缓冲区满或显式同步

    • 数据通常在缓冲区满了或者遇到特定的流操作(如插入换行符 std::endl)时被自动发送到目标设备。

    • 显式同步可以通过调用 std::flushstd::ios::sync 来实现。

      对于输出流,设置 unitbuf 操纵符可以保证每次输出操作后自动调用 flush() 方法,从而实现即时同步

  • 流绑定 (tie)

    输入流可以绑定到一个输出流,这样每次进行输入操作前,系统会自动刷新被绑定的输出流的缓冲区

基于 flush() / sync() / unitbuf 的显式同步

  • flush():用于输出流同步

    • flush() 函数用于输出流,它立即将缓冲区中的数据发送到输出设备,但不关闭流。
    • 对于文件输出流,flush() 可以确保文件内容被写入磁盘。
    • 对于控制台或标准输出流,flush() 可以确保缓冲区的内容被显示在屏幕上。
    c++ 复制代码
    std::cout << "Data to be output" << std::flush; // 立即输出缓冲区内容
  • sync():用于输入流同步,其实现逻辑是编译器所定义的

    • sync() 函数用于输入流,它尝试同步输入流的状态。
    • 对于某些类型的输入流(如文件输入流),sync() 可能会尝试从物理设备读取数据,直到到达预期的同步点。
    • sync() 的具体实现取决于编译器和底层操作系统。
    c++ 复制代码
    std::cin.sync(); // 尝试同步输入流
  • unitbuf :保证每次输出后自动同步

    • unitbuf 是一个流操纵符,当设置后,输出流将在每次输出操作后自动执行 flush() 操作。
    • 这意味着输出将立即被写入目标设备,而不会留在缓冲区中。
    • unitbuf 对于日志记录或任何需要即时输出的应用(如:std::cerr)非常有用。
    c++ 复制代码
    std::cout << std::unitbuf; // 设置输出流在每次输出后自动同步
    std::cout << "Immediate output"; // 这将立即被输出

注意事项:

  • 使用 flush()sync() 时,应该注意它们可能影响程序的性能,因为频繁的同步操作可能会导致 I/O 延迟。
  • unitbuf 在需要即时输出时非常有用,但也会增加 I/O 操作的频率。
  • 在设计程序时,应该根据实际需求来决定是否需要启用这些同步机制。

基于绑定 (tie) 的同步

在 C++ 中,流的同步也可以通过绑定(tie)机制实现。绑定一个流到另一个输出流上意味着每次进行输入/输出操作之前,系统会自动刷新被绑定的输出流的缓冲区。这种机制通常用于确保输出在进行新的输入操作之前是最新的,例如在需要输出和输入交替进行的交互式应用程序中。

  • tie():std::ios_base 类提供了 tie() 成员函数,返回一个指向输出流的指针,或者允许你设置一个新的输出流作为绑定。

    流对象如 std::cin 可以绑定到一个输出流(如 std::cout),这样每次从 std::cin 读取输入之前,std::cout 的缓冲区都会被清空。

示例:

c++ 复制代码
#include <iostream>

int main() {
    // 获取 cin 的当前绑定输出流
    std::ostream* pOut = std::cin.tie();
    std::cout << "Current tie of cin is: " << pOut << std::endl;

    // 绑定 cin 到 cout
    std::cin.tie(nullptr); // 取消绑定

    // 再次绑定 cin 到 cout
    std::cin.tie(&std::cout);

    // 输出一些内容
    std::cout << "Output before input" << std::endl;

    // 此时输入前会自动刷新 cout 的缓冲区
    std::cin.get(); // 等待用户输入

    return 0;
}

注意:

  • 绑定一个流到另一个流是可选的,并不是所有的输入流都需要绑定。
  • 默认情况下,std::cin 是绑定到 std::cout 上的,这意味着每次从 std::cin 读取之前 std::cout 的缓冲区都会被清空。
  • 可以通过传递 nullptrtie() 函数来取消流的绑定

与 C 语言标准 IO 库的同步

在 C++ 中,标准库的 I/O 流(如 std::cinstd::cout 等)与 C 语言的 I/O 函数(如 printfscanf 等)默认是同步的。这意味着,当你在 C++ 程序中使用 C++ 的 I/O 流进行输入输出操作后,再使用 C 语言的 I/O 函数,它们的输入输出缓冲区将会是同步的。这种同步行为确保了数据的一致性,防止了潜在的数据覆盖或混淆问题。

  • 默认情况下,C++ 的输入输出操作会与 C 的输入输出函数同步

    在 C++ 程序中,如果你调用了 C++ 的 I/O 函数(如 std::cout << someValue;),然后调用了 C 的 I/O 函数(如 printf("%d", someValue);),两个函数将共享相同的输入输出缓冲区状态。

  • 通过 sync_with_stdio 关闭该同步

    • 如果你需要在 C++ 程序中混合使用 C++ 的 I/O 流和 C 的 I/O 函数,并且希望它们不要互相影响缓冲区,你可以使用 std::ios_base::sync_with_stdio 函数来关闭同步。
    • 当你调用 std::ios_base::sync_with_stdio(false); 时,C++ 的 I/O 流和 C 的 I/O 函数将不再同步它们的缓冲区。

示例:

c++ 复制代码
#include <cstdio>
#include <iostream>
 
int main()
{
    std::ios::sync_with_stdio(false);
    std::cout << "a\n";
    std::printf("b\n");
    std::cout << "c\n";
}

由于关闭了同步,a一定出现c的前面,但b不一定,每次运行结果不一定一致。可能的输出为

c++ 复制代码
a
c
b

如果未关闭同步的话,运行结果是一定的。

相关推荐
ULTRA??18 分钟前
C加加中的结构化绑定(解包,折叠展开)
开发语言·c++
凌云行者1 小时前
OpenGL入门005——使用Shader类管理着色器
c++·cmake·opengl
凌云行者1 小时前
OpenGL入门006——着色器在纹理混合中的应用
c++·cmake·opengl
~yY…s<#>1 小时前
【刷题17】最小栈、栈的压入弹出、逆波兰表达式
c语言·数据结构·c++·算法·leetcode
可均可可2 小时前
C++之OpenCV入门到提高004:Mat 对象的使用
c++·opencv·mat·imread·imwrite
白子寰2 小时前
【C++打怪之路Lv14】- “多态“篇
开发语言·c++
小芒果_013 小时前
P11229 [CSP-J 2024] 小木棍
c++·算法·信息学奥赛
gkdpjj3 小时前
C++优选算法十 哈希表
c++·算法·散列表
王俊山IT3 小时前
C++学习笔记----10、模块、头文件及各种主题(一)---- 模块(5)
开发语言·c++·笔记·学习
-Even-3 小时前
【第六章】分支语句和逻辑运算符
c++·c++ primer plus