1、参考引用
- C++高级编程(第4版,C++17标准)马克·葛瑞格尔
2、建议先看《21天学通C++》 这本书入门,笔记链接如下
- 21天学通C++读书笔记(文章链接汇总)
1. 使用流
1.1 流的含义
- C++ 中预定义的流
- 缓冲的流和非缓冲的流的区别在于:前者不是立即将数据发送到目的地,而是缓冲输入的数据,然后以块方式发送,而非缓冲的流则立即将数据发送到目的地
- 缓冲的目的通常是提高性能,对于某些目的地(如文件)而言,一次性写入较大的块时速度更快
- 注意,始终可使用 flush() 方法刷新缓冲区,强制要求缓冲的流将其当前所有的缓冲数据发送到目的地
- 所有输入流都有一个关联的来源,所有输出流都有一个关联的目的地
- 有关流的另一个要点是:流不仅包含普通数据,还包含称为当前位置的特殊数据。当前位置指的是流将要进行下一次读写操作的位置
1.2 流的来源和目的地
- 流可应用于任何接收数据或生成数据的对象,C++ 中流可使用 3 个公共的来源和目的地:控制台、文件和字符串
- 1. 控制台
- 控制台输入流允许程序在运行时从用户那里获得输入,使程序具有交互性
- 控制台输出流向用户提供反馈和输出结果
- 2. 文件:文件流从文件系统中读取数据并向文件系统写入数据
- 文件输入流适用于读取配置数据、读取保存的文件,也适用于批处理基于文件的数据等任务
- 文件输出流适用于保存状态数据和提供输出等任务
- 文件流包含 C 语言输出函数 fprintf()、fwrite() 和 fputs() 功能,及输入函数 fscanf()、fread() 和 fgets() 功能
- 3. 字符串:字符串流是将流隐喻应用于字符串类型的例子
- 使用字符串流时,可像处理其他任何流一样处理字符数据
- 使用流式语法为优化提供了机会,而且比直接使用 string 类方便
- 字符串流包含 sprintf()、sprintf_s() 和 sscanf() 的功能,以及很多 C 语言字符串格式化函数的功能
- 1. 控制台
1.3 流式输出
1.3.1 输出的基本概念
- 输出流定义在 <ostream> 头文件中,大部分程序都包含 <iostream> 头文件,该头文件包含输入流和输出流的头文件
- 使用输出流的最简单方法是使用 << 运算符
int i = 7; cout << i << endl; char ch = 'a'; cout << ch << endl; string myString = "Hello World!"; cout << myString << endl;
// 输出 7 a Hello World!
- cout 流是写入控制台的内建流,控制台也称为标准输出。可将 << 的使用串联起来,从而输出多个数据段。这是因为 << 运算符返回一个流的引用,因此可以立即对同一个流再次应用 << 运算符
int j = 11; cout << "The value of j is " << j << "!" << endl;
// 输出 The value of j is 11!
- \n 和 endl 的区别是,\n 仅开始一个新行,而 endl 还会刷新缓存区
- 使用 endl 时要小心,因为过多的缓存区刷新会降低性能
1.3.2 输出流的方法
- put() 和 write()
- put() 接收单个字符,write() 接收一个字符数组
- 传给这些方法的数据按照原本的形式输出,没有做任何特殊的格式化和处理操作
cout.put('a'); const char* test = "hello there\n"; cout.write(test, strlen(test));
- flush()
- 向输出流写入数据时,流不一定会将数据立即写入目的地。大部分输出流都会进行缓冲,也就是积累数据,而不是立即将得到的数据写出去。缓冲的目的通常是提高性能,对于某些目的地(如文件)而言,与逐字符写入相比,一次性写入较大的块时速度更快(但是 cerr 输出流就不会缓存其输出)
- 显式要求流刷新缓存的方法是调用流的 flush() 方法
cout << "abc"; cout.flush(); cout << "def"; cout << endl;
1.3.3 处理输出错误
-
输出错误可能会在多种情况下出现:试图打开一个不存在的文件;因为磁盘错误导致写入操作失败,例如磁盘已满
-
调用流的 good() 方法可以判断这个流当前是否处于正常状态
if (cout.good()) { cout << "All good" << endl; }
-
方法 fail() 在最近一次操作失败时返回 true,但没有说明下一次操作是否也会失败。例如,对输出流调用 flush() 后,可调用 fail() 确保流仍然可用
cout.flush(); if (cout.fail()) { cerr << "Unable to flush to standard out" << endl; }
-
流具有可转换为 bool 类型的转换运算符。转换运算符与调用 !fail() 时返回的结果相同。因此,可将前面的代码段重写为
cout.flush(); if (!cout) { cerr << "Unable to flush to standard out" << endl; }
-
还可要求流在发生故障时抛出异常。然后编写一个 catch 处理程序来捕捉 ios_base::failure 异常,然后对这个异常调用 what() 方法,获得错误的描述信息,调用 code() 方法获得错误代码
cout.exceptions(ios::failbit | ios::badbit | ios::eofbit); try { cout << "Hello World." << endl; } catch (const ios_base::failure& ex) { cerr << "Caught exception: " << ex.what() << ", error code = " << ex.code() << endl; }
-
通过 clear() 方法重置流的错误状态
cout.clear();
1.4 流式输入
1.4.1 输入的基本概念
- 通过输入流,可采用两种简单方法来读取数据。第一种方法类似于 << 运算符,<< 向输出流输出数据。读入数据对应的运算符是 >>,通过 >> 从输入流读入数据时,代码提供的变量保存接收的值
string userInput; cin >> userInut; cout << "User input was " << userInput << endl;
- 默认情况下 >> 运算符根据空白字符对输入值进行标志化。例如,如果用户运行以上程序,并键入 hello there 作为输入,那么只有第一个空白字符 (在这个例子中为空格符) 之前的字符才会存储在 userInput 变量中,输出如下
User input was hello // 在输入中包含空白字符的一种方法是使用 get()
- 通过输入流可以读入多个值,并且可根据需要混合和匹配类型
- >> 运算符会根据空白字符符号化,因此 getReservationData() 函数不允许输入带有空白字符的姓名。一种解决方法是使用 unget() 方法
void getReservationData { string guestName; int partySize; cout << "Name and number of guests: "; cin >> guestName >> partySize; // 使用 cin 会立即刷新 cout 缓存区 cout << "Thank you, " << guestName << "." << endl; }
1.4.2 处理输入错误
- 输入流提供了一些方法用于检测异常情形。大部分和输入流有关的错误条件都发生在无数据可读时。例如,可能到达流尾(称为文件末尾,即使不是文件流)。查询输入流状态的最常见方法是在条件语句中访问输入流。例如,只要 cin 保持在 “良好” 状态,下面的循环就继续进行
while (cin) { // ... } // 同时还可输入数据 while (cin >> ch) { // ... }
- 还可在输入流上调用 good()、bad() 和 fail() 方法,就像输出流那样
1.4.3 输入方法
-
get()
- get() 方法允许从流中读入原始输入数据。get() 的最简单版本返回流中的下一个字符,其他版本一次读入多个字符。get() 常用于避免 >> 运算符的自动标志化(可包含空格)
string readName(istream& stream) { string name; while (stream) { int next = stream.get(); // ... } }
-
unget()
- 调用 unget() 会导致流回退一个位置,将读入的前一个字符放回流中。调用 fail() 方法可查看 unget() 是否成功。例如,如果当前位置就是流的起始位置,那么 unget() 会失败
-
putback()
- putback() 和 unget() 一样,允许在输入流中反向移动一个字符。区别在于 putback() 方法将放回流中的字符接收为参数
char ch1; cin >> ch1; cin.putback('e'); // 'e' 将是从流中读出的下一个字符
-
peek()
- 通过 peek() 方法可预览调用 get() 后返回的下一个值
- 非常适合于:在读取前需要预先查看一个值的场合
-
getline()
- 从输入流中获得一行数据是一种常见需求,有一个方法能完成这个任务:getline() 方法用一行数据填充字符缓存区,数据量最多至指定大小,指定的大小中包括 \0 字符。因此,下面的代码最多从 cin 中读取 kBufferSize - 1 个字符,或者读到行尾为止
char buffer[kBufferSize] = {0}; cin.getline(buffer, kBufferSize);
- 还有一个用于 C++ 字符串的 std::getline() 函数。该函数定义在 <string> 头文件和 std 名称空间中,接收一个流引用、一个字符串引用和一个可选的分隔符作为参数。该版本 getline() 函数的优点是不需要指定缓存区的大小
string myString; std::getline(cin, myString);
2. 字符串流
- 可通过字符串流将流语义用于字符串,通过这种方式,可得到一个内存中的流来表示文本数据
- 例如,在 GUI 应用程序中,可能需要用流来构建文本数据,但不是将文本输出到控制台或文件中,而是把结果显示在 GUI 元素中,例如消息框和编辑框
- 另一个例子是,要将一个字符串流作为参数传给不同函数,同时维护当前的读取位置,这样每个函数都可以处理流的下一部分。字符串流也非常适合于解析文本,因为流内建了标记化的功能
- std::ostringstream 类用于将数据写入字符串,std:istringstream 类用于从字符串中读出数据。这两个类都定义在 <sstream> 头文件中。ostringstream 和istringstream 把同样的行为分别继承为 ostream 和 istream
ostringstream outStream; while (cin) { string nextToken; cin >> nextToken; outStream << nextToken << "\t"; }
Muffin creatMuffin(istringstream& stream) { string description; int size; bool hasChips; // ... stream >> description >> size >> boolalpha >> hasChips; }
相对于标准 C++字符串,字符串流的主要优点是除了数据之外,这个对象还知道从哪里进行下一次读或写操作,这个位置也称为当前位置。与字符串相比,字符串流的另一个优势是支持操作算子和本地化,格式化功能更加强大
3. 文件流
- 文件本身非常符合流的抽象,因为读写文件时,除数据外,还涉及读写的位置。在 C++ 中,std:ofstream 和 std::ifstream 类提供了文件的输入输出功能。这两个类在 <fstream> 头文件中定义
- 输出文件流和其他输出流的唯一主要区别在于:文件流的构造函数可以接收文件名以及打开文件的模式作为参数
- 默认模式是写文件 (ios base:out),这种模式从文件开头写文件,改写任何已有的数据
- 给文件流构造函数的第二个参数指定常量 ios base::app,还可按追加模式打开输出文件流
-
可组合模式,例如,如果要打开文件用于输出(以二进制模式),同时截断现有数据,可采用如下方式指定打开模式
ios_base::out | ios_base::binary | ios_base::trunc
-
ifstream 自动包含 ios_base::in 模式,ofstream 自动包含 ios_base::out 模式,即使不显式地将 in 或 out 指定为模式,也同样如此。下面的程序打开文件 test.txt,并输出程序的参数。ifstream 和 ofstream 析构函数会自动关闭底层文件,因此不需要显式调用 close()
int main() { ofstream outFile("test.txt", ios_base::trunc); if (!outFile.good()) { cerr << "..." << endl; return -1; } // ... }
3.1 文本模式与二进制模式
- 默认情况下,文件流在文本模式中打开。如果指定 ios_base::binary 标志,将在二进制模式中打开文件。在二进制模式中,要求把流处理的字节写入文件。读取时,将完全按文件中的形式返回字节
- 在文本模式中,会执行一些隐式转换,写入文件或从文件中读取的每一行都以 \n 结束
3.2 通过 seek() 和 tell() 在文件中转移
- 所有的输入流和输出流都有 seek() 和 tell() 方法
- seek() 方法允许在输入流或输出流中移动到任意位置
- 可通过 tell() 方法查询流的当前位置,这个方法返回一个表示当前位置的 streampos 值。利用这个结果,可在执行 seek() 之前记住当前标记的位置,还可查询是否在某个特定位置
3.3 将流链接在一起
-
任何输入流和输出流之间都可以建立链接,从而实现 “访问时刷新” 的行为。换句话说,当从输入流请求数据时,链接的输出流会自动刷新。这种行为可用于所有流,但对于可能互相依赖的文件流来说特别有用
-
通过 tie() 方法完成流的链接。要将输出流链接至输入流,对输入流调用 tie() 方法,并传入输出流的地址。要解除链接,传入 nullptr
-
下面的程序将一个文件的输入流链接至一个完全不同的文件的输出流,也可链接至同一个文件的输出流,但是双向 I/O 可能是实现同时读写同一个文件的更优雅方式
ifstream inFile("input.txt"); ofstream outFile("output.txt"); inFile.tie(&outFile); // 建立链接 outFile << "Hello there!"; // outFile 未被刷新,因为 std::endl 未发送 // 会触发 flush() string nextToken; inFile >> nextToken; // outFile 被刷新
-
可通过这种机制保持两个相关文件的同步:每次写入一个文件时,发送给另一个文件的缓存数据会被刷新
- 这种流链接的一个例子是 cout 和 cin 之间的链接。每当从 cin 输入数据时,都会自动刷新 cout
- cerr 和 cout 之间也存在链接,这意味着到 cerr 的任何输出都会导致刷新 cout,而 clog 未链接到 cout
4. 双向 I/O
- 目前,本章把输入流和输出流当作独立但又关联的类来讨论。事实上,有一种流可同时执行输入和输出,双向流可同时以输入流和输出流的方式操作
- 双向流是 iostream 的子类,而 iostream 是 istream 和 ostream 的子类,因此这是一个多重继承示例。显然双向流支持 >> 和 << 运算符,还支持输入流和输出流的方法
- fstream 类提供了双向文件流。fstream 特别适用于需要替换文件中数据的应用程序,因为可通过读取文件找到正确的位置,然后立即切换为写入文件
- 还可通过 stringstream 类双向访问字符串流
双向流用不同的指针保存读位置和写位置。在读取和写入之间切换时,需要定位到正确的位置