C++ 流输入输出深度解析
1. 流的基本概念
在 C++ 中,输入输出操作是通过流(streams)来实现的,流是字节序列。在输入操作中,字节从设备(如键盘、磁盘驱动器、网络连接等)流向主内存;在输出操作中,字节从主内存流向设备(如显示屏、打印机、磁盘驱动器、网络连接等)。
应用程序会为字节赋予意义,这些字节可以代表字符、原始数据、图形图像、数字语音、数字视频或应用程序所需的任何其他信息。系统的输入输出机制需要稳定可靠地在设备和内存之间传输字节。这种传输通常涉及一些机械运动,如磁盘或磁带的旋转,或者键盘按键的敲击。这些传输所花费的时间通常远大于处理器内部处理数据所需的时间。因此,输入输出操作需要精心规划和调整,以确保最佳性能。
C++ 提供了“低级”和“高级”两种输入输出能力:
-
低级输入输出(未格式化输入输出)
:指定要在设备和内存之间传输的字节数量。在这种传输中,单个字节是关注的对象。这种低级能力提供了高速、大容量的传输,但不太方便。
-
高级输入输出(格式化输入输出)
:程序员通常更喜欢这种方式,在这种方式中,字节被分组为有意义的单元,如整数、浮点数、字符、字符串和用户定义的类型。除了大容量文件处理之外,这些面向类型的能力对于大多数输入输出操作来说是令人满意的。
2. 经典流与标准流
过去,C++ 的经典流库支持字符的输入输出。由于一个字符通常占用一个字节,它只能表示有限的字符集(如本书大多数读者使用的 ASCII 字符集或其他流行的字符集)。然而,许多语言使用的字母表包含的字符数量超过了单字节字符所能表示的范围。ASCII 字符集无法提供这些字符,而 Unicode 字符集可以。Unicode 是一个广泛的国际字符集,它代表了世界上大多数“商业上可行”的语言、数学符号等。
C++ 包含标准流库,它使开发人员能够构建能够处理 Unicode 字符输入输出操作的系统。为此,C++ 引入了
wchar_t
类型,它可以存储 Unicode 字符。C++ 标准还将只处理字符的经典 C++ 流类重新设计为类模板,并分别针对
char
和
wchar_t
类型的字符处理进行了特化。我们通常使用
char
特化版本。
wchar_t
类型的大小没有在标准中指定。C++11 新增了
char16_t
和
char32_t
类型来表示 Unicode 字符,以提供具有明确指定大小的字符类型。
3. iostream 库头文件
C++ 的
iostream
库提供了数百种输入输出能力,几个头文件包含了该库接口的部分内容:
-
<iostream>
:大多数 C++ 程序都会包含这个头文件,它声明了所有流输入输出操作所需的基本服务。该头文件定义了
cin
、
cout
、
cerr
和
clog
对象,它们分别对应标准输入流、标准输出流、无缓冲的标准错误流和有缓冲的标准错误流。同时提供了未格式化和格式化的输入输出服务。
-
<iomanip>
:声明了使用所谓的参数化流操纵器(如
setw
和
setprecision
)进行格式化输入输出时有用的服务。
-
<fstream>
:声明了文件处理的服务。
4. 流输入输出类和对象
iostream
库提供了许多处理常见输入输出操作的模板:
-
basic_istream
:支持流输入操作。
-
basic_ostream
:支持流输出操作。
-
basic_iostream
:支持流输入和流输出操作。
每个模板都有一个预定义的模板特化,用于支持字符输入输出。此外,
iostream
库还提供了一组
typedef
来为这些模板特化提供别名。
typedef
用于为数据类型声明同义词(别名),有时可以使用它来创建更短或更易读的类型名称。例如:
typedef Card *CardPtr;
上述语句定义了一个额外的类型名
CardPtr
,作为
Card *
类型的同义词。使用
typedef
创建名称并不会创建新的数据类型,只是创建了一个新的类型名。
typedef
的相关特化如下:
-
istream
:表示
basic_istream<char>
,支持字符输入。
-
ostream
:表示
basic_ostream<char>
,支持字符输出。
-
iostream
:表示
basic_iostream<char>
,支持字符的输入和输出。
流输入输出模板的继承关系如下:
classDiagram
class basic_ios
class basic_istream
class basic_ostream
class basic_iostream
basic_istream --|> basic_ios
basic_ostream --|> basic_ios
basic_iostream --|> basic_istream
basic_iostream --|> basic_ostream
运算符重载为输入输出操作提供了方便的表示法:
-
左移运算符(
<<
)
:被重载用于表示流输出,称为流插入运算符。
-
右移运算符(
>>
)
:被重载用于表示流输入,称为流提取运算符。
这些运算符可以与标准流对象
cin
、
cout
、
cerr
和
clog
一起使用,也可以与你在自己的代码中创建的流对象一起使用。
5. 标准流对象
-
cin:预定义对象,是istream的实例,连接到标准输入设备,通常是键盘。例如:
int grade;
cin >> grade; // 数据按照箭头方向流动
编译器会确定
grade
的数据类型,并选择合适的重载流提取运算符。如果
grade
已经正确声明,流提取运算符不需要额外的类型信息。
>>
运算符被重载用于输入基本类型、字符串和指针值的数据项。
-
cout
:预定义对象,是
ostream
的实例,连接到标准输出设备,通常是显示屏。例如:
int grade = 80;
cout << grade; // 数据按照箭头方向流动
编译器会确定
grade
的数据类型(假设
grade
已经正确声明),并选择合适的流插入运算符。
<<
运算符被重载用于输出基本类型、字符串和指针值的数据项。
-
cerr
:预定义对象,是
ostream
的实例,连接到标准错误设备,通常是屏幕。对
cerr
的输出是无缓冲的,这意味着每次向
cerr
插入流都会立即显示输出,这适用于及时向用户通知错误。
-
clog
:预定义对象,是
ostream
类的实例,连接到标准错误设备。对
clog
的输出是有缓冲的。这意味着每次向
clog
插入数据可能会将其输出保存在缓冲区(即内存中的一个区域)中,直到缓冲区满或缓冲区被刷新。缓冲是操作系统课程中讨论的一种输入输出性能增强技术。
6. 文件处理模板
C++ 的文件处理使用以下类模板:
-
basic_ifstream
:用于文件输入。
-
basic_ofstream
:用于文件输出。
-
basic_fstream
:用于文件的输入和输出。
与标准流一样,C++ 为这些类模板提供了
typedef
:
-
ifstream
:表示
basic_ifstream<char>
,支持从文件中进行字符输入。
-
ofstream
:表示
basic_ofstream<char>
,支持向文件中进行字符输出。
-
fstream
:表示
basic_fstream<char>
,支持从文件中进行字符输入和向文件中进行字符输出。
basic_ifstream
继承自
basic_istream
,
basic_ofstream
继承自
basic_ostream
,
basic_fstream
继承自
basic_iostream
。其继承关系如下:
classDiagram
class basic_ios
class basic_istream
class basic_ostream
class basic_iostream
class basic_ifstream
class basic_ofstream
class basic_fstream
basic_istream --|> basic_ios
basic_ostream --|> basic_ios
basic_iostream --|> basic_istream
basic_iostream --|> basic_ostream
basic_ifstream --|> basic_istream
basic_ofstream --|> basic_ostream
basic_fstream --|> basic_iostream
完整的流输入输出类层次结构提供了大部分所需的功能。你可以查阅 C++ 系统的类库参考以获取更多文件处理信息。
7. 流输出
ostream
提供了格式化和未格式化的输出能力,包括:
- 使用流插入运算符(
<<
)输出标准数据类型。
- 通过
put
成员函数输出字符。
- 通过
write
成员函数进行未格式化输出。
- 以十进制、八进制和十六进制格式输出整数。
- 以各种精度、强制小数点、科学记数法和固定记数法输出浮点数。
- 在指定宽度的字段中对齐输出数据。
- 在字段中用指定字符填充输出数据。
- 在科学记数法和十六进制记数法中输出大写字母。
7.1 char * 变量的输出
C++ 会自动确定数据类型,这比 C 有所改进,但有时这个特性会带来一些问题。例如,假设我们想打印存储在
char *
指针中的地址。
<<
运算符已被重载,将
char *
作为以空字符结尾的 C 风格字符串输出。要输出地址,可以将
char *
强制转换为
void *
(可以对任何指针变量进行此操作)。示例代码如下:
#include <iostream>
using namespace std;
int main()
{
const char *const word = "again";
cout << "Value of word is: " << word << endl
<< "Value of static_cast< const void * >( word ) is: "
<< static_cast< const void * >( word ) << endl;
return 0;
}
输出结果:
Value of word is: again
Value of static_cast< const void * >( word ) is: 0135CC70
地址通常以十六进制(基数为 16)数字形式打印,一般来说,地址的打印方式取决于具体实现。
7.2 使用 put 成员函数输出字符
可以使用
put
成员函数输出字符。例如:
cout.put( 'A' ); // 显示单个字符 A
cout.put( 'A' ).put( '\n' ); // 输出字母 A 后换行
put
函数也可以接受表示 ASCII 值的数值表达式作为参数,例如:
cout.put( 65 ); // 同样输出 A
put
成员函数返回对接收
put
调用的
ostream
对象(如
cout
)的引用,因此可以进行链式调用。
8. 流输入
istream
提供了格式化和未格式化的输入能力。流提取运算符(
>>
)通常会跳过输入流中的空白字符(如空格、制表符和换行符),后续会介绍如何改变这种行为。每次输入后,流提取运算符会返回对接收提取消息的流对象(如
cin
在表达式
cin >> grade
中)的引用。如果将该引用用作条件(如在
while
语句的循环继续条件中),流的重载
void *
转换运算符函数会被隐式调用,根据最后一次输入操作的成功或失败,将引用转换为非空指针值或空指针。非空指针转换为布尔值
true
表示成功,空指针转换为布尔值
false
表示失败。当尝试读取超过流的末尾时,流的重载
void *
转换运算符会返回空指针以表示文件结束。
每个流对象都包含一组状态位,用于控制流的状态(如格式化、设置错误状态等)。这些位由流的重载
void *
转换运算符用于确定是返回非空指针还是空指针。如果输入了错误类型的数据,流提取会设置流的
failbit
;如果操作失败,会设置流的
badbit
。
8.1 get 和 getline 成员函数
-
无参数的
get成员函数 :从指定流中输入一个字符(包括空白字符和其他非图形字符,如表示文件结束的键序列),并将其作为函数调用的值返回。当在流中遇到文件结束时,此版本的get返回EOF。示例代码如下:
#include <iostream>
using namespace std;
int main()
{
int character; // 使用 int 类型,因为 char 无法表示 EOF
cout << "Before input, cin.eof() is " << cin.eof() << endl
<< "Enter a sentence followed by end-of-file:" << endl;
while ((character = cin.get()) != EOF)
{
cout.put(character);
}
cout << "\nEOF in this system is: " << character << endl;
cout << "After input of EOF, cin.eof() is " << cin.eof() << endl;
return 0;
}
该程序首先打印
cin.eof()
的值(即
false
,输出为 0),表示
cin
上尚未发生文件结束。用户输入一行文本,然后按 Enter 键,接着输入文件结束符(在 Microsoft Windows 系统上是
<Ctrl> z
,在 Linux 和 Mac 系统上是
<Ctrl> d
)。程序逐字符读取输入并输出,当遇到文件结束符时,
while
循环结束,最后显示
cin.eof()
的值(此时为
true
,输出为 1)。
-
带字符引用参数的
get
成员函数
:从输入流中输入下一个字符(即使是空白字符),并将其存储在字符参数中。此版本的
get
返回对调用该成员函数的
istream
对象的引用。
-
三个参数的
get
成员函数
:接受一个
char
类型的内置数组、一个大小限制和一个分隔符(默认值为
'\n'
)。该版本从输入流中读取字符,要么读取比指定最大字符数少一个的字符并终止,要么在读取到分隔符时终止。会在用作缓冲区的字符数组中插入一个空字符来终止输入字符串。分隔符不会放入字符数组,但会留在输入流中(分隔符将是下一次读取的下一个字符)。因此,连续两次调用
get
的结果可能是一个空行,除非从输入流中移除分隔符(可能使用
cin.ignore()
)。
下面对比使用
cin
和
cin.get
进行输入的示例代码:
#include <iostream>
using namespace std;
int main()
{
const int SIZE = 80;
char buffer1[SIZE];
char buffer2[SIZE];
cout << "Enter a sentence:" << endl;
cin >> buffer1;
cout << "\nThe string read with cin was:" << endl
<< buffer1 << endl << endl;
cin.get(buffer2, SIZE);
cout << "The string read with cin.get was:" << endl
<< buffer2 << endl;
return 0;
}
cin
读取字符直到遇到空白字符,而
cin.get
会读取空白字符。
-
getline成员函数 :与三个参数的get成员函数操作类似,会在char类型的内置数组中的行后面插入一个空字符。getline函数会从流中移除分隔符(即读取该字符并丢弃它),但不会将其存储在字符数组中。示例代码如下:
#include <iostream>
using namespace std;
int main()
{
const int SIZE = 80;
char buffer[SIZE];
cout << "Enter a line of text:" << endl;
cin.getline(buffer, SIZE);
cout << "The line you entered is: " << buffer << endl;
return 0;
}
总结
本文详细介绍了 C++ 中流输入输出的相关知识,涵盖了流的基本概念、经典流与标准流的区别、
iostream
库头文件、流输入输出类和对象、标准流对象、文件处理模板、流输出和流输入等方面。通过具体的代码示例展示了各种输入输出操作的使用方法,包括如何处理不同类型的数据、如何控制输出格式、如何处理文件输入输出以及如何处理输入流中的特殊情况等。掌握这些知识对于编写高效、可靠的 C++ 程序至关重要。
以下是 C++ 流输入输出相关操作的总结表格:
| 操作类型 | 相关对象/函数 | 功能描述 |
| ---- | ---- | ---- |
| 标准流对象 |
cin
| 连接到标准输入设备,用于输入数据 |
| |
cout
| 连接到标准输出设备,用于输出数据 |
| |
cerr
| 连接到标准错误设备,无缓冲输出错误信息 |
| |
clog
| 连接到标准错误设备,有缓冲输出错误信息 |
| 输出操作 |
<<
运算符 | 流插入运算符,用于输出标准数据类型 |
| |
put
成员函数 | 输出单个字符 |
| |
write
成员函数 | 进行未格式化输出 |
| 输入操作 |
>>
运算符 | 流提取运算符,用于输入基本数据类型 |
| |
get
成员函数 | 输入字符,有不同的重载版本 |
| |
getline
成员函数 | 输入一行文本 |
| 文件处理 |
ifstream
| 从文件中进行字符输入 |
| |
ofstream
| 向文件中进行字符输出 |
| |
fstream
| 进行文件的输入和输出 |
整个流输入输出的操作流程可以用以下 mermaid 流程图表示:
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px
A([开始]):::startend --> B(初始化流对象):::process
B --> C{选择操作类型}:::process
C -->|输出| D(选择输出方式):::process
D -->|流插入运算符| E(输出标准数据类型):::process
D -->|put 函数| F(输出字符):::process
D -->|write 函数| G(未格式化输出):::process
C -->|输入| H(选择输入方式):::process
H -->|流提取运算符| I(输入基本数据类型):::process
H -->|get 函数| J(输入字符):::process
H -->|getline 函数| K(输入一行文本):::process
C -->|文件处理| L(选择文件操作类型):::process
L -->|读取文件| M(使用 ifstream 输入):::process
L -->|写入文件| N(使用 ofstream 输出):::process
L -->|读写文件| O(使用 fstream 操作):::process
E --> P([结束]):::startend
F --> P
G --> P
I --> P
J --> P
K --> P
M --> P
N --> P
O --> P
通过上述内容,希望读者能够对 C++ 流输入输出有更深入的理解和掌握,能够在实际编程中灵活运用这些知识来实现各种输入输出需求。
348



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



