65、C++文件输入输出操作全解析

C++文件输入输出操作全解析

1. 流操纵符

流操纵符用于控制数据在流中的输入输出格式,它们并非数据值,而是指向特定类型函数的指针。当使用流操纵符时,会调用接受函数指针作为参数的 operator<<() 版本,并将操纵符作为参数传递。

hex 操纵符为例,它是指向 ios 头文件中定义的如下函数的指针:

ios_base& hex(ios_base& str);

使用示例:

std::cout << std::hex << i;

这等价于:

(std::cout.operator<<(std::hex)).operator<<(i);

第一次调用 operator<<() 时,将函数指针 std::hex 作为参数。在 operator<<() 函数内部,会使用该函数指针调用 hex() 函数,将输出格式设置为以十六进制形式将 i 的值传输到流中。

ios_base 类是 ios 类的基类,由于它是所有流类的基类, ios_base& 类型可以引用任何流对象。所有操纵符都是指向具有 ios_base 引用类型参数和返回类型的函数的指针,因此它们都会调用相同版本的 operator<<() ,该函数会调用参数指向的函数。 ios_base 定义了控制流的标志,使用操纵符时调用的函数会修改相应的标志以产生所需的结果。虽然可以使用 std::setf() std::unsetf() 函数直接修改这些标志,但使用操纵符更为简便。

2. 带参数的流操纵符

有些流操纵符需要接受参数,这些操纵符在 iomanip 头文件中定义。使用这些操纵符函数的方式与其他操纵符相同,即通过将函数调用有效地插入到流中。以下是一些常用的带参数操纵符:
| 操纵符 | 效果 |
| ---- | ---- |
| setprecision(int n) | 将浮点输出的精度设置为 n 位,该设置一直有效,直到被更改。 |
| setw(int n) | 将下一个输出值的字段宽度设置为 n 个字符,每次输出后会重置为默认设置,即输出值的字段宽度刚好足以容纳该值。 |
| setfill(char ch) | 将输出字段中用作填充的字符设置为 ch ,该设置是模态的,直到再次更改才会失效。 |
| setbase(int base) | 将整数的输出表示设置为八进制、十进制或十六进制,对应参数值为 8、10 或 16,其他值将保持数字基数不变。 |

示例代码:

std::cout << std::endl << std::setw(10) << std::setfill('*') << std::left << i << std::endl;

该语句将值 i 左对齐输出到一个宽度为 10 个字符的字段中,值右侧未使用的字符位置将用 * 填充。填充字符对后续输出值仍然有效,但必须在每个输出值之前显式设置字段宽度。

需要注意的是,对于不需要参数的操纵符(如 std::left ),使用括号是错误的,不要将它们与 iomanip 中需要参数的操纵符混淆。

iomanip 头文件还声明了 setiosflags() resetiosflags() 函数,它们通过指定掩码来设置或重置控制流格式的标志。可以使用按位或运算符组合 ios_base 类中定义的标志来构造掩码。例如,设置左对齐、十六进制输出的标志:

std::cout << std::endl << std::setw(10)
          << std::setiosflags(std::ios::left | std::ios::hex) << i << std::endl;

这将把 i 作为左对齐的十六进制值输出到一个宽度为 10 个字符的字段中。这里可以使用 ios 代替 ios_base 作为标志名称的限定符,因为 ios 类从 ios_base 继承了这些标志。

3. 文件流

处理文件的流对象有三种类型: ifstream ofstream fstream 。这些类分别以 istream ostream iostream 为基类。 istream 对象表示文件输入流,只能进行读取操作; ofstream 对象表示文件输出流,只能进行写入操作; fstream 是一个可以进行读写操作的文件流。

可以在创建文件流对象时将其与物理文件关联,也可以先创建一个未关联文件的文件流对象,然后调用成员函数来建立与特定文件的连接。要读取或写入文件,必须“打开”文件,这会通过操作系统将文件附加到程序,并赋予一组权限来确定可以对文件执行的操作。如果创建文件流对象时初始关联了一个文件,该文件将立即打开并可供使用。还可以更改与文件流对象关联的文件,例如可以使用单个 ofstream 对象在不同时间写入不同的文件。

文件流具有一些重要属性:
- 长度:对应流中字符的数量。
- 起始位置:流中第一个字符的索引位置。
- 结束位置:流中最后一个字符之后的索引位置。
- 当前位置:流中下一次读写操作开始的字符的索引位置,文件流中第一个字符的索引位置为 0。

这些属性提供了一种在文件中移动的方式,以便读取感兴趣的特定部分或覆盖文件的选定区域。

4. 文本模式下写入文件

要开始研究文件流,先来看如何向文件流写入数据。输出文件由 ofstream 对象表示,可以这样创建:

std::ofstream outFile {"filename"};

ofstream 构造函数的参数是文件名,可以是字符串字面量、指向 C 风格字符串的 char* 变量或字符串对象。文件名可以包含文件路径,如果不包含,则文件应位于当前目录。文本模式是默认模式,因此由 filename 标识的文件将自动以文本模式打开以供写入,并且可以立即进行写入操作。如果 filename 文件不存在,将创建具有该名称和路径的文件;如果文件已存在,将以文件位置为 0(文件开头)打开文件,写入的任何内容都将覆盖现有内容。

outFile 对象包含一个 ostream 子对象,因此之前讨论的标准输出流的流操作同样适用于文本模式下的输出文件流。以下是一个示例,用于找出素数并将它们写入文件:

// Ex17_01.cpp
// Writing primes to a file
#include <cmath>                                 // For sqrt() function
#include <fstream>                               // For file streams
#include <iomanip>                               // For stream manipulators
#include <iostream>                              // For standard streams
#include <string>                                // For string type
#include <vector>                                // For vector container
using ulong = unsigned long long;

int main()
{
  size_t max {};                                 // Number of primes required
  std::cout << "How many prime would you like (at least 4)? : ";
  std::cin >> max;
  if (max < 4) max = 4;

  std::vector<ulong> primes {2ULL, 3ULL, 5ULL};  // First three primes defined
  ulong trial {5ULL};                            // Candidate prime
  bool isprime {false};                          // true when a prime is found
  ulong limit {};                                // Maximum divisor

  while (primes.size() < max)
  {
    trial += 2;                                  // Next value for checking
    limit = static_cast<ulong>(std::sqrt(trial));
    for (auto prime : primes)
    {
      if (prime > limit) break;                  // Only check divisors < square root
      isprime = trial % prime > 0;               // false for exact division...
      if (!isprime) break;                       // ...if so it's not a prime
    }
    if (isprime)                                 // If we found one...
      primes.push_back(trial);                   // ...save it
  }

  std::string filename {"d:\\Example_Data\\primes.txt"};
  std::ofstream outFile {filename};              // Define file stream object

  // Output primes to file
  size_t perline {5};                            // Prime values per line
  size_t count {};
  for (auto prime : primes)
  {
    outFile << std::setw(10) << prime;
    if (++count % perline == 0)                  // New line after every perline primes
      outFile << std::endl;
  }
  outFile << std::endl;
  std::cout << max << " primes written to " << filename << std::endl;
}

示例输出:

How many prime would you like (at least 4)? : 1000
1000 primes written to d:\Example_Data\primes.txt

所有素数都会被写入文件。如果打算使用代码中指定的路径,必须在执行程序之前在 D 盘创建 Example_Data 目录;否则,将 filename 的初始值更改为为示例设置的路径。注意,在指定文件路径的字符串中,可以使用单个正斜杠 / 代替 \\ 作为分隔符。标准库没有提供创建目录或文件夹的方法,因为这些操作依赖于系统,编译器附带的库可能提供了创建目录的函数。可以使用任何文本编辑器查看示例创建的文件内容。

这里将 ulong 定义为 unsigned long long 类型的别名,以节省输入并缩短一些语句的长度。确定一个整数是否为素数的大部分细节之前已经见过,这里的变化是代码只检查到 trial 值的平方根的素数除数,因此当 trial 是素数时执行速度会更快。如果没有将平方根作为除数的上限,将尝试用向量中的所有素数进行除法运算。非素数整数有两个因子,它们的乘积是非素数值。如果两个因子相同,它们就是该值的平方根;如果不同,其中一个必须小于平方根。因此,任何非素数整数总是至少有一个小于或等于其平方根的因子。

std::sqrt() 是在 cmath 头文件中通过模板定义的,对于整数参数,它返回 double 类型的值。如果不将结果显式转换为 ulong 类型,可能会因为将浮点值隐式转换为整数时可能的数据丢失而收到编译器警告。素数存储在向量容器中,因为在检查候选素数时需要在内存中保存已知的素数。当然,也可以将一部分素数保存在内存中,其余的保存在文件中,但这需要具备读取文件的能力,后续会进行解释。

fstream 头文件定义了 ofstream 类型以及 ifstream fstream ,因此将该头文件包含到源文件中可以创建用于所有文件读写操作组合的流对象。 ofstream 对象是通过将标识文件名和路径的 filename 传递给构造函数来创建的,也可以在这里传递字面量,但通常需要允许用户输入来确定要使用的文件。输出流默认处于文本模式,使用 << 运算符将素数写入文件的方式与写入 cout 完全相同,流操纵符的工作方式也相同。 setw() 操纵符在这里很重要,若不使用它,相邻素数之间将没有空白。

文件在 ostream 对象销毁时会自动关闭(在本例中是程序结束时),也可以通过调用 ostream 对象的 close() 成员函数来关闭文件,例如:

outFile.close();

即使知道文件最终会自动关闭,当不再需要文件时,显式关闭文件也是一个好的做法。执行该语句后,将无法再向文件写入数据。可以通过运行程序两次(第二次使用较少的素数)并查看每次运行后的文件内容来验证文件内容是否被覆盖。

5. 文本模式下读取文件

要读取文件,需要创建一个封装该文件的 ifstream 对象,例如:

string filename {"D:\\Example_Data\\primes.txt"};
std::ifstream inFile {filename};

这将定义一个 ifstream 对象 inFile ,它封装了 D 盘 Example_Data 目录下的 primes.txt 文件,并以文本模式打开该文件,文件位置为 0,准备进行读取操作。如果要读取文件,该文件必须已经存在且不能为空,但实际情况并不总是如预期。如果尝试读取之前未准备好的文件会发生什么呢?

6. 检查文件流状态

ifstream 对象的定义而言,什么都不会发生,只是得到一个无法正常工作的文件流对象。要确定一切是否正常,必须测试文件的状态,有几种方法可以做到这一点:
- 调用 ifstream 对象的 is_open() 成员函数,如果文件已打开则返回 true ,否则返回 false 。如果文件不存在,显然无法打开。
- 调用文件流类从 ios 类继承的 fail() 函数,如果发生任何文件错误,该函数返回 true
- 对文件流对象使用 ! 运算符,该运算符在 ios 类中被重载,用于检查流状态指示器。当应用于流对象时,如果流状态不满意则返回 true ,使用 ! 运算符函数等同于调用流对象的 fail() 函数。

为确保流对象处于满意状态并准备好使用,可以这样编写代码:

if(!inFile)
{
  std::cout << "Failed to open file " << filename << std::endl;
  return 1;
}

可以用完全相同的方式测试输出文件流对象是否可用,因为 ofstream 类也继承自 ios ,它也继承了 fail() 函数并实现了 is_open() 函数。

流类还从 basic_ios 类继承了 operator bool() 的重载版本,如果流对象准备好进行输入输出操作,该函数返回 true 。该重载是显式的,因此只能通过显式转换来测试流,例如:

if(static_cast<bool>(inFile))
{
  // No error states so we can read the file...
}
else
{
  std::cout << "Failed to open file " << filename << std::endl;
  return 1;
}

虽然将其转换为 bool 类型提供了一种正向检查机制,但使用 !inFile.fail() 表达式更清晰。还有一个更简单的选项, basic_ios 类定义了 operator void*() ,该运算符函数重载了将对象转换为 void* 类型指针的操作,它不是显式定义的,因此编译器可以将其作为隐式转换插入。如果调用流对象的 fail() 函数返回 true ,该函数返回 nullptr ,否则返回非空指针。由于指针在 if while 循环表达式中会隐式转换为 bool 类型,因此可以这样验证输入文件流对象:

if(inFile)
{
  // No error states so we can read the file...
}
else
{
  std::cout << "Failed to open file " << filename << std::endl;
  return 1;
}

这与前面的代码片段等效,但输入更少。后续会进一步探讨流错误状态。

文本模式下读取文件与从 cin 读取类似,使用提取运算符的方式完全相同。然而,不一定知道文件中有多少个数据值,那么如何知道何时到达文件末尾呢? ofstream 类从 basic_ios 继承的 eof() 函数提供了一个简洁的解决方案,当到达文件末尾(EOF)时,它返回 true ,因此可以持续读取数据直到该情况发生。

7. 读取文件示例

现在已经了解了输入文件流的工作原理,下面是一个读取前面示例写入的文件并将素数输出到屏幕的示例:

// Ex17_02.cpp
// Reading the primes file
#include <fstream>
#include <iostream>
#include <iomanip>
#include <string>
using ulong = unsigned long long;

int main()
{
  std::string filename {"D:\\Example_Data\\primes.txt"};    // Input file name
  std::ifstream inFile {filename};                          // Create input stream object

  // Make sure the file stream is good
  if (!inFile)
  {
    std::cout << "Failed to open file " << filename << std::endl;
    return 1;
  }

  ulong aprime {};
  size_t count {};
  size_t perline {6};
  while (true)                                              // Continue until EOF is found
  {
    inFile >> aprime;                                       // Read a value from the file
    if (inFile.eof()) break;                                // Break if EOF reached

    std::cout << (count++ % perline == 0 ? "\n" : "") << std::setw(10) << aprime;
  }
  std::cout << "\n" << count << " primes read from " << filename << std::endl;
}

输出将是 Ex17_01.cpp 写入的素数列表,文件中每行写入 5 个素数,这里每行输出 6 个素数以作区别。换行符是 << 运算符函数在流输入时会忽略的空白字符,因此对输出到 cout 没有影响。然而,尽管在这种情况下使用文本编辑器查看该文件很容易,但一般来说读取该文件存在潜在问题。每个值以宽度为 10 的字段写入文件,当值少于 10 位时没问题,但如果其中一个值为 10 位或更多位,相邻值之间将没有空白。这种情况在这里不太可能发生,但一般情况下是可能的。相邻值之间没有空白会导致文件无法正确读取, >> 运算符依赖于空白来分隔相邻值,如果没有空白,输入过程无法确定一个值的结束位置和下一个值的开始位置,连续的数字序列将被作为单个输入值读取。因此,在文本模式下写入文件时,最好确保相邻值之间至少有一个换行符。

文件输入流对象是 ifstream 类型,将文件名传递给构造函数的方式与创建 ofstream 对象类似。假设文件存在,将以文件位置为 0(文件内容的开始位置)打开文件准备读取。创建 inFile 后,在 if 语句中对该对象应用 ! 运算符来验证文件是否存在,若在文件名中引入错误(例如拼写为 prrimes.txt ),可以看到该检查失败。

使用 while 无限循环和流对象的 >> 运算符从 inFile 读取素数,通过调用 inFile eof() 函数的 if 语句来确定是否到达文件末尾,从而结束循环。需要注意的是,EOF 条件是在读取文件中最后一个数据项之后设置的,此时文件位置将是文件末尾(最后一个字节之后),而不是读取最后一个数据项时设置。这意味着只有在所有数据都已读取(或者通过其他方式将文件位置设置为末尾)并且执行另一次读取操作时,对 EOF 的检查才会为 true 。当读取操作检测到 EOF 时,流对象中的 EOF 标志会被设置,但不会传输数据,因此在读取最后一个素数后执行的读取操作不会改变 aprime 的值。

综上所述,在 C++ 中进行文件输入输出操作时,需要注意流操纵符的使用、文件流的创建和管理以及文件状态的检查。同时,在文本模式下读写文件时,要考虑数据的分隔和文件格式的问题,以确保数据的正确读写。

C++文件输入输出操作全解析

8. 总结与注意事项

在 C++ 中进行文件输入输出操作时,有多个关键要点需要牢记,以下为你详细总结:

8.1 流操纵符
  • 无参操纵符 :像 hex std::left 这类无参操纵符,本质是指向特定函数的指针。使用时,会调用接受函数指针作为参数的 operator<<() 版本。例如 std::cout << std::hex << i; 会调用相应函数设置输出格式。
  • 带参操纵符 :定义在 iomanip 头文件中,如 setprecision setw setfill setbase 等。使用时将函数调用插入流中,为输出设置特定格式。但要注意,无参操纵符不能加括号,避免与带参操纵符混淆。
  • 标志设置函数 setiosflags resetiosflags 可通过指定掩码设置或重置控制流格式的标志,掩码由按位或运算符组合 ios_base 类中的标志构成。
8.2 文件流类型
  • ifstream :用于文件读取,继承自 istream ,只能进行读操作。
  • ofstream :用于文件写入,继承自 ostream ,只能进行写操作。
  • fstream :可进行读写操作,继承自 iostream

文件流对象创建时可关联文件,也可后续建立连接。打开文件后,可根据需求进行读写操作,还能更改关联的文件。

8.3 文本模式文件操作
  • 写入文件 :使用 ofstream 对象,默认以文本模式打开文件。若文件不存在则创建,存在则覆盖内容。写入操作使用 << 运算符,与标准输出流操作类似。
  • 读取文件 :使用 ifstream 对象,文件必须存在且非空。读取前需检查文件状态,可通过 is_open() fail() ! 运算符进行检查。读取时使用 >> 运算符,通过 eof() 函数判断是否到达文件末尾。
8.4 数据处理与格式问题
  • 素数判断优化 :在判断素数时,只检查到候选值的平方根的素数除数,可提高执行效率。
  • 数据分隔 :文本模式下写入文件时,要确保相邻值之间有空白分隔,最好使用换行符,避免读取时出现错误。
9. 操作流程梳理

为了更清晰地展示 C++ 文件输入输出操作的流程,下面通过 mermaid 流程图进行说明:

graph LR
    classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px
    classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px

    A([开始]):::startend --> B(选择操作类型):::process
    B -->|写入文件| C(创建 ofstream 对象):::process
    C --> D{文件是否存在}:::decision
    D -->|是| E(打开文件并覆盖内容):::process
    D -->|否| F(创建新文件):::process
    E --> G(写入数据):::process
    F --> G
    G --> H(关闭文件):::process
    B -->|读取文件| I(创建 ifstream 对象):::process
    I --> J{文件是否存在且非空}:::decision
    J -->|是| K(打开文件):::process
    J -->|否| L(输出错误信息):::process
    K --> M(读取数据):::process
    M --> N{是否到达文件末尾}:::decision
    N -->|否| M
    N -->|是| O(关闭文件):::process
    H --> P([结束]):::startend
    L --> P
    O --> P
10. 常见问题及解决方案

在进行 C++ 文件输入输出操作时,可能会遇到以下常见问题及相应的解决方案:

问题描述 可能原因 解决方案
文件无法打开 文件不存在、路径错误、文件被其他程序占用 检查文件路径和名称,确保文件存在;关闭占用该文件的其他程序
数据读取错误 相邻值之间无空白分隔、文件格式不匹配 在写入文件时确保相邻值之间有空白分隔,最好使用换行符;检查文件格式是否与读取代码匹配
编译器警告 数据类型转换可能导致数据丢失 对可能出现数据丢失的转换进行显式类型转换,如将 std::sqrt() 的结果显式转换为所需整数类型
11. 代码优化建议

为了提高代码的性能和可读性,可对文件输入输出代码进行以下优化:

  • 错误处理 :在文件操作前后进行全面的错误检查,确保程序的健壮性。例如,在打开文件和读取写入数据时,及时处理可能出现的错误。
  • 资源管理 :使用 RAII(资源获取即初始化)原则,确保文件在使用完毕后自动关闭,避免资源泄漏。 ofstream ifstream 对象在销毁时会自动关闭关联的文件,可充分利用这一特性。
  • 代码复用 :将文件操作的通用逻辑封装成函数或类,提高代码的复用性。例如,将文件打开、读取和写入操作封装成独立的函数,方便在不同场景下调用。
12. 拓展应用

C++ 文件输入输出操作在实际应用中非常广泛,以下是一些拓展应用场景:

  • 数据存储与读取 :将程序运行过程中的数据保存到文件中,下次运行时读取文件恢复数据,实现数据的持久化。
  • 日志记录 :将程序的运行信息、错误信息等写入日志文件,方便后续的调试和分析。
  • 文件处理 :对文本文件、二进制文件进行处理,如数据提取、格式转换等。

通过掌握 C++ 文件输入输出操作的基本原理和技巧,结合实际需求进行灵活应用,可开发出高效、稳定的文件处理程序。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值