第十四章 C++中的代码重用#
本章内容包括:
- has-a 关系;
- 包含对象成员的类;
- 模板类valarray
- 私有和保护继承;
- 多重继承;
- 虚基类;
- 创建类模板;
- 使用类模板;
- 模板的具体化。
14.1 包含对象成员的类#
14.1.1 valarray类简介#
valarray类是由头文件 valarray 支持的。这个类用于处理数值,它支持诸如将数组中所有元素的值相加以及在数组中找出最大和最小的值等操作。valarray被定义为一个模板类,以便能够处理不同的数据类型。例子:

从中可知,可以创建长度为零的空数组、指定长度的空数组、所有元素度被初始化为指定值的数组、用常规数组中的值进行初始化的数组。
这个类有一些内置方法:
- operator:访问各个元素;
size(): 返回包含的元素数;sum(): 返回所有元素的总和;max(): 返回最大的元素;min(): 返回最小的元素。
等等,https://cplusplus.com/reference/valarray/valarray/ 。
14.1.2 Student类的设计#


14.2 私有继承#
C++还有另一种实现has-a关系的途径——私有继承。使用私有继 承,基类的公有成员和保护成员都将成为派生类的私有成员。这意味着 基类方法将不会成为派生对象公有接口的一部分,但可以在派生类的成 员函数中使用它们。
要进行私有继承,请使用关键字 private 而不是 public 来定义类(实 际上,private是默认值,因此省略访问限定符也将导致私有继承)。
使用私有继承时,只能在派生类的方法中使用基类的方法。
用类名显式地限定函数名不适合于友元函数,这是因为友元不属于 类。然而,可以通过显式地转换为基类来调用正确的函数。
如果两个基类都提供了函数operator«( ),由于这个类使用的是多重继承,编译器将无法确定 应转换成哪个基类。
14.2.2 使用包含还是私有继承#
大多数C++程序员倾向于使用包含。首先,它易于理 解。类声明中包含表示被包含类的显式命名对象,代码可以通过名称引 用这些对象,而使用继承将使关系更抽象。其次,继承会引起很多问 题,尤其从多个基类继承时,可能必须处理很多问题,如包含同名方法 的独立的基类或共享祖先的独立基类。总之,使用包含不太可能遇到这 样的麻烦。另外,包含能够包括多个同类的子对象。如果某个类需要3 个string对象,可以使用包含声明3个独立的string成员。而继承则只能使 用一个这样的对象(当对象都没有名称时,将难以区分)。
然而,私有继承所提供的特性确实比包含多。例如,假设类包含保 护成员(可以是数据成员,也可以是成员函数),则这样的成员在派生 类中是可用的,但在继承层次结构外是不可用的。如果使用组合将这样 的类包含在另一个类中,则后者将不是派生类,而是位于继承层次结构 之外,因此不能访问保护成员。但通过继承得到的将是派生类,因此它 能够访问保护成员。
另一种需要使用私有继承的情况是需要重新定义虚函数。派生类可 以重新定义虚函数,但包含类不能。使用私有继承,重新定义的函数将 只能在类中使用,而不是公有的。
14.2.3 保护继承#
保护继承是私有继承的变体。保护继承在列出基类时使用关键字 protected。使用保护继承时,基类的公有成员和保护成员都将成为派生类的保护成员。

14.2.4 使用using重新定义访问权限#
使用保护派生或私有派生时,基类的公有成员将成为保护成员或私 有成员。假设要让基类的方法在派生类外面可用,方法之一是定义一个使用该基类方法的派生类方法。
另一种方法是,将函数调用包装在另一个函数调用中,即使用一个 using声明(就像名称空间那样)来指出派生类可以使用特定的基类成 员,即使采用的是私有派生。
14.3 多重继承#
【短期内不会用到,多重继承、模板类和虚基类等没细看。】

14.5 总结#
C++提供了几种重用代码的手段。无论使用哪种继 承,基类的公有接口都将成为派生类的内部接口。这有时候被称为继承 实现,但并不继承接口,因为派生类对象不能显式地使用基类的接口。 因此,不能将派生对象看作是一种基类对象。
还可以通过开发包含对象成员的类来重用类代码。这种方法被称为 包含、层次化或组合,它建立的也是has-a关系。另一方面,如果需要使用某个类的几个对象,则用包含更适合。
多重继承(MI)使得能够在类设计中重用多个类的代码。
第十五章 友元、异常和其他#
本章内容包括:
- 友元类;
- 友元类方法;
- 嵌套类;
- 引发异常、
try块和catch块; - 异常类;
- 运行阶段类型识别(RTTI);
dynamic_cast和typeid;static_cast、const_cast和reiterpret_cast。

2.bad_alloc异常和new
对于使用new导致的内存分配问题,C++的最新处理方式是让new引发bad_alloc异常。头文件new包含bad_alloc类的声明,它是从exception 类公有派生而来的。但在以前,当无法分配请求的内存量时,new返回 一个空指针,然后我们可以 exit(EXIT_FAILURE)。
15.6 总结#
友元使得能够为类开发更灵活的接口。类可以将其他函数、其他类 和其他类的成员函数作为友元。在某些情况下,可能需要使用前向声 明,需要特别注意类和方法声明的顺序,以正确地组合友元。
嵌套类是在其他类中声明的类,它有助于设计这样的助手类,即实 现其他类,但不必是公有接口的组成部分。
第十六章 string类和标准模板库#
本章内容包括:
- 标准C++ string类;
- 模板auto_ptr、unique_ptr 和 shared_ptr;
- 标准模板库;
- 容器类;
- 迭代器
- 函数对象;
- STL算法
- 模板initializer_list
16.4.1 为何使用迭代器#
模板使得算法独立于存储的数据类型,而迭代器使算法独立于使用的容器类型。
第十七章 输入、输出和文件#
本章内容包括:
- C++ 角度的输入和输出;
- iostream 类系列;
- 重定向;
- ostream;
- 格式化输出;
- istream类方法;
- 流状态;
- 文件I/O;
- 使用 ifstream 类从文件输入;
- 使用 ofstream 类输出到文件;
- 使用 fstream 类进行文件输入和输出;
- 命令行处理;
- 二进制文件;
- 随机文件访问;
- 内核格式化。
17.1 C++输入和输出概述#
C++依赖于C++的I/O解决方案,而不是C语言的I/O解决方 案,前者是在头文件iostream(以前为iostream.h)和fstream(以前为 fstream.h)中定义一组类。
17.1.1 流和缓冲区#
C++程序把输入和输出看作字节流。输入时,程序从输入流中抽取字节;输出时,程序将字节插入到输出流中。
输入流中的字节可能来自键盘,也可能来自存储设备(如硬 盘)或其他程序。同样,输出流中的字节可以流向屏幕、打印机、存储 设备或其他程序。流充当了程序和流源或流目标之间的桥梁。
C++程序只是检查字节流,而不需要知道字节来自何方。同理,通过使 用流,C++程序处理输出的方式将独立于其去向。因此管理输入包含两 步:
- 将流与输入去向的程序关联起来;
- 将流与文件连接起来。
换句话说,输入流需要两个连接,每端各一个。文件端部连接提供了流的来源,程序端连接将流的流出部分转储到程序中(文件端连接可以是文件,也可以是设备,如键盘)。同样,对输出的管理包括将输出流连接到程序以及将输出目标与流关联起来。这就像将字节(而不是水)引入到水管中(参见图17.1)。

通常,通过使用缓冲区可以更高效地处理输入和输出。缓冲区是用作中介的内存块,它是将信息从设备传输到程序或从程序传输给设备的临时存储工具。

C++程序通常在用户按下回车 键时刷新输入缓冲区。对于屏幕输出,C++程序通 常在用户发送换行符时刷新输出缓冲区。程序也可能会在其他情况下刷新输入,例如输入即将到来时,这取决于实现。
17.1.2 流、缓冲区和iostream文件#

- cin对象对应于标准输入流;
- cout对象与标准输出流相对应;
- cerr对象与标准错误流相对应,可用于显示错误消息。这个流没有 被缓冲,这意味着信息将被直接发送给屏幕,而不会等到缓冲区填 满或新的换行符;
- clog对象也对应着标准错误流。在默认情况下,这个流被关联到标 准输出设备(通常为显示器),这个流被缓冲;
17.1.3 重定向#
标准输入和输出流通常连接着键盘和屏幕。重定向这个方法可以改变标准输入和标准输出。通过输入重定向(<)和输出重定向(>)可以将输入和输出重置为文件。在 UNIX和Linux中,运算符 2> 重定向标准错误。
17.2 使用cout进行输出#
17.2.2 其他ostream方法#
除了各种 operator<<() 函数外,ostream 类还提供了 put() 方法和write() 方法,前者用于显示字符,后者用于显示字符串。
在程序清单17.3的输出中各列并没有对齐,这是因为数字的字段宽度不相同。可以使用width成员函数将长度不同的数字放到宽度相同的字段中。
width() 方法只影响将显示的下一个项目,然后字段宽度将恢复为默认值:
| |
输出:

12被放到宽度为12个字符的字段的最右边,这被称为右对齐。然后,字段宽度恢复为默认值,并将两个#符号以及24放在宽度与它们的长度相等的字段中。
- 设置填充字符
在默认情况下,cout用空格填充字段中未被使用的部分,可以用 fill( )成员函数来改变填充字符。例如,下面的函数调用将填充字符改为星号:
| |
- 设置浮点数的显示精度
C++的默认精度为6位(但末尾的0将不显 示)。precision() 成员函数使得能够选择其他值。
- 在谈
setf()

17.3 使用 cin 进行输入#
17.3.3 其他istream类方法#
get( ) 和 getline( ) 方法:
- 函数
get(char&)和get(void)提供不跳过空白的单字符输入功能; - 函数
get(char*, int, char)和getline(char*, int, char)在默认情况下读取整行而不是一个单词。

这里的重点是,通过使用 get(ch),代码读取、显示并考虑空格和可打印字符。
假设程序使用的是 >>,那么代码将跳过空格,因此最后的输出就压缩成了:
IC++clearly.
并且程序由于>>运算符跳过了最后的换行符,所以 while 循环不会结束。
到达文件尾后(不管是真正的文件尾还是模拟的文件尾), cin.get(void) 都将返回值 EOF——头文件 iostream 提供的一个符号常量。

这里应将 ch 的类型声明为 int,而不是 char,因为值 EOF 可能无法使 用 char 类型来表示。

2.采用哪种单字符输入形式
假设可以选择 >>、get(char &) 或 get(void),应使用哪一个呢? 首先,应确定是否希望跳过空白。如果跳过空白更方便,则使用抽取运算符 >>。
如果希望程序检查每个字符,则使用 get() 方法,例如,计算字数的程序可以使用空格来判断单词何时结束。
在 get() 方法中,get(char &)的接口更佳。get(void)的主要优点是,它与标准C语言中的 getchar() 函数极其类似,这意味着可以通过包含iostream(而不是 stdio.h),并用 cin.get() 替换所有的 getchar(),用 cout.put(ch) 替换所有的 putchar(ch),来将C程序转换为C++程序。
3.字符串输入:getline()、get() 和 ignore()
getline( )成员函 数和get( )的字符串读取版本都读取字符串,它们的函数特征标相同(这 是从更为通用的模板声明简化而来的):

第一个参数是用于放置输入字符串的内存单元的地址。第二个参数比要读取的最大字符数大1(额外的一个字符用于存储结尾的空字符, 以便将输入存储为一个字符串)。第三个参数指定用作分界符的字符, 只有两个参数的版本将换行符用作分界符。上述函数都在读取最大数目的字符或遇到换行符后为止。
get() 和 getline() 之间的主要区别在于,get() 将换行符留在输入流中,这样接下来的输入操作首先看到是将是换行符, 而 gerline() 抽取并丢弃输入流中的换行符。
第三个参数用于指定分界符,遇到分界字符后, 输入将停止,即使还未读取最大数目的字符。在默认情况下,如果在读取指定数目的字符之前到达行尾,这两种方法都将停止读取输 入。并且,get() 将分界字符留在输入队列中,而getline() 不保留。
ignore() 成员函数,该函数接受两个参数:一个是数字,指定要读取的最大字符数;另一个是字符,用作输入分界符。下面的函数调用读取并丢弃接下来的255个字符或直到到达第一个换行符:
| |
原型为两个参数提供的默认值为1和 EOF, 该函数的返回类型为 istream &。
- 意外字符串输入
get(char *, int) 和 getline( ) 的某些输入形式将影响流状态。与其他输 入函数一样,这两个函数在遇到文件尾时将设置 eofbit,遇到流被破坏 (如设备故障)时将设置 badbit。另外两种特殊情况是无输入以及输入到达或超过函数调用指定的最大字符数。
假设输入队列中的字符数等于或超过了输入方法指定的最大字 符数。首先,来看getline() 和下面的代码:
| |
getline( )方法将从输入队列中读取字符,将它们放到temp数组的元 素中,直到(按测试顺序)到达文件尾、将要读取的字符是换行符或存 储了29个字符为止。如果遇到文件尾,则设置eofbit;如果将要读取的 字符是换行符,则该字符将被读取并丢弃;如果读取了29个字符,并且下一个字符不是换行符,则设置failbit。因此,包含30个或更多字符的输入行将终止输入。
get(char *, int) 方法首先测试字符数,然后测试是否为 文件尾以及下一个字符是否是换行符。如果它读取了最大数目的字符,则不设置 failbit 标记。然而,由此可以知道终止读取是否是由于输入字 符过多引起的。可以用 peek()(下一节)来查看下一个输入字符。 如果它是换行符,则说明 get() 已读取了整行;如果不是换行符,则说明 get() 是在到达行尾前停止的。这种技术对getline() 不适用,因为 getline() 读取并丢弃换行符,因此查看下一个字符无法知道任何情况。然而, 如果使用的是 get(),则可以知道是否读取了整个一行。

与 getline() 和 get() 不同的是,read() 不会在输入后加上空值字符, 因此不能将输入转换为字符串。read() 方法不是专为键盘输入设计的, 它最常与 ostream write() 函数结合使用,来完成文件输入和输出。该方法的返回类型为 istream &。

17.4 文件输入和输出#
重定向虽然可以提供一些文件 支持,但它比显式程序中的文件I/O的局限性更大。另外,重定向来自操作系统,而非C++,因此并非所有系统都有这样的功能。
要写入文件,需要创建一个ofstream对象,并使用 ostream 方法,如 << 插入运算符或 write()。要读取文件,需要创建一个 ifstream 对象,并使用 istream 方法,如 >> 抽取运算符或 get()。
17.4.1 简单的文件 I/O#
程序写文件需要包含头文件 fstream,并这样做:
- 声明并创建一个
ofstream对象来管理输出流; - 将该对象和特定文件关联起来,
open方法; - 以 cout 的方式使用该对象,区别是输出将进入文件,而不是屏幕。
ostream 是 ofstream 类的基类,因此可以使用所有的 ostream 方法,包括各种插入运算符定义、格式化方法和控制符。ofstream 类使用被缓冲的输出,因此程序在创建像 fout 这样的 ofstream 对象时,将为输出缓冲区分配空间。如果创建了两个 ofstream 对象,程序将创建两个缓冲区,每个对象各一个。
程序读取文件的要求与写入文件相似:
- 创建一个
ifstream对象来管理输入流; - 将该对象与特定的文件关联起来,
open()方法; - 以使用
cin的方式使用该对象。
输入和输出一样,也是被缓冲的,因此创建ifstream对象与fin一 样,将创建一个由fin对象管理的输入缓冲区。与输出一样,通过缓冲, 传输数据的速度比逐字节传输要快得多。
当输入和输出流对象过期(如程序终止)时,到文件的连接将自动 关闭。另外,也可以使用 close() 方法来显式地关闭到文件的连接。
关闭这样的连接并不会删除流,而只是断开流到文件的连接,但流管理装置仍被保留。关闭文件将刷新缓冲区,从而确保文件被更新。
17.4.2 流状态检查和 is_open()#
如果一切顺利,则流状态为零(没有消息就是好消息)。其他状态都是通过将特定位设置为1来记录的。试图打开一个不存在的文件进行 输入时,将设置failbit位,因此可以这样进行检查:
| |
由于ifstream对象和istream对象一样,被放在需要bool类型的地方 时,将被转换为bool值,因此您也可以这样做:
| |
较新的C++实现提供了一种更好的检查文件是否被打开的方 法——is_open()方法:
| |
这种方式之所以更好,是因为它能够检测出其他方式不能检测出的 微妙问题:

17.4.3 打开多个文件#
可能要依次处理一组文件。例如,可能要计算某个名称在10 个文件中出现的次数。在这种情况下,可以打开一个流,并将它依次关 联到各个文件。这在节省计算机资源方面,比为每个文件打开一个流的效率高。 使用这种方法,首先需要声明一个ifstream 对象(不对它进行初始化),然后使用 open() 方法将这个流与文件关联起来。例如,下面 是依次读取两个文件的代码:

17.4.4 命令行处理技术#
文件处理程序通常使用命令行参数来指定文件。命令行参数是用户 在输入命令时,在命令行中输入的参数。
C++有一种让在命令行环境中运行的程序能够访问命令行参数的机制,方法是使用下面的 main() 函数:
| |
argc 为命令行中的参数个数,其中包括命令名本身。argv 变量为一个指针,它指向一个指向 char 的指针。例如:argv[0] 是一个指针,指向存储第一个命令行参数的字符串的第一个字符,依此类推。也就是说, argv[0]是命令行中的第一个字符串,依此类推。例子:
| |
则 argc 为4,argv[0] 为 wc,argv[1] 为 report1,依此类推。
17.4.5 文件模式#
文件模式描述的是文件将被如何使用:读、写、追加等。将流与文件关联时(无论是使用文件名初始化文件流对象,还是使用 open() 方法),都可以提供指定文件模式的第二个参数,下表给出了第二个参数的具体信息。

| |
注意,C++ mode 是一个open mode值,如 ios_base::in;而 c mode是相 应的 C模式字符串,如“r”。


17.4.6 随机存取#
随机存取指的是直接移 动(不是依次移动)到文件的任何位置。
使用临时文件#
开发应用程序时,经常需要使用临时文件,这种文件的存在是短暂的,必须受程序控制。创建临时文件、复制另一个文件的内容并 删除文件其实都很简单。首先,需要为临时文件制定一个命名方案,但如何确保每个文件都 被指定了独一无二的文件名呢?cstdio 中声明的 tmpnam() 标准函数可以帮助您。
| |
tmpnam() 函数创建一个临时文件名,将它放在 pszName 指向的C-风格字符串中。常量 L_tmpnam 和 TMP_MAX(二者都是在cstdio中定义的)限制了文件名包含的字符数以及在确保当前目录中不生成重复文件名的情况下 tmpnam() 可被调用的最多次数。下面是生成10个临时 文件名的代码:
| |
更具体地说,使用tmpnam( )可以生成TMP_NAM个不同的文件名,其中每个文件名包含 的字符不超过L_tmpnam个。生成什么样的文件名取决于实现。
17.5 内核格式化#
17.6 总结#
流是进出程序的字节流。缓冲区是内存中的临时存储区域,是程序 与文件或其他I/O设备之间的桥梁。
istream类定义了多个版本的抽取 运算符(»),用于识别所有基本的C++类型,并将字符输入转换为这 些类型。get( )方法族和getline( )方法为单字符输入和字符串输入提供了 进一步的支持。同样,ostream类定义了多个版本的插入运算符 («),用于识别所有的C++基本类型,并将它们转换为相应的字符输 出。put( )方法对单字符输出提供了进一步的支持。wistream和wostream 类对宽字符提供了类似的支持。
fstream文件提供了将iostream方法扩展到文件I/O的类定义。ifstream 类是从istream类派生而来的。通过将ifstream对象与文件关联起来,可 以使用所有的istream方法来读取文件。同样,通过将ofstream对象与文 件关联起来,可以使用ostream方法来写文件;通过将fstream对象与文件 关联起来,可以将输入和输出方法用于文件。
要将文件与流关联起来,可以在初始化文件流对象时提供文件名, 也可以先创建一个文件流对象,然后用open( )方法将这个流与文件关联 起来。close( )方法终止流与文件之间的连接。类构造函数和open( )方法 接受可选的第二个参数,该参数提供文件模式。
seekg() 和 seekp() 函数提供对文件的随机存取。这些类方法使得能够将文件指针放置到相对于文件开头、文件尾和当前位置的某个位置。tellg() 和tellp() 方法报告当前的文件位置。
第十八章 探讨C++新标准#
(暂时不看)