17.1 C++输入和输出概述
C++ 程序经常需要与外部世界进行交互:从键盘读取用户输入,将结果显示到屏幕,或者读写文件。C++ 使用流 (Stream) 的概念来处理输入和输出 (I/O) 操作。
17.1.1 流和缓冲区
流 (Stream) 是一个抽象的概念,它表示一个字节序列,这些字节可以从某个源(如键盘、文件、网络连接)流向程序(输入流),或者从程序流向某个目的地(如屏幕、文件、网络连接)(输出流)。将 I/O 操作视为字节流可以提供一个统一的接口来处理各种不同的 I/O 设备。
缓冲区 (Buffer) 是一块内存区域,用于临时存储数据。I/O 操作通常涉及物理设备(如硬盘、屏幕),这些设备的速度往往比 CPU 和内存慢得多。为了提高效率,数据通常不会在程序和设备之间逐个字节地传输,而是先暂存在缓冲区中:
- 输出: 当程序执行输出操作时(例如使用
cout <<
),数据通常先被发送到输出缓冲区。当缓冲区满了、遇到换行符(对于行缓冲的流)、程序结束或显式刷新缓冲区时,缓冲区中的内容才会被一次性地传输到最终的输出设备。 - 输入: 当程序请求输入时(例如使用
cin >>
),系统可能会一次性从输入设备读取比程序请求的更多的数据,并将其存储在输入缓冲区中。程序随后从缓冲区中获取所需的数据。如果缓冲区为空,程序会等待输入设备提供数据。
使用缓冲区的好处:
- 提高效率: 减少了与慢速 I/O 设备的直接交互次数。
- 平滑数据流: 允许程序和设备以各自的速度工作。
缓冲区的类型:
- 完全缓冲 (Fully Buffered): 只有当缓冲区满了,或者显式刷新时,才会进行实际的 I/O 操作(通常用于文件 I/O)。
- 行缓冲 (Line Buffered): 除了缓冲区满或显式刷新外,遇到换行符 (
\n
) 时也会进行 I/O 操作(通常用于标准输出cout
,当它连接到终端时)。 - 无缓冲 (Unbuffered): 数据尽快进行实际的 I/O 操作(通常用于标准错误输出
cerr
,以确保错误信息能立即显示)。
17.1.2 流、缓冲区和 iostream 文件
C++ 的 I/O 功能主要定义在 <iostream>
头文件中(以及其他相关头文件如 <fstream>
, <sstream>
)。<iostream>
定义了一系列用于处理流的类和对象。
核心 I/O 类库:
-
ios_base
: 提供了与流无关的基本属性和方法(如格式化标志、流状态)。 -
ios
: 基于ios_base
,增加了与具体字符类型相关的错误状态信息。 -
ostream
: 基于ios
,提供了输出流的功能(如operator<<
,put()
,write()
)。 -
istream
: 基于ios
,提供了输入流的功能(如operator>>
,get()
,getline()
,read()
)。 -
iostream
: 同时继承自istream
和ostream
,支持输入和输出操作。
标准流对象: <iostream>
还定义了几个预定义的全局流对象,用于处理标准输入和输出:
-
cin
(console input):istream
类的对象,通常连接到标准输入设备(键盘)。 -
cout
(console output):ostream
类的对象,通常连接到标准输出设备(屏幕)。它通常是行缓冲的。 -
cerr
(console error):ostream
类的对象,通常连接到标准错误设备(屏幕)。它通常是无缓冲的,用于立即显示错误消息。 -
clog
(console log):ostream
类的对象,也通常连接到标准错误设备。但它通常是缓冲的。
1 |
|
这些标准流对象使得基本的控制台 I/O 非常方便。
17.1.3 重定向
许多操作系统(如 Linux, macOS, Windows)支持I/O 重定向 (Redirection)**。重定向允许你改变程序的标准输入来源和标准输出/标准错误的目的地,而无需修改程序代码**。
输出重定向 (
>
): 将程序的标准输出(cout
的内容)发送到指定文件,而不是屏幕。如果文件已存在,通常会覆盖它。1
./my_program > output.txt
执行
my_program
,其cout
输出的内容将写入output.txt
文件。输出重定向(追加
>>
): 将程序的标准输出追加到指定文件的末尾。如果文件不存在,则创建它。1
./my_program >> output.txt
输入重定向 (
<
): 使程序从指定文件读取其标准输入(cin
的内容),而不是从键盘。1
./my_program < input.txt
my_program
会从input.txt
文件中读取它期望通过cin
获得的数据。错误重定向: 重定向标准错误 (
cerr
,clog
) 的方式因 shell 而异。- bash/sh/zsh: 使用
2>
(覆盖) 或2>>
(追加)。1
2
3./my_program 2> error.log # 重定向 stderr 到 error.log
./my_program > output.txt 2> error.log # 重定向 stdout 和 stderr
./my_program > combined.log 2>&1 # 将 stderr 重定向到 stdout,然后一起重定向到文件 - Windows (cmd.exe): 类似地使用
2>
和2>>
。
- bash/sh/zsh: 使用
重定向是一个强大的工具,它允许我们将程序的输入和输出与文件连接起来,方便进行测试、日志记录和数据处理,而无需修改程序本身来处理文件 I/O。程序仍然使用 cin
, cout
, cerr
,但操作系统负责将这些标准流连接到文件。
17.2 使用 cout 进行输出
std::cout
是 C++ 标准库 <iostream>
中预定义的一个 ostream
类的全局对象,通常连接到标准输出设备(如屏幕)。它是 C++ 中最常用的输出工具。
17.2.1 重载的 << 运算符
cout
最常见的用法是配合插入运算符 (<<
) 使用。这个运算符被重载 (overloaded) 以接受 C++ 中所有的基本数据类型(如 int
, float
, double
, char
, 指针类型)以及一些库类型(如 std::string
, C 风格字符串 const char*
)。
工作原理:
-
cout << value;
表达式的值是cout
对象本身 (ostream&
)。 - 这使得可以链接 (chain) 多个插入运算符:
cout << "Value: " << x << " and " << y;
。这会被解释为((cout << "Value: ") << x) << " and ") << y;
。 -
<<
运算符会根据value
的类型,将其转换为适当的字符表示形式,并发送到输出流(通常先到缓冲区)。
示例:
1 |
|
对于用户自定义的类类型,如果想让 cout << myObject;
能够工作,需要为该类重载 operator<<
(通常作为友元函数),如第 11 章所述。
17.2.2 其他 ostream 方法
除了 <<
运算符,ostream
类(cout
是其对象)还提供了其他输出方法:
put(char c)
: 输出单个字符c
。1
2
3std::cout.put('H');
std::cout.put('i');
std::cout.put('!').put('\n'); // put() 也返回 ostream&,可以链接write(const char* s, streamsize n)
: 输出从地址s
开始的内存块中的n
个字节。它不会在遇到空字符\0
时停止,而是精确地写入n
个字节。这对于输出二进制数据或非空字符结尾的字符数组很有用。1
2
3
4
5
6
7const char* message = "Binary Data";
// 输出前 6 个字节 "Binary"
std::cout.write(message, 6) << std::endl;
char buffer[] = {'A', 'B', '\0', 'C', 'D'};
// 输出整个 buffer 的 5 个字节,包括空字符
std::cout.write(buffer, sizeof(buffer)) << std::endl;
17.2.3 刷新输出缓冲区
如前所述,输出通常是缓冲的。有时我们需要确保缓冲区中的内容立即被发送到输出设备,这称为刷新 (Flushing) 缓冲区。
有几种方法可以刷新 cout
的缓冲区:
std::endl
(Manipulator): 这是最常用的方法。endl
不仅向流中插入一个换行符 (\n
)**,还会显式刷新**输出缓冲区。1
std::cout << "Line 1" << std::endl; // 输出 "Line 1",换行,并刷新
std::flush
(Manipulator): 只刷新输出缓冲区,不插入任何字符。1
2
3std::cout << "Processing..." << std::flush; // 立即显示 "Processing...",不换行
// ...长时间操作...
std::cout << " Done." << std::endl;std::ends
(Manipulator): 向流中插入一个空字符 (\0
)**,然后刷新**缓冲区。这在与其他需要空字符结尾字符串的系统交互时可能有用,但不太常见。1
std::cout << "Null terminated" << std::ends;
程序正常结束: 当
main
函数返回或调用exit()
时,通常会自动刷新所有打开的输出流。缓冲区满: 当缓冲区满时会自动刷新。
与输入关联: 当
cin
需要从用户那里读取数据,并且cout
的缓冲区中有待输出的内容时,通常会先刷新cout
的缓冲区(以确保提示信息先显示出来)。
endl
vs. '\n'
:
-
cout << endl;
等价于cout << '\n' << flush;
-
cout << '\n';
只插入换行符,不保证立即刷新缓冲区(除非流是行缓冲且连接到终端)。
在性能敏感的代码中(例如大量循环输出),频繁使用 endl
可能会因为不必要的刷新而降低效率。在这种情况下,如果不需要每次都立即看到输出,可以只使用 '\n'
,让缓冲区在更合适的时机(如缓冲区满或程序结束)刷新。
17.2.4 用 cout 进行格式化
默认情况下,cout
会根据数据的类型选择一种“合理”的格式进行输出。但我们可以通过多种方式控制输出的格式,例如字段宽度、小数位数、对齐方式、数字基数等。
格式化可以通过以下方式设置:
-
ios_base
成员函数:ios_base
类(ostream
的基类)提供了一些成员函数来设置格式标志。 - 操纵符 (Manipulators): 定义在
<iostream>
和<iomanip>
中的特殊函数,可以像插入值一样插入到cout
语句中来改变流的状态。使用操纵符通常更方便。
1. 修改数值的基数 (dec
, hex
, oct
)
可以使用操纵符 dec
, hex
, oct
来改变整数输出的基数(默认为十进制 dec
)。这些设置是粘性 (sticky) 的,即一旦设置,后续所有整数输出都会采用该基数,直到被重新设置为止。
1 |
|
-
showbase
: 显示数值基数的前缀(0x
或0X
表示十六进制,0
表示八进制)。 -
noshowbase
: 不显示基数前缀(默认)。 -
uppercase
: 十六进制输出时使用大写字母A-F
和X
。 -
nouppercase
: 使用小写字母(默认)。
2. 调整字段宽度 (width()
, setw()
)
可以指定输出下一个值时使用的最小字段宽度。如果值的实际宽度小于指定宽度,则用填充字符(默认为空格)填充。
-
cout.width(int w)
(成员函数): 设置下一次输出的最小宽度为w
。只对紧随其后的下一次输出有效,之后会自动恢复默认(宽度为 0)。 -
setw(int w)
(操纵符,<iomanip>
): 与width()
效果相同,也是只对下一次输出有效。使用setw
通常更方便。
1 |
|
3. 填充字符 (fill()
)
当字段宽度大于值的实际宽度时,用于填充空白区域的字符可以通过 fill()
成员函数或 setfill()
操纵符设置。这个设置是粘性的。
-
cout.fill(char ch)
(成员函数): 设置填充字符为ch
。返回之前的填充字符。 -
setfill(char ch)
(操纵符,<iomanip>
): 设置填充字符为ch
。
1 |
|
4. 浮点数精度 (precision()
, setprecision()
)
可以控制浮点数输出的小数位数或总有效数字位数,具体取决于浮点数格式(见下文)。
-
cout.precision(int p)
(成员函数): 设置精度为p
。返回之前的精度值。默认精度通常是 6。这个设置是粘性的。 -
setprecision(int p)
(操纵符,<iomanip>
): 设置精度为p
。
1 |
|
5. 浮点数格式 (setf()
, unsetf()
, fixed
, scientific
)
可以控制浮点数是以定点表示法 (fixed) 还是科学计数法 (scientific) 显示。
-
fixed
(操纵符): 使用定点表示法。此时,precision()
控制的是小数点后的位数。 -
scientific
(操纵符): 使用科学计数法(例如1.23e+04
)。此时,precision()
控制的是小数点后的位数。 - 默认格式: 如果既未设置
fixed
也未设置scientific
,则cout
会自动选择一种格式(定点或科学计数法),以产生更紧凑的表示。此时,precision()
控制的是总的有效数字位数(整数部分+小数部分)。 -
ios_base::floatfield
:fixed
和scientific
都是格式标志位。可以使用cout.setf(ios_base::fixed, ios_base::floatfield)
和cout.setf(ios_base::scientific, ios_base::floatfield)
来设置,使用cout.unsetf(ios_base::floatfield)
来清除设置恢复默认行为。使用操纵符通常更简单。
1 |
|
6. 其他格式标志和操纵符
对齐方式 (
left
,right
,internal
):left
: 在字段内左对齐输出,填充字符放在右边。right
: 在字段内右对齐输出,填充字符放在左边(默认)。internal
: 符号(正负号或基数前缀)左对齐,数值右对齐,填充字符放在中间。可以通过
setf()
设置ios_base::left
,ios_base::right
,ios_base::internal
标志,或使用同名操纵符。1
2
3
4std::cout << "*" << std::setw(10) << std::left << "Hi" << "*" << std::endl; // 输出 *Hi *
std::cout << "*" << std::setw(10) << std::right << -12 << "*" << std::endl; // 输出 * -12*
std::cout << "*" << std::setw(10) << std::internal << -12 << "*" << std::endl; // 输出 *- 12*
std::cout << std::right; // 恢复默认右对齐
显示小数点 (
showpoint
,noshowpoint
):showpoint
: 强制显示浮点数的小数点和末尾的零(即使小数部分为零)。noshowpoint
: 不强制显示(默认)。1
2
3
4
5std::cout << std::fixed << std::setprecision(2);
std::cout << "Default: " << 12.0 << std::endl; // 可能输出 12 (取决于实现)
std::cout << std::showpoint;
std::cout << "Showpoint: " << 12.0 << std::endl; // 输出 12.00
std::cout << std::noshowpoint;
显示正号 (
showpos
,noshowpos
):showpos
: 对于非负数,在前面显示+
号。noshowpos
: 不显示正号(默认)。1
2
3
4std::cout << +10 << " " << -10 << std::endl; // 输出 10 -10
std::cout << std::showpos;
std::cout << +10 << " " << -10 << std::endl; // 输出 +10 -10
std::cout << std::noshowpos;
布尔值格式 (
boolalpha
,noboolalpha
):boolalpha
: 将bool
值输出为字符串 “true” 或 “false”。noboolalpha
: 将bool
值输出为整数 1 或 0(默认)。1
2
3
4
5bool flag = true;
std::cout << "Default bool: " << flag << std::endl; // 输出 1
std::cout << std::boolalpha;
std::cout << "Boolalpha: " << flag << std::endl; // 输出 true
std::cout << std::noboolalpha;
7. 设置格式 (setf()
)
setf()
是一个更底层的设置格式标志的方法。它有两个版本:
-
fmtflags setf(fmtflags flags)
: 设置flags
中指定的位。例如cout.setf(ios_base::showpos)
。 -
fmtflags setf(fmtflags flags, fmtflags mask)
: 先清除mask
中指定的位,然后设置flags
中指定的位。这用于设置互斥的标志组,如对齐方式 (adjustfield
) 或浮点数格式 (floatfield
)。例如cout.setf(ios_base::left, ios_base::adjustfield)
。
虽然 setf()
提供了完全的控制,但使用操纵符通常更易读、更方便。
保存和恢复格式状态: 有时你可能想临时改变格式,之后再恢复。可以保存 flags()
和 precision()
的值,之后再用 flags(old_flags)
和 precision(old_precision)
恢复。
1 |
|
通过组合使用这些格式化工具,可以精确地控制 cout
输出数据的外观。
17.3 使用 cin 进行输入
std::cin
是 C++ 标准库 <iostream>
中预定义的一个 istream
类的全局对象,通常连接到标准输入设备(如键盘)。它是 C++ 中获取用户输入的主要方式。
cin
最常见的用法是配合提取运算符 (>>
) 使用。这个运算符也被重载以接受各种基本数据类型(int
, float
, double
, char
等)以及 std::string
和 C 风格字符数组。
cin >>
的工作方式:
- 跳过空白:
>>
运算符默认会跳过输入流中所有前导的空白字符(空格、制表符、换行符)。 - 读取和转换: 它会根据目标变量的类型,从缓冲区读取非空白字符,并尝试将这些字符转换为目标类型的值。
- 停止读取: 读取会在遇到不适合目标类型的字符(例如,读取
int
时遇到字母)或下一个空白字符时停止。 - 存储值: 成功转换的值被存储在目标变量中。
- 留下分隔符: 导致读取停止的那个不合适的字符或空白字符会留在输入缓冲区中,等待下一次读取。
示例:
1 |
|
17.3.1 cin >> 如何检查输入
cin >>
表达式本身会返回 cin
对象 (istream&
)。C++ 允许在需要布尔值的地方(如 if
或 while
条件)使用流对象。当流对象被用作条件时,它会检查流的**状态 (state)**。如果流处于“良好”状态(没有发生错误),条件为 true
;如果流发生错误(如读取失败、到达文件末尾),条件为 false
。
这使得可以编写如下循环:
1 |
|
17.3.2 流状态
istream
类(以及 ostream
和 ios
)内部维护一组状态位 (state flags) 来表示流的当前状态。这些状态位定义在 ios_base
类中,类型为 iostate
(一种位掩码类型)。
主要的状态位有:
-
goodbit
: 表示流处于正常状态,没有发生错误。值为 0。 -
eofbit
: 表示已到达输入流的**末尾 (End Of File)**。当尝试从文件末尾或关闭的输入流读取时设置。 -
failbit
: 表示发生可恢复的格式错误。通常是因为输入的数据格式与期望读取的类型不匹配(例如,期望int
但输入了字母)。设置failbit
后,后续的 I/O 操作通常会失败,直到流状态被清除。 -
badbit
: 表示发生不可恢复的严重错误,可能涉及流本身的损坏或底层 I/O 操作失败(例如,读取磁盘时发生硬件错误)。
一个流可能同时设置了多个状态位(例如,在文件末尾尝试读取失败可能同时设置 eofbit
和 failbit
)。
检查流状态的成员函数:
-
good()
: 如果流状态为goodbit
(即所有错误位都未设置),返回true
。 -
eof()
: 如果设置了eofbit
,返回true
。 -
fail()
: 如果设置了failbit
或badbit
,返回true
。这是检查读取操作是否失败的最常用方法。 -
bad()
: 如果设置了badbit
,返回true
。 -
rdstate()
: 返回当前所有状态位的组合(一个iostate
值)。 -
operator bool()
: 重载的布尔转换运算符。如果fail()
返回false
,则转换为true
;否则转换为false
。这就是为什么可以在if(cin)
或while(cin >> value)
中使用流对象的原因。 -
operator!()
: 重载的逻辑非运算符。如果fail()
返回true
,则!cin
为true
。
清除流状态:
当流进入 failbit
或 badbit
状态后,后续的 I/O 操作通常会立即失败。为了继续使用该流,必须清除错误状态。
-
clear(iostate state = goodbit)
: 重置流的状态位。默认情况下(不带参数或参数为goodbit
),它会清除所有错误位 (eofbit
,failbit
,badbit
),使流恢复到good()
状态。也可以用它来设置特定的状态位。
示例:处理输入错误
1 |
|
这个循环会一直要求用户输入,直到成功读取一个整数为止。关键在于读取失败后,必须先 clear()
错误标志,然后 ignore()
掉导致错误的无效输入,否则下一次循环的 cin >> number
还会因为同样的原因失败。
17.3.3 其他 istream 类方法
除了 >>
运算符,istream
类还提供了其他用于输入的成员函数,它们提供了更精细的控制。
1. 单字符输入: get()
get()
有几个版本用于读取单个字符:
-
int get()
: 读取下一个字符(即使是空白字符),并将其作为int
类型返回。如果到达文件末尾,返回EOF
(一个特殊的负整数常量,定义在<iostream>
或<cstdio>
中)。 -
istream& get(char& ch)
: 读取下一个字符(即使是空白字符),并将其存储在ch
中。返回istream
对象的引用。如果读取失败(如到达文件末尾),会设置failbit
。
1 |
|
get()
不会跳过空白字符。
2. 字符串输入: get()
和 getline()
istream
提供了用于读取 C 风格字符串(字符数组)的 get()
和 getline()
版本。
istream& get(char* s, streamsize n, char delim = '\n')
:- 从输入流读取字符,存储到字符数组
s
中。 - 最多读取
n-1
个字符(为末尾的空字符\0
留出空间)。 - 遇到分隔符
delim
时停止读取。 - 分隔符
delim
会被留在输入流中,不会被读取到s
中。 - 总是在读取的字符序列末尾添加空字符
\0
。 - 如果因为读取了
n-1
个字符而停止,会设置failbit
。
- 从输入流读取字符,存储到字符数组
istream& getline(char* s, streamsize n, char delim = '\n')
:- 与
get()
类似,最多读取n-1
个字符。 - 遇到分隔符
delim
时停止读取。 - 分隔符
delim
会从输入流中被读取并丢弃,不会存储在s
中。 - 总是在读取的字符序列末尾添加空字符
\0
。 - 如果因为读取了
n-1
个字符而停止(在遇到分隔符之前),会设置failbit
。
- 与
主要区别: getline()
会读取并丢弃分隔符,而 get()
会将分隔符留在流中。
1 |
|
注意: 这些是用于 C 风格字符串的版本。对于 std::string
对象,应使用全局的 getline(cin, str)
函数(如 16.1 节所述)。
17.3.4 其他 istream 方法
read(char* s, streamsize n)
: 从流中读取精确n
个字节,并存储到从s
开始的内存中。它不会在遇到空字符或分隔符时停止,也不会添加空字符。主要用于读取二进制数据。如果读取的字节数少于n
(例如到达文件末尾),会设置eofbit
和failbit
。peek()
: 返回输入流中的下一个字符的整数值,但不从流中移除该字符。如果到达文件末尾,返回EOF
。可用于在实际读取前查看下一个字符。gcount()
: 返回上一次未格式化读取操作(如get()
,getline()
,read()
)实际读取的字符数。对于>>
运算符无效。putback(char c)
: 将字符c
放回输入流中,使其成为下一个被读取的字符。通常只能放回上一个读取的字符。ignore(streamsize n = 1, int delim = EOF)
: 读取并丢弃输入流中的字符。- 最多丢弃
n
个字符。 - 如果在此之前遇到并读取了分隔符
delim
,则停止丢弃。 - 分隔符
delim
本身也会被丢弃。 - 常用于清除缓冲区中的无效输入或跳过不需要的部分。
- 最多丢弃
1 |
|
通过组合使用 cin >>
和这些 istream
成员函数,可以实现灵活多样的输入处理逻辑,并能有效地处理输入错误。
17.4 文件输入和输出
除了与控制台进行交互(cin
, cout
),程序经常需要从文件中读取数据或将数据写入文件。C++ 的 iostream 库通过 <fstream>
头文件提供了用于文件 I/O 的类。
<fstream>
定义了三个主要的类:
-
ifstream
(input file stream): 继承自istream
,专门用于从文件读取数据。 -
ofstream
(output file stream): 继承自ostream
,专门用于向文件写入数据。 -
fstream
: 继承自iostream
,可以同时支持文件的读取和写入。
这些文件流类的使用方式与 cin
和 cout
非常相似,因为它们继承了相同的基类并支持相同的操作符(如 <<
, >>
) 和方法(如 get()
, getline()
, write()
, read()
)。
17.4.1 简单的文件 I/O
进行文件 I/O 的基本步骤:
- 包含头文件:
#include <fstream>
。 - 创建流对象: 创建一个
ifstream
(用于读取) 或ofstream
(用于写入) 或fstream
(用于读写) 对象。 - 关联文件: 将流对象与一个具体的文件关联起来。这可以在创建对象时通过构造函数完成,也可以之后使用
open()
方法。 - 检查是否成功打开: 使用
is_open()
方法或检查流对象的状态来确认文件是否成功打开。 - 进行 I/O 操作: 使用
<<
,>>
,get()
,getline()
,write()
,read()
等方法读写文件,就像使用cin
和cout
一样。 - 关闭文件: 使用
close()
方法关闭文件,断开流对象与文件的关联。重要: 当文件流对象离开作用域时(例如函数结束),其析构函数会自动调用close()
,因此显式调用close()
通常不是必需的,但有时为了明确或检查关闭状态可以这样做。
示例:写入文件
1 |
|
示例:读取文件
1 |
|
17.4.2 流状态检查和 is_open()
-
is_open()
: 文件流对象提供is_open()
成员函数,用于检查文件是否成功打开并与流关联。在尝试进行任何 I/O 操作之前,务必调用此函数进行检查。 - 流状态位: 文件流对象也具有与
cin
相同的状态位 (goodbit
,eofbit
,failbit
,badbit
)。- 如果
open()
失败,流对象的状态通常会设置为failbit
。因此,if (!outFile)
或if (!inFile)
也可以用来检查文件是否成功打开。 - 在读取过程中,如果遇到文件末尾,会设置
eofbit
。 - 如果读取的数据格式不匹配(例如,试图将文本读入
int
),会设置failbit
。 - 如果发生底层 I/O 错误,会设置
badbit
。 - 可以使用
good()
,eof()
,fail()
,bad()
来检查这些状态,并使用clear()
来清除错误状态(如果可恢复)。
- 如果
17.4.3 打开多个文件
一个程序可以同时打开多个文件,只需为每个文件创建一个对应的文件流对象即可。
1 |
|
17.4.4 命令行处理技术
通常,我们希望在运行程序时通过命令行参数来指定要处理的文件名,而不是将文件名硬编码在程序中。main
函数可以接收命令行参数:
1 | int main(int argc, char *argv[]) { |
-
argc
(argument count): 一个整数,表示传递给程序的命令行参数的数量(包括程序本身的名称)。 argv
(argument vector): 一个指向 C 风格字符串(char*
)的指针数组。-
argv[0]
通常是程序本身的名称。 -
argv[1]
是第一个命令行参数。 -
argv[2]
是第二个命令行参数,依此类推。 -
argv[argc]
是一个空指针 (nullptr
)。
-
示例:使用命令行参数指定输入和输出文件
1 |
|
编译后,可以这样运行:./my_program data.in results.out
17.4.5 文件模式
在打开文件时(通过构造函数或 open()
方法),可以指定文件模式 (file mode) 来控制文件的打开方式。文件模式是定义在 ios_base
类中的常量,类型为 openmode
(一种位掩码类型)。
常用的文件模式标志:
-
ios_base::in
: 以读取模式打开 (ifstream 默认)。 -
ios_base::out
: 以写入模式打开 (ofstream 默认)。如果文件已存在,清空其内容;如果不存在,则创建。 -
ios_base::app
(append): 以追加模式打开。写入操作将在文件末尾进行。如果文件不存在,则创建。此模式下通常隐含ios_base::out
。 -
ios_base::ate
(at end): 打开文件并将初始位置定位到文件末尾。但仍可以在文件中的任何位置写入(与app
不同,app
强制写入到末尾)。 -
ios_base::binary
: 以二进制模式打开文件,而不是文本模式。在二进制模式下,不会对特殊字符(如换行符\n
或回车符\r
)进行转换。这对于读写非文本文件(如图像、可执行文件)或需要精确字节控制的情况至关重要。 -
ios_base::trunc
(truncate): 如果文件已存在,清空其内容。ios_base::out
模式默认包含此行为。
可以使用按位或运算符 (|
) 来组合多个模式标志。
open()
方法:
除了构造函数,还可以使用 open()
方法打开文件。
1 | ofstream outFile; |
默认模式:
-
ifstream
默认模式是ios_base::in
。 -
ofstream
默认模式是ios_base::out | ios_base::trunc
。 -
fstream
没有默认模式,必须显式指定。
17.4.6 随机存取
默认情况下,文件流是按顺序读取或写入的。但有时我们需要直接跳转到文件中的特定位置进行读写,这称为**随机存取 (Random Access)**。
文件流维护着内部的位置指针:
- 输入指针 (Get Pointer):
istream
(及ifstream
,fstream
) 维护,指示下一次读取操作将从哪里开始。 - 输出指针 (Put Pointer):
ostream
(及ofstream
,fstream
) 维护,指示下一次写入操作将在哪里进行。
可以使用以下方法来操作这些位置指针:
seekg(offset, direction)
(seek get): 移动输入指针。seekp(offset, direction)
(seek put): 移动输出指针。-
offset
: 一个整数值(类型通常是streamoff
),表示要移动的字节数。可以是正数(向文件末尾移动)或负数(向文件开头移动)。 direction
: 一个枚举值(类型seekdir
,定义在ios_base
中),指定offset
的参考点:-
ios_base::beg
: 从文件开头计算偏移量。 -
ios_base::cur
: 从文件当前位置计算偏移量。 -
ios_base::end
: 从文件末尾计算偏移量(此时offset
通常为负数或零)。
-
-
tellg()
(tell get): 返回输入指针的当前位置(类型通常是streampos
,可以转换为整数)。tellp()
(tell put): 返回输出指针的当前位置。
注意: 随机存取通常在二进制模式 (ios_base::binary
) 下使用更可靠,因为文本模式下的字符转换可能会干扰精确的字节定位。
示例:随机访问文件
假设有一个存储记录的文件 records.dat
,每条记录固定大小为 sizeof(Record)
。
1 |
|
这个例子演示了如何使用 seekg
和 seekp
定位到文件中的特定字节位置,并使用 read
和 write
进行二进制数据的读写。
17.5 内核格式化
到目前为止,我们讨论的 I/O 操作都是将数据发送到外部设备(如屏幕 cout
)或文件(ofstream
),或者从外部设备(cin
)或文件(ifstream
)读取数据。但有时,我们希望在内存中对数据进行格式化,将各种类型的数据(数字、字符等)转换成一个字符串,或者反过来,从一个字符串中按特定格式解析出各种类型的数据。
C++ 标准库通过 <sstream>
头文件提供了字符串流 (String Streams) 类来实现这种内核格式化 (In-memory Formatting) 或 内存中的 I/O。
<sstream>
定义了三个主要的类:
-
ostringstream
(output string stream): 继承自ostream
。它允许你像使用cout
一样,使用插入运算符 (<<
) 和其他ostream
方法将各种类型的数据写入到一个内部的字符串缓冲区中。之后可以获取这个格式化好的字符串。 -
istringstream
(input string stream): 继承自istream
。它允许你将一个已有的std::string
或 C 风格字符串作为数据源,然后像使用cin
一样,使用提取运算符 (>>
) 和其他istream
方法从中读取(解析)数据。 -
stringstream
: 继承自iostream
。它结合了ostringstream
和istringstream
的功能,允许对同一个内部字符串缓冲区进行读取和写入操作。
这些字符串流类的行为与文件流或控制台流非常相似,因为它们共享相同的基类和接口。你可以使用同样的格式化操纵符(如 setw
, setprecision
, fixed
, hex
等)来控制字符串流中的格式。
使用 ostringstream
进行格式化输出
当你需要将不同类型的数据组合成一个格式化的字符串时,ostringstream
非常有用。
步骤:
- 包含头文件
#include <sstream>
。 - 创建一个
ostringstream
对象。 - 像使用
cout
一样,使用<<
将数据插入到流中。可以使用格式化操纵符。 - 使用
str()
成员函数获取流内部缓冲区中积累的std::string
副本。
示例:
1 |
|
ostringstream
常用于需要动态构建包含数值和其他数据的日志消息、文件名或显示文本的场景。
使用 istringstream
进行格式化输入
当你有一个字符串,并且想从中按特定格式提取数据(例如,解析配置文件的一行或用户输入的复合数据)时,istringstream
非常有用。
步骤:
- 包含头文件
#include <sstream>
。 - 准备好包含待解析数据的
std::string
。 - 使用该字符串创建一个
istringstream
对象。 - 像使用
cin
一样,使用>>
从流中提取数据到变量中。 - 可以检查流的状态(如
good()
,fail()
,eof()
) 来判断提取是否成功。
示例:
1 |
|
istringstream
常用于解析从文件、网络或用户界面接收到的文本数据。
使用 stringstream
进行读写
stringstream
允许你在同一个字符串缓冲区上进行输入和输出操作。你可以先向其中写入数据,然后从中读取数据,或者反过来。
示例:
1 |
|
stringstream
在需要对字符串进行多次格式化转换或解析的场景中很有用,例如在不同格式之间转换数据。
总结: 字符串流提供了一种强大而灵活的方式,可以在内存中完成数据的格式化和解析,其接口与标准的控制台和文件 I/O 保持一致,易于学习和使用。
17.6 总结
本章详细介绍了 C++ 的输入/输出 (I/O) 系统,该系统基于流 (Stream) 的概念,提供了一个统一的接口来处理来自不同源(键盘、文件)和去往不同目的地(屏幕、文件)的数据流。
主要内容回顾:
I/O 概述:
- 流: 字节序列的抽象,用于连接程序和 I/O 设备。
- 缓冲区: 临时存储区域,用于提高 I/O 效率。分为完全缓冲、行缓冲、无缓冲。
-
<iostream>
: 提供了核心 I/O 类(ios_base
,ios
,istream
,ostream
,iostream
)和标准流对象 (cin
,cout
,cerr
,clog
)。 - 重定向: 操作系统功能,允许改变标准输入/输出/错误流的来源和目的地,而无需修改代码。
使用
cout
输出:- 主要使用重载的插入运算符 (
<<
) 输出各种数据类型。 - 支持链接操作 (
cout << a << b;
)。 - 提供
put()
输出单个字符,write()
输出指定字节数的内存块。 - 缓冲区刷新: 可以通过
endl
(换行并刷新)、flush
(仅刷新)、ends
(插入\0
并刷新) 或在特定条件下自动刷新。'\n'
只插入换行符。 - 格式化: 可以通过成员函数 (
width()
,precision()
,fill()
,setf()
) 或操纵符 (setw()
,setprecision()
,setfill()
,fixed
,scientific
,hex
,oct
,dec
,left
,right
,boolalpha
等,需包含<iomanip>
) 控制输出的宽度、精度、填充、对齐、基数、浮点数表示等。
- 主要使用重载的插入运算符 (
使用
cin
输入:- 主要使用重载的提取运算符 (
>>
) 读取数据。默认跳过前导空白,读取直到遇到不匹配字符或空白。 - 流状态:
cin
维护状态位 (goodbit
,eofbit
,failbit
,badbit
)。可以通过good()
,eof()
,fail()
,bad()
或流对象本身在布尔上下文中检查状态。failbit
表示格式错误,eofbit
表示到达文件尾。 - 错误处理: 读取失败后,需使用
clear()
清除错误状态,并使用ignore()
丢弃缓冲区中的无效输入。 - 其他方法:
get()
(读取单个字符,不跳过空白)、getline()
(读取一行到 C 风格数组,丢弃分隔符)、read()
(读取指定字节数)、peek()
(查看下一个字符)、gcount()
(获取上次读取的字符数)、putback()
(放回字符)、ignore()
(丢弃字符)。
- 主要使用重载的提取运算符 (
文件 I/O (
<fstream>
):- 使用
ifstream
(输入)、ofstream
(输出)、fstream
(输入/输出) 类。 - 通过构造函数或
open()
方法关联文件,并指定文件模式 (in
,out
,app
,ate
,binary
,trunc
)。 - 必须使用
is_open()
或检查流状态来确认文件是否成功打开。 - 使用与
cin
/cout
类似的操作符和方法进行读写。 - 文件流对象在销毁时自动关闭文件。
- 可以通过
argc
和argv
处理命令行参数来指定文件名。 - 随机存取: 使用
seekg()
/tellg()
(输入指针) 和seekp()
/tellp()
(输出指针) 在文件中定位读写位置,常用于二进制文件。
- 使用
内核格式化 (
<sstream>
):- 在内存中进行格式化转换。
-
ostringstream
: 将各种数据类型格式化输出到一个内部字符串缓冲区,通过str()
获取结果字符串。 -
istringstream
: 将已有字符串作为输入源,从中按格式解析提取数据。 -
stringstream
: 支持对同一个内部字符串缓冲区进行读写操作。 - 接口与文件流和控制台流一致,支持格式化操纵符。
C++ 的 iostream 库提供了一个类型安全、可扩展且功能丰富的 I/O 框架,适用于控制台、文件以及内存中的格式化操作。