64、文件输入输出:流操作全解析

文件输入输出:流操作全解析

一、理解流

标准库提供的输入输出(I/O)功能涉及使用流。流是输入设备或输出设备的抽象表示,是程序中数据的顺序源或目的地,并且流由类类型表示。可以将流想象成字节序列在外部设备和计算机主内存之间流动。可以向输出流写入数据,从输入流读取数据,有些流同时具备输入和输出能力。从根本上说,所有的输入和输出都是从某个外部设备读取或写入字节序列。

当从外部设备读取数据时,需要正确解释数据。从外部源读取的字节可能是 8 位字符序列、UCS 字符序列、各种类型的二进制值,或者是它们的混合。无法从流中的数据判断其具体类型,必须事先了解数据的结构和类型,并相应地进行读取和解释。

二、数据传输模式

数据与流之间的传输有两种模式:文本模式和二进制模式。
1. 文本模式 :数据被解释为字符序列,通常组织成以换行符 '\n' 结尾的一行或多行。数值由一个或多个空白字符分隔。在文本模式下,流在从物理设备读取或写入换行符时可能会进行转换,这种转换是否发生以及字符如何改变取决于系统。例如,在 Microsoft Windows 系统中,单个换行符会作为两个字符(回车符和换行符)写入流;而在 Unix 系统中,换行符保持为单个字符。
2. 二进制模式 :数据不进行转换,原始字节在内存和流之间直接传输。

三、文本模式操作

在文本模式下,可以使用提取运算符 >> 和插入运算符 << 读写各种类型的数据,就像在从 std::cin 读取和向 std::cout 写入时一样。这些是文本模式下的格式化 I/O 操作。二进制数值数据(如整数和浮点数)在写入流之前会转换为字符表示,读取时则进行相反的过程。不过,以文本模式写入的数据在文件中仍然只是字节序列,也可以以二进制模式读取,但以二进制模式读取文本模式写入的数据可能没有意义。

四、二进制模式操作

在二进制模式下,无论数据类型如何,都是读写字节。例如,内存中占用四个字节的 int 类型二进制值会作为四个字节写入,4 字节的 float 值或四个 char 类型字符序列也是如此。这意味着数据会精确地按照计算机内部的形式记录,避免了文本模式下浮点数转换可能引入的小误差。写入文件的字节只是字节,没有关于其原始类型的指示,因此如何解释这些字节取决于用户,必须知道写入文件的数据类型才能合理读取。

二进制读写操作可以针对单个字节、给定数量的字节或由某种分隔符终止的字节序列。这些是未格式化的输入/输出操作,因为数据在内存和流之间传输时不进行修改。写入流的数据可以是字符串和各种类型的二进制数值的任意组合,但最终写入流的是构成内存中数据值的字节。

五、使用流的优点

使用流进行 I/O 操作的主要原因是使操作代码与物理设备无关,这有两个优点:
1. 无需担心每个设备的详细机制,这些都在幕后处理。
2. 程序可以在各种不同的物理设备上运行,而无需更改源代码。

输出流的物理实体可以是任何能够传输字节序列的设备,文件流 I/O 通常是针对磁盘或固态硬盘上的文件。标准库定义了三个标准输出流对象 cout cerr clog ,它们通常与显示屏相关联。 std::cout 是标准输出流, std::cerr std::clog 都连接到标准错误流,用于错误报告。 cerr 是无缓冲的,数据会立即写入输出设备;而 clog 是有缓冲的,只有当缓冲区满时才会写入数据。

输入流原则上可以是任何串行数据源,但通常是磁盘文件或键盘。流是类类型的对象,标准流是预定义的对象,与系统上的特定外部设备相关联。

以下是一些设备及其对应的流类型:
| 设备 | 流类型 | 模式 |
| ---- | ---- | ---- |
| 键盘 | std::cin | 文本模式 |
| 显示器 | std::cout std::cerr std::clog | 文本模式 |
| 磁盘或固态硬盘 | my_in_stream my_out_stream | 文本或二进制模式 |

六、流类

流 I/O 涉及多个类,其中大部分由类模板定义。主要的流类及其关系如下:

graph TD;
    ios_base --> ios;
    ios --> istream;
    ios --> ostream;
    istream --> ifstream;
    ostream --> ofstream;
    istream & ostream --> iostream;
    iostream --> fstream;

ios_base 是普通类,其他类是模板实例。例如, istream basic_istream<char> 模板的实例, ios basic_ios<char> 模板的实例。所有流类共享一个公共基类 ios ,它定义了记录流状态和有效格式模式的标志。因此,所有提供 I/O 操作的流类共享一组公共的状态和格式标志,以及查询和设置这些标志的函数。

iostream 头文件定义了标准流对象,它包含了 ios istream ostream streambuf 头文件。标准输入流 cin istream 类型的对象,标准输出流 cout cerr clog ostream 类型的对象。文件处理的流类 ifstream fstream ofstream 都以 istream ostream 或两者为基类,因此使用标准流 cin cout 的功能也适用于文件流。需要注意的是, fstream 派生自 iostream ,而不是 ifstream ofstream ,因此文件流类彼此独立。

流类模板通常有指定特定流字符集的类型参数,图中显示的类名是处理 char 类型字符的流的别名。还有处理 wchar_t 类型字符的流的别名,如 wistream wostream 等,但它们的工作方式与字节流类相同。

流类模板的别名定义如下:

typedef basic_ios<char>      ios;
typedef basic_istream<char>  istream;
typedef basic_ostream<char>  ostream;
typedef basic_iostream<char> iostream;
typedef basic_ifstream<char> ifstream;
typedef basic_ofstream<char> ofstream;
typedef basic_fstream<char>  fstream;
七、标准流对象

标准流在 iostream 头文件中作为 std 命名空间中的对象定义如下:

extern istream cin;
extern ostream cout;
extern ostream cerr;
extern ostream clog;

iostream 头文件还定义了等效的宽字符流对象:

extern wistream wcin;
extern wostream wcout;
extern wostream wcerr;
extern wostream wclog;

已经广泛使用过标准输入流 cin 和标准输出流 cout cerr clog 的使用方式与 cout 相同。接下来将重点介绍流操作的背景知识,以及如何将这些技术应用于文件操作。

八、流插入和提取操作

与标准流对象一起使用的插入运算符 << 和提取运算符 >> 对其他类型的流对象同样适用。标准流仅在文本模式下操作,因为数据源和目的地本质上是基于字符的。插入和提取运算符主要用于在数据的内部二进制表示和外部字符表示之间进行转换。当与标准流以外的流一起使用时,这些运算符仅在文本模式下使用。

1. 流提取操作

istream 类的成员函数模板实现了 operator>>() 函数,支持从任何输入流读取以下类型的数据:
- short
- int
- long
- long long
- unsigned
- unsigned int
- unsigned long
- unsigned long long
- float
- double
- long double
- bool
- void*

支持 void* 类型的 operator>>() 函数允许将地址值读入任何类型的指针,但指向 char 类型的指针(表示以空字符结尾的字符串)是特殊情况。非成员模板定义了 operator<<() 的重载版本,用于将字符读入 char signed char unsigned char 类型的变量,以及将不包含空白字符的字符序列作为 C 风格字符串读入 char* signed char* unsigned char* 类型的数组。

例如,以下代码:

int i {};
double x {};
std::cin >> i >> x;

可以转换为:

(std::cin.operator>>(i)).operator>>(x);

每次使用提取运算符都会调用一次 operator>>() 函数,该函数返回对调用它的流对象的引用,因此可以使用返回值调用下一个运算符函数。 operator>>() 的参数必须是引用,以便函数将从流中读取的数据值存储在作为参数传递的变量中。

需要注意的是,空白字符被视为值之间的分隔符并被忽略,因此不能使用 istream 类的任何 operator>>() 函数读取空白字符。如果需要读取包含空白字符的文本行,可以使用 std::getline() 函数。

2. 流插入操作

ostream 类重载了 operator<<() 函数,用于格式化输出基本类型的值。向 cout 输出数据的操作与从 cin 输入数据类似。例如:

std::cout << i << ' ' << x;

可以转换为三个 operator<<() 函数调用:

std::cout.operator<<(std::cout.operator<<(i), ' ').operator<<(x);

所有版本的 operator<<() 函数都返回对调用它的流对象的引用,因此可以使用返回值调用下一个 operator<<() 函数。将单个字符和以空字符结尾的字符串写入流的 operator<<() 函数作为非成员函数实现。

输出单个字符到输出流的函数原型如下:

ostream& operator<<(ostream& out, char ch);
ostream& operator<<(ostream& out, signed char ch);
ostream& operator<<(ostream& out, unsigned char ch);

输出以空字符结尾的字符串的函数原型如下:

ostream& operator<<(ostream& out, const char* str);
ostream& operator<<(ostream& out, const signed char* str);
ostream& operator<<(ostream& out, const unsigned char* str);

可以使用指向 char 类型的指针输出字符串,但指向其他类型的指针总是作为地址写入流。如果要输出指针中包含的地址而不是字符串,必须将其显式转换为 void* 类型。例如:

const char* message {"More is less and less is more."};
std::cout << message;  // 输出字符串
std::cout << static_cast<void*>(message);  // 输出地址
九、流操纵符

已经广泛使用操纵符来控制文本模式下的流输出格式。以下是一些基本的流操纵符及其作用:
| 操纵符 | 作用 |
| ---- | ---- |
| dec | 将整数的默认基数设置为十进制 |
| oct | 将整数的默认基数设置为八进制 |
| hex | 将整数的默认基数设置为十六进制 |
| fixed | 以定点表示法输出浮点数,不显示指数 |
| scientific | 以科学记数法输出浮点数,显示指数 |
| hexfloat | 启用十六进制浮点数格式 |
| defaultfloat | 恢复浮点数的默认格式 |
| boolalpha | 将 bool 值表示为字母形式(如 true false ) |
| noboolalpha | 将 bool 值表示为 1 0 (默认) |
| showbase | 显示八进制( 0 前缀)和十六进制( 0x 前缀)整数的基数指示 |
| noshowbase | 省略八进制和十六进制整数的基数指示 |
| showpoint | 始终将浮点数输出到流中并带有小数点 |
| noshowpoint | 输出整数形式的浮点数时不显示小数点 |
| showpos | 为正整数显示 + 前缀 |
| noshowpos | 不为正整数显示 + 前缀 |
| skipws | 在输入时跳过空白字符 |
| noskipws | 在输入时不跳过空白字符 |
| uppercase | 十六进制数字 A F 以及指数使用大写字母 |
| nouppercase | 十六进制数字 a f 以及指数使用小写字母 |
| internal | 插入“填充字符”以将输出填充到字段宽度 |
| left | 在输出字段中左对齐值 |
| right | 在输出字段中右对齐值 |
| endl | 向流缓冲区写入换行符并将缓冲区内容写入流 |
| flush | 将流缓冲区中的数据写入流 |

所有这些操纵符都可以使用 << 运算符直接插入到流中。例如:

int i {1000};
std::cout << std::hex << std::uppercase << i << std::endl;

这将以十六进制大写形式输出整数 i 的值,并在后面添加换行符,输出结果为 3E8

文件输入输出:流操作全解析

十、流操纵符的工作原理

操纵符的工作方式比较特别。当使用插入运算符 << 插入操纵符时,调用的 operator<<() 函数与处理普通数据类型的函数不同。前面介绍的 operator<<() 函数主要处理特定类型数据的输出,但操纵符的作用并不是直接输出数据,而是修改流的状态或行为。

例如, std::hex 操纵符会将流的整数输出基数设置为十六进制。当执行 std::cout << std::hex << i; 时, std::hex 会改变 std::cout 流的内部状态,使得后续输出的整数都以十六进制形式显示。这种机制使得可以方便地控制流的输出格式,而不需要每次都手动设置复杂的格式参数。

十一、格式化与未格式化 I/O 操作的对比

格式化 I/O 操作(如文本模式下使用 >> << 运算符)和未格式化 I/O 操作(如二进制模式下的读写)各有优缺点。

  1. 格式化 I/O 操作

    • 优点 :数据以人类可读的文本形式表示,便于调试和查看。例如,将整数写入文件时,会以十进制字符串的形式存储,方便直接打开文件查看内容。
    • 缺点 :存在数据转换开销,特别是对于浮点数,转换过程可能引入小误差。而且,格式化输出的文件大小通常比二进制输出大,因为需要额外的字符来表示数据。
  2. 未格式化 I/O 操作

    • 优点 :数据以原始二进制形式存储,没有转换开销,文件大小更小,读写速度更快。对于大量数据的存储和传输,二进制模式更高效。
    • 缺点 :数据不是人类可读的,需要知道数据的原始类型和结构才能正确解释。如果不了解数据格式,很难直接从二进制文件中获取有意义的信息。
十二、流状态和错误处理

流对象有状态标志,用于表示流的当前状态。常见的状态标志包括:
- good() :表示流处于正常状态,可以进行读写操作。
- eof() :表示已经到达文件末尾。
- fail() :表示操作失败,但不是由于文件末尾引起的,可能是格式错误或其他问题。
- bad() :表示发生了严重错误,流可能无法继续使用。

在进行流操作时,应该检查流的状态,以确保操作成功。例如:

#include <iostream>
#include <fstream>

int main() {
    std::ifstream inFile("example.txt");
    if (!inFile.good()) {
        std::cerr << "Failed to open file." << std::endl;
        return 1;
    }
    int num;
    while (inFile >> num) {
        std::cout << num << std::endl;
    }
    if (inFile.eof()) {
        std::cout << "Reached end of file." << std::endl;
    } else if (inFile.fail()) {
        std::cerr << "Input format error." << std::endl;
    } else if (inFile.bad()) {
        std::cerr << "Serious error occurred." << std::endl;
    }
    inFile.close();
    return 0;
}

在这个例子中,首先检查文件是否成功打开。在读取数据时,使用 while (inFile >> num) 循环,该循环会在读取失败时自动终止。最后,根据流的状态输出相应的信息。

十三、文件流的打开和关闭

要使用文件流进行文件操作,需要先打开文件,操作完成后再关闭文件。

  1. 打开文件
    可以使用构造函数或 open() 成员函数打开文件。例如:
#include <fstream>

int main() {
    // 使用构造函数打开文件
    std::ifstream inFile("input.txt");
    // 使用 open() 函数打开文件
    std::ofstream outFile;
    outFile.open("output.txt");
    return 0;
}

打开文件时,可以指定打开模式,如只读、只写、追加等。常见的打开模式如下:
| 模式 | 描述 |
| ---- | ---- |
| ios::in | 以只读模式打开文件 |
| ios::out | 以只写模式打开文件,如果文件不存在则创建,如果存在则截断 |
| ios::app | 以追加模式打开文件,数据将追加到文件末尾 |
| ios::binary | 以二进制模式打开文件 |
| ios::trunc | 如果文件存在,打开时截断文件内容 |
| ios::ate | 打开文件后将文件指针定位到文件末尾 |

例如,以二进制追加模式打开文件:

std::ofstream outFile("data.bin", std::ios::binary | std::ios::app);
  1. 关闭文件
    使用完文件流后,应该调用 close() 成员函数关闭文件,以释放系统资源。例如:
inFile.close();
outFile.close();

也可以让文件流对象在超出作用域时自动关闭文件,因为析构函数会调用 close() 方法。

十四、文件流的定位

文件流有文件指针,用于指示当前读写位置。可以使用 seekg() seekp() 函数来定位文件指针。

  1. 输入流定位( seekg()
    seekg() 用于定位输入流的文件指针。它有两种重载形式:
// 绝对定位
void seekg (streampos pos);
// 相对定位
void seekg (streamoff off, ios_base::seekdir way);

例如,将文件指针定位到文件开头:

std::ifstream inFile("example.txt");
inFile.seekg(0, std::ios::beg);

将文件指针从当前位置向后移动 10 个字节:

inFile.seekg(10, std::ios::cur);
  1. 输出流定位( seekp()
    seekp() 用于定位输出流的文件指针,用法与 seekg() 类似。例如,将输出文件指针定位到文件末尾:
std::ofstream outFile("output.txt");
outFile.seekp(0, std::ios::end);
十五、自定义流操纵符

除了标准库提供的流操纵符,还可以自定义流操纵符。自定义流操纵符可以实现特定的格式化或操作功能。

例如,定义一个自定义操纵符来输出当前日期:

#include <iostream>
#include <iomanip>
#include <ctime>

std::ostream& currentDate(std::ostream& os) {
    std::time_t now = std::time(nullptr);
    std::tm* localTime = std::localtime(&now);
    os << std::put_time(localTime, "%Y-%m-%d");
    return os;
}

int main() {
    std::cout << "Today's date is: " << currentDate << std::endl;
    return 0;
}

在这个例子中, currentDate 是一个自定义流操纵符,它接受一个 std::ostream 引用作为参数,在其中输出当前日期,然后返回该流引用。

十六、总结

文件输入输出是编程中常见的操作,流机制为提供了一种方便、灵活且与设备无关的方式来处理数据的读写。通过理解流的概念、数据传输模式、格式化和未格式化 I/O 操作、流操纵符等知识,可以更好地控制文件的读写过程,处理各种数据类型,并根据需求选择合适的操作模式。

在实际应用中,要注意文件操作的安全性,避免意外覆盖重要文件。同时,合理使用流状态检查和错误处理机制,确保程序的健壮性。无论是处理文本文件还是二进制文件,流操作都能帮助高效地完成任务。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值