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

被折叠的 条评论
为什么被折叠?



