67、文件输入输出操作全解析

文件输入输出操作全解析

1. 相对位置移动

在移动到相对位置时,可以使用 seekg() seekp() 的双参数版本。第一个参数是偏移量,类型为 off_type 的整数值,第二个参数必须是 ios 类中定义的表 1 中的值之一。

描述
beg 偏移量相对于文件中的第一个字符。
cur 偏移量相对于当前文件位置。
end 偏移量相对于文件中最后一个字符之后的位置。

这些常量不是流位置,而是 std::ios_base::seekdir 类型的常量。偏移值相对于 ios::beg 必须为正,相对于 ios::end 必须为负。例如,相对于 ios::end 的偏移量为 0 会将位置设置到文件末尾,可用于追加文件。相对于 ios::cur ,可以使用正或负的偏移值向任一方向移动。

以下是移动文件位置并读取数据的示例:

inFile.seekg(10, inFile.beg) >> value;

此语句将文件位置移动到距文件开头十个字符的偏移处,并从该点读取一个数据项到 value 中。

也可以使用 seekp() 函数移动到下一次输出操作的起始位置,其参数与 seekg() 完全相同。下次写入流将从新位置开始覆盖字符。相对查找操作在文本模式下不起作用,绝对查找也不可靠,因此建议避免在文本模式下进行查找操作。

还可以确定流的长度:

auto stream_length = inFile.seekg(0, inFile.end).tellg();

seekg() 函数返回文件流,用于调用同一流对象的 tellg() 。由于 seekg() 将位置移动到最后一个字符之后, tellg() 函数返回的值将是流中的字符数。

2. 无格式流操作

除了文本模式下格式化 I/O 的插入和提取运算符外,流类还有用于无格式数据传输的成员函数。提取运算符将空白字符视为分隔符,而无格式流输入函数不会跳过空白字符,它们会像读取其他字符一样读取并存储空白字符。

2.1 无格式流输入

istream 类定义了大量无格式输入函数,以下是一些常用的函数:
- get() 函数 :有两种形式用于从流中读取单个字符。
- std::istream::int_type get(); :从流中读取单个字符并返回,若到达文件末尾,返回表示文件结束的字符,若无法读取字符,将设置 ios::failbit 错误标志。
- std::istream& get(char& ch); :从流中读取单个字符并存储在 ch 中,返回流对象的引用。
- peek() 函数 :与第一个 get() 函数类似,读取流中的下一个字符,但会将字符留在流中,可再次读取,返回读取的字符。
- unget() 函数 :将最后读取的字符返回到流中,移动文件位置到该字符,返回 istream 对象的引用。例如:

std::ifstream& skipnondigits(std::ifstream& in)
{
  std::istream::int_type ch {};
  while (true)
  {
    ch = in.get();
    if(ch == EOF) break;
    if(std::isdigit(ch))
    {
      in.unget();
      break;
    }
  }
  return in;
}

此函数的作用是移动文件位置,直到找到数字或到达文件末尾。调用者可以测试是否到达文件末尾,以判断是否找到数字:

if(!skipnondigits(in).eof())
{
  // Read a numerical value...
}
  • putback() 函数 :与 unget() 效果类似,但需要指定要放回流中的字符作为参数。例如 in.putback(ch); ,参数必须是最后读取的字符,否则结果未定义,该函数也返回流的引用。
  • 读取字符序列的 get() 函数
  • std::istream& get(char* pArray, std::streamsize n); :从流中读取最多 n - 1 个字符并存储在数组 pArray 中,末尾添加空终止符。读取到换行符、文件结束或已读取 n - 1 个字符时停止。若读取到换行符,不存储该字符,但始终在读取的字符序列末尾添加 '\0'
  • std::istream& get(char* pArray, std::streamsize n, char delim); :与上一个函数类似,但可以指定分隔符 delim 来结束输入过程,找到分隔符时,不存储该字符,将其留在流中。
  • getline() 函数 :与读取文本行的 get() 函数几乎等效,但 getline() 会从流中移除分隔符。
std::istream& getline(char* pArray, std::streamsize n);
std::istream& getline(char* pArray, std::streamsize n, char delim);
  • gcount() 函数 :调用无格式输入函数后,可以调用 gcount() 确定读取的字符数,返回上一次无格式输入操作读取的字符数,类型为 std::streamsize
  • read() 函数 :从流中读取指定数量的字符(如果可用),包括换行符和空字符。若在读取 n 个字符之前到达文件末尾,将在输入对象中设置 ios::failbit
std::istream& read(char* pArray, std::streamsize n);
  • readsome() 函数 :与 read() 类似,但返回读取的字符数。若流中可用字符少于 n 个,将设置 ios::eofbit 标志。
std::streamsize readsome(char* pstr, std::streamsize n);
  • ignore() 函数 :跳过输入流中的字符,最多读取 n 个字符并丢弃,读取到 delim 字符或读取了 n 个字符时结束。参数 n delim 的默认值分别为 1 和表示文件结束的字符。
inFile.ignore(); // 跳过一个字符
inFile.ignore(20); // 跳过 20 个字符直到文件末尾
inFile.ignore(20, '\n'); // 跳过当前行中的 20 个字符

2.2 无格式流输出

与众多无格式输入函数形成鲜明对比的是,文本模式下用于无格式输出到流的函数只有两个: put() write()
- put() 函数 :将单个字符 ch 写入流,并返回流对象的引用。

std::ostream& put(char ch);
  • write() 函数 :从数组 pArray 向流中写入 n 个字符,可以写入任何类型的字符,包括空字符。
std::ostream& write(const char* pArray, std::streamsize n);
  • flush() 函数 :通常,输出到流是缓冲的,有时希望无论缓冲区是否已满,都将流缓冲区的内容写入流。调用 ostream 对象的 flush() 成员可以实现这一点,它将流缓冲区的内容写入设备并返回流对象的引用。

3. 流输入/输出错误

所有流类都将流的状态存储在一个整数数据成员中,该成员通常为 0,发生流错误时,状态将设置为一个或多个错误标志的组合。这些标志由 std::ios_base::iostate 类型的整数常量(位掩码)定义。

标志 含义
ios::badbit 当流处于无法继续使用的状态时设置,例如发生 I/O 错误,不可恢复。
ios::eofbit 到达文件末尾时设置。
ios::failbit 输入操作未读取到预期字符或输出操作未能成功写入字符时设置,通常由于转换或格式化错误或读取超出流末尾,后续操作将失败,但情况可能可恢复。
ios::goodbit 定义为 0,若流状态为 goodbit ,则没有错误。

如果读取到文件结束符,流状态将为 ios::eofbit ;如果在读取流时发生严重错误且无法读取任何字符, ios::badbit ios::failbit 标志都将被设置。

一旦设置了标志,除非重置,否则标志将保持设置状态。可以调用 clear() 重置流对象的所有错误标志:

infile.clear(); // 清除所有错误状态

可以通过以下方式测试流的状态:

while(!inFile.fail())
{
  // Read from inFile...
}
inFile.clear(); // 清除任何错误状态

也可以写成:

while(inFile)
{
  // Read from inFile...
}
inFile.clear(); // 清除任何错误状态

流类继承了测试单个标志状态的成员函数,如下表所示:

函数 操作
bad() 如果流对象中设置了 badbit ,则返回 true
eof() 如果流对象中设置了 eofbit ,则返回 true
fail() 如果流对象中设置了 failbit badbit ,则返回 true
good() 如果流对象中设置了 goodbit (意味着其他标志未设置),则返回 true

可以进行更详细的流状态分析:

while(inFile)
{
  // Read from inFile...
}
if(inFile.bad())
{
  std::cout << "Non-recoverable file input error." << std::endl;
  std::exit(1);
}
inFile.clear();

4. 输入/输出错误与异常

I/O 操作发生错误时可能会抛出异常,流错误的异常类型为 std::ios_base::failure ,可以使用 ios::failure 作为异常类型。流对象的一个掩码成员决定了设置特定流状态标志时是否抛出异常。可以通过将掩码传递给流对象的 exceptions() 成员来设置该掩码,指定希望抛出异常的标志位。

例如,若希望在名为 inFile 的流的任何标志位被设置时抛出异常,可以使用以下语句:

inFile.exceptions(ios::badbit | ios::eofbit | ios::failbit);

通常,最好使用前面讨论的方法测试错误标志,而不是使用异常处理 I/O 错误,至少对于 eofbit failbit 标志是这样。大多数情况下,处理的是 failbit eofbit 标志,因为它们是处理流输入和输出的正常过程的一部分。大多数开发环境的默认设置是流错误不抛出异常。

可以通过调用无参数的 exceptions() 版本来检查是否会抛出异常,它返回 iostate 类型的值,反映哪些错误标志会导致抛出异常:

std::ios::iostate willthrow {inFile.exceptions()};
if(willthrow & std::ios::badbit)
  std::cout << "Causing badbit to be set will throw an exception" << std::endl;

5. 二进制模式下的流操作

在许多情况下,文本模式并不合适或方便,有时会导致困难。例如,在某些系统上换行符会转换为两个字符,这使得相对查找操作对于要在两种环境中运行的程序不可靠。使用二进制模式可以避免这些复杂性,使流操作更加简单。

以下是一个复制任何文件的程序示例:

// Ex17_04.cpp
// Copying files
#include <iostream>                                   // For standard streams
#include <cctype>                                     // For character functions
#include <fstream>                                    // For file streams
#include <string>                                     // For string type
#include <stdexcept>                                  // For standard exceptions
using std::string;
using std::ios;

void validate_files(string source, string target);    // Validate the files
int main(int argc, char* argv[])
try
{
  // Verify correct number of arguments
  if (argc != 3)
    throw std::invalid_argument {"Input and output file names required.\n"};

  // Check for output file identical to input file
  const string source {argv[1]};                      // The input file
  const string target {argv[2]};                      // The destination for the copy
  if (source == target)
    throw std::invalid_argument {string("Cannot copy ") + source + " to itself.\n"};
  validate_files(source, target);

  // Create file streams
  std::ifstream in {source, ios::in | ios::binary};
  std::ofstream out(target, ios::out | ios::binary | ios::trunc);

  // Copy the file
  char ch {};
  while (in.get(ch))
    out.put(ch);

  if (in.eof())
    std::cout << source << " copied to  " << target << " successfully." << std::endl;
  else
    std::cout << "Error copying file" << std::endl;
}
catch (std::exception& ex)
{
  std::cout << std::endl << typeid(ex).name() << ": " << ex.what();
  return 1;
}

// Verify input file exists and check output file for overwriting
void validate_files(string infile, string outfile)
{
  std::ifstream in {infile, ios::in | ios::binary};
  if (!in)                                            // Stream object
    throw ios::failure {string("Input file ") + infile + " not found"};

  // Check if output file exists
  std::ifstream temp {outfile, ios::in | ios::binary};
  if (temp)
  { // If the file stream object is ok then the output file exists
    temp.close();                                     // Close the stream
    std::cout << outfile << " exists, do you want to  overwrite it? (y or n): ";
    if (std::toupper(std::cin.get()) != 'Y')
    {
      std::cout << "Destination file contents to be kept. Terminating..." << std::endl;
    }
  }
}

此程序需要输入文件和输出文件的名称作为命令行参数。例如,在 Windows 系统上运行:

Ex17_04.exe D:\Example_Data\primes.txt D:\Example_Data\primes_copy.txt

输出结果可能为:

D:\Example_Data\primes.txt copied to D:\Example_Data\primes_copy.txt successfully.

综上所述,文件输入输出操作涵盖了相对位置移动、无格式流操作、错误处理以及二进制模式操作等多个方面。掌握这些操作可以更高效地处理文件数据,避免因模式选择不当或错误处理不及时导致的问题。无论是简单的文件复制,还是复杂的数据读写,都能通过合理运用这些知识和函数来实现。

6. 操作总结与流程梳理

为了更清晰地理解上述文件输入输出操作,下面对各个操作进行总结,并梳理其操作流程。

6.1 相对位置移动操作

相对位置移动主要使用 seekg() seekp() 函数,操作步骤如下:
1. 确定偏移量和基准位置 :偏移量是 off_type 类型的整数值,基准位置可以是 beg (相对于文件开头)、 cur (相对于当前位置)、 end (相对于文件末尾)。
2. 调用函数移动位置 :根据需求调用 seekg() (用于输入流)或 seekp() (用于输出流)函数。
3. 进行后续操作 :移动位置后,可以进行读取或写入操作。

例如,将文件位置移动到距文件开头十个字符的偏移处并读取数据:

inFile.seekg(10, inFile.beg) >> value;
6.2 无格式流输入操作

无格式流输入操作涉及多个函数,操作流程如下:

graph TD;
    A[开始] --> B[选择输入函数];
    B --> C{是否为get()系列};
    C -- 是 --> D[读取单个字符或字符序列];
    C -- 否 --> E{是否为peek()};
    E -- 是 --> F[读取下一个字符但不移动位置];
    E -- 否 --> G{是否为unget()或putback()};
    G -- 是 --> H[将字符放回流中];
    G -- 否 --> I{是否为read()或readsome()};
    I -- 是 --> J[读取指定数量的字符];
    I -- 否 --> K[是否为ignore()];
    K -- 是 --> L[跳过指定数量的字符];
    D --> M[判断是否到达文件末尾或满足条件];
    F --> M;
    H --> M;
    J --> M;
    L --> M;
    M -- 是 --> N[结束];
    M -- 否 --> B;
6.3 无格式流输出操作

无格式流输出操作主要使用 put() write() 函数,操作步骤如下:
1. 选择输出函数 :根据需要输出单个字符还是字符序列,选择 put() write() 函数。
2. 调用函数输出数据 :调用相应的函数将数据输出到流中。
3. 刷新缓冲区(可选) :如果需要立即将缓冲区内容写入流,可以调用 flush() 函数。

例如,输出单个字符:

outFile.put('A');

输出字符序列:

const char* str = "Hello";
outFile.write(str, 5);
6.4 错误处理操作

错误处理操作主要涉及错误标志的设置、检查和重置,操作流程如下:

graph TD;
    A[开始] --> B[进行I/O操作];
    B --> C{是否发生错误};
    C -- 是 --> D{错误类型};
    D --> E{是否为badbit};
    E -- 是 --> F[严重错误,可能不可恢复];
    E -- 否 --> G{是否为eofbit};
    G -- 是 --> H[到达文件末尾];
    G -- 否 --> I{是否为failbit};
    I -- 是 --> J[输入或输出操作失败];
    F --> K[处理错误或终止程序];
    H --> L[可选择重置错误标志继续操作];
    J --> L;
    C -- 否 --> M[继续正常操作];
    L --> M;
    M --> N[结束];

7. 注意事项与最佳实践

在进行文件输入输出操作时,有一些注意事项和最佳实践可以遵循,以提高代码的可靠性和效率。

7.1 文本模式与二进制模式的选择
  • 文本模式 :适用于处理文本文件,如配置文件、日志文件等。但在不同系统上,换行符的处理可能不同,相对查找操作可能不可靠。
  • 二进制模式 :适用于处理二进制文件,如图像文件、音频文件等。使用二进制模式可以避免换行符转换等问题,使流操作更加简单。
7.2 错误处理
  • 检查错误标志 :在进行 I/O 操作后,及时检查错误标志,如 fail() eof() bad() 等,以确保操作成功。
  • 重置错误标志 :当到达文件末尾或发生可恢复的错误时,使用 clear() 函数重置错误标志,以便继续操作。
  • 异常处理 :虽然可以使用异常处理 I/O 错误,但对于 eofbit failbit 标志,建议优先使用错误标志检查,而不是依赖异常。
7.3 缓冲区管理
  • 刷新缓冲区 :在需要立即将缓冲区内容写入流时,使用 flush() 函数刷新缓冲区。
  • 避免频繁刷新 :频繁刷新缓冲区会降低性能,应根据实际情况合理安排刷新操作。

8. 示例代码的分析与扩展

以复制文件的示例代码为例,对其进行分析并探讨可能的扩展。

8.1 示例代码分析

示例代码通过 get() put() 函数逐字符复制文件,主要步骤如下:
1. 验证命令行参数 :确保输入和输出文件名称正确。
2. 检查文件是否相同 :避免将文件复制到自身。
3. 验证文件存在性 :检查输入文件是否存在,输出文件是否需要覆盖。
4. 创建文件流 :以二进制模式打开输入和输出文件流。
5. 复制文件 :逐字符读取输入文件并写入输出文件。
6. 检查复制结果 :根据是否到达文件末尾判断复制是否成功。

8.2 代码扩展

可以对示例代码进行扩展,实现更复杂的功能,如:
- 批量文件复制 :支持一次复制多个文件。
- 进度显示 :在复制过程中显示复制进度。
- 错误日志记录 :将复制过程中的错误信息记录到日志文件中。

以下是一个简单的批量文件复制的扩展示例:

#include <iostream>
#include <cctype>
#include <fstream>
#include <string>
#include <stdexcept>
#include <vector>

using std::string;
using std::ios;

void validate_files(string source, string target);

int main(int argc, char* argv[])
try
{
    if (argc < 3 || (argc - 1) % 2 != 0)
        throw std::invalid_argument {"Pairs of input and output file names required.\n"};

    for (int i = 1; i < argc; i += 2)
    {
        const string source {argv[i]};
        const string target {argv[i + 1]};
        if (source == target)
            throw std::invalid_argument {string("Cannot copy ") + source + " to itself.\n"};
        validate_files(source, target);

        std::ifstream in {source, ios::in | ios::binary};
        std::ofstream out(target, ios::out | ios::binary | ios::trunc);

        char ch {};
        while (in.get(ch))
            out.put(ch);

        if (in.eof())
            std::cout << source << " copied to  " << target << " successfully." << std::endl;
        else
            std::cout << "Error copying file: " << source << std::endl;
    }
}
catch (std::exception& ex)
{
    std::cout << std::endl << typeid(ex).name() << ": " << ex.what();
    return 1;
}

void validate_files(string infile, string outfile)
{
    std::ifstream in {infile, ios::in | ios::binary};
    if (!in)
        throw ios::failure {string("Input file ") + infile + " not found"};

    std::ifstream temp {outfile, ios::in | ios::binary};
    if (temp)
    {
        temp.close();
        std::cout << outfile << " exists, do you want to  overwrite it? (y or n): ";
        if (std::toupper(std::cin.get()) != 'Y')
        {
            std::cout << "Destination file contents to be kept. Terminating..." << std::endl;
        }
    }
}

通过以上扩展,程序可以一次复制多对文件,提高了复制效率。

9. 总结

文件输入输出操作是编程中常见的任务,涉及相对位置移动、无格式流操作、错误处理和二进制模式操作等多个方面。掌握这些操作的原理和使用方法,可以帮助我们更高效地处理文件数据。在实际应用中,需要根据具体需求选择合适的操作模式和函数,并注意错误处理和缓冲区管理。通过合理运用这些知识和技巧,可以编写出更加健壮、高效的文件处理程序。同时,对示例代码进行扩展和优化,可以满足不同场景下的需求。希望本文的内容能对大家理解和应用文件输入输出操作有所帮助。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值