《C++ Primer Plus 6》读书笔记; C++是在 C 语言基础上开发的一种集面向对象编程、泛型编程和过程化编程于一体的编程语言,是C 语言的超集。本书是根据 2003 年的 ISO/ANSI C++标准编写的,并专辟一章介绍了 C++11 新增的功能。本书针对 C++初学者,书中从 C 语言基础知识开始介绍,然后在此基础上详细阐述 C++新增的特性,因此不要求读者有 C 语言方面的背景知识。
目录
内容提要
略。
作者简介
肯定是大牛。。。
前言
第1 章 预备知识
本章介绍 Bjarne Stroustrup 如何通过在 C 语言的基础上添加对面向对象编程的支持,来创造 C++编程语言。讨论面向过程语言(如C 语言)与面向对象语言(如C++)之间的区别。您将了解 ANSI/ISO 在制定 C++标准方面所做的工作。本章还讨论了创建 C++程序的技巧,介绍了当前几种 C++编译器使用的方法。最后,本章介绍了本书的一些约定。
第2 章 开始学习 C++
本章介绍创建简单 C++程序的步骤。您可以学习到 main( )函数扮演的角色以及 C++程序使用的一些语句。您将使用预定义的 cout 和c in 对象来实现程序输出和输入,学习如何创建和使用变量。最后,本章还将介绍函数——C++的编程模块。
第3 章 处理数据
C++提供了内置类型来存储两种数据:整数(没有小数的数字)和浮点数(带小数的数字)。为满足程序员的各种需求,C++为每一种数据都提供了几个类型。本章将要讨论这些类型,包括创建变量和编写各种类型的常量。另外,还将讨论 C++是如何处理不同类型之间的隐式和显式转换的。
第4 章 复合类型
C++让程序员能够使用基本的内置类型来创建更复杂的类型。最高级的形式是类,这将在第 9 章~第1 3 章讨论。本章讨论其他形式,包括数组(存储多个同类型的值)、结构(存储多个不同类型的值)、指针(标识内存位置)。您还将学习如何创建和存储文本字符串及如何使用 C-风格字符数组和 C++ string 类来处理文本输入和输出。最后,还将学习 C++处理内存分配的一些方法,其中包括用于显式地管理内存的 new 和d elete 运算符。
第5 章 循环和关系表达式
程序经常需要执行重复性操作,为此 C++提供了 3 种循环结构:for 循环、while 循环和 do while 循环。这些循环必须知道何时终止,C++的关系运算符使程序员能够创建测试来引导循环。本章还将介绍如何创建逐字符地读取和处理输入的循环。最后,您将学习如何创建二维数组以及如何使用嵌套循环来处理它们。
第6 章 分支语句和逻辑运算符
如果程序可以根据实际情况调整执行,我们就说程序能够智能地行动。在本章,您将了解到如何使用 if、if else 和s witch 语句及条件运算符来控制程序流程,学习如何使用逻辑运算符来表达决策测试。另外,本章还将介绍确定字符关系(如测试字符是数字还是非打印字符)的函数库 cctype。最后,还将简要地介绍文件输入/输出。
第7 章 函数——C++的编程模块
函数是 C++的基本编程部件。本章重点介绍 C++函数与 C 函数共同的特性。具体地说,您将复习函数定义的通用格式,了解函数原型是如何提高程序可靠性的。同时,还将学习如何编写函数来处理数组、字符串和结构。还要学习有关递归的知识(即函数在什么情况下调用自身)以及如何用它来实现分而治之的策略。最后将介绍函数指针,它使程序员能够通过函数参数来命令函数使用另一个函数。
第8 章 函数探幽
本章将探索 C++中函数新增的特性。您将学习内联函数,它可以提高程序的执行速度,但会增加程序的长度;还将使用引用变量,它们提供了另一种将信息传递给函数的方式。默认参数使函数能够自动为函数调用中省略的函数参数提供值。函数重载使程序员能够创建多个参数列表不同的同名函数。类设计中经常使用这些特性。另外,您还将学习函数模板,它们使程序员能够指定相关函数族的设计。
第9 章 内存模型和名称空间
本章讨论如何创建多文件程序,介绍分配内存的各种方式、管理内存的各种方式以及作用域、链接、名称空间,这些内容决定了变量在程序的哪些部分是可见的。
第1 0 章 对象和类
类是用户定义的类型,对象(如变量)是类的实例。本章介绍面向对象编程和类设计。对象声明描述的是存储在对象中的信息以及可对对象执行的操作(类方法)。对象的某些组成部分对于外界来说是可见的(公有部分),而某些部分却是隐藏的(私有部分)。特殊的类方法(构造函数和析构函数)在对象创建和释放时发挥作用。在本章中,您将学习所有这些内容以及其他类知识,了解如何使用类来实现 ADT,如栈。
第1 1 章 使用类
在本章中,您将深入了解类。首先了解运算符重载,它使程序员能够定义与类对象一起使用的运算符,如+。还将学习友元函数,这些函数可以访问外部世界不可访问的类数据。同时还将了解一些构造函数和重载运算符成员函数是如何被用来管理类类型转换的。
第1 2 章 类和动态内存分配
一般来说,让类成员指向动态分配的内存很有用。如果程序员在类构造函数中使用 new 来分配动态内存,就有责任提供适当的析构函数,定义显式拷贝构造函数和显式赋值运算符。本章介绍了在程序员没有提供显式定义时,将如何隐式地生成成员函数以及这些成员函数的行为。您还将通过使用对象指针,了解队列模拟问题,扩充类方面的知识。
第1 3 章 类继承
在面向对象编程中,继承是功能最强大的特性之一,通过继承,派生类可以继承基类的特性,可重用基类代码。本章讨论公有继承,这种继承模拟了 is-a 关系,即派生对象是基对象的特例。例如,物理学家是科学家的特例。有些继承关系是多态的,这意味着相同的方法名称可能导致依赖于对象类型的行为。要实现这种行为,需要使用一种新的成员函数——虚函数。有时,使用抽象基类是实现继承关系的最佳方式。本章讨论了这些问题,说明了公有继承在什么情况下合适,在什么情况下不合适。
第1 4 章 C++中的代码重用
公有继承只是代码重用的方式之一。本章将介绍其他几种方式。如果一个类包含了另一个类的对象,则称为包含。包含可以用来模拟 has-a 关系,其中一个类包含另一个类的对象。例如,汽车有马达。也可以使用私有继承和保护继承来模拟这种关系。本章说明了各种方法之间的区别。同时,您还将学习类模板,它让程序员能够使用泛型定义类,然后使用模板根据具体类型创建特定的类。例如,栈模板使程序员能够创建整数栈或字符串栈。最后,本章还将介绍多重公有继承,使用这种方式,一个类可以从多个类派生而来。
第1 5 章 友元、异常和其他
本章扩展了对友元的讨论,讨论了友元类和友元成员函数。然后从异常开始介绍了 C++的几项新特性。异常为处理程序异常提供了一种机制,如函数参数值不正确或内存耗尽等。您还将学习 RTTI,这种机制用来确定对象类型。最后,本章还将介绍一种更安全的方法来替代不受限制的强制类型转换。
第1 6 章 string 类和标准模板库
本章讨论 C++语言中新增的一些类库。对于传统的 C-风格字符串来说,string 类是一种方便且功能强大的替代方式。Auto_ptr 类帮助管理动态分配的内存。STL 提供了几种类容器(包括数组、队列、链表、集合和映射)的模板表示。它还提供了高效的泛型算法库,这些算法可用于 STL 容器,也可用于常规数组。模板类 valarray 为数值数组提供了支持。
第1 7 章 输入、输出和文件
本章复习 C++ I/O,并讨论如何格式化输出。您将要学习如何使用类方法来确定输入或输出流的状态,了解输入类型是否匹配或是否检测到了文件尾。C++使用继承来派生用于管理文件输入和输出的类。您将学习如何打开文件,以进行输入和输出,如何在文件中追加数据,如何使用二进制文件,如何获得对文件的随机访问权。最后,还将学习如何使用标准的 I/O 方法来读取和写入字符串。
第1 8 章 探讨 C++新标准
本章首先复习之前介绍过的几项 C++11 新功能,包括新类型、统一的初始化语法、自动类型推断、新的智能指针以及作用域内枚举。然后,讨论新增的右值引用类型以及如何使用它来实现移动语义。接下来,介绍了新增的类功能、lambda 表达式和可变参数模板。最后,概述了众多其他的新功能。
附录
附录 A 计数系统
本附录讨论八进制数、十六进制数和二进制数。
附录 B C++保留字
本附录列出了 C++关键字。
附录 C ASCII 字符集
本附录列出了 ASCII 字符集及其十进制、八进制、十六进制和二进制表示。
附录 D 运算符优先级
本附录按优先级从高到低的顺序列出了 C++的运算符。
附录 E 其他运算符
本附录总结了正文中没有介绍的其他 C++运算符,如按位运算符等。
附录 F 模板类 string
本附录总结了 string 类方法和函数。
附录 G 标准模板库方法和函数
本附录总结了 STL 容器方法和通用的 STL 算法函数。
附录 H 精选读物和网上资源
本附录列出一些参考书,帮助您深入了解 C++。
附录 I 转换为 ISO 标准 C++
本附录提供了从 C 和老式 C++实现到标准 C++的转换指南。
附录 J 复习题答案
本附录提供各章结尾的复习题的答案。
第1 章 预备知识
- 本章内容包括:
- C 语言和 C++的发展历史和基本原理。
- 过程性编程和面向对象编程。
- C++是如何在 C 语言的基础上添加面向对象概念的。
- C++是如何在 C 语言的基础上添加泛型编程概念的。
- 编程语言标准。
- 创建程序的技巧。
1.1 C++简介
C++融合了 3 种不同的编程方式:C 语言代表的过程性语言、C++在C 语言基础上添加的类代表的面向对象语言、C++模板支持的泛型编程。
1.2 C++简史
1.2.1 C 语言
Ritchie 希望有一种语言能将低级语言的效率、硬件访问能力和高级语言的通用性、可移植性融合在一起,于是他在旧语言的基础上开发了 C 语言。
1.2.2 C 语言编程原理
一般来说,计算机语言要处理两个概念——数据和算法。数据是程序使用和处理的信息,而算法是程序使用的方法。C 语言使用结构化编程将分支(决定接下来应执行哪个指令)限制为一小组行为良好的结构。另一个原则是自顶向下(top-down)的设计。在C 语言中,其理念是将大型程序分解成小型、便于管理的任务。如果其中的一项任务仍然过大,则将它分解为更小的任务。这一过程将一直持续下去,直到将程序划分为小型的、易于编写的模块。结构化编程技术反映了过程性编程的思想,根据执行的操作来构思一个程序。
1.2.3 面向对象编程
与强调算法的过程性编程不同的是,OOP 强调的是数据。OOP 不像过程性编程那样,试图使问题满足语言的过程性方法,而是试图让语言来满足问题的要求。其理念是设计与问题的本质特性相对应的数据格式。
1.2.4 C++和泛型编程
型编程(generic programming)是C++支持的另一种编程模式。它与 OOP 的目标相同,即使重用代码和抽象通用概念的技术更简单。不过 OOP 强调的是编程的数据方面,而泛型编程强调的是独立于特定数据类型。它们的侧重点不同。OOP 是一个管理大型项目的工具,而泛型编程提供了执行常见任务(如对数据排序或合并链表)的工具。术语泛型(generic)指的是创建独立于类型的代码。
1.2.5 C++的起源
与C 语言一样,C++也是在贝尔实验室诞生的,Bjarne Stroustrup 于2 0 世纪 80 年代在这里开发出了这种语言。C++是C 语言的超集,这意味着任何有效的 C 程序都是有效的 C++程序。OOP 部分赋予了 C++语言将问题所涉及的概念联系起来的能力,C 部分则赋予了 C++语言紧密联系硬件的能力(参见图 1.2),这种能力上的结合成就了 C++的广泛传播。从程序的一个方面转到另一个方面时,思维方式也要跟着转换(确实,有些 OOP 正统派把为 C 添加 OOP 特性看作是为猪插上翅膀,虽然这是头瘦骨嶙峋、非常能干的猪)。
1.3 可移植性和标准
1.3.1 C++的发展
Stroustrup 编写的《 The Programming Language》包含 65 页的参考手册,它成了最初的 C++事实标准。 下一个事实标准是 Ellis 和S troustrup 编写的《 The Annotated C++ Reference Manual》。 C++98 标准新增了大量特性,其篇幅将近 800页,且包含的说明很少。 C++11 标准的篇幅长达 1350页,对旧标准做了大量的补充。
1.3.2 本书遵循的 C++标准
详尽地介绍了 C++98,并涵盖了 C++11 新增的一些特性。
1.4 程序创建的技巧
1.4.1 创建源代码文件
详尽地介绍了 C++98,并涵盖了 C++11 新增的一些特性。
Table 1: 源代码文件的扩展名C++实现 源代码文件扩展名 UNIX C, cc, cxx, c GNU C++ C, cc, cxx, cpp, c++ Digital Mars cpp, cxx Borland C++ cpp Watcom cpp Microsoft Visual C++ cpp, cxx, cc Freestyle CodeWarrior cp, cpp, cc, cxx, c++ 1.4.2 编译和链接
1.5 总结
C 语言新增了诸如控制结构和函数等特性,以便更好地控制程序流程,支持结构化和模块化程度更高的方法;而C++增加了对面向对象编程和泛型编程的支持,这有助于提高模块化和创建可重用代码,从而节省编程时间并提高程序的可靠性。
第2 章 开始学习 C++
- 本章内容包括:
- 创建 C++程序。
- C++程序的一般格式。
- #include 编译指令。
- main( )函数。
- 使用 cout 对象进行输出。
- 在C++程序中加入注释。
- 何时以及如何使用 endl。
- 声明和使用变量。
- 使用 cin 对象进行输入。
- 定义和使用简单函数。
2.1 进入 C++
注释都以//打头,编译器将忽略它们。C++对大小写敏感,也就是说区分大写字符和小写字符。 您使用函数来创建 C++程序。通常,先将程序组织为主要任务,然后设计独立的函数来处理这些任务。
- 注释,由前缀//标识。
- 预处理器编译指令# include。
- 函数头:int main( )。
- 编译指令 using namespace。
- 函数体,用{和}括起。
- 使用 C++的c out 工具显示消息的语句。
结束 main( )函数的 return 语句。
2.1.1 main( )函数
- 就目前而言,需要记住的主要一点是,C++句法要求 main( )函数的定义以函数头 int main( )开始。
- 在C 语言中,省略返回类型相当于说函数的类型为 int。然而,C++逐步淘汰了这种用法。
- 在括号中使用关键字 void 明确地指出,函数不接受任何参数。在C++(不是 C)中,让括号空着与在括号中使用 void 等效(在C中,让括号空着意味着对是否接受参数保持沉默)。
- 如果编译器到达 main( )函数末尾时没有遇到返回语句,则认为 main( )函数以如下语句结尾:
return 0;
这条隐含的返回语句只适用于 main( )函数,而不适用于其他函数。
2.1.2 C++注释
C++注释以双斜杠(//)打头。注释是程序员为读者提供的说明,通常标识程序的一部分或解释代码的某个方面。编译器忽略注释,毕竟,它对 C++的了解至少和程序员一样,在任何情况下,它都不能理解注释。C++注释以//打头,到行尾结束。注释可以位于单独的一行上,也可以和代码位于同一行。由于 C-风格注释(
/* C 风格的注释 */
)以*/结束,而不是到行尾结束,因此可以跨越多行。可以在程序中使用 C 或C++风格的注释,也可以同时使用这两种注释。但应尽量使用 C++注释,因为这不涉及到结尾符号与起始符号的正确配对,所以它产生问题的可能性很小。应使用注释来说明程序。程序越复杂,注释的价值越大。注释不仅有助于他人理解这些代码,也有助于程序员自己理解代码,特别是隔了一段时间没有接触该程序的情况下。2.1.3 C++预处理器和 iostream 文件
如果程序要使用 C++输入或输出工具,请提供这样两行代码:
1 2
#include <iostream> using namespace std;
C++和C 一样,也使用一个预处理器,该程序在进行主编译之前对源文件进行处理。不必执行任何特殊的操作来调用该预处理器,它会在编译程序时自动运行。使用 cin 和c out 进行输入和输出的程序必须包含文件 iostream。
2.1.4 头文件名
像i ostream 这样的文件叫做包含文件(include file)—由于它们被包含在其他文件中;也叫头文件(header file)—由于它们被包含在文件起始处。C++编译器自带了很多头文件,每个头文件都支持一组特定的工具。
Table 2: 头文件命名约定头文件类型 约定 实例 说明 C++旧式风格 以. h 结尾 iostream.h C++程序可以使用 C 旧式风格 以. h 结尾 math.h C/C++程序可以使用 C++新式风格 没有扩展名 iostream C++程序可以使用,使用 namespace std 转换后的 C 加上前缀 c,没有扩展名 cmath C++程序可以使用,可以使用不是 C 的特性,如n amespace std 2.1.5 名称空间
果使用 iostream,而不是 iostream.h,则应使用下面的名称空间编译指令来使 iostream 中的定义对程序可用:
using namespace std;
这叫做 using 编译指令。最简单的办法是,现在接受这个编译指令,以后再考虑它(例如,到第 9 章再考虑它)。名称空间支持是一项 C++特性,旨在让您编写大型程序以及将多个厂商现有的代码组合起来的程序时更容易,它还有助于组织程序。2.1.6 使用 cout 进行 C++输出
现在来看一看如何显示消息。程序使用下面的 C++语句:
cout << "Hello world";
双引号括起的部分是要打印的消息。在C++中,用双引号括起的一系列字符叫做字符串,因为它是由若干字符组合而成的。<<符号表示该语句将把这个字符串发送给cout
;该符号指出了信息流动的路径。- 控制符
endl
;cout << endl;
endl
是一个特殊的 C++符号,表示一个重要的概念:重起一行。在输出流中插入endl
将导致屏幕光标移到下一行开头。 - 换行符;C++还提供了另一种在输出中指示换行的旧式方法:C 语言符号\ n:
cout << "What's next?\n"
\n
被视为一个字符,名为换行符。显示字符串时,在字符串中包含换行符,而不是在末尾加上endl
,可减少输入量 如果要生成一个空行,则两种方法的输入量相同,但对大多数人而言,输入endl
更为方便.
- 控制符
2.1.7 C++源代码的格式化
在C++中,分号标示了语句的结尾。因此,在C++中,回车的作用就和空格或制表符相同。也就是说,在C++中,通常可以在能够使用回车的地方使用空格,反之亦然。这说明既可以把一条语句放在几行上,也可以把几条语句放在同一行上。
- 源代码中的标记和空白;一行代码中不可分割的元素叫做标记。通常,必须用空格、制表符或回车将两个标记分开,空格、制表符和回车统称为空白(white space)。有些字符(如括号和逗号)是不需要用空白分开的标记。
- C++源代码风格;
- 每条语句占一行。
- 每个函数都有一个开始花括号和一个结束花括号,这两个花括号各占一行。
- 函数中的语句都相对于花括号进行缩进。
- 与函数名称相关的圆括号周围没有空白。帮助区分函数和一些也使用圆括号的 C++内置结构(如循环)。
2.2 C++语句
2.2.1 声明语句和变量
声明通常指出了要存储的数据类型和程序对存储在这里的数据使用的名称。
2.2.2 赋值语句
符号=叫做赋值运算符。C++(和C)有一项不寻常的特性—可以连续使用赋值运算符。
yamaha = baldwin = carrots = 25;
2.2.3 cout 的新花样
cout << carrots;
在打印之前,cout 必须将整数形式的数字转换为字符串形式。cout 的智能行为源自 C++的面向对象特性。实际上,C++插入运算符(<<)将根据其后的数据类型相应地调整其行为,这是一个运算符重载的例子。在后面的章节中学习函数重载和运算符重载时,将知道如何实现这种智能设计。
2.3 其他 C++语句
2.3.1 使用 cin
cin >> carrots;
从这条语句可知,信息从 cin 流向 carrots。这是与 cout 对应的用于输入的对象。2.3.2 使用 cout 进行拼接
cout << "Now you have " << carrots << " carrots." << endl;
2.3.3 类简介
类是用户定义的一种数据类型。要定义类,需要描述它能够表示什么信息和可对数据执行哪些操作。类之于对象就像类型之于变量。也就是说,类定义描述的是数据格式及其用法,而对象则是根据数据格式规范创建的实体。如果了解其他 OOP 术语,就知道 C++类对应于某些语言中的对象类型,而C++对象对应于对象实例或实例变量。类描述了一种数据类型的全部属性(包括可使用它执行的操作),对象是根据这些描述创建的实体。
2.4 函数
C++函数分两种:有返回值的和没有返回值的。
2.4.1 使用有返回值的函数
有返回值的函数将生成一个值,而这个值可赋给变量或在其他表达式中使用。例如,标准 C/C++库包含一个名为 sqrt( )的函数,它返回平方根。
x = sqrl(6.25);
2.4.2 函数变体
有些函数需要多项信息。这些函数使用多个参数,参数间用逗号分开。
pow(5.0, 8.0)
2.4.3 用户定义的函数
标准 C 库提供了 140 多个预定义的函数。如果其中的函数能满足要求,则应使用它们。但用用户经常需要编写自己的函数,尤其是在设计类的时候。
函数格式;
1 2 3 4
type functionname(argumentlist) { statements }
函数头
void simon(int n)
2.4.4 用户定义的有返回值的函数
1 2 3 4
int stonetolb(int sts) { return 14 * sts; }
这些例子表明,函数原型描述了函数接口,即函数如何与程序的其他部分交互。参数列表指出了何种信息将被传递给函数,函数类型指出了返回值的类型。程序员有时将函数比作一个由出入它们的信息所指定的黑盒子(black boxes)(电工用语)。函数原型将这种观点诠释得淋漓尽致。函数 stonetolb( )短小、简单,但包含了全部的函数特性:
- 有函数头和函数体;
- 接受一个参数;
- 返回一个值;
- 需要一个原型。
2.4.5 在多函数程序中使用 using 编译指令
- 将u sing namespace std;放在函数定义之前,让文件中所有的函数都能够使用名称空间 std 中所有的元素。
- 将u sing namespace std;放在特定的函数定义中,让该函数能够使用名称空间 std 中的所有元素。
- 在特定的函数中使用类似 using std::cout;这样的编译指令,而不是 using namespace std;,让该函数能够使用指定的元素,如c out。
- 完全不使用编译指令 using,而在需要使用名称空间 std 中的元素时,使用前缀 std::。
2.5 总结
有多种类型的 C++语句,包括下述 6种:
- 声明语句:定义函数中使用的变量的名称和类型。
- 赋值语句:使用赋值运算符(=)给变量赋值。
- 消息语句:将消息发送给对象,激发某种行动。
- 函数调用:执行函数。被调用的函数执行完毕后,程序返回到函数调用语句后面的语句。
- 函数原型:声明函数的返回类型、函数接受的参数数量和类型。
- 返回语句:将一个值从被调用的函数那里返回到调用函数中。
2.6 复习题
2.7 编程练习
第3 章 处理数据
- 本章内容包括:
- C++变量的命名规则。
- C++内置的整型——unsigned long、long、unsigned int、int、unsigned short、short、char、unsigned char、signed char 和b ool。
- C++11 新增的整型:unsigned long long 和l ong long。
- 表示各种整型的系统限制的 climits 文件。
- 各种整型的数字字面值(常量)。
- 使用 const 限定符来创建符号常量。
- C++内置的浮点类型:float、double 和l ong double。
- 表示各种浮点类型的系统限制的 cfloat 文件。
- 各种浮点类型的数字字面值。
- C++的算术运算符。
- 自动类型转换。
- 强制类型转换。
3.1 简单变量
为把信息存储在计算机中,程序必须记录 3 个基本属性:
- 信息将存储在哪里;
- 要存储什么值;
存储何种类型的信息。
3.1.1 变量名
C++提倡使用有一定含义的变量名。必须遵循几种简单的 C++命名规则。
- 在名称中只能使用字母字符、数字和下划线(_)。
- 名称的第一个字符不能是数字。
- 区分大写字符与小写字符。
- 不能将 C++关键字用作名称。
- 以两个下划线或下划线和大写字母打头的名称被保留给实现(编译器及其使用的资源)使用。以一个下划线开头的名称被保留给实现,用作全局标识符。
- C++对于名称的长度没有限制,名称中所有的字符都有意义,但有些平台有长度限制。
3.1.2 整型
整数就是没有小数部分的数字,不同 C++整型使用不同的内存量来存储整数。
3.1.3 整型 short、int、long 和l ong long
计算机内存由一些叫做位(bit)的单元组成。C++的s hort、int、long 和l ong long 类型通过使用不同数目的位来存储值,最多能够表示 4 种不同的整数宽度。C++提供了一种灵活的标准,它确保了最小长度(从C 语言借鉴而来),如下所示:
- short 至少 16位;
- int 至少与 short 一样长;
- long 至少 32位,且至少与 int 一样长;
- long long 至少 64位,且至少与 long 一样长。
- 要知道系统中整数的最大长度,可以在程序中使用 C++工具来检查类型的长度。首先,sizeof 运算符返回类型或变量的长度,单位为字节(运算符是内置的语言元素,对一个或多个数据进行运算,并生成一个值。其次,头文件 climits(在老式实现中为 limits.h)中包含了关于整型限制的信息。具体地说,它定义了表示各种限制的符号名称。例如,INT_MAX 为i nt 的最大取值,CHAR_BIT 为字节的位数。
- 初始化将赋值与声明合并在一起:
int n_int = INT_MAX;
- 还有另一种初始化方式,这种方式用于数组和结构,但在 C++98中,也可用于单值变量:
int hamburgers = {24};
3.1.4 无符号类型
前面介绍的 4 种整型都有一种不能存储负数值的无符号变体,其优点是可以增大变量能够存储的最大值。当然,仅当数值不会为负时才应使用无符号类型,如人口、粒数等。要创建无符号版本的基本整型,只需使用关键字 unsigned 来修改声明即可:
unsigned int rovert;
3.1.5 选择整型类型
C++提供了大量的整型,应使用哪种类型呢?通常,int 被设置为对目标计算机而言最为“自然”的长度。自然长度(natural size)指的是计算机处理起来效率最高的长度。如果没有非常有说服力的理由来选择其他类型,则应使用 int。
- 如果变量表示的值不可能为负,如文档中的字数,则可以使用无符号类型,这样变量可以表示更大的值。
- 如果知道变量可能表示的整数值大于 16 位整数的最大可能值,则使用 long。即使系统上 int 为3 2位,也应这样做。
- 如果 short 比i nt小,则使用 short 可以节省内存。通常,仅当有大型整型数组时,才有必要使用 short。(数组是一种数据结构,在内存中连续存储同类型的多个值。)如果节省内存很重要,则应使用 short 而不是使用 int,即使它们的长度是一样的。请记住,节省一点就是赢得一点。
3.1.6 整型字面值
与C 相同,C++能够以三种不同的计数方式来书写整数:基数为 10、基数为 8(老式 UNIX 版本)和基数为 16(硬件黑客的最爱)。C++使用前一(两)位来标识数字常量的基数。
- 如果第一位为 1~9,则基数为 10(十进制);因此 93 是以 10 为基数的。
- 如果第一位是 0,第二位为 1~7,则基数为 8(八进制);因此 042 的基数是 8,它相当于十进制数 34。
- 如果前两位为 0x 或0 X,则基数为 16(十六进制);因此 0x42 为十六进制数,相当于十进制数 66。对于十六进制数,字符 a~f 和A~F 表示了十六进制位,对应于 10~15。
3.1.7 C++如何确定常量的类型
除非有理由存储为其他类型(如使用了特殊的后缀来表示特定的类型,或者值太大,不能存储为 int),否则 C++将整型常量存储为 int 类型。后缀是放在数字常量后面的字母,用于表示类型。整数后面的 l 或L 后缀表示该整数为 long 常量,u 或U 后缀表示 unsigned int 常量,ul(可以采用任何一种顺序,大写小写均可)表示 unsigned long 常量(由于小写 l 看上去像 1,因此应使用大写 L 作后缀)。long long 的后缀 ll 和L L,还提供了用于表示类型 unsigned long long 的后缀 ull、Ull、uLL 和U LL。
3.1.8 char 类型:字符和小整数
char 类型是专为存储字符(如字母和数字)而设计的。书写字符常量的方式有多种。对于常规字符(如字母、标点符号和数字),最简单的方法是将字符用单引号括起。这种表示法代表的是字符的数值编码。
char A = 'A';
与i nt 不同的是,char 在默认情况下既不是没有符号,也不是有符号。是否有符号由 C++实现决定,这样编译器开发人员可以最大限度地将这种类型与硬件属性匹配起来。如果 char 有某种特定的行为对您来说非常重要,则可以显式地将类型设置为 signed char 或u nsigned char。wcha_t
(宽字符类型)可以表示扩展字符集,它是一种整数类型有足够的空间,可以表示系统使用的最大扩展字符集。char16_t
无符号的,长1 6位,char32_t
无符号的,长3 2位。3.1.9 bool 类型
在计算中,布尔变量的值可以是 true 或f alse。过去,C++和C 一样,也没有布尔类型。C++将非零值解释为 true,将零解释为 false。然而,现在可以使用 bool 类型来表示真和假了,它们分别用预定义的字面值 true 和f alse 表示。
bool is_ready = true;
任何数字值或指针值都可以被隐式转换(即不用显式强制转换)为b ool值。任何非零值都被转换为 true,而零被转换为 false。
3.2 const 限定符
常量被初始化后,其值就被固定了,编译器将不允许再修改该常量的值。关键字 const 叫做限定符,因为它限定了声明的含义。 const type name = value;
如果在声明常量时没有提供值,则该常量的值将是不确定的,且无法修改。
3.3 浮点数
浮点类型,它们是 C++的第二组基本类型。浮点数能够表示带小数部分的数字,它们提供的值范围也更大。
3.3.1 书写浮点数
- 第一种是使用常用的标准小数点表示法:
- 第二种表示浮点值的方法叫做 E 表示法,d.dddE+n 指的是将小数点向右移 n位,而d.dddE~n 指的是将小数点向左移 n位。之所以称为“浮点”,就是因为小数点可移动。
3.3.2 浮点类型
和A NSI C 一样,C++也有 3 种浮点类型:float、double 和l ong double。float 至少 32位,double 至少 48位,且不少于 float,long double 至少和 double 一样多。这三种类型的有效位数可以一样多。然而,通常,float 为3 2位,double 为6 4位,long double 为8 0、96 或1 28位。另外,这3 种类型的指数范围至少是− 37 到3 7。
3.3.3 浮点常量
在默认情况下,像8.24 和2.4E8 这样的浮点常量都属于 double 类型。如果希望常量为 float 类型,请使用 f 或F 后缀。对于 long double 类型,可使用 l 或L 后缀(由于 l 看起来像数字 1,因此 L 是更好的选择)。
3.3.4 浮点数的优缺点
与整数相比,浮点数有两大优点。首先,它们可以表示整数之间的值。其次,由于有缩放因子,它们可以表示的范围大得多。另一方面,浮点运算的速度通常比整数运算慢,且精度将降低。
3.4 C++算术运算符
变量和常量都可以用作操作数, %
的操作数只能是整数,下面是 5 种基本的 C++算术运算符:
+
运算符对操作数执行加法运算。例如,4 + 20 = 24
。−
运算符从第一个数中减去第二个数。例如,12 − 3 = 9
。*
运算符将操作数相乘。例如,28 * 4 = 112
。/
运算符用第一个数除以第二个数。例如,1000 /5 = 200
。如果两个操作数都是整数,则结果为商的整数部分。例如,17 / 3 = 5
,小数部分被丢弃。%
运算符求模。也就是说,它生成第一个数除以第二个数后的余数。例如,19 % 6 = 1
,因为 19 是6 的3 倍余 1。两个操作数必须都是整型,将该运算符用于浮点数将导致编译错误。如果其中一个是负数,则结果的符号满足如下规则:(a/b)*b + a%b = a
。3.4.1 运算符优先级和结合性
算术运算符遵循通常的代数优先级,先乘除,后加减。当两个运算符的优先级相同时,C++将看操作数的结合性(associativity)是从左到右,还是从右到左。从左到右的结合性意味着如果两个优先级相同的运算符被同时用于同一个操作数,则首先应用左侧的运算符。从右到左的结合性则首先应用右侧的运算符。
3.4.2 除法分支
除法运算符(/)的行为取决于操作数的类型。如果两个操作数都是整数,则C++将执行整数除法。这意味着结果的小数部分将被丢弃,使得最后的结果是一个整数。如果其中有一个(或两个)操作数是浮点值,则小数部分将保留,结果为浮点数。
3.4.3 求模运算符
求模运算符返回整数除法的余数。它与整数除法相结合,尤其适用于解决要求将一个量分成不同的整数单元的问题。
3.4.4 类型转换
C++丰富的类型允许根据需求选择不同的类型,这也使计算机的操作更复杂。C++自动执行很多类型转换:
- 将一种算术类型的值赋给另一种算术类型的变量时,C++将对值进行转换;
- 表达式中包含不同的类型时,C++将对值进行转换;
- 将参数传递给函数时,C++将对值进行转换。
下面详细地介绍进行这些自动转换时将发生的情况:
- 初始化和赋值进行的转换; C++允许将一种类型的值赋给另一种类型的变量。这样做时,值将被转换为接收变量的类型。
- 以{ }方式初始化时进行的转换(C++11);C++11 将使用大括号的初始化称为列表初始化(list-initialization),因为这种初始化常用于给复杂的数据类型提供值列表。它对类型转换的要求更严格,列表初始化不允许缩窄(narrowing),即变量的类型可能无法表示赋给它的值。
- 表达式中的转换;当同一个表达式中包含两种不同的算术类型时,C++将执行两种自动转换:首先,一些类型在出现时便会自动转换;其次,有些类型在与其他类型同时出现在表达式中时将被转换。
- 如果有一个操作数的类型是 long double,则将另一个操作数转换为 long double。
- 否则,如果有一个操作数的类型是 double,则将另一个操作数转换为 double。
- 否则,如果有一个操作数的类型是 float,则将另一个操作数转换为 float。
- 否则,说明操作数都是整型,因此执行整型提升。
- 在这种情况下,如果两个操作数都是有符号或无符号的,且其中一个操作数的级别比另一个低,则转换为级别高的类型。
- 如果一个操作数为有符号的,另一个操作数为无符号的,且无符号操作数的级别比有符号操作数高,则将有符号操作数转换为无符号操作数所属的类型。
- 否则,如果有符号类型可表示无符号类型的所有可能取值,则将无符号操作数转换为有符号操作数所属的类型。
- 否则,将两个操作数都转换为有符号类型的无符号版本。
- 传递参数时的转换;传递参数时的类型转换通常由 C++函数原型控制。然而,也可以取消原型对参数传递的控制。在这种情况下,C++将对 char 和s hort 类型(signed 和u nsigned)应用整型提升。float 参数提升为 double。
- 强制类型转换;C++还允许通过强制类型转换机制显式地进行类型转换。(C++认识到,必须有类型规则,而有时又需要推翻这些规则。)强制类型转换的格式有两种。强制类型转换不会修改 thorn 变量本身,而是创建一个新的、指定类型的值,可以在表达式中使用这个值。
(typeName) value
typeName (value)
- C++还引入了 4 个强制类型转换运算符
3.4.5 C++11 中的 auto 声明
C++11 新增了一个工具,让编译器能够根据初始值的类型推断变量的类型。在初始化声明中,如果使用关键字 auto,而不指定变量的类型,编译器将把变量的类型设置成与初始值相同。
3.5 总结
C++的基本类型分为两组:一组由存储为整数的值组成,另一组由存储为浮点格式的值组成。整型之间通过存储值时使用的内存量及有无符号来区分。整型从最小到最大依次是:bool、char、signed char、unsigned char、short、unsigned short、int、unsigned int、long、unsigned long 以及 C++11 新增的 long long 和u nsigned long long。还有一种 wchar_t 类型,它在这个序列中的位置取决于实现。C++11 新增了类型 char16_t 和c har32_t,它们的宽度足以分别存储 16 和3 2 位的字符编码。C++确保了 char 足够大,能够存储系统基本字符集中的任何成员,而w char_t 则可以存储系统扩展字符集中的任意成员,short 至少为 16位,而i nt 至少与 short 一样长,long 至少为 32位,且至少和 int 一样长。确切的长度取决于实现。 字符通过其数值编码来表示。I/O 系统决定了编码是被解释为字符还是数字。 浮点类型可以表示小数值以及比整型能够表示的值大得多的值。3 种浮点类型分别是 float、double 和l ong double。C++确保 float 不比 double长,而d ouble 不比 long double长。通常,float 使用 32 位内存,double 使用 64位,long double 使用 80 到1 28位。 通过提供各种长度不同、有符号或无符号的类型,C++使程序员能够根据特定的数据要求选择合适的类型。 C++使用运算符来提供对数字类型的算术运算:加、减、乘、除和求模。当两个运算符对同一个操作数进行操作时,C++的优先级和结合性规则可以确定先执行哪种操作。 对变量赋值、在运算中使用不同类型、使用强制类型转换时,C++将把值从一种类型转换为另一种类型。很多类型转换都是“安全的”,即可以在不损失和改变数据的情况下完成转换。例如,可以把 int 值转换为 long值,而不会出现任何问题。对于其他一些转换,如将浮点类型转换为整型,则需要更加小心。
3.6 复习题
3.7 编程练习
第4 章 复合类型
- 本章内容包括:
- 创建和使用数组。
- 创建和使用 C-风格字符串。
- 创建和使用 string 类字符串。
- 使用方法 getline( )和g et( )读取字符串。
- 混合输入字符串和数字。
- 创建和使用结构。
- 创建和使用共用体。
- 创建和使用枚举。
- 创建和使用指针。
- 使用 new 和d elete 管理动态内存。
- 创建动态数组。
- 创建动态结构。
- 自动存储、静态存储和动态存储。
- vector 和a rray 类简介。
4.1 数组
数组(array)是一种数据格式,能够存储多个同类型的值。每个值都存储在一个独立的数组元素中,计算机在内存中依次存储数组的各个元素。要创建数组,可使用声明语句。数组声明应指出以下三点: typeName arrayName[arraySize];
- 存储在每个元素中的值的类型;
- 数组名;
数组中的元素数。
4.1.1 程序说明
sizeof 运算符返回类型或数据对象的长度(单位为字节)。注意,如果将 sizeof 运算符用于数组名,得到的将是整个数组中的字节数。但如果将 sizeof 用于数组元素,则得到的将是元素的长度(单位为字节)。
4.1.2 数组的初始化规则
只有在定义数组时才能使用初始化,此后就不能使用了,也不能将一个数组赋给另一个数组
4.1.3 C++11 数组初始化方法
- 首先,初始化数组时,可省略等号(=)
double carnings[4] {1.2e4, 1.6e4, 1.1e4, 1.7e4};
; - 其次,可不在大括号内包含任何东西,这将把所有元素都设置为零:
unsigned int counts[10] = {};
- 第三,列表初始化禁止缩窄转换
- 首先,初始化数组时,可省略等号(=)
4.2 字符串
字符串是存储在内存的连续字节中的一系列字符。C++处理字符串的方式有两种。第一种来自 C 语言,常被称为 C-风格字符串(C-style string)。
char dog[8] = { 'b', 'a', '\0'};
char fish[] = "Bubbles";
4.2.1 拼接字符串常量
C++允许拼接字符串字面值,即将两个用引号括起的字符串合并为一个。事实上,任何两个由空白(空格、制表符和换行符)分隔的字符串常量都将自动拼接成一个。
4.2.2 在数组中使用字符串
要将字符串存储到数组中,最常用的方法有两种—将数组初始化为字符串常量、将键盘或文件输入读入到数组中。
4.2.3 字符串输入
4.2.4 每次读取一行字符串输入
- 面向行的输入:getline( );它使用通过回车键输入的换行符来确定输入结尾。要调用这种方法,可以使用 cin.getline( )。该函数有两个参数。第一个参数是用来存储输入行的数组的名称,第二个参数是要读取的字符数。
cin.getline(name, 20);
- 面向行的输入:get( );该函数有几种变体。其中一种变体的工作方式与 getline( )类似,它们接受的参数相同,解释参数的方式也相同,并且都读取到行尾。但g et 并不再读取并丢弃换行符,而是将其留在输入队列中。另一种变体。使用不带任何参数的 cin.get( )调用可读取下一个字符(即使是换行符),因此可以用它来处理换行符,为读取下一行输入做好准备。
- 面向行的输入:getline( );它使用通过回车键输入的换行符来确定输入结尾。要调用这种方法,可以使用 cin.getline( )。该函数有两个参数。第一个参数是用来存储输入行的数组的名称,第二个参数是要读取的字符数。
4.2.5 混合输入字符串和数字
4.3 string 类简介
string 类型的变量(使用 C++的话说是对象)而不是字符数组来存储字符串。要使用 string类,必须在程序中包含头文件 string。string 类位于名称空间 std中,因此您必须提供一条 using 编译指令,或者使用 std::string 来引用它。string 类定义隐藏了字符串的数组性质,让您能够像处理普通变量那样处理字符串。
4.3.1 C++11 字符串初始化
C++11 也允许将列表初始化用于 C-风格字符串和 string 对象
4.3.2 赋值、拼接和附加
- 可以将一个 string 对象赋给另一个 string 对象
- 可以使用运算符+将两个 string 对象合并起来,还可以使用运算符+=将字符串附加到 string 对象的末尾。
4.3.3 string 类的其他操作
- 可以使用函数 strcpy( )将字符串复制到字符数组中,使用函数 strcat( )将字符串附加到字符数组末尾
- strncat( )和s trncpy( ),它们接受指出目标数组最大允许长度的第三个参数,因此更为安全,但使用它们进一步增加了编写程序的复杂度。
- 函数 strlen( )是一个常规函数,它接受一个 C-风格字符串作为参数,并返回该字符串包含的字符数。函数 size( )的功能基本上与此相同,但句法不同:str1 不是被用作函数参数,而是位于函数名之前,它们之间用句点连接。
4.3.4 string 类I/O
可以使用 cin 和运算符<<来将输入存储到 string 对象中,使用 cout 和运算符<<来显示 string 对象,其句法与处理 C-风格字符串相同。但每次读取一行而不是一个单词时,使用的句法不同,程序清单 4.10 说明了这一点。
4.3.5 其他形式的字符串字面值
4.4 结构简介
结构是用户定义的类型,而结构声明定义了这种类型的数据属性。定义了类型后,便可以创建这种类型的变量。因此创建结构包括两步。首先,定义结构描述—它描述并标记了能够存储在结构中的各种数据类型。然后按描述创建结构变量(结构数据对象)。在C++中,结构标记的用法与基本类型名相同。这种变化强调的是,结构声明定义了一种新类型。在C++中,省略 struct 不会出错。
4.4.1 在程序中使用结构
结构声明的位置很重要。外部声明可以被其后面的任何函数使用,而内部声明只能被该声明所属的函数使用。通常应使用外部声明,这样所有函数都可以使用这种类型的结构。变量也可以在函数内部和外部定义,外部变量由所有的函数共享。C++不提倡使用外部变量,但提倡使用外部结构声明。另外,在外部声明符号常量通常更合理。
4.4.2 C++11 结构初始化
- 与数组一样,C++11 也支持将列表初始化用于结构,且等号(=)是可选的
- 如果大括号内未包含任何东西,各个成员都将被设置为零
- 不允许缩窄转换
4.4.3 结构可以将 string 类作为成员吗
答案是肯定的,只要您使用的编译器支持对以 string 对象作为成员的结构进行初始化。 一定要让结构定义能够访问名称空间 std。为此,可以将编译指令 using 移到结构定义之前;也可以像前面那样,将n ame 的类型声明为 std::string。
4.4.4 其他结构属性
C++使用户定义的类型与内置类型尽可能相似。例如,可以将结构作为参数传递给函数,也可以让函数返回一个结构。另外,还可以使用赋值运算符(=)将结构赋给另一个同类型的结构,这样结构中每个成员都将被设置为另一个结构中相应成员的值,即使成员是数组。这种赋值被称为成员赋值(memberwise assignment),与C 结构不同,C++结构除了成员变量之外,还可以有成员函数。
4.4.5 结构数组
要初始化结构数组,可以结合使用初始化数组的规则(用逗号分隔每个元素的值,并将这些值用花括号括起)和初始化结构的规则(用逗号分隔每个成员的值,并将这些值用花括号括起)。由于数组中的每个元素都是结构,因此可以使用结构初始化的方式来提供它的值。因此,最终结果为一个被括在花括号中、用逗号分隔的值列表,其中每个值本身又是一个被括在花括号中、用逗号分隔的值列表。
4.4.6 结构中的位字段
与C 语言一样,C++也允许指定占用特定位数的结构成员,这使得创建与某个硬件设备上的寄存器对应的数据结构非常方便。字段的类型应为整型或枚举(稍后将介绍),接下来是冒号,冒号后面是一个数字,它指定了使用的位数。可以使用没有名称的字段来提供间距。每个成员都被称为位字段(bit field)。
1 2 3 4 5 6 7 8
struct torgle_register { unsigned int SN : 4; unsigned int : 4; bool goodIn : 1; bool goodTorgle : 1; }
4.5 共用体
共用体(union)是一种数据格式,它能够存储不同的数据类型,但只能同时存储其中的一种类型。也就是说,结构可以同时存储 int、long 和d ouble,共用体只能存储 int、long 或d ouble。共用体的句法与结构相似,但含义不同。共用体的用途之一是,当数据项使用两种或更多种格式(但不会同时使用)时,可节省空间。
|
|
匿名共用体(anonymous union)没有名称,其成员将成为位于相同地址处的变量。显然,每次只有一个成员是当前的成员。
|
|
4.6 枚举
C++的e num 工具提供了另一种创建符号常量的方式,这种方式可以代替 const。它还允许定义新类型,但必须按严格的限制进行。使用 enum 的句法与使用结构相似。
|
|
- 让s pectrum 成为新类型的名称;spectrum 被称为枚举(enumeration),就像 struct 变量被称为结构一样。
- 将r ed、orange、yellow 等作为符号常量,它们对应整数值 0~4。这些常量叫作枚举量(enumerator)。
- 在默认情况下,将整数值赋给枚举量,第一个枚举量的值为 0,第二个枚举量的值为 1,依次类推。可以通过显式地指定整数值来覆盖默认值。
枚举量是整型,可被提升为 int 类型,但i nt 类型不能自动转换为枚举类型
4.6.1 设置枚举量的值
1
enum bigstep{first, second = 100, third};
- 可以使用赋值运算符来显式地设置枚举量的值
- first 在默认情况下为 0。后面没有被初始化的枚举量的值将比其前面的枚举量大 1。因此,third 的值为 101。
- 可以创建多个值相同的枚举量
4.6.2 枚举的取值范围
- 首先,要找出上限,需要知道枚举量的最大值。找到大于这个最大值的、最小的 2 的幂,将它减去 1,得到的便是取值范围的上限。
- 要计算下限,需要知道枚举量的最小值。如果它不小于 0,则取值范围的下限为 0;否则,采用与寻找上限方式相同的方式,但加上负号。
4.7 指针和自由存储空间
指针是一个变量,其存储的是值的地址,而不是值本身。只需对变量应用地址运算符(&),就可以获得它的位置;例如,如果 home 是一个变量,则& home 是它的地址。使用常规变量时,值是指定的量,而地址为派生量。下面来看看指针策略,它是 C++内存管理编程理念的核心(面向对象编程与传统的过程性编程的区别在于,OOP 强调的是在运行阶段(而不是编译阶段)进行决策。)。
4.7.1 声明和初始化指针
指针声明必须指定指针指向的数据的类型。
int * p_updates;
*
运算符两边的空格是可选的。4.7.2 指针的危险
在C++中创建指针时,计算机将分配用来存储地址的内存,但不会分配用来存储指针所指向的数据的内存。为数据提供空间是一个独立的步骤,忽略这一步无疑是自找麻烦,一定要在对指针应用解除引用运算符(*)之前,将指针初始化为一个确定的、适当的地址。这是关于使用指针的金科玉律。
4.7.3 指针和数字
指针不是整型,虽然计算机通常把地址当作整数来处理。从概念上看,指针与整数是截然不同的类型。整数是可以执行加、减、除等运算的数字,而指针描述的是位置,将两个地址相乘没有任何意义。从可以对整数和指针执行的操作上看,它们也是彼此不同的。
4.7.4 使用 new 来分配内存
在C 语言中,可以用库函数 malloc( )来分配内存;在C++中仍然可以这样做,但C++还有更好的方法— new 运算符。
typeName * pointer_name = new typeName;
地址本身只指出了对象存储地址的开始,而没有指出其类型(使用的字节数)。4.7.5 使用 delete 释放内存
delete 运算符,它使得在使用完内存后,能够将其归还给内存池,这是通向最有效地使用内存的关键一步。归还或释放(free)的内存可供程序的其他部分使用。使用 delete时,后面要加上指向内存块的指针(这些内存块最初是用 new 分配的)
4.7.6 使用 new 来创建动态数组
对于大型数据(如数组、字符串和结构),应使用 new,这正是 new 的用武之地。如果通过声明来创建数组,则在程序被编译时将为它分配内存空间。不管程序最终是否使用数组,数组都在那里,它占用了内存。在编译时给数组分配内存被称为静态联编(static binding),意味着数组是在编译时加入到程序中的。但使用 new时,如果在运行阶段需要数组,则创建它;如果不需要,则不创建。还可以在程序运行时选择数组的长度。这被称为动态联编(dynamic binding),意味着数组是在程序运行时创建的。这种数组叫作动态数组(dynamic array)。使用静态联编时,必须在编写程序时指定数组的长度;使用动态联编时,程序将在运行时确定数组的长度。 为数组分配内存的通用格式如下:
type_name * pointer_name = new type_name [num_elements];
使用 new 和d elete时,应遵守以下规则:- 不要使用 delete 来释放不是 new 分配的内存。
- 不要使用 delete 释放同一个内存块两次。
- 如果使用 new [ ]为数组分配内存,则应使用 delete [ ]来释放。
- 如果使用 new [ ]为一个实体分配内存,则应使用 delete(没有方括号)来释放。
- 对空指针应用 delete 是安全的。
4.8 指针、数组和指针算术
指针和数组基本等价的原因在于指针算术(pointer arithmetic)和C++内部处理数组的方式。首先,我们来看一看算术。将整数变量加 1后,其值将增加 1;但将指针变量加 1后,增加的量等于它指向的类型的字节数。
4.8.1 程序说明
4.8.2 指针小结
4.8.3 指针和字符串
4.8.4 使用 new 创建动态结构
4.8.5 自动存储、静态存储和动态存储
C++有3 种管理数据内存的方式:自动存储、静态存储和动态存储(有时也叫作自由存储空间或堆)。
- 自动存储,在函数内部定义的常规变量使用自动存储空间,被称为自动变量(automatic variable),这意味着它们在所属的函数被调用时自动产生,在该函数结束时消亡。实际上,自动变量是一个局部变量,其作用域为包含它的代码块。代码块是被包含在花括号中的一段代码。自动变量通常存储在栈中。这意味着执行代码块时,其中的变量将依次加入到栈中,而在离开代码块时,将按相反的顺序释放这些变量,这被称为后进先出(LIFO)。因此,在程序执行过程中,栈将不断地增大和缩小。
- 静态存储,静态存储是整个程序执行期间都存在的存储方式。使变量成为静态的方式有两种:一种是在函数外面定义它;另一种是在声明变量时使用关键字 static:
static double fee = 56.50;
- 动态存储,new 和d elete 运算符提供了一种比自动变量和静态变量更灵活的方法。它们管理了一个内存池,这在 C++中被称为自由存储空间(free store)或堆(heap)。该内存池同用于静态变量和自动变量的内存是分开的。
4.9 类型组合
数组、结构和指针,可以各种方式组合它们。
4.10 数组的替代品
4.10.1 模板类 vector
模板类 vector 类似于 string类,也是一种动态数组。您可以在运行阶段设置 vector 对象的长度,可在末尾附加新数据,还可在中间插入新数据。基本上,它是使用 new 创建动态数组的替代品。实际上,vector 类确实使用 new 和d elete 来管理内存,但这种工作是自动完成的。
vector<typeName> vt(n_elem);
4.10.2 模板类 array(C++11)
vector 类的功能比数组强大,但付出的代价是效率稍低。如果您需要的是长度固定的数组,使用数组是更佳的选择,但代价是不那么方便和安全。有鉴于此,C++11 新增了模板类 array,它也位于名称空间 std中。与数组一样,array 对象的长度也是固定的,也使用栈(静态内存分配),而不是自由存储区,因此其效率与数组相同,但更方便,更安全。要创建 array 对象,需要包含头文件 array。
array<typeName, n_elem> arr;
与创建 vector 对象不同的是,n_elem 不能是变量。4.10.3 比较数组、vector 对象和 array 对象
- 首先,注意到无论是数组、vector 对象还是 array 对象,都可使用标准数组表示法来访问各个元素。
- 其次,从地址可知,array 对象和数组存储在相同的内存区域(即栈)中,而v ector 对象存储在另一个区域(自由存储区或堆)中。
- 第三,注意到可以将一个 array 对象赋给另一个 array 对象;而对于数组,必须逐元素复制数据。
4.11 总结
数组、结构和指针是 C++的3 种复合类型。数组可以在一个数据对象中存储多个同种类型的值。通过使用索引或下标,可以访问数组中各个元素。 结构可以将多个不同类型的值存储在同一个数据对象中,可以使用成员关系运算符(.)来访问其中的成员。使用结构的第一步是创建结构模板,它定义结构存储了哪些成员。模板的名称将成为新类型的标识符,然后就可以声明这种类型的结构变量。 共用体可以存储一个值,但是这个值可以是不同的类型,成员名指出了使用的模式。 指针是被设计用来存储地址的变量。我们说,指针指向它存储的地址。指针声明指出了指针指向的对象的类型。对指针应用解除引用运算符,将得到指针指向的位置中的值。 字符串是以空字符为结尾的一系列字符。字符串可用引号括起的字符串常量表示,其中隐式包含了结尾的空字符。可以将字符串存储在 char 数组中,可以用被初始化为指向字符串的 char 指针表示字符串。函数 strlen( )返回字符串的长度,其中不包括空字符。函数 strcpy( )将字符串从一个位置复制到另一个位置。在使用这些函数时,应当包含头文件 cstring 或s tring.h。 头文件 string 支持的 C++ string 类提供了另一种对用户更友好的字符串处理方法。具体地说,string 对象将根据要存储的字符串自动调整其大小,用户可以使用赋值运算符来复制字符串。 new 运算符允许在程序运行时为数据对象请求内存。该运算符返回获得内存的地址,可以将这个地址赋给一个指针,程序将只能使用该指针来访问这块内存。如果数据对象是简单变量,则可以使用解除引用运算符(*)来获得其值;如果数据对象是数组,则可以像使用数组名那样使用指针来访问元素;如果数据对象是结构,则可以用指针解除引用运算符(->)来访问其成员。 指针和数组紧密相关。如果 ar 是数组名,则表达式 ar[i]被解释为*(ar + i),其中数组名被解释为数组第一个元素的地址。这样,数组名的作用和指针相同。反过来,可以使用数组表示法,通过指针名来访问 new 分配的数组中的元素。 运算符 new 和d elete 允许显式控制何时给数据对象分配内存,何时将内存归还给内存池。自动变量是在函数中声明的变量,而静态变量是在函数外部或者使用关键字 static 声明的变量,这两种变量都不太灵活。自动变量在程序执行到其所属的代码块(通常是函数定义)时产生,在离开该代码块时终止。静态变量在整个程序周期内都存在。 C++98 新增的标准模板库(STL)提供了模板类 vector,它是动态数组的替代品。C++11 提供了模板类 array,它是定长数组的替代品。
4.12 复习题
4.13 编程练习
第5 章 循环和关系表达式
- 本章内容包括:
- for 循环。
- 表达式和语句。
- 递增运算符和递减运算符:++和−−。
- 组合赋值运算符。
- 复合语句(语句块)。
- 逗号运算符。
- 关系运算符:>、>=、= =、<=、<和!=。
- while 循环。
- typedef 工具。
- do while 循环。
- 字符输入方法 get( )。
- 文件尾条件。
- 嵌套循环和二维数组。
5.1 for 循环
5.1.1 for 循环的组成部分
- 设置初始值。
- 执行测试,看看循环是否应当继续进行。
- 执行循环操作。
更新用于测试的值。
1 2
for (initialization; test-expression; update-expression) body
表达式和语句
for 语句的控制部分使用 3 个表达式。由于其自身强加的句法限制,C++成为非常具有表现力的语言。任何值或任何有效的值和运算符的组合都是表达式。
非表达式和语句
对任何表达式加上分号都可以成为语句,但是这句话反过来说就不对了。也就是说,从语句中删除分号,并不一定能将它转换为表达式。就我们目前使用的语句而言,返回语句、声明语句和 for 语句都不满足“语句=表达式+分号”这种模式。
5.1.2 回到 for 循环
5.1.3 修改步长
5.1.4 使用 for 循环访问字符串
5.1.5 递增运算符(++)和递减运算符(−−)
这两个运算符执行两种极其常见的循环操作:将循环计数加 1 或减 1。然而,它们还有很多特点不为读者所知。这两个运算符都有两种变体。前缀(prefix)版本位于操作数前面,如++x;后缀(postfix)版本位于操作数后面,如x++。两个版本对操作数的影响是一样的,但是影响的时间不同。
5.1.6 副作用和顺序点
首先,副作用(side effect)指的是在计算表达式时对某些东西(如存储在变量中的值)进行了修改;顺序点(sequence point)是程序执行过程中的一个点,在这里,进入下一步之前将确保对所有的副作用都进行了评估。在C++中,语句中的分号就是一个顺序点,这意味着程序处理下一条语句之前,赋值运算符、递增运算符和递减运算符执行的所有修改都必须完成。另外,任何完整的表达式末尾都是一个顺序点。 何为完整表达式呢?它是这样一个表达式:不是另一个更大表达式的子表达式。
5.1.7 前缀格式和后缀格式
显然,如果变量被用于某些目的(如用作函数参数或给变量赋值),使用前缀格式和后缀格式的结果将不同。然而,如果递增表达式的值没有被使用,使用前缀格式和后缀格式没有任何区别。表达式的值未被使用,因此只存在副作用。 总之,对于内置类型,采用哪种格式不会有差别;但对于用户定义的类型,如果有用户定义的递增和递减运算符,则前缀格式的效率更高。用户这样定义前缀函数:将值加 1,然后返回结果;但后缀版本首先复制一个副本,将其加 1,然后将复制的副本返回。
5.1.8 递增/递减运算符和指针
可以将递增运算符用于指针和基本变量。本书前面介绍过,将递增运算符用于指针时,将把指针的值增加其指向的数据类型占用的字节数,这种规则适用于对指针递增和递减;前缀递增、前缀递减和解除引用运算符的优先级相同,以从右到左的方式进行结合。后缀递增和后缀递减的优先级相同,但比前缀运算符的优先级高,这两个运算符以从左到右的方式进行结合。
5.1.9 组合赋值运算符
+=运算符将两个操作数相加,并将结果赋给左边的操作数。这意味着左边的操作数必须能够被赋值,如变量、数组元素、结构成员或通过对指针解除引用来标识的数据。
+=
-=
*=
/+
%=
i = i + by; i += by;
5.1.10 复合语句(语句块)
代码块由一对花括号和它们包含的语句组成,被视为一条语句,从而满足句法的要求。复合语句还有一种有趣的特性。如果在语句块中定义一个新的变量,则仅当程序执行该语句块中的语句时,该变量才存在。执行完该语句块后,变量将被释放。这表明此变量仅在该语句块中才是可用的。
5.1.11 其他语法技巧—逗号运算符
语句块允许把两条或更多条语句放到按 C++句法只能放一条语句的地方。逗号运算符对表达式完成同样的任务,允许将两个表达式放到 C++句法只允许放一个表达式的地方。逗号运算符是一个顺序点。
5.1.12 关系表达式
对于所有的关系表达式,如果比较结果为真,则其值将为 true,否则为 false,因此可将其用作循环测试表达式。< <= > >= != ==
5.1.13 赋值、比较和可能犯的错误
不要混淆等于运算符(==)与赋值运算符(=)。
5.1.14 C-风格字符串的比较
数组名是数组的地址。同样,用引号括起的字符串常量也是其地址。应使用 C-风格字符串库中的 strcmp( )函数来比较。
5.1.15 比较 string 类字符串
如果使用 string 类字符串而不是 C-风格字符串,比较起来将简单些,因为类设计让您能够使用关系运算符进行比较。这之所以可行,是因为类函数重载(重新定义)了这些运算符。
5.2 while 循环
|
|
5.2.1 for 与w hile
- 在C++中,for 和w hile 循环本质上是相同的。
- 首先,在f or 循环中省略了测试条件时,将认为条件为 true;
- 其次,在f or 循环中,可使用初始化语句声明一个局部变量,但在 while 循环中不能这样做;
- 最后,如果循环体中包括 continue 语句,情况将稍有不同
- 通常,程序员使用 for 循环来为循环计数,因为 for 循环格式允许将所有相关的信息—初始值、终止值和更新计数器的方法—放在同一个地方。在无法预先知道循环将执行的次数时,程序员常使用 while 循环。
5.2.2 等待一段时间:编写延时循环
5.3 do while 循环
它不同于另外两种循环,因为它是出口条件(exit condition)循环。这意味着这种循环将首先执行循环体,然后再判定测试表达式,决定是否应继续执行循环。如果条件为 false,则循环终止;否则,进入新一轮的执行和测试。这样的循环通常至少执行一次,因为其程序流必须经过循环体后才能到达测试条件。下面是其句法:
|
|
5.4 基于范围的 for 循环(C++11)
C++11 新增了一种循环:基于范围(range-based)的f or 循环。这简化了一种常见的循环任务:对数组(或容器类,如v ector 和a rray)的每个元素执行相同的操作。
|
|
5.5 循环和文本输入
5.5.1 使用原始的 cin 进行输入
5.5.2 使用 cin.get(char)进行补救
5.5.3 使用哪一个 cin.get( )
5.5.4 文件尾条件
5.5.5 另一个 cin.get( )版本
5.6 嵌套循环和二维数组
5.6.1 初始化二维数组
5.6.2 使用二维数组
5.7 总结
C++提供了 3 种循环:for 循环、while 循环和 do while 循环。如果循环测试条件为 true 或非零,则循环将重复执行一组指令;如果测试条件为 false 或0,则结束循环。for 循环和 while 循环都是入口条件循环,这意味着程序将在执行循环体中的语句之前检查测试条件。do while 循环是出口条件循环,这意味着其将在执行循环体中的语句之后检查条件。 每种循环的句法都要求循环体由一条语句组成。然而,这条语句可以是复合语句,也可以是语句块(由花括号括起的多条语句)。 关系表达式对两个值进行比较,常被用作循环测试条件。关系表达式是通过使用 6 种关系运算符之一构成的:<、<=、= =、>=、>或! =。关系表达式的结果为 bool 类型,值为 true 或f alse。 许多程序都逐字节地读取文本输入或文本文件,istream 类提供了多种可完成这种工作的方法。如果 ch 是一个 char 变量,则下面的语句将输入中的下一个字符读入到 ch中: 然而,它将忽略空格、换行符和制表符。下面的成员函数调用读取输入中的下一个字符(而不管该字符是什么)并将其存储到 ch中: 成员函数调用 cin.get( )返回下一个输入字符—包括空格、换行符和制表符,因此,可以这样使用它: cin.get(char)成员函数调用通过返回转换为 false 的b ool 值来指出已到达 EOF,而c in.get( )成员函数调用则通过返回 EOF 值来指出已到达 EOF,EOF 是在文件 iostream 中定义的。 嵌套循环是循环中的循环,适合用于处理二维数组。
5.8 复习题
5.9 编程练习
第6 章 分支语句和逻辑运算符
- 本章内容包括:
- if 语句。
- if else 语句。
- 逻辑运算符:&&、||和!。
- cctype 字符函数库。
- 条件运算符:?:。
- switch 语句。
- continue 和b reak 语句。
- 读取数字的循环。
- 基本文件输入/输出。
6.1 if 语句
|
|
6.1.1 if else 语句
if 语句让程序决定是否执行特定的语句或语句块,而i f else 语句则让程序决定执行两条语句或语句块中的哪一条,这种语句对于选择其中一种操作很有用。
1 2 3 4
if (test-condition) statement1 else statement2
6.1.2 格式化 if else 语句
6.1.3 if else if else 结构
与实际生活中发生的情况类似,计算机程序也可能提供两个以上的选择。可以将 C++的i f else 语句进行扩展来满足这种需求。正如读者知道的,else 之后应是一条语句,也可以是语句块。由于 if else 语句本身是一条语句,所以可以放在 else 的后面
1 2 3 4 5 6
if (test-condition1) statement1 else if (test-condition2) statement2 else statement3
6.2 逻辑表达式
C++提供了 3 种逻辑运算符,来组合或修改已有的表达式。这些运算符分别是逻辑 OR(||)、逻辑 AND(&&)和逻辑 NOT(!)。
6.2.1 逻辑 OR 运算符:||
当两个条件中有一个或全部满足某个要求时,可以用单词 or 来指明这种情况。C++可以采用逻辑 OR 运算符(||),将两个表达式组合在一起。如果原来表达式中的任何一个或全部都为 true(或非零),则得到的表达式的值为 true;否则,表达式的值为 false。由于||的优先级比关系运算符低,||运算符是个顺序点(sequence point)。如果左侧的表达式为 true,则C++将不会去判定右侧的表达式,因为只要一个表达式为 true,则整个逻辑表达式为 true(读者可能还记得,冒号和逗号运算符也是顺序点)。
6.2.2 逻辑 AND 运算符:&&
逻辑 AND 运算符(&&),也是将两个表达式组合成一个表达式。仅当原来的两个表达式都为 true时,得到的表达式的值才为 true。&&的优先级低于关系运算符,和||运算符一样,&&运算符也是顺序点,因此将首先判定左侧,并且在右侧被判定之前产生所有的副作用。如果左侧为 false,则整个逻辑表达式必定为 false,在这种情况下,C++将不会再对右侧进行判定。
6.2.3 用&&来设置取值范围
&&运算符还允许建立一系列 if else if else 语句,其中每种选择都对应于一个特定的取值范围。
6.2.4 逻辑 NOT 运算符:!
!运算符将它后面的表达式的真值取反。
6.2.5 逻辑运算符细节
6.2.6 其他表示方式
运算符 另一种表示方式 && and \verti{}| or ! not
6.3 字符函数库 cctype
函数名称 | 返回值 |
---|---|
isalnum() | 如果参数是字母数字,即字母或数字,该函数返回 true |
isalpha() | 如果参数是字母,该函数返回 true |
iscntrl() | 如果参数是控制字符,该函数返回 true |
isdigit() | 如果参数是数字(0~9),该函数返回 true |
isgraph() | 如果参数是除空格之外的打印字符,该函数返回 true |
islower() | 如果参数是小写字母,该函数返回 true |
isprint() | 如果参数是打印字符(包括空格),该函数返回 true |
ispunct() | 如果参数是标点符号,该函数返回 true |
isspace() | 如果参数是标准空白字符,如空格、进纸、换行符、回车、水平制表符或者垂直制表符,该函数返回 true |
isupper() | 如果参数是大写字母,该函数返回 true |
isxdigit() | 如果参数是十六进制数字,即0~9、a~f 或A~F,该函数返回 true |
tolower() | 如果参数是大写字符,则返回其小写,否则返回该参数 |
toupper() | 如果参数是小写字符,则返回其大写,否则返回该参数 |
6.4 ?:运算符
C++有一个常被用来代替 if else 语句的运算符,这个运算符被称为条件运算符(?:),它是 C++中唯一一个需要 3 个操作数的运算符。该运算符的通用格式如下: expression1 ? expression2 : expression3;
如果 expression1 为t rue,则整个条件表达式的值为 expression2 的值;否则,整个表达式的值为 expression3 的值。
6.5 switch 语句
下面是 switch 语句的通用格式:
|
|
6.5.1 将枚举量用作标签
cin 无法识别枚举类型(它不知道程序员是如何定义它们的),因此该程序要求用户选择选项时输入一个整数。当s witch 语句将 int 值和枚举量标签进行比较时,将枚举量提升为 int。另外,在w hile 循环测试条件中,也会将枚举量提升为 int 类型。
6.5.2 switch 和i f else
switch 语句和 if else 语句都允许程序从选项中进行选择。相比之下,if else 更通用。switch 并不是为处理取值范围而设计的。switch 语句中的每一个 case 标签都必须是一个单独的值。另外,这个值必须是整数(包括 char),因此 switch 无法处理浮点测试。另外 case 标签值还必须是常量。如果选项涉及取值范围、浮点测试或两个变量的比较,则应使用 if else 语句。 然而,如果所有的选项都可以使用整数常量来标识,则可以使用 switch 语句或 if else 语句。由于 switch 语句是专门为这种情况设计的,因此,如果选项超过两个,则就代码长度和执行速度而言,switch 语句的效率更高。如果既可以使用 if else if 语句,也可以使用 switch 语句,则当选项不少于 3 个时,应使用 switch 语句。
6.6 break 和c ontinue 语句
break 和c ontinue 语句都使程序能够跳过部分代码。可以在 switch 语句或任何循环中使用 break 语句,使程序跳到 switch 或循环后面的语句处执行。continue 语句用于循环中,让程序跳过循环体中余下的代码,并开始新一轮循环。虽然 continue 语句导致该程序跳过循环体的剩余部分,但不会跳过循环的更新表达式。在f or 循环中,continue 语句使程序直接跳到更新表达式处,然后跳到测试表达式处。然而,对于 while 循环来说,continue 将使程序直接跳到测试表达式处,因此 while 循环体中位于 continue 之后的更新表达式都将被跳过。
6.7 读取数字的循环
对于文件输入,C++使用类似于 cout 的东西。下面来复习一些有关将 cout 用于控制台输出的基本事实,为文件输出做准备。
- 必须包含头文件 iostream。
- 头文件 iostream 定义了一个用处理输出的 ostream类。
- 头文件 iostream 声明了一个名为 cout 的o stream 变量(对象)。
- 必须指明名称空间 std;例如,为引用元素 cout 和e ndl,必须使用编译指令 using 或前缀 std::。
- 可以结合使用 cout 和运算符<<来显示各种类型的数据。
文件输出与此极其相似。
- 必须包含头文件 fstream。
- 头文件 fstream 定义了一个用于处理输出的 ofstream类。
- 需要声明一个或多个 ofstream 变量(对象),并以自己喜欢的方式对其进行命名,条件是遵守常用的命名规则。
- 必须指明名称空间 std;例如,为引用元素 ofstream,必须使用编译指令 using 或前缀 std::。
- 需要将 ofstream 对象与文件关联起来。为此,方法之一是使用 open( )方法。
- 使用完文件后,应使用方法 close( )将其关闭。
- 可结合使用 ofstream 对象和运算符<<来输出各种类型的数据。
总之,使用文件输出的主要步骤如下。
- 包含头文件 fstream。
- 创建一个 ofstream 对象。
- 将该 ofstream 对象同一个文件关联起来。
- 就像使用 cout 那样使用该 ofstream 对象。
6.8 简单文件输入/输出
6.8.1 文本 I/O 和文本文件
6.8.2 写入到文本文件中
6.8.3 读取文本文件
接下来介绍文本文件输入,它是基于控制台输入的。控制台输入涉及多个方面,下面首先总结这些方面。
- 必须包含头文件 iostream。
- 头文件 iostream 定义了一个用处理输入的 istream类。
- 头文件 iostream 声明了一个名为 cin 的i stream 变量(对象)。
- 必须指明名称空间 std;例如,为引用元素 cin,必须使用编译指令 using 或前缀 std::。
- 可以结合使用 cin 和运算符>>来读取各种类型的数据。
- 可以使用 cin 和g et( )方法来读取一个字符,使用 cin 和g etline( )来读取一行字符。
- 可以结合使用 cin 和e of( )、fail( )方法来判断输入是否成功。
- 对象 cin 本身被用作测试条件时,如果最后一个读取操作成功,它将被转换为布尔值 true,否则被转换为 false。
文件输出与此极其相似:
- 必须包含头文件 fstream。
- 头文件 fstream 定义了一个用于处理输入的 ifstream类。
- 需要声明一个或多个 ifstream 变量(对象),并以自己喜欢的方式对其进行命名,条件是遵守常用的命名规则。
- 必须指明名称空间 std;例如,为引用元素 ifstream,必须使用编译指令 using 或前缀 std::。
- 需要将 ifstream 对象与文件关联起来。为此,方法之一是使用 open( )方法。
- 使用完文件后,应使用 close( )方法将其关闭。
- 可结合使用 ifstream 对象和运算符>>来读取各种类型的数据。
- 可以使用 ifstream 对象和 get( )方法来读取一个字符,使用 ifstream 对象和 getline( )来读取一行字符。
- 可以结合使用 ifstream 和e of( )、fail( )等方法来判断输入是否成功。
- ifstream 对象本身被用作测试条件时,如果最后一个读取操作成功,它将被转换为布尔值 true,否则被转换为 false。
6.9 总结
使用引导程序选择不同操作的语句后,程序和编程将更有趣(这是否也能引起程序员们的兴趣,我没有做过研究)。C++提供了 if 语句、if else 语句和 switch 语句来管理选项。if 语句使程序有条件地执行语句或语句块,也就是说,如果满足特定的条件,程序将执行特定的语句或语句块。if else 语句程序选择执行两个语句或语句块之一。可以在这条语句后再加上 if else,以提供一系列的选项。switch 语句引导程序执行一系列选项之一。 C++还提供了帮助决策的运算符。第5 章讨论了关系表达式,这种表达式对两个值进行比较。if 和i f else 语句通常使用关系表达式作为测试条件。通过使用逻辑运算符(&&、||和!),可以组合或修改关系表达式,创建更细致的测试。条件运算符(?:)提供了一种选择两个值之一的简洁方式。 cctype 字符函数库提供了一组方便的、功能强大的工具,可用于分析字符输入。 对于文件 I/O 来说,循环和选择语句是很有用的工具;文件 I/O 与控制台 I/O 极其相似。声明 ifstream 和o fstream 对象,并将它们同文件关联起来后,便可以像使用 cin 和c out 那样使用这些对象。 使用循环和决策语句,便可以编写有趣的、智能的、功能强大的程序。不过我们刚开始涉足 C++的强大功能,下一章将介绍函数。
6.10 复习题
6.11 编程练习
第7 章 函数——C++的编程模块
- 本章内容包括:
- 函数基本知识。
- 函数原型。
- 按值传递函数参数。
- 设计处理数组的函数。
- 使用 const 指针参数。
- 设计处理文本字符串的函数。
- 设计处理结构的函数。
- 设计处理 string 对象的函数。
- 调用自身的函数(递归)。
- 指向函数的指针。
7.1 复习函数的基本知识
要使用 C++函数,必须完成如下工作:
- 提供函数定义;
- 提供函数原型;
- 调用函数。
7.1.1 定义函数
可以将函数分成两类:没有返回值的函数和有返回值的函数。没有返回值的函数被称为 void 函数,其通用格式如下:
1 2 3 4
vod functionName(parameterList) { statements(s); }
有返回值的函数将生成一个值,并将它返回给调用函数。这种函数的类型被声明为返回值的类型,其通用格式如下:
1 2 3 4 5
typeName functionName(parameterList) { statements(s); return typeNameValue; }
对于有返回值的函数,必须使用返回语句,以便将值返回给调用函数。值本身可以是常量、变量,也可以是表达式,只是其结果的类型必须为 typeName 类型或可以被转换为 typeName,函数将最终的值返回给调用函数。C++对于返回值的类型有一定的限制:不能是数组,但可以是其他任何类型——整数、浮点数、指针,甚至可以是结构和对象!
7.1.2 函数原型和函数调用
- 首先,需要知道 C++要求提供原型的原因。原型描述了函数到编译器的接口,也就是说,它将函数返回值的类型(如果有的话)以及参数的类型和数量告诉编译器。
- 其次,由于 C++要求提供原型,因此还应知道正确的语法。函数原型是一条语句,因此必须以分号结束。获得原型最简单的方法是,复制函数定义中的函数头,并添加分号。
- 最后,应当感谢原型所做的一切。原型确保以下几点:
- 编译器正确处理函数返回值;
- 编译器检查使用的参数数目是否正确;
- 编译器检查使用的参数类型是否正确。如果不正确,则转换为正确的类型(如果可能的话)。
7.2 函数参数和按值传递
下面详细介绍一下函数参数。C++通常按值传递参数,这意味着将数值参数传递给函数,而后者将其赋给一个新的变量。
7.2.1 多个参数
函数可以有多个参数。在调用函数时,只需使用逗号将这些参数分开即可。
7.2.2 另外一个接受两个参数的函数
7.3 函数和数组
7.3.1 函数如何使用指针来处理数组
在大多数情况下,C++和C 语言一样,也将数组名视为指针。第4 章介绍过,C++将数组名解释为其第一个元素的地址,该规则有一些例外。首先,数组声明使用数组名来标记存储位置;其次,对数组名使用 sizeof 将得到整个数组的长度(以字节为单位);第三,正如第 4 章指出的,将地址运算符&用于数组名时,将返回整个数组的地址,例如& cookies 将返回一个 32 字节内存块的地址(如果 int 长4 字节)。当(且仅当)用于函数头或函数原型中,int * arr 和i nt arr [ ]的含义才是相同的。当(且仅当)用于函数头或函数原型中,int * arr 和i nt arr [ ]的含义才是相同的。
7.3.2 将数组作为参数意味着什么
实际上并没有将数组内容传递给函数,而是将数组的位置(地址)、包含的元素种类(类型)以及元素数目(n 变量)提交给函数。有了这些信息后,函数便可以使用原来的数组。传递常规变量时,函数将使用该变量的拷贝;但传递数组时,函数将使用原来的数组。将数组地址作为参数可以节省复制整个数组所需的时间和内存。如果数组很大,则使用拷贝的系统开销将非常大;程序不仅需要更多的计算机内存,还需要花费时间来复制大块的数据。另一方面,使用原始数据增加了破坏数据的风险。
7.3.3 更多数组函数示例
- 由于接受数组名参数的函数访问的是原始数组,而不是其副本,因此可以通过调用该函数将值赋给数组元素。该函数的一个参数是要填充的数组的名称。
- 为防止函数无意中修改数组的内容,可在声明形参时使用关键字 const。
- 若函数将修改数组的值,在声明时不能使用 const。
7.3.4 使用数组区间的函数
通过传递两个指针来完成:一个指针标识数组的开头,另一个指针标识数组的尾部。
7.3.5 指针和 const
如果数据类型本身并不是指针,则可以将 const 数据或非 const 数据的地址赋给指向 const 的指针,但只能将非 const 数据的地址赋给非 const 指针。尽可能使用 const 将指针参数声明为指向常量数据的指针有两条理由:
- 这样可以避免由于无意间修改数据而导致的编程错误;
- 使用 const 使得函数能够处理 const 和非 const 实参,否则将只能接受非 const 数据。
7.4 函数和二维数组
7.5 函数和 C-风格字符串
7.5.1 将C-风格字符串作为参数的函数
假设要将字符串作为参数传递给函数,则表示字符串的方式有三种:
- char 数组;
- 用引号括起的字符串常量(也称字符串字面值);
- 被设置为字符串的地址的 char 指针。
但上述 3 种选择的类型都是 char 指针(准确地说是 char*),因此可以将其作为字符串处理函数的参数
7.5.2 返回 C-风格字符串的函数
假设要编写一个返回字符串的函数。是的,函数无法返回一个字符串,但可以返回字符串的地址,这样做的效率更高。
7.6 函数和结构
使用结构编程时,最直接的方式是像处理基本类型那样来处理结构;也就是说,将结构作为参数传递,并在需要时将结构用作返回值使用。
7.6.1 传递和返回结构
7.6.2 另一个处理结构的函数示例
7.6.3 传递结构的地址
7.7 函数和 string 对象
虽然 C-风格字符串和 string 对象的用途几乎相同,但与数组相比,string 对象与结构的更相似。例如,可以将一个结构赋给另一个结构,也可以将一个对象赋给另一个对象。可以将结构作为完整的实体传递给函数,也可以将对象作为完整的实体进行传递。如果需要多个字符串,可以声明一个 string 对象数组,而不是二维 char 数组。
7.8 函数与 array 对象
在C++中,类对象是基于结构的,因此结构编程方面的有些考虑因素也适用于类。例如,可按值将对象传递给函数,在这种情况下,函数处理的是原始对象的副本。另外,也可传递指向对象的指针,这让函数能够操作原始对象。
7.9 递归
C++函数有一种有趣的特点——可以调用自己(然而,与C 语言不同的是,C++不允许 main( )调用自己),这种功能被称为递归。
7.9.1 包含一个递归调用的递归
如果递归函数调用自己,则被调用的函数也将调用自己,这将无限循环下去,除非代码中包含终止调用链的内容。通常的方法将递归调用放在 if 语句中。
7.9.2 包含多个递归调用的递归
7.10 函数指针
与数据项相似,函数也有地址。函数的地址是存储其机器语言代码的内存的开始地址。
7.10.1 函数指针的基础知识
- 获取函数的地址 获取函数的地址很简单:只要使用函数名(后面不跟参数)即可。一定要区分传递的是函数的地址还是函数的返回值。
- 声明函数指针 声明指向某种数据类型的指针时,必须指定指针指向的类型。同样,声明指向函数的指针时,也必须指定指针指向的函数类型。这意味着声明应指定函数的返回类型以及函数的特征标(参数列表)。也就是说,声明应像函数原型那样指出有关函数的信息。 通常,要声明指向特定类型的函数的指针,可以首先编写这种函数的原型,然后用(*pf)替换函数名。这样 pf 就是这类函数的指针。
- 现在进入最后一步,即使用指针来调用被指向的函数。线索来自指针声明。前面讲过,(*pf)扮演的角色与函数名相同,因此使用(*pf)时,只需将它看作函数名
7.10.2 函数指针示例
7.10.3 深入探讨函数指针
7.10.4 使用 typedef 进行简化
7.11 总结
函数是 C++的编程模块。要使用函数,必须提供定义和原型,并调用该函数。函数定义是实现函数功能的代码;函数原型描述了函数的接口:传递给函数的值的数目和种类以及函数的返回类型。函数调用使得程序将参数传递给函数,并执行函数的代码。
在默认情况下,C++函数按值传递参数。这意味着函数定义中的形参是新的变量,它们被初始化为函数调用所提供的值。因此,C++函数通过使用拷贝,保护了原始数据的完整性。
C++将数组名参数视为数组第一个元素的地址。从技术上讲,这仍然是按值传递的,因为指针是原始地址的拷贝,但函数将使用指针来访问原始数组的内容。当且仅当声明函数的形参时,下面两个声明才是等价的: typeName []
typeName *
这两个声明都表明,arr 是指向 typeName 的指针,但在编写函数代码时,可以像使用数组名那样使用 arr 来访问元素:arr[i]。即使在传递指针时,也可以将形参声明为 const 指针,来保护原始数据的完整性。由于传递数据的地址时,并不会传输有关数组长度的信息,因此通常将数组长度作为独立的参数来传递。另外,也可传递两个指针(其中一个指向数组开头,另一个指向数组末尾的下一个元素),以指定一个范围,就像 STL 使用的算法一样。
C++提供了 3 种表示 C-风格字符串的方法:字符数组、字符串常量和字符串指针。它们的类型都是 char*(char 指针),因此被作为 char*类型参数传递给函数。C++使用空值字符(\0)来结束字符串,因此字符串函数检测空值字符来确定字符串的结尾。
C++还提供了 string类,用于表示字符串。函数可以接受 string 对象作为参数以及将 string 对象作为返回值。string 类的方法 size( )可用于判断其存储的字符串的长度。
C++处理结构的方式与基本类型完全相同,这意味着可以按值传递结构,并将其用作函数返回类型。然而,如果结构非常大,则传递结构指针的效率将更高,同时函数能够使用原始数据。这些考虑因素也适用于类对象。
C++函数可以是递归的,也就是说,函数代码中可以包括对函数本身的调用。
C++函数名与函数地址的作用相同。通过将函数指针作为参数,可以传递要调用的函数的名称。
7.12 复习题
7.13 编程练习
第8 章 函数探幽
- 本章内容包括:
- 内联函数。
- 引用变量。
- 如何按引用传递函数参数。
- 默认参数。
- 函数重载。
- 函数模板。
- 函数模板具体化。
8.1 C++内联函数
内联函数是 C++为提高程序运行速度所做的一项改进。常规函数和内联函数之间的主要区别不在于编写方式,而在于 C++编译器如何将它们组合到程序中。内联函数的编译代码与其他程序代码“内联”起来了。也就是说,编译器将使用相应的函数代码替换函数调用。对于内联代码,程序无需跳到另一个位置处执行代码,再跳回来。因此,内联函数的运行速度比常规函数稍快,但代价是需要占用更多内存。
- 在函数声明前加上关键字 inline;
- 在函数定义前加上关键字 inline。
程序员请求将函数作为内联函数时,编译器并不一定会满足这种要求。它可能认为该函数过大或注意到函数调用了自己(内联函数不能递归),因此不将其作为内联函数;而有些编译器没有启用或实现这种特性。
8.2 引用变量
C++新增了一种复合类型——引用变量。引用是已定义的变量的别名(另一个名称)。引用变量的主要用途是用作函数的形参。通过将引用变量用作参数,函数将使用原始数据,而不是其副本。
8.2.1 创建引用变量
C 和C++使用&符号来指示变量的地址。C++给&符号赋予了另一个含义,将其用来声明引用。例如,要将 rodents 作为 rats 变量的别名。必须在声明引用时将其初始化,而不能像指针那样,先声明,再赋值。可以通过初始化声明来设置引用,但不能通过赋值来设置。
8.2.2 将引用用作函数参数
引用经常被用作函数参数,使得函数中的变量名成为调用程序中的变量的别名。这种传递参数的方法称为按引用传递。按引用传递允许被调用的函数能够访问调用函数中的变量。C++新增的这项特性是对 C 语言的超越,C 语言只能按值传递。按值传递导致被调用函数使用调用程序的值的拷贝。当然,C 语言也允许避开按值传递的限制,采用按指针传递的方式。
8.2.3 引用的属性和特别之处
- 如果引用参数是 const,则编译器将在下面两种情况下生成临时变量:
- 实参的类型正确,但不是左值;
- 实参的类型不正确,但可以转换为正确的类型。
- 应尽可能使用 const, 将引用参数声明为常量数据的引用的理由有三个:
- 使用 const 可以避免无意中修改数据的编程错误;
- 使用 const 使函数能够处理 const 和非 const 实参,否则将只能接受非 const 数据;
- 如果引用参数是 const,则编译器将在下面两种情况下生成临时变量:
8.2.4 将引用用于结构
引用非常适合用于结构和类(C++的用户定义类型)。确实,引入引用主要是为了用于这些类型的,而不是基本的内置类型。 使用结构引用参数的方式与使用基本变量引用相同,只需在声明结构参数时使用引用运算符&即可。
8.2.5 将引用用于类对象
将类对象传递给函数时,C++通常的做法是使用引用。例如,可以通过使用引用,让函数将类 string、ostream、istream、ofstream 和i fstream 等类的对象作为参数。
8.2.6 对象、继承和引用
8.2.7 何时使用引用参数
使用引用参数的主要原因有两个:
- 程序员能够修改调用函数中的数据对象。
- 通过传递引用而不是整个数据对象,可以提高程序的运行速度。
当数据对象较大时(如结构和类对象),第二个原因最重要。这些也是使用指针参数的原因。这是有道理的,因为引用参数实际上是基于指针的代码的另一个接口。那么,什么时候应使用引用、什么时候应使用指针呢?什么时候应按值传递呢?下面是一些指导原则:
- 对于使用传递的值而不作修改的函数。
- 如果数据对象很小,如内置数据类型或小型结构,则按值传递。
- 如果数据对象是数组,则使用指针,因为这是唯一的选择,并将指针声明为指向 const 的指针。
- 如果数据对象是较大的结构,则使用 const 指针或 const 引用,以提高程序的效率。这样可以节省复制结构所需的时间和空间。
- 如果数据对象是类对象,则使用 const 引用。类设计的语义常常要求使用引用,这是 C++新增这项特性的主要原因。因此,传递类对象参数的标准方式是按引用传递。
- 对于修改调用函数中数据的函数:
- 如果数据对象是内置数据类型,则使用指针。如果看到诸如 fixit(&x)这样的代码(其中 x 是i nt),则很明显,该函数将修改 x。
- 如果数据对象是数组,则只能使用指针。
- 如果数据对象是结构,则使用引用或指针。
- 如果数据对象是类对象,则使用引用。
8.3 默认参数
下面介绍 C++的另一项新内容——默认参数。默认参数指的是当函数调用中省略了实参时自动使用的一个值。对于带参数列表的函数,必须从右向左添加默认值。也就是说,要为某个参数设置默认值,则必须为它右边的所有参数提供默认值。实参按从左到右的顺序依次被赋给相应的形参,而不能跳过任何参数。
8.4 函数重载
函数多态是 C++在C 语言的基础上新增的功能。默认参数让您能够使用不同数目的参数调用同一个函数,而函数多态(函数重载)让您能够使用多个同名的函数。术语“多态”指的是有多种形式,因此函数多态允许函数可以有多种形式。类似地,术语“函数重载”指的是可以有多个同名的函数,因此对名称进行了重载。这两个术语指的是同一回事,但我们通常使用函数重载。可以通过函数重载来设计一系列函数——它们完成相同的工作,但使用不同的参数列表。 函数重载的关键是函数的参数列表——也称为函数特征标(function signature)。如果两个函数的参数数目和类型相同,同时参数的排列顺序也相同,则它们的特征标相同,而变量名是无关紧要的。C++允许定义名称相同的函数,条件是它们的特征标不同。如果参数数目和/或参数类型不同,则特征标也不同。
8.4.1 重载示例
8.4.2 何时使用函数重载
然函数重载很吸引人,但也不要滥用。仅当函数基本上执行相同的任务,但使用不同形式的数据时,才应采用函数重载。另外,您可能还想知道,是否可以通过使用默认参数来实现同样的目的。
8.5 函数模板
现在的 C++编译器实现了 C++新增的一项特性——函数模板。函数模板是通用的函数描述,也就是说,它们使用泛型来定义函数,其中的泛型可用具体的类型(如i nt 或d ouble)替换。通过将类型作为参数传递给模板,可使编译器生成该类型的函数。由于模板允许以泛型(而不是具体类型)的方式编写程序,因此有时也被称为通用编程。由于类型是用参数表示的,因此模板特性有时也被称为参数化类型(parameterized types)。如果需要多个将同一种算法用于不同类型的函数,请使用模板。如果不考虑向后兼容的问题,并愿意键入较长的单词,则声明类型参数时,应使用关键字 typename 而不使用 class。
8.5.1 重载的模板
需要多个对不同类型使用同一种算法的函数时,可使用模板。然而,并非所有的类型都使用相同的算法。为满足这种需求,可以像重载常规函数定义那样重载模板定义。和常规重载一样,被重载的模板的函数特征标必须不同。
8.5.2 模板的局限性
8.5.3 显式具体化
第三代具体化(ISO/ANSI C++标准):
- 对于给定的函数名,可以有非模板函数、模板函数和显式具体化模板函数以及它们的重载版本。
- 显式具体化的原型和定义应以 template<>打头,并通过名称来指出类型。
- 具体化优先于常规模板,而非模板函数优先于具体化和常规模板。
8.5.4 实例化和具体化
模板
1 2 3 4 5 6 7 8
template <typename T> void Swap(T &a, T &b) { T temp; temp = a; a = b; b = temp; }
实例化
template void Swap<int>(int, int);
具体化,两种形式等效,可以重新编写函数体
template <> void Swap<int>(int, int){};
template <> void Swap(int, int){};
试图在同一个文件(或转换单元)中使用同一种类型的显式实例和显式具体化将出错。
8.5.5 编译器选择使用哪个函数版本
- 对于函数重载、函数模板和函数模板重载,C++需要(且有)一个定义良好的策略,来决定为函数调用使用哪一个函数定义,尤其是有多个参数时。这个过程称为重载解析(overloading resolution)。详细解释这个策略将需要将近一章的篇幅,因此我们先大致了解一下这个过程是如何进行的。
- 第1步:创建候选函数列表。其中包含与被调用函数的名称相同的函数和模板函数。
- 第2步:使用候选函数列表创建可行函数列表。这些都是参数数目正确的函数,为此有一个隐式转换序列,其中包括实参类型与相应的形参类型完全匹配的情况。例如,使用 float 参数的函数调用可以将该参数转换为 double,从而与 double 形参匹配,而模板可以为 float 生成一个实例。
- 第3步:确定是否有最佳的可行函数。如果有,则使用它,否则该函数调用出错。
- 从最佳到最差的顺序如下所述。
- 完全匹配,但常规函数优先于模板。
- 提升转换(例如,char 和s horts 自动转换为 int,float 自动转换为 double)。
- 标准转换(例如,int 转换为 char,long 转换为 double)。
- 用户定义的转换,如类声明中定义的转换。
- 对于函数重载、函数模板和函数模板重载,C++需要(且有)一个定义良好的策略,来决定为函数调用使用哪一个函数定义,尤其是有多个参数时。这个过程称为重载解析(overloading resolution)。详细解释这个策略将需要将近一章的篇幅,因此我们先大致了解一下这个过程是如何进行的。
8.5.6 模板函数的发展
8.6 总结
1
|
C++扩展了 C 语言的函数功能。通过将 inline 关键字用于函数定义,并在首次调用该函数前提供其函数定义,可以使得 C++编译器将该函数视为内联函数。也就是说,编译器不是让程序跳到独立的代码段,以执行函数,而是用相应的代码替换函数调用。只有在函数很短时才能采用内联方式。 |
引用变量是一种伪装指针,它允许为变量创建别名(另一个名称)。引用变量主要被用作处理结构和类对象的函数的参数。通常,被声明为特定类型引用的标识符只能指向这种类型的数据;然而,如果一个类(如o fstream)是从另一个类(如o stream)派生出来的,则基类引用可以指向派生类对象。 C++原型让您能够定义参数的默认值。如果函数调用省略了相应的参数,则程序将使用默认值;如果函数调用提供了参数值,则程序将使用这个值(而不是默认值)。只能在参数列表中从右到左提供默认参数。因此,如果为某个参数提供了默认值,则必须为该参数右边所有的参数提供默认值。 函数的特征标是其参数列表。程序员可以定义两个同名函数,只要其特征标不同。这被称为函数多态或函数重载。通常,通过重载函数来为不同的数据类型提供相同的服务。 函数模板自动完成重载函数的过程。只需使用泛型和具体算法来定义函数,编译器将为程序中使用的特定参数类型生成正确的函数定义。
8.7 复习题
8.8 编程练习
第9 章 内存模型和名称空间
- 本章内容包括:
- 单独编译。
- 存储持续性、作用域和链接性。
- 定位(placement)new 运算符。
- 名称空间。
9.1 单独编译
和C 语言一样,C++也允许甚至鼓励程序员将组件函数放在独立的文件中。可以单独编译这些文件,然后将它们链接成可执行的程序。(通常,C++编译器既编译程序,也管理链接器。)如果只修改了一个文件,则可以只重新编译该文件,然后将它与其他文件的编译版本链接。这使得大程序的管理更便捷。
- 可以将原来的程序分成三部分。
- 头文件:包含结构声明和使用这些结构的函数的原型。
- 源代码文件:包含与结构有关的函数的代码。
- 源代码文件:包含调用与结构相关的函数的代码。
- 下面列出了头文件中常包含的内容。
- 函数原型。
- 使用# define 或c onst 定义的符号常量。
- 结构声明。
- 类声明。
- 模板声明。
- 内联函数。
注意,在包含头文件时,我们使用“ coordin.h”,而不是< coodin.h>。如果文件名包含在尖括号中,则C++编译器将在存储标准头文件的主机系统的文件系统中查找;但如果文件名包含在双引号中,则编译器将首先查找当前的工作目录或源代码目录(或其他目录,这取决于编译器)。如果没有在那里找到头文件,则将在标准位置查找。因此在包含自己的头文件时,应使用引号而不是尖括号。
9.2 存储持续性、作用域和链接性
C++使用三种(在C++11 中是四种)不同的方案来存储数据,这些方案的区别就在于数据保留在内存中的时间。
- 自动存储持续性:在函数定义中声明的变量(包括函数参数)的存储持续性为自动的。它们在程序开始执行其所属的函数或代码块时被创建,在执行完函数或代码块时,它们使用的内存被释放。C++有两种存储持续性为自动的变量。
- 静态存储持续性:在函数定义外定义的变量和使用关键字 static 定义的变量的存储持续性都为静态。它们在程序整个运行过程中都存在。C++有3 种存储持续性为静态的变量。
- 线程存储持续性(C++11):当前,多核处理器很常见,这些 CPU 可同时处理多个执行任务。这让程序能够将计算放在可并行处理的不同线程中。如果变量是使用关键字 thread_local 声明的,则其生命周期与所属的线程一样长。
- 动态存储持续性:用n ew 运算符分配的内存将一直存在,直到使用 delete 运算符将其释放或程序结束为止。这种内存的存储持续性为动态,有时被称为自由存储(free store)或堆(heap)。
9.2.1 作用域和链接
作用域(scope)描述了名称在文件(翻译单元)的多大范围内可见。链接性(linkage)描述了名称如何在不同单元间共享。链接性为外部的名称可在文件间共享,链接性为内部的名称只能由一个文件中的函数共享。自动变量的名称没有链接性,因为它们不能共享。不同的 C++存储方式是通过存储持续性、作用域和链接性来描述的。
9.2.2 自动存储持续性
- 在默认情况下,在函数中声明的函数参数和变量的存储持续性为自动,作用域为局部,没有链接性。
- 如果在代码块中定义了变量,则该变量的存在时间和作用域将被限制在该代码块内。
- 有两个同名的变量(一个位于外部代码块中,另一个位于内部代码块中),在这种情况下,程序执行内部代码块中的语句时,将解释为局部代码块变量。新的定义隐藏了(hide)以前的定义,新定义可见,旧定义暂时不可见。在程序离开该代码块时,原来的定义又重新可见。
- 可以使用任何在声明时其值为已知的表达式来初始化自动变量;
- 由于自动变量的数目随函数的开始和结束而增减,因此程序必须在运行时对自动变量进行管理。常用的方法是留出一段内存,并将其视为栈,以管理变量的增减。
- 关键字 register 只是显式地指出变量是自动的。鉴于关键字 register 只能用于原本就是自动的变量,使用它的唯一原因是,指出程序员想使用一个自动变量,这个变量的名称可能与外部变量相同。这与 auto 以前的用途完全相同。
9.2.3 静态持续变量
和C 语言一样,C++也为静态存储持续性变量提供了 3 种链接性:外部链接性(可在其他文件中访问)、内部链接性(只能在当前文件中访问)和无链接性(只能在当前函数或代码块中访问)。这3 种链接性都在整个程序执行期间存在,与自动变量相比,它们的寿命更长。由于静态变量的数目在程序运行期间是不变的,因此程序不需要使用特殊的装置(如栈)来管理它们。编译器将分配固定的内存块来存储所有的静态变量,这些变量在整个程序执行期间一直存在。另外,如果没有显式地初始化静态变量,编译器将把它设置为 0。在默认情况下,静态数组和结构将每个元素或成员的所有位都设置为 0。 要想创建链接性为外部的静态持续变量,必须在代码块的外面声明它;要创建链接性为内部的静态持续变量,必须在代码块的外面声明它,并使用 static 限定符;要创建没有链接性的静态持续变量,必须在代码块内声明它,并使用 static 限定符。 关键字 static 的两种用法,但含义有些不同:用于局部声明,以指出变量是无链接性的静态变量时,static 表示的是存储持续性;而用于代码块外的声明时,static 表示内部链接性,而变量已经是静态持续性了。有人称之为关键字重载,即关键字的含义取决于上下文。 除默认的零初始化外,还可对静态变量进行常量表达式初始化和动态初始化。零初始化和常量表达式初始化被统称为静态初始化,这意味着在编译器处理文件(翻译单元)时初始化变量。动态初始化意味着变量将在编译后初始化。 C++11 新增了关键字 constexpr,这增加了创建常量表达式的方式。
9.2.4 静态持续性、外部链接性
链接性为外部的变量通常简称为外部变量,它们的存储持续性为静态,作用域为整个文件。外部变量是在函数外部定义的,因此对所有函数而言都是外部的。
- 单定义规则
- 一方面,在每个使用外部变量的文件中,都必须声明它;另一方面,C++有“单定义规则”(One Definition Rule,ODR),该规则指出,变量只能有一次定义。为满足这种需求,C++提供了两种变量声明。一种是定义声明(defining declaration)或简称为定义(definition),它给变量分配存储空间;另一种是引用声明(referencing declaration)或简称为声明(declaration),它不给变量分配存储空间,因为它引用已有的变量。 引用声明使用关键字 extern,且不进行初始化;否则,声明为定义,导致分配存储空间
- 单定义规则并非意味着不能有多个变量的名称相同。
- 全局变量和局部变量
- 首先,全局变量很有吸引力——因为所有的函数能访问全局变量,因此不用传递参数。但易于访问的代价很大——程序不可靠。
- 通常情况下,应使用局部变量,应在需要知晓时才传递数据,而不应不加区分地使用全局变量来使数据可用。
- 然而,全局变量也有它们的用处。例如,可以让多个函数可以使用同一个数据块(如月份名数组或原子量数组)。外部存储尤其适于表示常量数据,因为这样可以使用关键字 const 来防止数据被修改。
- 单定义规则
9.2.5 静态持续性、内部链接性
- 将s tatic 限定符用于作用域为整个文件的变量时,该变量的链接性将为内部的。
- 但如果文件定义了一个静态外部变量,其名称与另一个文件中声明的常规外部变量相同,则在该文件中,静态变量将隐藏常规外部变量
- 如果将作用域为整个文件的变量变为静态的,就不必担心其名称与其他文件中的作用域为整个文件的变量发生冲突。
- 在多文件程序中,可以在一个文件(且只能在一个文件)中定义一个外部变量。使用该变量的其他文件必须使用关键字 extern 声明它。
9.2.6 静态存储持续性、无链接性
- 在代码块中使用 static时,将导致局部变量的存储持续性为静态的。这意味着虽然该变量只在该代码块中可用,但它在该代码块不处于活动状态时仍然存在。因此在两次函数调用之间,静态局部变量的值将保持不变。
- 如果初始化了静态局部变量,则程序只在启动时进行一次初始化。以后再调用函数时,将不会像自动变量那样再次被初始化。
9.2.7 说明符和限定符
- 在同一个声明中不能使用多个说明符,但t hread_local 除外,它可与 static 或e xtern 结合使用。下面是存储说明符:
- auto(在C++11 中不再是说明符);
- register;
- static;
- extern;
- thread_local(C++11 新增的);
- mutable。
- 限定符
- 关键字 volatile 表明,即使程序代码没有对内存单元进行修改,其值也可能发生变化。
- 现在回到 mutable。可以用它来指出,即使结构(或类)变量为 const,其某个成员也可以被修改。
- 在C++(但不是在 C 语言)中,const 限定符对默认存储类型稍有影响。在默认情况下全局变量的链接性为外部的,但c onst 全局变量的链接性为内部的。也就是说,在C++看来,全局 const 定义(如下述代码段所示)就像使用了 static 说明符一样。
- 在同一个声明中不能使用多个说明符,但t hread_local 除外,它可与 static 或e xtern 结合使用。下面是存储说明符:
9.2.8 函数和链接性
- 和C 语言一样,C++不允许在一个函数中定义另外一个函数,因此所有函数的存储持续性都自动为静态的,即在整个程序执行期间都一直存在。
- 在默认情况下,函数的链接性为外部的,即可以在文件间共享。实际上,可以在函数原型中使用关键字 extern 来指出函数是在另一个文件中定义的,不过这是可选的(要让程序在另一个文件中查找函数,该文件必须作为程序的组成部分被编译,或者是由链接程序搜索的库文件)。
- 还可以使用关键字 static 将函数的链接性设置为内部的,使之只能在一个文件中使用。必须同时在原型和函数定义中使用该关键字
- 单定义规则也适用于非内联函数,因此对于每个非内联函数,程序只能包含一个定义。对于链接性为外部的函数来说,这意味着在多文件程序中,只能有一个文件(该文件可能是库文件,而不是您提供的)包含该函数的定义,但使用该函数的每个文件都应包含其函数原型。
- 内联函数不受这项规则的约束,这允许程序员能够将内联函数的定义放在头文件中。这样,包含了头文件的每个文件都有内联函数的定义。然而,C++要求同一个函数的所有内联定义都必须相同。
9.2.9 语言链接性
另一种形式的链接性——称为语言链接性(language linking)也对函数有影响。链接程序要求每个不同的函数都有不同的符号名。
9.2.10 存储方案和动态分配
前面介绍 C++用来为变量(包括数组和结构)分配内存的 5 种方案(线程内存除外),它们不适用于使用 C++运算符 new(或C 函数 malloc( ))分配的内存,这种内存被称为动态内存。第4 章介绍过,动态内存由运算符 new 和d elete 控制,而不是由作用域和链接性规则控制。因此,可以在一个函数中分配动态内存,而在另一个函数中将其释放。与自动内存不同,动态内存不是 LIFO,其分配和释放顺序要取决于 new 和d elete 在何时以何种方式被使用。通常,编译器使用三块独立的内存:一块用于静态变量(可能再细分),一块用于自动变量,另外一块用于动态存储。
9.3 名称空间
C++标准提供了名称空间工具,以便更好地控制名称的作用域。
9.3.1 传统的 C++名称空间
- 第一个需要知道的术语是声明区域(declaration region)。声明区域是可以在其中进行声明的区域。
- 第二个需要知道的术语是潜在作用域(potential scope)。变量的潜在作用域从声明点开始,到其声明区域的结尾。因此潜在作用域比声明区域小,这是由于变量必须定义后才能使用。
- 变量并非在其潜在作用域内的任何位置都是可见的,变量对程序而言可见的范围被称为作用域(scope)。
9.3.2 新的名称空间特性
C++新增了这样一种功能,即通过定义一种新的声明区域来创建命名的名称空间,这样做的目的之一是提供一个声明名称的区域。
- 一个名称空间中的名称不会与另外一个名称空间的相同名称发生冲突,同时允许程序的其他部分使用该名称空间中声明的东西。
- 名称空间可以是全局的,也可以位于另一个名称空间中,但不能位于代码块中。因此,在默认情况下,在名称空间中声明的名称的链接性为外部的(除非它引用了常量)。
- 除了用户定义的名称空间外,还存在另一个名称空间——全局名称空间(global namespace)。它对应于文件级声明区域,因此前面所说的全局变量现在被描述为位于全局名称空间中。
- 未被装饰的名称(如p ail)称为未限定的名称(unqualified name);包含名称空间的名称(如J ack::pail)称为限定的名称(qualified name)。
9.3.3 名称空间示例
9.3.4 名称空间及其前途
- 随着程序员逐渐熟悉名称空间,将出现统一的编程理念。下面是当前的一些指导原则。
- 使用在已命名的名称空间中声明的变量,而不是使用外部全局变量。
- 使用在已命名的名称空间中声明的变量,而不是使用静态全局变量。
- 如果开发了一个函数库或类库,将其放在一个名称空间中。事实上,C++当前提倡将标准函数库放在名称空间 std中,这种做法扩展到了来自 C 语言中的函数。例如,头文件 math.h 是与 C 语言兼容的,没有使用名称空间,但C++头文件 cmath 应将各种数学库函数放在名称空间 std中。实际上,并非所有的编译器都完成了这种过渡。
- 仅将编译指令 using 作为一种将旧代码转换为使用名称空间的权宜之计。
- 不要在头文件中使用 using 编译指令。首先,这样做掩盖了要让哪些名称可用;另外,包含头文件的顺序可能影响程序的行为。如果非要使用编译指令 using,应将其放在所有预处理器编译指令# include 之后。
- 导入名称时,首选使用作用域解析运算符或 using 声明的方法。
- 对于 using 声明,首选将其作用域设置为局部而不是全局。
- 随着程序员逐渐熟悉名称空间,将出现统一的编程理念。下面是当前的一些指导原则。
9.4 总结
C++鼓励程序员在开发程序时使用多个文件。一种有效的组织策略是,使用头文件来定义用户类型,为操纵用户类型的函数提供函数原型;并将函数定义放在一个独立的源代码文件中。头文件和源代码文件一起定义和实现了用户定义的类型及其使用方式。最后,将m ain( )和其他使用这些函数的函数放在第三个文件中。 C++的存储方案决定了变量保留在内存中的时间(储存持续性)以及程序的哪一部分可以访问它(作用域和链接性)。自动变量是在代码块(如函数体或函数体中的代码块)中定义的变量,仅当程序执行到包含定义的代码块时,它们才存在,并且可见。自动变量可以通过使用存储类型说明符 register 或根本不使用说明符来声明,没有使用说明符时,变量将默认为自动的。register 说明符提示编译器,该变量的使用频率很高,但C++11 摒弃了这种用法。 静态变量在整个程序执行期间都存在。对于在函数外面定义的变量,其所属文件中位于该变量的定义后面的所有函数都可以使用它(文件作用域),并可在程序的其他文件中使用(外部链接性)。另一个文件要使用这种变量,必须使用 extern 关键字来声明它。对于文件间共享的变量,应在一个文件中包含其定义声明(无需使用 extern,但如果同时进行初始化,也可使用它),并在其他文件中包含引用声明(使用 extern 且不初始化)。在函数的外面使用关键字 static 定义的变量的作用域为整个文件,但是不能用于其他文件(内部链接性)。在代码块中使用关键字 static 定义的变量被限制在该代码块内(局部作用域、无链接性),但在整个程序执行期间,它都一直存在并且保持原值。 在默认情况下,C++函数的链接性为外部,因此可在文件间共享;但使用关键字 static 限定的函数的链接性为内部的,被限制在定义它的文件中。 动态内存分配和释放是使用 new 和d elete 进行的,它使用自由存储区或堆来存储数据。调用 new 占用内存,而调用 delete 释放内存。程序使用指针来跟踪这些内存单元。 名称空间允许定义一个可在其中声明标识符的命名区域。这样做的目的是减少名称冲突,尤其当程序非常大,并使用多个厂商的代码时。可以通过使用作用域解析运算符、using 声明或 using 编译指令,来使名称空间中的标识符可用。
9.5 复习题
9.6 编程练习
第1 0 章 对象和类
- 本章内容包括:
- 过程性编程和面向对象编程。
- 类概念。
- 如何定义和实现类。
- 公有类访问和私有类访问。
- 类的数据成员。
- 类方法(类函数成员)。
- 创建和使用类对象。
- 类的构造函数和析构函数。
- const 成员函数。
- this 指针。
- 创建对象数组。
- 类作用域。
- 抽象数据类型。
10.1 过程性编程和面向对象编程
- 采用过程性编程方法时,首先考虑要遵循的步骤,然后考虑如何表示这些数据(并不需要程序一直运行,用户可能希望能够将数据存储在一个文件中,然后从这个文件中读取数据)。
- 采用 OOP 方法时,首先从用户的角度考虑对象——描述对象所需的数据以及描述用户与数据交互所需的操作。完成对接口的描述后,需要确定如何实现接口和数据存储。最后,使用新的设计方案创建出程序。
10.2 抽象和类
生活中充满复杂性,处理复杂性的方法之一是简化和抽象。在计算中,为了根据信息与用户之间的接口来表示它,抽象是至关重要的。也就是说,将问题的本质特征抽象出来,并根据特征来描述解决方案。
10.2.1 类型是什么
- 指定基本类型完成了三项工作:
- 决定数据对象需要的内存数量;
- 决定如何解释内存中的位(long 和f loat 在内存中占用的位数相同,但将它们转换为数值的方法不同);
- 决定可使用数据对象执行的操作或方法。
对于内置类型来说,有关操作的信息被内置到编译器中。但在 C++中定义用户自定义的类型时,必须自己提供这些信息。付出这些劳动换来了根据实际需要定制新数据类型的强大功能和灵活性。
- 指定基本类型完成了三项工作:
10.2.2 C++中的类
类是一种将抽象转换为用户定义类型的 C++工具,它将数据表示和操纵数据的方法组合成一个整洁的包。
- 接下来定义类。一般来说,类规范由两个部分组成。
- 类声明:以数据成员的方式描述数据部分,以成员函数(被称为方法)的方式描述公有接口。
- 类方法定义:描述如何实现类成员函数。
- 简单地说,类声明提供了类的蓝图,而方法定义则提供了细节。
- 关键字 private 和p ublic 也是新的,它们描述了对类成员的访问控制。使用类对象的程序都可以直接访问公有部分,但只能通过公有成员函数(或友元函数,参见第 11章)来访问对象的私有成员。因此,公有成员函数是程序和对象的私有成员之间的桥梁,提供了对象和程序之间的接口。防止程序直接访问数据被称为数据隐藏(参见图 10.1)。C++还提供了第三个访问控制关键字 protected
- 无论类成员是数据成员还是成员函数,都可以在类的公有部分或私有部分中声明它。但由于隐藏数据是 OOP 主要的目标之一,因此数据项通常放在私有部分,组成类接口的成员函数放在公有部分;否则,就无法从程序中调用这些函数。
- 类描述看上去很像是包含成员函数以及 public 和p rivate 可见性标签的结构声明。实际上,C++对结构进行了扩展,使之具有与类相同的特性。它们之间唯一的区别是,结构的默认访问类型是 public,而类为 private。C++程序员通常使用类来实现类描述,而把结构限制为只表示纯粹的数据对象(常被称为普通老式数据(POD,Plain Old Data)结构)。
- 接下来定义类。一般来说,类规范由两个部分组成。
10.2.3 实现类成员函数
- 成员函数定义与常规函数定义非常相似,它们有函数头和函数体,也可以有返回类型和参数。但是它们还有两个特殊的特征:
- 定义成员函数时,使用作用域解析运算符(::)来标识函数所属的类;
- 类方法可以访问类的 private 组件。
- 成员函数定义与常规函数定义非常相似,它们有函数头和函数体,也可以有返回类型和参数。但是它们还有两个特殊的特征:
10.2.4 使用类
10.2.5 修改实现
10.2.6 小结
指定类设计的第一步是提供类声明。类声明类似结构声明,可以包括数据成员和函数成员。声明有私有部分,在其中声明的成员只能通过成员函数进行访问;声明还具有公有部分,在其中声明的成员可被使用类对象的程序直接访问。通常,数据成员被放在私有部分中,成员函数被放在公有部分中,因此典型的类声明的格式如下:
10.3 类的构造函数和析构函数
C++提供了一个特殊的成员函数——类构造函数,专门用于构造新对象、将值赋给它们的数据成员。更准确地说,C++为这些成员函数提供了名称和使用语法,而程序员需要提供方法定义。
10.3.1 声明和定义构造函数
名称与类名相同。构造函数没有声明类型。
10.3.2 使用构造函数
- C++提供了两种使用构造函数来初始化对象的方式。
- 第一种方式是显式地调用构造函数:
Stock garment = Stock("Furry Mason", 50, 2.5);
- 另一种方式是隐式地调用构造函数格式更紧凑:
Stock garment("Furry Mason", 50, 2.5);
- 第一种方式是显式地调用构造函数:
- 将构造函数与 new 一起使用的方法:
Stock * garment = new Stock("Furry Mason", 50, 2.5);
- C++提供了两种使用构造函数来初始化对象的方式。
10.3.3 默认构造函数
- 默认构造函数是在未提供显式初始值时,用来创建对象的构造函数。
- 默认构造函数没有参数,因为声明中不包含值。
- 当且仅当没有定义任何构造函数时,编译器才会提供默认构造函数。
- 为类定义了构造函数后,程序员就必须为它提供默认构造函数。
- 定义默认构造函数的方式有两种。由于只能有一个默认构造函数,因此不要同时采用这两种方式。
- 一种是给已有构造函数的所有参数提供默认值:
Stock(const string & co = "Error", int n = 0, double pr = 0.0);
- 另一种方式是通过函数重载来定义另一个构造函数——一个没有参数的构造函数:
Stock();
- 一种是给已有构造函数的所有参数提供默认值:
10.3.4 析构函数
用构造函数创建对象后,程序负责跟踪该对象,直到其过期为止。对象过期时,程序将自动调用一个特殊的成员函数,该函数的名称令人生畏——析构函数。析构函数完成清理工作,因此实际上很有用。和构造函数一样,析构函数的名称也很特殊:在类名前加上~。另外,和构造函数一样,析构函数也可以没有返回值和声明类型。与构造函数不同的是,析构函数没有参数,因此 Stock 析构函数的原型必须是这样的:
~Stock();
- 什么时候应调用析构函数呢?这由编译器决定,通常不应在代码中显式地调用析构函数。
- 如果创建的是静态存储类对象,则其析构函数将在程序结束时自动被调用。
- 如果创建的是自动存储类对象(就像前面的示例中那样),则其析构函数将在程序执行完代码块时(该对象是在其中定义的)自动被调用。
- 如果对象是通过 new 创建的,则它将驻留在栈内存或自由存储区中,当使用 delete 来释放内存时,其析构函数将自动被调用。
- 最后,程序可以创建临时对象来完成特定的操作,在这种情况下,程序将在结束对该对象的使用时自动调用其析构函数。
由于在类对象过期时析构函数将自动被调用,因此必须有一个析构函数。如果程序员没有提供析构函数,编译器将隐式地声明一个默认析构函数,并在发现导致对象被删除的代码后,提供默认析构函数的定义。
- 什么时候应调用析构函数呢?这由编译器决定,通常不应在代码中显式地调用析构函数。
10.3.5 改进 Stock 类
- 在默认情况下,将一个对象赋给同类型的另一个对象时,C++将源对象的每个数据成员的内容复制到目标对象中相应的数据成员中。
- 如果既可以通过初始化,也可以通过赋值来设置对象的值,则应采用初始化方式。通常这种方式的效率更高。
- 在C++11中,可将列表初始化语法用于类吗?可以,只要提供与某个构造函数的参数列表匹配的内容,并用大括号将它们括起
- 就像应尽可能将 const 引用和指针用作函数形参一样,只要类方法不修改调用对象,就应将其声明为 const。从现在开始,我们将遵守这一规则。
- 声明和定义的类函数被称为 const 成员函数:
声明
void show() const;
定义void stock::show() const {...}
10.3.6 构造函数和析构函数小结
- 构造函数是一种特殊的类成员函数,在创建类对象时被调用。
- 构造函数的名称和类名相同,但通过函数重载,可以创建多个同名的构造函数,条件是每个函数的特征标(参数列表)都不同。另外,构造函数没有声明类型。
- 通常,构造函数用于初始化类对象的成员,初始化应与构造函数的参数列表匹配。
- 接受一个参数的构造函数允许使用赋值语法将对象初始化为一个值:
ClassName object = value;
- 默认构造函数没有参数,因此如果创建对象时没有进行显式地初始化,则将调用默认构造函数。
- 如果程序中没有提供任何构造函数,则编译器会为程序定义一个默认构造函数;否则,必须自己提供默认构造函数。
- 默认构造函数可以没有任何参数;如果有,则必须给所有参数都提供默认值
- 每个类都只能有一个析构函数。
- 析构函数没有返回类型(连v oid 都没有),也没有参数,其名称为类名称前加上~。
- 如果构造函数使用了 new,则必须提供使用 delete 的析构函数。
10.4 this 指针
this 指针指向用来调用成员函数的对象(this 被作为隐藏参数传递给方法)。每个成员函数(包括构造函数和析构函数)都有一个 this 指针。this 指针指向调用对象。如果方法需要引用整个调用对象,则可以使用表达式* this。在函数的括号后面使用 const 限定符将 this 限定为 const,这样将不能使用 this 来修改对象的值。 this 是对象的地址,,即* this(将解除引用运算符*用于指针,将得到指针指向的值)。可以将* this 作为调用对象的别名。
10.5 对象数组
声明对象数组的方法与声明标准类型数组相同
10.6 类作用域
在类中定义的名称(如类数据成员名和类成员函数名)的作用域都为整个类,作用域为整个类的名称只在该类中是已知的,在类外是不可知的。因此,可以在不同类中使用相同的类成员名而不会引起冲突。另外,类作用域意味着不能从外部直接访问类的成员,公有成员函数也是如此。也就是说,要调用公有成员函数,必须通过对象。 总之,在类声明或成员函数定义中,可以使用未修饰的成员名称(未限定的名称)。构造函数名称在被调用时,才能被识别,因为它的名称与类名相同。在其他情况下,使用类成员名时,必须根据上下文使用直接成员运算符(.)、间接成员运算符(->)或作用域解析运算符(::)。
10.6.1 作用域为类的常量
有时候,使符号常量的作用域为类很有用。由于该常量对于所有对象来说都是相同的,因此创建一个由所有对象共享的常量是个不错的主意。有两种方式可以实现这个目标,并且效果相同。
- 第一种方式是在类中声明一个枚举。在类声明中声明的枚举的作用域为整个类,因此可以用枚举为整型常量提供作用域为整个类的符号名称。
- C++提供了另一种在类中定义常量的方式——使用关键字 static
10.6.2 作用域内枚举(C++11)
C++11 提供了一种新枚举,其枚举量的作用域为类。枚举量的作用域为类后,不同枚举定义中的枚举量就不会发生名称冲突了。
10.7 抽象数据类型
程序员常常通过定义类来表示更通用的概念。例如,就实现计算机专家们所说的抽象数据类型(abstract data type,ADT)而言,使用类是一种非常好的方式。顾名思义,ADT 以通用的方式描述数据类型,而没有引入语言或实现细节。
10.8 总结
面向对象编程强调的是程序如何表示数据。使用 OOP 方法解决编程问题的第一步是根据它与程序之间的接口来描述数据,从而指定如何使用数据。然后,设计一个类来实现该接口。一般来说,私有数据成员存储信息,公有成员函数(又称为方法)提供访问数据的唯一途径。类将数据和方法组合成一个单元,其私有性实现数据隐藏。 通常,将类声明分成两部分组成,这两部分通常保存在不同的文件中。类声明(包括由函数原型表示的方法)应放到头文件中。定义成员函数的源代码放在方法文件中。这样便将接口描述与实现细节分开了。从理论上说,只需知道公有接口就可以使用类。当然,可以查看实现方法(除非只提供了编译形式),但程序不应依赖于其实现细节,如知道某个值被存储为 int。只要程序和类只通过定义接口的方法进行通信,程序员就可以随意地对任何部分做独立的改进,而不必担心这样做会导致意外的不良影响。 类是用户定义的类型,对象是类的实例。这意味着对象是这种类型的变量,例如由 new 按类描述分配的内存。C++试图让用户定义的类型尽可能与标准类型类似,因此可以声明对象、指向对象的指针和对象数组。可以按值传递对象、将对象作为函数返回值、将一个对象赋给同类型的另一个对象。如果提供了构造函数,则在创建对象时,可以初始化对象。如果提供了析构函数方法,则在对象消亡后,程序将执行该函数。 每个对象都存储自己的数据,而共享类方法。如果 mr_object 是对象名,try_me( )是成员函数,则可以使用成员运算符句点调用成员函数:mr_object.try_me( )。在O OP中,这种函数调用被称为将 try_me 消息发送给 mr_object 对象。在t ry_me( )方法中引用类数据成员时,将使用 mr_object 对象相应的数据成员。同样,函数调用 i_object.try_me( )将访问 i_object 对象的数据成员。 如果希望成员函数对多个对象进行操作,可以将额外的对象作为参数传递给它。如果方法需要显式地引用调用它的对象,则可以使用 this 指针。由于 this 指针被设置为调用对象的地址,因此* this 是该对象的别名。 类很适合用于描述 ADT。公有成员函数接口提供了 ADT 描述的服务,类的私有部分和类方法的代码提供了实现,这些实现对类的客户隐藏。
10.9 复习题
10.10 编程练习
第1 1 章 使用类
- 本章内容包括:
- 运算符重载。
- 友元函数。
- 重载<<运算符,以便用于输出。
- 状态成员。
- 使用 rand( )生成随机值。
- 类的自动转换和强制类型转换。
- 类转换函数。
11.1 运算符重载
运算符重载是一种形式的 C++多态。要重载运算符,需使用被称为运算符函数的特殊函数形式。运算符函数的格式如下:
operator op(argument-list)
11.2 计算时间:一个运算符重载示例
11.2.1 添加加法运算符
11.2.2 重载限制
- 重载后的运算符必须至少有一个操作数是用户定义的类型,这将防止用户为标准类型重载运算符。
- 使用运算符时不能违反运算符原来的句法规则。同样,不能修改运算符的优先级。
- 不能创建新运算符。
- 不能重载下面的运算符。
sizeof
sizeof 运算符。.
成员运算符。. *
成员指针运算符。::
作用域解析运算符。?:
条件运算符。typeid
一个 RTTI 运算符。const_cast
强制类型转换运算符。dynamic_cast
强制类型转换运算符。reinterpret_cast
强制类型转换运算符。static_cast
强制类型转换运算符。
- 大多数运算符都可以通过成员或非成员函数进行重载,但下面的运算符只能通过成员函数进行重载。
=
赋值运算符。( )
函数调用运算符。[ ]
下标运算符。->
通过指针访问类成员的运算符。
可重载的运算符
+ - * / % ^ & | ~= ! = < > += -= *= /= %= ^= &= |= << >> >>= <<= == != <= >= && || ++ – , ->* -> () [] new delete new [] delete []
11.2.3 其他重载运算符
11.3 友元
您知道,C++控制对类对象私有部分的访问。通常,公有类方法提供唯一的访问途径,但是有时候这种限制太严格,以致于不适合特定的编程问题。在这种情况下,C++提供了另外一种形式的访问权限:友元。
友元有 3种:
- 友元函数;
- 友元类;
- 友元成员函数。
11.3.1 创建友元
- 创建友元函数的第一步是将其原型放在类声明中,并在原型声明前加上关键字 friend
- 虽然 operator *( )函数是在类声明中声明的,但它不是成员函数,因此不能使用成员运算符来调用;
- 虽然 operator *( )函数不是成员函数,但它与成员函数的访问权限相同。
- 第二步是编写函数定义。因为它不是成员函数,所以不要使用 Time::限定符。另外,不要在定义中使用关键字 friend,定义应该如下
- 创建友元函数的第一步是将其原型放在类声明中,并在原型声明前加上关键字 friend
11.3.2 常用的友元:重载<<运算符
11.4 重载运算符:作为成员函数还是非成员函数
非成员版本的重载运算符函数所需的形参数目与运算符使用的操作数数目相同;而成员版本所需的参数数目少一个,因为其中的一个操作数是被隐式地传递的调用对象。 那么哪种格式最好呢?对于某些运算符来说(如前所述),成员函数是唯一合法的选择。在其他情况下,这两种格式没有太大的区别。有时,根据类设计,使用非成员函数版本可能更好(尤其是为类定义类型转换时)。
11.5 再谈重载:一个矢量类
11.5.1 使用状态成员
11.5.2 为V ector 类重载算术运算符
11.5.3 对实现的说明
11.5.4 使用 Vector 类来模拟随机漫步
11.6 类的自动转换和强制类型转换
只接受一个参数的构造函数定义了从参数类型到类类型的转换。如果使用关键字 explicit 限定了这种构造函数,则它只能用于显示转换,否则也可以用于隐式转换。 编译器在什么时候将使用 Stonewt(double)函数呢?如果在声明中使用了关键字 explicit,则S tonewt(double)将只用于显式强制类型转换,否则还可以用于下面的隐式转换。
在下述任意一种情况下,当且仅当转换不存在二义性时,使用可转换为 double 类型的内置类型时。
- 将S tonewt 对象初始化为 double 值时。
- 将d ouble 值赋给 Stonewt 对象时。
- 将d ouble 值传递给接受 Stonewt 参数的函数时。
- 返回值被声明为 Stonewt 的函数试图返回 double 值时。
11.6.1 转换函数
构造函数只用于从某种类型到类类型的转换。要进行相反的转换,必须使用特殊的 C++运算符函数——转换函数。转换函数是用户定义的强制类型转换,可以像使用强制类型转换那样使用它们。 要转换为 typeName 类型,需要使用这种形式的转换函数:
operator rypeName();
- 请注意以下几点:
- 转换函数必须是类方法;
- 转换函数不能指定返回类型;
- 转换函数不能有参数。
- 应谨慎地使用隐式转换函数。通常,最好选择仅在被显式地调用时才会执行的函数。
- 总之,C++为类提供了下面的类型转换。
- 只有一个参数的类构造函数用于将类型与该参数相同的值转换为类类型。例如,将i nt 值赋给 Stonewt 对象时,接受 int 参数的 Stonewt 类构造函数将自动被调用。然而,在构造函数声明中使用 explicit 可防止隐式转换,而只允许显式转换。
- 被称为转换函数的特殊类成员运算符函数,用于将类对象转换为其他类型。转换函数是类成员,没有返回类型、没有参数、名为 operator typeName( ),其中,typeName 是对象将被转换成的类型。将类对象赋给 typeName 变量或将其强制转换为 typeName 类型时,该转换函数将自动被调用。
- 请注意以下几点:
11.6.2 转换函数和友元函数
将加法定义为友元可以让程序更容易适应自动类型转换。原因在于,两个操作数都成为函数参数,因此与函数原型匹配。
- 要将 double 量和 Stonewt 量相加,有两种选择。
- 第一种方法是(刚介绍过)将下面的函数定义为友元函数,让S tonewt(double)构造函数将 double 类型的参数转换为 Stonewt 类型的参数:
operator+(const Stonewt &, const Stonewt &)
- 第二种方法是,将加法运算符重载为一个显式使用 double 类型参数的函数:
Stonewt operator+(double x, Stonewt & s);
friend Stonewt operator+(double x, Stonewt & s)
- 第一种方法是(刚介绍过)将下面的函数定义为友元函数,让S tonewt(double)构造函数将 double 类型的参数转换为 Stonewt 类型的参数:
- 每一种方法都有其优点。如果程序经常需要将 double 值与 Stonewt 对象相加,则重载加法更合适;如果程序只是偶尔使用这种加法,则依赖于自动转换更简单,但为了更保险,可以使用显式转换。
- 第一种方法(依赖于隐式转换)使程序更简短,因为定义的函数较少。这也意味程序员需要完成的工作较少,出错的机会较小。这种方法的缺点是,每次需要转换时,都将调用转换构造函数,这增加时间和内存开销。
- 第二种方法(增加一个显式地匹配类型的函数)则正好相反。它使程序较长,程序员需要完成的工作更多,但运行速度较快。
- 要将 double 量和 Stonewt 量相加,有两种选择。
11.7 总结
本章介绍了定义和使用类的许多重要方面,其中的一些内容可能较难理解,但随着实践经验的不断增加,您将逐渐掌握它们。
一般来说,访问私有类成员的唯一方法是使用类方法。C++使用友元函数来避开这种限制。要让函数成为友元,需要在类声明中声明该函数,并在声明前加上关键字 friend。
C++扩展了对运算符的重载,允许自定义特殊的运算符函数,这种函数描述了特定的运算符与类之间的关系。运算符函数可以是类成员函数,也可以是友元函数(有一些运算符函数只能是类成员函数)。要调用运算符函数,可以直接调用该函数,也可以以通常的句法使用被重载的运算符。对于运算符 op,其运算符函数的格式如下: operator op(argument-list)
argument-list 表示该运算符的操作数。如果运算符函数是类成员函数,则第一个操作数是调用对象,它不在 argument-list中。例如,本章通过为 Vector 类定义 operator +( )成员函数重载了加法。如果 up、right 和r esult 都是 Vector 对象,则可以使用下面的任何一条语句来调用矢量加法:
result = up.operator+(right);
result = up + right;
在第二条语句中,由于操作数 up 和r ight 的类型都是 Vector,因此 C++将使用 Vector 的加法定义。
当运算符函数是成员函数时,则第一个操作数将是调用该函数的对象。例如,在前面的语句中,up 对象是调用函数的对象。定义运算符函数时,如果要使其第一个操作数不是类对象,则必须使用友元函数。这样就可以将操作数按所需的顺序传递给函数了。
最常见的运算符重载任务之一是定义<<运算符,使之可与 cout 一起使用,来显示对象的内容。要让 ostream 对象成为第一个操作数,需要将运算符函数定义为友元;要使重新定义的运算符能与其自身拼接,需要将返回类型声明为 ostream &。下面的通用格式能够满足这种要求:
|
|
然而,如果类包含这样的方法,它返回需要显示的数据成员的值,则可以使用这些方法,无需在 operator<<( )中直接访问这些成员。在这种情况下,函数不必(也不应当)是友元。
C++允许指定在类和基本类型之间进行转换的方式。首先,任何接受唯一一个参数的构造函数都可被用作转换函数,将类型与该参数相同的值转换为类。如果将类型与该参数相同的值赋给对象,则C++将自动调用该构造函数。例如,假设有一个 String类,它包含一个将 char *值作为其唯一参数的构造函数,那么如果 bean 是S tring 对象,则可以使用下面的语句: bean = "pinto";
然而,如果在该构造函数的声明前加上了关键字 explicit,则该构造函数将只能用于显式转换: bean = String("pinto")
要将类对象转换为其他类型,必须定义转换函数,指出如何进行这种转换。转换函数必须是成员函数。将类对象转换为 typeName 类型的转换函数的原型如下: operator typeName();
注意,转换函数没有返回类型、没有参数,但必须返回转换后的值(虽然没有声明返回类型)。例如,下面是将 Vector 转换为 double 类型的函数:
|
|
经验表明,最好不要依赖于这种隐式转换函数。 您可能已经注意到了,与简单的 C-风格结构相比,使用类时,必须更谨慎、更小心,但作为补偿,它们为我们完成的工作也更多。
11.8 复习题
11.9 编程练习
第1 2 章 类和动态内存分配
- 本章内容包括:
- 对类成员使用动态内存分配。
- 隐式和显式复制构造函数。
- 隐式和显式重载赋值运算符。
- 在构造函数中使用 new 所必须完成的工作。
- 使用静态类成员。
- 将定位 new 运算符用于对象。
- 使用指向对象的指针。
- 实现队列抽象数据类型(ADT)。
12.1 动态内存和类
让程序在运行时决定内存分配,而不是在编译时决定。这样,可根据程序的需要,而不是根据一系列严格的存储类型规则来使用内存。C++使用 new 和d elete 运算符来动态控制内存。
12.1.1 复习示例和静态类成员
- 不能在类声明中初始化静态成员变量,这是因为声明描述了如何分配内存,但并不分配内存。
- 对于静态类成员,可以在类声明之外使用单独的语句来进行初始化,这是因为静态类成员是单独存储的,而不是对象的组成部分。
- 初始化语句指出了类型,并使用了作用域运算符,但没有使用关键字 static。
- 初始化是在方法文件中,而不是在类声明文件中进行的,这是因为类声明位于头文件中,程序可能将头文件包括在其他几个文件中。如果在头文件中进行初始化,将出现多个初始化语句副本,从而引发错误。
- 静态数据成员在类声明中声明,在包含类方法的文件中初始化。初始化时使用作用域运算符来指出静态成员所属的类。但如果静态成员是整型或枚举型 const,则可以在类声明中初始化。
12.1.2 特殊成员函数
- C++自动提供了下面这些成员函数:
- 默认构造函数,如果没有定义构造函数;
- 如果没有提供任何构造函数,C++将创建默认构造函数。也就是说,编译器将提供一个不接受任何参数,也不执行任何操作的构造函数(默认的默认构造函数),这是因为创建对象时总是会调用构造函数
- 如果定义了构造函数,C++将不会定义默认构造函数。如果希望在创建对象时不显式地对它进行初始化,则必须显式地定义默认构造函数。这种构造函数没有任何参数,但可以使用它来设置特定的值
- 带参数的构造函数也可以是默认构造函数,只要所有参数都有默认值。
- 只能有一个默认构造函数。
- 默认析构函数,如果没有定义;
- 复制构造函数,如果没有定义;
- 赋值运算符,如果没有定义;
- 地址运算符,如果没有定义。
- 默认构造函数,如果没有定义构造函数;
- C++自动提供了下面这些成员函数:
12.1.3 回到 Stringbad:复制构造函数的哪里出了问题
- 复制构造函数用于将一个对象复制到新创建的对象中。也就是说,它用于初始化过程中(包括按值传递参数),而不是常规的赋值过程中。
- 类的复制构造函数原型通常如下:
Class_name(const Class_name &);
- 由于按值传递对象将调用复制构造函数,因此应该按引用传递对象。这样可以节省调用构造函数的时间以及存储新对象的空间。
- 默认的复制构造函数逐个复制非静态成员(成员复制也称为浅复制),复制的是成员的值。如果成员本身就是类对象,则将使用这个类的复制构造函数来复制成员对象。静态函数(如n um_strings)不受影响,因为它们属于整个类,而不是各个对象。
12.1.4 Stringbad 的其他问题:赋值运算符
- 与复制构造函数相似,赋值运算符的隐式实现也对成员进行逐个复制。如果成员本身就是类对象,则程序将使用为这个类定义的赋值运算符来复制该成员,但静态数据成员不受影响。
- 提供赋值运算符(进行深度复制)定义。其实现与复制构造函数相似,但也有一些差别。
- 由于目标对象可能引用了以前分配的数据,所以函数应使用 delete[ ]来释放这些数据。
- 函数应当避免将对象赋给自身;否则,给对象重新赋值前,释放内存操作可能删除对象的内容。
- 函数返回一个指向调用对象的引用。
12.2 改进后的新 String 类
12.2.1 修订后的默认构造函数
12.2.2 比较成员函数
12.2.3 使用中括号表示法访问字符
12.2.4 静态类成员函数
- 函数声明必须包含关键字 static,但如果函数定义是独立的,则其中不能包含关键字 static
- 首先,不能通过对象调用静态成员函数;实际上,静态成员函数甚至不能使用 this 指针。如果静态成员函数是在公有部分声明的,则可以使用类名和作用域解析运算符来调用它。
- 其次,由于静态成员函数不与特定的对象相关联,因此只能使用静态数据成员。
- 可以使用静态成员函数设置类级(classwide)标记,以控制某些类接口的行为。
12.2.5 进一步重载赋值运算符
12.3 在构造函数中使用 new 时应注意的事项
您知道使用 new 初始化对象的指针成员时必须特别小心。具体地说,应当这样做。
- 如果在构造函数中使用 new 来初始化指针成员,则应在析构函数中使用 delete。
- new 和d elete 必须相互兼容。new 对应于 delete,new[ ]对应于 delete[ ]。
- 如果有多个构造函数,则必须以相同的方式使用 new,要么都带中括号,要么都不带。因为只有一个析构函数,所有的构造函数都必须与它兼容。然而,可以在一个构造函数中使用 new 初始化指针,而在另一个构造函数中将指针初始化为空(0 或C++11 中的 nullptr),这是因为 delete(无论是带中括号还是不带中括号)可以用于空指针。 NULL、0 还是 nullptr:以前,空指针可以用 0 或N ULL(在很多头文件中,NULL 是一个被定义为 0 的符号常量)来表示。C 程序员通常使用 NULL 而不是 0,以指出这是一个指针,就像使用‘\0’而不是 0 来表示空字符,以指出这是一个字符一样。然而,C++传统上更喜欢用简单的 0,而不是等价的 NULL。但正如前面指出的,C++11 提供了关键字 nullptr,这是一种更好的选择。
- 应定义一个复制构造函数,通过深度复制将一个对象初始化为另一个对象。通常,这种构造函数与下面类似。 具体地说,复制构造函数应分配足够的空间来存储复制的数据,并复制数据,而不仅仅是数据的地址。另外,还应该更新所有受影响的静态类成员。
- 应当定义一个赋值运算符,通过深度复制将一个对象复制给另一个对象。 检查自我赋值的情况,释放成员指针以前指向的内存,复制数据而不仅仅是数据的地址,并返回一个指向调用对象的引用。 -
12.3.1 应该和不应该
12.3.2 包含类成员的类的逐成员复制
12.4 有关返回对象的说明
12.4.1 返回指向 const 对象的引用
12.4.2 返回指向非 const 对象的引用
12.4.3 返回对象
12.4.4 返回 const 对象
12.5 使用指向对象的指针
12.5.1 再谈 new 和d elete
12.5.2 指针和对象小结
12.5.3 再谈定位 new 运算符
12.6 复习各种技术
12.6.1 重载<<运算符
要重新定义 << 运算符,以便将它和 cout 一起用来显示对象的内容,请定义下面的友元运算符函数:
1 2 3 4 5
ostream & operator<<(ostream & os, const c_name & obj) { os << ...; return os; }
其中 c_name 是类名。如果该类提供了能够返回所需内容的公有方法,则可在运算符函数中使用这些方法,这样便不用将它们设置为友元函数了。
12.6.2 转换函数
要将单个值转换为类类型,需要创建原型如下所示的类构造函数:
c_name(type_name value)
其中 c_name 为类名,type_name 是要转换的类型的名称。 要将类转换为其他类型,需要创建原型如下所示的类成员函数:operator type_name();
虽然该函数没有声明返回类型,但应返回所需类型的值。 使用转换函数时要小心。可以在声明构造函数时使用关键字 explicit,以防止它被用于隐式转换。12.6.3 其构造函数使用 new 的类
如果类使用 new 运算符来分配类成员指向的内存,在设计时应采取一些预防措施。
前面总结了这些预防措施,应牢记这些规则,这是因为编译器并不知道这些规则,因此无法发现错误
- 对于指向的内存是由 new 分配的所有类成员,都应在类的析构函数中对其使用 delete,该运算符将释放分配的内存。
- 如果析构函数通过对指针类成员使用 delete 来释放内存,则每个构造函数都应当使用 new 来初始化指针,或将它设置为空指针。
- 构造函数中要么使用 new [],要么使用 new,而不能混用。如果构造函数使用的是 new[],则析构函数应使用 delete [];如果构造函数使用的是 new,则析构函数应使用 delete。
- 应定义一个分配内存(而不是将指针指向已有内存)的复制构造函数。这样程序将能够将类对象初始化为另一个类对象。这种构造函数的原型通常如下:
className(const className &)
应定义一个重载赋值运算符的类成员函数,其函数定义如下(其中 c_pointer 是c_name 的类成员,类型为指向 type_name 的指针)。下面的示例假设使用 new []来初始化变量 c_pointer:
1 2 3 4 5 6 7 8 9 10 11
c_name & c_name::operator=(const c_name & cn) { if (this == &cn) { return *this; } delete [] c_pointer; c_pointer = new type_name[size]; ... return *this; }
12.7 队列模拟
12.7.1 队列类
12.7.2 Customer 类
12.7.3 ATM 模拟
12.8 总结
本章介绍了定义和使用类的许多重要方面。其中的一些方面是非常微妙甚至很难理解的概念。如果其中的某些概念对于您来说过于复杂,也不用害怕——这些问题对于大多数 C++的初学者来说都是很难的。通常,对于诸如复制构造函数等概念,都是在由于忽略它们而遇到了麻烦后逐步理解的。本章介绍的一些内容乍看起来非常难以理解,但是随着经验越来越丰富,对其理解也将越透彻。
在类构造函数中,可以使用 new 为数据分配内存,然后将内存地址赋给类成员。这样,类便可以处理长度不同的字符串,而不用在类设计时提前固定数组的长度。在类构造函数中使用 new,也可能在对象过期时引发问题。如果对象包含成员指针,同时它指向的内存是由 new 分配的,则释放用于保存对象的内存并不会自动释放对象成员指针指向的内存。因此在类构造函数中使用 new 类来分配内存时,应在类析构函数中使用 delete 来释放分配的内存。这样,当对象过期时,将自动释放其指针成员指向的内存。
如果对象包含指向 new 分配的内存的指针成员,则将一个对象初始化为另一个对象,或将一个对象赋给另一个对象时,也会出现问题。在默认情况下,C++逐个对成员进行初始化和赋值,这意味着被初始化或被赋值的对象的成员将与原始对象完全相同。如果原始对象的成员指向一个数据块,则副本成员将指向同一个数据块。当程序最终删除这两个对象时,类的析构函数将试图删除同一个内存数据块两次,这将出错。解决方法是:定义一个特殊的复制构造函数来重新定义初始化,并重载赋值运算符。在上述任何一种情况下,新的定义都将创建指向数据的副本,并使新对象指向这些副本。这样,旧对象和新对象都将引用独立的、相同的数据,而不会重叠。由于同样的原因,必须定义赋值运算符。对于每一种情况,最终目的都是执行深度复制,也就是说,复制实际的数据,而不仅仅是复制指向数据的指针。
对象的存储持续性为自动或外部时,在它不再存在时将自动调用其析构函数。如果使用 new 运算符为对象分配内存,并将其地址赋给一个指针,则当您将 delete 用于该指针时将自动为对象调用析构函数。然而,如果使用定位 new 运算符(而不是常规 new 运算符)为类对象分配内存,则必须负责显式地为该对象调用析构函数,方法是使用指向该对象的指针调用析构函数方法。C++允许在类中包含结构、类和枚举定义。这些嵌套类型的作用域为整个类,这意味着它们被局限于类中,不会与其他地方定义的同名结构、类和枚举发生冲突。
C++为类构造函数提供了一种可用来初始化数据成员的特殊语法。这种语法包括冒号和由逗号分隔的初始化列表,被放在构造函数参数的右括号后,函数体的左括号之前。每一个初始化器都由被初始化的成员的名称和包含初始值的括号组成。从概念上来说,这些初始化操作是在对象创建时进行的,此时函数体中的语句还没有执行。语法如下: queue(int qs) : qsize(qs), items(0), front(NULL), rear(NULL) { }
如果数据成员是非静态 const 成员或引用,则必须采用这种格式,但可将 C++11 新增的类内初始化用于非静态 const 成员。
C++11 允许类内初始化,即在类定义中进行初始化:
|
|
这与使用成员初始化列表等价。然而,使用成员初始化列表的构造函数将覆盖相应的类内初始化。 您可能已经注意到,与简单的 C 结构相比,需要注意的类细节要多得多。作为回报,它们的功能也更强。
12.9 复习题
12.10 编程练习
第1 3 章 类继承
- 本章内容包括:
- is-a 关系的继承。
- 如何以公有方式从一个类派生出另一个类。
- 保护访问。
- 构造函数成员初始化列表。
- 向上和向下强制转换。
- 虚成员函数。
- 早期(静态)联编与晚期(动态)联编。
- 抽象基类。
- 纯虚函数。
- 何时及如何使用公有继承。
13.1 一个简单的基类
13.1.1 派生一个类
- 派生类对象将具有以下特征:
- 派生类对象存储了基类的数据成员(派生类继承了基类的实现);
- 派生类对象可以使用基类的方法(派生类继承了基类的接口)。
- 需要在继承特性中添加什么呢?
- 派生类需要自己的构造函数。
- 派生类可以根据需要添加额外的数据成员和成员函数。
- 派生类对象将具有以下特征:
13.1.2 构造函数:访问权限的考虑
派生类不能直接访问基类的私有成员,而必须通过基类方法进行访问。具体地说,派生类构造函数必须使用基类构造函数。 创建派生类对象时,程序首先创建基类对象。从概念上说,这意味着基类对象应当在程序进入派生类构造函数之前被创建。C++使用成员初始化列表语法来完成这种工作。
- 有关派生类构造函数的要点如下:
- 首先创建基类对象;
- 派生类构造函数应通过成员初始化列表将基类信息传递给基类构造函数;
- 派生类构造函数应初始化派生类新增的数据成员。
- 有关派生类构造函数的要点如下:
13.1.3 使用派生类
要使用派生类,程序必须要能够访问基类声明。
13.1.4 派生类和基类之间的特殊关系
- 派生类与基类之间有一些特殊关系。
- 其中之一是派生类对象可以使用基类的方法,条件是方法不是私有的
- 基类指针可以在不进行显式类型转换的情况下指向派生类对象;
- 基类引用可以在不进行显式类型转换的情况下引用派生类对象
- 派生类与基类之间有一些特殊关系。
13.2 继承:is-a 关系
派生类和基类之间的特殊关系是基于 C++继承的底层模型的。实际上,C++有3 种继承方式:公有继承、保护继承和私有继承。公有继承是最常用的方式,它建立一种 is-a 关系,即派生类对象也是一个基类对象,可以对基类对象执行的任何操作,也可以对派生类对象执行。在C++中,完全可以使用公有继承来建立 has-a、is-implemented-as-a 或u ses-a 关系;然而,这样做通常会导致编程方面的问题。因此,还是坚持使用 is-a 关系吧。
13.3 多态公有继承
同一个方法在派生类和基类中的行为是不同的。换句话来说,方法的行为应取决于调用该方法的对象。这种较复杂的行为称为多态——具有多种形态,即同一个方法的行为随上下文而异。
有两种重要的机制可用于实现多态公有继承;
- 在派生类中重新定义基类的方法。
- 使用虚方法。如果要在派生类中重新定义基类的方法,通常应将基类方法声明为虚的。这样,程序将根据对象类型而不是引用或指针的类型来选择方法版本。为基类声明一个虚析构函数也是一种惯例。
13.3.1 开发 Brass 类和 BrassPlus 类
13.4 静态联编和动态联编
在编译过程中进行联编被称为静态联编(static binding),又称为早期联编(early binding)。编译器必须生成能够在程序运行时选择正确的虚方法的代码,这被称为动态联编(dynamic binding),又称为晚期联编(late binding)。
13.4.1 指针和引用类型的兼容性
通常,C++不允许将一种类型的地址赋给另一种类型的指针,也不允许一种类型的引用指向另一种类型; 然而,正如您看到的,指向基类的引用或指针可以引用派生类对象,而不必进行显式类型转换。 将派生类引用或指针转换为基类引用或指针被称为向上强制转换(upcasting),这使公有继承不需要进行显式类型转换。该规则是 is-a 关系的一部分。相反的过程——将基类指针或引用转换为派生类指针或引用——称为向下强制转换(downcasting)。如果不使用显式类型转换,则向下强制转换是不允许的。原因是 is-a 关系通常是不可逆的。派生类可以新增数据成员,因此使用这些数据成员的类成员函数不能应用于基类。隐式向上强制转换使基类指针或引用可以指向基类对象或派生类对象,因此需要动态联编。C++使用虚成员函数来满足这种需求。
13.4.2 虚成员函数和动态联编
编译器对非虚方法使用静态联编。编译器对虚方法使用动态联编。在大多数情况下,动态联编很好,因为它让程序能够选择为特定类型设计的方法。由于静态联编的效率更高,因此被设置为 C++的默认选择。Strousstrup说,C++的指导原则之一是,不要为不使用的特性付出代价(内存或者处理时间)。仅当程序设计确实需要虚函数时,才使用它们。
- 使用虚函数时,在内存和执行速度方面有一定的成本,包括:
- 每个对象都将增大,增大量为存储地址的空间;
- 对于每个类,编译器都创建一个虚函数地址表(数组);
- 对于每个函数调用,都需要执行一项额外的操作,即到表中查找地址。
- 使用虚函数时,在内存和执行速度方面有一定的成本,包括:
13.4.3 有关虚函数注意事项
- 在基类方法的声明中使用关键字 virtual 可使该方法在基类以及所有的派生类(包括从派生类派生出来的类)中是虚的。
- 如果使用指向对象的引用或指针来调用虚方法,程序将使用为对象类型定义的方法,而不使用为引用或指针类型定义的方法。这称为动态联编或晚期联编。这种行为非常重要,因为这样基类指针或引用可以指向派生类对象。
- 如果定义的类将被用作基类,则应将那些要在派生类中重新定义的类方法声明为虚的。
- 构造函数不能是虚函数。创建派生类对象时,将调用派生类的构造函数,而不是基类的构造函数,然后,派生类的构造函数将使用基类的一个构造函数,这种顺序不同于继承机制。因此,派生类不继承基类的构造函数,所以将类构造函数声明为虚的没什么意义。
- 析构函数应当是虚函数,除非类不用做基类。即使基类不需要显式析构函数提供服务,也不应依赖于默认构造函数,而应提供虚析构函数,即使它不执行任何操作;给类定义一个虚析构函数并非错误,即使这个类不用做基类;这只是一个效率方面的问题。
- 友元不能是虚函数,因为友元不是类成员,而只有成员才能是虚函数。如果由于这个原因引起了设计问题,可以通过让友元函数使用虚成员函数来解决。
- 如果派生类没有重新定义函数,将使用该函数的基类版本。如果派生类位于派生链中,则将使用最新的虚函数版本,例外的情况是基类版本是隐藏的。
- 重新定义将隐藏方法
- 第一,如果重新定义继承的方法,应确保与原来的原型完全相同,但如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针(这种例外是新出现的)。这种特性被称为返回类型协变(covariance of return type),因为允许返回类型随类类型的变化而变化;注意,这种例外只适用于返回值,而不适用于参数。
- 第二,如果基类声明被重载了,则应在派生类中重新定义所有的基类版本。如果只重新定义一个版本,则其它版本将被隐藏,派生类对象将无法使用它们。注意,如果不需要修改,则新定义可只调用基类版本:
13.5 访问控制:protected
到目前为止,本书的类示例已经使用了关键字 public 和p rivate 来控制对类成员的访问。还存在另一个访问类别,这种类别用关键字 protected 表示。关键字 protected 与p rivate 相似,在类外只能用公有类成员来访问 protected 部分中的类成员。private 和p rotected 之间的区别只有在基类派生的类中才会表现出来。派生类的成员可以直接访问基类的保护成员,但不能直接访问基类的私有成员。因此,对于外部世界来说,保护成员的行为与私有成员相似;但对于派生类来说,保护成员的行为与公有成员相似。最好对类数据成员采用私有访问控制,不要使用保护访问控制;同时通过基类方法使派生类能够访问基类数据。 然而,对于成员函数来说,保护访问控制很有用,它让派生类能够访问公众不能使用的内部函数。
13.6 抽象基类
当类声明中包含纯虚函数时,则不能创建该类的对象。这里的理念是,包含纯虚函数的类只用作基类。要成为真正的 ABC,必须至少包含一个纯虚函数。原型中的= 0 使虚函数成为纯虚函数。
13.6.1 应用 ABC 概念
13.6.2 ABC 理念
可以将 ABC 看作是一种必须实施的接口。ABC 要求具体派生类覆盖其纯虚函数——迫使派生类遵循 ABC 设置的接口规则。这种模型在基于组件的编程模式中很常见,在这种情况下,使用 ABC 使得组件设计人员能够制定“接口约定”,这样确保了从 ABC 派生的所有组件都至少支持 ABC 指定的功能。
13.7 继承和动态内存分配
13.7.1 第一种情况:派生类不使用 new
- 默认析构函数是合适的。
- 默认复制构造函数执行成员复制是合适的。
- 对于赋值来说,也是如此。类的默认赋值运算符将自动使用基类的赋值运算符来对基类组件进行赋值。因此,默认赋值运算符也是合适的。
13.7.2 第二种情况:派生类使用 new
- 对于析构函数,这是自动完成的;
- 对于构造函数,这是通过在初始化成员列表中调用基类的复制构造函数来完成的;如果不这样做,将自动调用基类的默认构造函数。
- 对于赋值运算符,这是通过使用作用域解析运算符显式地调用基类的赋值运算符来完成的。
13.7.3 使用动态内存分配和友元的继承示例
因为友元不是成员函数,所以不能使用作用域解析运算符来指出要使用哪个函数。这个问题的解决方法是使用强制类型转换,以便匹配原型时能够选择正确的函数。
13.8 类设计回顾
13.8.1 编译器生成的成员函数
- 默认构造函数
- 默认构造函数要么没有参数,要么所有的参数都有默认值。如果没有定义任何构造函数,编译器将定义默认构造函数,让您能够创建对象。
- 自动生成的默认构造函数的另一项功能是,调用基类的默认构造函数以及调用本身是对象的成员所属类的默认构造函数。
- 另外,如果派生类构造函数的成员初始化列表中没有显式调用基类构造函数,则编译器将使用基类的默认构造函数来构造派生类对象的基类部分。
- 如果定义了某种构造函数,编译器将不会定义默认构造函数。在这种情况下,如果需要默认构造函数,则必须自己提供。
- 提供构造函数的动机之一是确保对象总能被正确地初始化。另外,如果类包含指针成员,则必须初始化这些成员。因此,最好提供一个显式默认构造函数,将所有的类数据成员都初始化为合理的值。
- 复制构造函数
- 复制构造函数接受其所属类的对象作为参数。
- 在下述情况下,将使用复制构造函数:
- 将新对象初始化为一个同类对象;
- 按值将对象传递给函数;
- 函数按值返回对象;
- 编译器生成临时对象。
- 如果程序没有使用(显式或隐式)复制构造函数,编译器将提供原型,但不提供函数定义;否则,程序将定义一个执行成员初始化的复制构造函数。新对象的每个成员都被初始化为原始对象相应成员的值。如果成员为类对象,则初始化该成员时,将使用相应类的复制构造函数。
- 在某些情况下,成员初始化是不合适的。例如,使用 new 初始化的成员指针通常要求执行深复制,或者类可能包含需要修改的静态变量。在上述情况下,需要定义自己的复制构造函数。
- 赋值运算符
- 默认的赋值运算符用于处理同类对象之间的赋值。
- 不要将赋值与初始化混淆了。如果语句创建新的对象,则使用初始化;如果语句修改已有对象的值,则是赋值
- 默认赋值为成员赋值。如果成员为类对象,则默认成员赋值将使用相应类的赋值运算符。
- 如果需要显式定义复制构造函数,则基于相同的原因,也需要显式定义赋值运算符。
- 默认构造函数
13.8.2 其他的类方法
- 构造函数 构造函数不同于其他类方法,因为它创建新的对象,而其他类方法只是被现有的对象调用。这是构造函数不被继承的原因之一。继承意味着派生类对象可以使用基类的方法,然而,构造函数在完成其工作之前,对象并不存在。
- 析构函数 一定要定义显式析构函数来释放类构造函数使用 new 分配的所有内存,并完成类对象所需的任何特殊的清理工作。对于基类,即使它不需要析构函数,也应提供一个虚析构函数。
- 转换
- 使用一个参数就可以调用的构造函数定义了从参数类型到类类型的转换。
- 在带一个参数的构造函数原型中使用 explicit 将禁止进行隐式转换,但仍允许显式转换
- 要将类对象转换为其他类型,应定义转换函数(参见第 11章)。转换函数可以是没有参数的类成员函数,也可以是返回类型被声明为目标类型的类成员函数。即使没有声明返回类型,函数也应返回所需的转换值。
- 应理智地使用这样的函数,仅当它们有帮助时才使用。另外,对于某些类,包含转换函数将增加代码的二义性。
- 按值传递对象与传递引用
- 编写使用对象作为参数的函数时,应按引用而不是按值来传递对象。这样做的原因之一是为了提高效率。
- 按引用传递对象的另外一个原因是,在继承使用虚函数时,被定义为接受基类引用参数的函数可以接受派生类。这在本章前面介绍过。
- 返回对象和返回引用
- 有时方法必须返回对象,但如果可以不返回对象,则应返回引用。
- 返回引用可节省时间和内存。直接返回对象与按值传递对象相似:它们都生成临时副本。同样,返回引用与按引用传递对象相似:调用和被调用的函数对同一个对象进行操作。
- 如果函数返回在函数中创建的临时对象,则不要使用引用。
- 如果函数返回的是通过引用或指针传递给它的对象,则应按引用返回对象。
- 使用 const
- 使用 const 时应特别注意。可以用它来确保方法不修改参数
- 可以使用 const 来确保方法不修改调用它的对象
- 如果函数将参数声明为指向 const 的引用或指针,则不能将该参数传递给另一个函数,除非后者也确保了参数不会被修改。
13.8.3 公有继承的考虑因素
- is-a 关系
- 要遵循 is-a 关系。如果派生类不是一种特殊的基类,则不要使用公有派生。
- 在某些情况下,最好的方法可能是创建包含纯虚函数的抽象数据类,并从它派生出其他的类。
- is-a 关系的方式之一是,无需进行显式类型转换,基类指针就可以指向派生类对象,基类引用可以引用派生类对象。
- 什么不能被继承
- 构造函数是不能继承的,也就是说,创建派生类对象时,必须调用派生类的构造函数。然而,派生类构造函数通常使用成员初始化列表语法来调用基类构造函数,以创建派生对象的基类部分。如果派生类构造函数没有使用成员初始化列表语法显式调用基类构造函数,将使用基类的默认构造函数。在继承链中,每个类都可以使用成员初始化列表将信息传递给相邻的基类。C++11 新增了一种让您能够继承构造函数的机制,但默认仍不继承构造函数。
- 析构函数也是不能继承的。然而,在释放对象时,程序将首先调用派生类的析构函数,然后调用基类的析构函数。如果基类有默认析构函数,编译器将为派生类生成默认析构函数。通常,对于基类,其析构函数应设置为虚的。
- 赋值运算符是不能继承的,原因很简单。派生类继承的方法的特征标与基类完全相同,但赋值运算符的特征标随类而异,这是因为它包含一个类型为其所属类的形参。
- 赋值运算符 如果派生类包含了这样的构造函数,即对将基类对象转换为派生类对象进行了定义,则可以将基类对象赋给派生对象。如果派生类定义了用于将基类对象赋给派生对象的赋值运算符,则也可以这样做。如果上述两个条件都不满足,则不能这样做,除非使用显式强制类型转换。
- 私有成员与保护成员
- 对派生类而言,保护成员类似于公有成员;但对于外部而言,保护成员与私有成员类似。
- 派生类可以直接访问基类的保护成员,但只能通过基类的成员函数来访问私有成员。
- 将基类成员设置为私有的可以提高安全性,而将它们设置为保护成员则可简化代码的编写工作,并提高访问速度。
- 使用私用数据成员比使用保护数据成员更好,但保护方法很有用。
- 虚方法 设计基类时,必须确定是否将类方法声明为虚的。如果希望派生类能够重新定义方法,则应在基类中将方法定义为虚的,这样可以启用晚期联编(动态联编);如果不希望重新定义方法,则不必将其声明为虚的,这样虽然无法禁止他人重新定义方法,但表达了这样的意思:您不希望它被重新定义。
- 析构函数 正如前面介绍的,基类的析构函数应当是虚的。这样,当通过指向对象的基类指针或引用来删除派生对象时,程序将首先调用派生类的析构函数,然后调用基类的析构函数,而不仅仅是调用基类的析构函数。
- 友元函数 由于友元函数并非类成员,因此不能继承。然而,您可能希望派生类的友元函数能够使用基类的友元函数。为此,可以通过强制类型转换将,派生类引用或指针转换为基类引用或指针,然后使用转换后的指针或引用来调用基类的友元函数
- 有关使用基类方法的说明
- 派生类对象自动使用继承而来的基类方法,如果派生类没有重新定义该方法。
- 派生类的构造函数自动调用基类的构造函数。
- 派生类的构造函数自动调用基类的默认构造函数,如果没有在成员初始化列表中指定其他构造函数。
- 派生类构造函数显式地调用成员初始化列表中指定的基类构造函数。
- 派生类方法可以使用作用域解析运算符来调用公有的和受保护的基类方法。
- 派生类的有元函数可以通过强制类型转换,将派生类引用或指针转换为基类引用或指针,然后使用该引用或指针来调用基类的友元函数。
- is-a 关系
13.8.4 类函数小结
C++类函数有很多不同的变体,其中有些可以继承,有些不可以。有些运算符函数既可以是成员函数,也可以是友元,而有些运算符函数只能是成员函数。
函数 能否继承 成员还是友元 默认能否生成 能否为虚函数 是否有返回类型 构造函数 否 成员 能 否 否 析构函数 否 成员 能 能 否 = 否 成员 能 能 能 & 能 任意 能 能 能 转换函数 能 成员 否 能 能 () 能 成员 否 能 能 -> 能 成员 否 能 能 op= 能 任意 否 能 能 new 能 静态成员 否 否 void* delete 能 静态成员 否 否 void other op 能 任意 否 能 能 其它成员 能 成员 否 能 能 友元 否 友元 否 否 能
13.9 总结
继承通过使用已有的类(基类)定义新的类(派生类),使得能够根据需要修改编程代码。公有继承建立 is-a 关系,这意味着派生类对象也应该是某种基类对象。作为 is-a 模型的一部分,派生类继承基类的数据成员和大部分方法,但不继承基类的构造函数、析构函数和赋值运算符。派生类可以直接访问基类的公有成员和保护成员,并能够通过基类的公有方法和保护方法访问基类的私有成员。可以在派生类中新增数据成员和方法,还可以将派生类用作基类,来做进一步的开发。每个派生类都必须有自己的构造函数。程序创建派生类对象时,将首先调用基类的构造函数,然后调用派生类的构造函数;程序删除对象时,将首先调用派生类的析构函数,然后调用基类的析构函数。
如果要将类用作基类,则可以将成员声明为保护的,而不是私有的,这样,派生类将可以直接访问这些成员。然而,使用私有成员通常可以减少出现编程问题的可能性。如果希望派生类可以重新定义基类的方法,则可以使用关键字 virtual 将它声明为虚的。这样对于通过指针或引用访问的对象,能够根据对象类型来处理,而不是根据引用或指针的类型来处理。具体地说,基类的析构函数通常应当是虚的。
可以考虑定义一个 ABC:只定义接口,而不涉及实现。例如,可以定义抽象类 Shape,然后使用它派生出具体的形状类,如C ircle 和S quare。ABC 必须至少包含一个纯虚方法,可以在声明中的分号前面加上= 0 来声明纯虚方法。
virtual double area() const = 0;
不一定非得定义纯虚方法。对于包含纯虚成员的类,不能使用它来创建对象。纯虚方法用于定义派生类的通用接口。
13.10 复习题
13.11 编程练习
第1 4 章 C++中的代码重用
- 本章内容包括:
- has-a 关系。
- 包含对象成员的类。
- 模板类 valarray。
- 私有和保护继承。
- 多重继承。
- 虚基类。
- 创建类模板。
- 使用类模板。
- 模板的具体化。
14.1 包含对象成员的类
14.1.1 valarray 类简介
valarray 类是由头文件 valarray 支持的。顾名思义,这个类用于处理数值(或具有类似特性的类),它支持诸如将数组中所有元素的值相加以及在数组中找出最大和最小的值等操作。valarray 被定义为一个模板类,以便能够处理不同的数据类型。
- 下面是这个类的一些方法。
- operator :让您能够访问各个元素。
- size( ):返回包含的元素数。
- sum( ):返回所有元素的总和。
- max( ):返回最大的元素。
- min( ):返回最小的元素。
- 下面是这个类的一些方法。
14.1.2 Student 类的设计
使用公有继承时,类可以继承接口,可能还有实现(基类的纯虚函数提供接口,但不提供实现)。获得接口是 is-a 关系的组成部分。而使用组合,类可以获得实现,但不能获得接口。不继承接口是 has-a 关系的组成部分。对于 has-a 关系来说,类对象不能自动获得被包含对象的接口是一件好事。
14.1.3 Student 类示例
- C++包含让程序员能够限制程序结构的特性——使用 explicit 防止单参数构造函数的隐式转换,使用 const 限制方法修改数据,等等。这样做的根本原因是:在编译阶段出现错误优于在运行阶段出现错误。
- 当初始化列表包含多个项目时,这些项目被初始化的顺序为它们被声明的顺序,而不是它们在初始化列表中的顺序。但如果代码使用一个成员的值作为另一个成员的初始化表达式的一部分时,初始化顺序就非常重要了。
14.2 私有继承
C++还有另一种实现 has-a 关系的途径——私有继承。使用私有继承,基类的公有成员和保护成员都将成为派生类的私有成员。这意味着基类方法将不会成为派生对象公有接口的一部分,但可以在派生类的成员函数中使用它们。使用公有继承,基类的公有方法将成为派生类的公有方法。总之,派生类将继承基类的接口;这是 is-a 关系的一部分。使用私有继承,基类的公有方法将成为派生类的私有方法。总之,派生类不继承基类的接口。正如从被包含对象中看到的,这种不完全继承是 has-a 关系的一部分。因此私有继承提供的特性与包含相同:获得实现,但不获得接口。所以,私有继承也可以用来实现 has-a 关系。接下来介绍如何使用私有继承来重新设计 Student类。
14.2.1 Student 类示例(新版本)
- 要进行私有继承,请使用关键字 private 而不是 public 来定义类(实际上,private 是默认值,因此省略访问限定符也将导致私有继承)。
- 使用多个基类的继承被称为多重继承(multiple inheritance,MI)。通常,MI 尤其是公有 MI 将导致一些问题,必须使用额外的语法规则来解决它们,这将在本章后面介绍。
- 包含版本提供了两个被显式命名的对象成员,而私有继承提供了两个无名称的子对象成员。这是这两种方法的第一个主要区别。
- 对于继承类,新版本的构造函数将使用成员初始化列表语法,它使用类名而不是成员名来标识构造函数
- 使用包含时将使用对象名来调用方法,而使用私有继承时将使用类名和作用域解析运算符来调用方法。
- 使用作用域解析运算符可以访问基类的方法,但如果要使用基类对象本身,该如何做呢? 答案是使用强制类型转换。
- 用类名显式地限定函数名不适合于友元函数,这是因为友元不属于类。然而,可以通过显式地转换为基类来调用正确的函数。
- 在私有继承中,在不进行显式类型转换的情况下,不能将指向派生类的引用或指针赋给基类引用或指针。
14.2.2 使用包含还是私有继承
- 大多数 C++程序员倾向于使用包含。
- 首先,它易于理解。类声明中包含表示被包含类的显式命名对象,代码可以通过名称引用这些对象,而使用继承将使关系更抽象。
- 其次,继承会引起很多问题,尤其从多个基类继承时,可能必须处理很多问题,如包含同名方法的独立的基类或共享祖先的独立基类。
- 总之,使用包含不太可能遇到这样的麻烦。另外,包含能够包括多个同类的子对象。如果某个类需要 3 个s tring 对象,可以使用包含声明 3 个独立的 string 成员。而继承则只能使用一个这样的对象(当对象都没有名称时,将难以区分)。
- 然而,私有继承所提供的特性确实比包含多。
- 但通过继承得到的将是派生类,它能够访问保护成员。
- 另一种需要使用私有继承的情况是需要重新定义虚函数。派生类可以重新定义虚函数,但包含类不能。使用私有继承,重新定义的函数将只能在类中使用,而不是公有的。
- 通常,应使用包含来建立 has-a 关系;如果新类需要访问原有类的保护成员,或需要重新定义虚函数,则应使用私有继承。
- 大多数 C++程序员倾向于使用包含。
14.2.3 保护继承
使用保护继承时,基类的公有成员和保护成员都将成为派生类的保护成员。和私有私有继承一样,基类的接口在派生类中也是可用的,但在继承层次结构之外是不可用的。当从派生类派生出另一个类时,私有继承和保护继承之间的主要区别便呈现出来了。使用私有继承时,第三代类将不能使用基类的接口,这是因为基类的公有方法在派生类中将变成私有方法;使用保护继承时,基类的公有方法在第二代中将变成受保护的,因此第三代派生类可以使用它们。
14.2.4 使用 using 重新定义访问权限
- 使用保护派生或私有派生时,基类的公有成员将成为保护成员或私有成员。假设要让基类的方法在派生类外面可用,方法之一是定义一个使用该基类方法的派生类方法。
- 另一种方法是,将函数调用包装在另一个函数调用中,即使用一个 using 声明(就像名称空间那样)来指出派生类可以使用特定的基类成员,即使采用的是私有派生。
14.3 多重继承
MI 描述的是有多个直接基类的类。与单继承一样,公有 MI 表示的也是 is-a 关系。
MI 可能会给程序员带来很多新问题。其中两个主要的问题是:
- 从两个不同的基类继承同名方法;
- 从两个或更多相关基类那里继承同一个类的多个实例。
14.3.1 有多少 Worker
- 虚基类使得从多个类(它们的基类相同)派生出的对象只继承一个基类对象。
- 使用虚基类时,需要对类构造函数采用一种新的方法。对于非虚基类,唯一可以出现在初始化列表中的构造函数是即时基类构造函数。但这些构造函数可能需要将信息传递给其基类。
- 如果类有间接虚基类,则除非只需使用该虚基类的默认构造函数,否则必须显式地调用该虚基类的某个构造函数。但对于非虚基类,则是非法的。
14.3.2 哪个方法
总之,在祖先相同时,使用 MI 必须引入虚基类,并修改构造函数初始化列表的规则。另外,如果在编写这些类时没有考虑到 MI,则还可能需要重新编写它们。
14.3.3 MI 小结
如果一个类通过多种途径继承了一个非虚基类,则该类从每种途径分别继承非虚基类的一个实例。从虚基类的一个或多个实例派生而来的类将只继承了一个基类对象。
- 为实现这种特性,必须满足其他要求:
- 有间接虚基类的派生类包含直接调用间接基类构造函数的构造函数,这对于间接非虚基类来说是非法的;
- 通过优先规则解决名称二义性。
- 为实现这种特性,必须满足其他要求:
14.4 类模板
14.4.1 定义类模板
- 和模板函数一样,模板类以下面这样的代码开头:
template <typename T>
- 如果在类声明中定义了方法(内联定义),则可以省略模板前缀和类限定符。
- 由于模板不是函数,它们不能单独编译。模板必须与特定的模板实例化请求一起使用。为此,最简单的方法是将所有模板信息放在一个头文件中,并在要使用这些模板的文件中包含该头文件。
- 和模板函数一样,模板类以下面这样的代码开头:
14.4.2 使用模板类
- 仅在程序包含模板并不能生成模板类,而必须请求实例化。为此,需要声明一个类型为模板类的对象,方法是使用所需的具体类型替换泛型名。
- 泛型标识符——例如这里的 Type——称为类型参数(type parameter),这意味着它们类似于变量,但赋给它们的不能是数字,而只能是类型。
- 注意,必须显式地提供所需的类型,这与常规的函数模板是不同的,因为编译器可以根据函数的参数类型来确定要生成哪种函数
14.4.3 深入探讨模板类
14.4.4 数组模板示例和非类型参数
template <class T, int n>
关键字 class(或在这种上下文中等价的关键字 typename)指出 T 为类型参数,int 指出 n 的类型为 int。这种参数(指定特殊的类型而不是用作泛型名)称为非类型(non-type)或表达式(expression)参数。- 表达式参数有一些限制。表达式参数可以是整型、枚举、引用或指针。
- 构造函数方法更通用,这是因为数组大小是作为类成员(而不是硬编码)存储在定义中的。这样可以将一种尺寸的数组赋给另一种尺寸的数组,也可以创建允许数组大小可变的类。
14.4.5 模板多功能性
可以将用于常规类的技术用于模板类。模板类可用作基类,也可用作组件类,还可用作其他模板的类型参数。
- 递归使用模板
ArrayTP< ArrayTP<int, 5>, 10> twodee;
在模板语法中,维的顺序与等价的二维数组相反。int twodee[10][5];
- 使用多个类型参数
模板可以包含多个类型参数。
template <class T1, class T2>
- 默认类型模板参数
类模板的另一项新特性是,可以为类型参数提供默认值:
template <class T1, class T2 = int>
虽然可以为类模板类型参数提供默认值,但不能为函数模板参数提供默认值。然而,可以为非类型参数提供默认值,这对于类模板和函数模板都是适用的。
- 递归使用模板
14.4.6 模板的具体化
类模板与函数模板很相似,因为可以有隐式实例化、显式实例化和显式具体化,它们统称为具体化(specialization)。模板以泛型的方式描述类,而具体化是使用具体的类型生成类声明。
- 隐式实例化 它们声明一个或多个对象,指出所需的类型,而编译器使用通用模板提供的处方生成具体的类定义,编译器在需要对象之前,不会生成类的隐式实例化
- 显式实例化 当使用关键字 template 并指出所需类型来声明类时,编译器将生成类声明的显式实例化(explicit instantiation)。声明必须位于模板定义所在的名称空间中。在这种情况下,虽然没有创建或提及类对象,编译器也将生成类声明(包括方法定义)。和隐式实例化一样,也将根据通用模板来生成具体化。
- 显式具体化(explicit specialization)是特定类型(用于替换模板中的泛型)的定义。有时候,可能需要在为特殊类型实例化时,对模板进行修改,使其行为不同。在这种情况下,可以创建显式具体化。
- 部分具体化 C++还允许部分具体化(partial specialization),即部分限制模板的通用性。
14.4.7 成员模板
模板可用作结构、类或模板类的成员。要完全实现 STL 的设计,必须使用这项特性。
14.4.8 将模板用作参数
模板可以包含类型参数(如t ypename T)和非类型参数(如i nt n)。模板还可以包含本身就是模板的参数,这种参数是模板新增的特性,用于实现 STL。
14.4.9 模板类和友元
- 模板类声明也可以有友元。模板的友元分 3类:
- 模板类的非模板友元函数;要提供模板类参数,必须指明具体化。
- 模板类的约束模板友元函数;要使类的每一个具体化都获得与友元匹配的具体化。这比非模板友元复杂些,包含以下 3步。
- 首先,在类定义的前面声明每个模板函数。
- 然后,在函数中再次将模板声明为友元。这些语句根据类模板参数的类型声明具体化
- 程序必须满足的第三个要求是,为友元提供模板定义。
- 模板类的非约束模板友元函数;通过在类内部声明模板,可以创建非约束友元函数,即每个函数具体化都是每个类具体化的友元。
- 模板类声明也可以有友元。模板的友元分 3类:
14.4.10 模板别名(C++11)
如果能为类型指定别名,将很方便,在模板设计中尤其如此。
- 可使用 typedef 为模板具体化指定别名:
typedef const char * pc1;
- C++11 允许将语法 using = 用于非模板。用于非模板时,这种语法与常规 typedef 等价:
using pc2 = const char *;
- 可使用 typedef 为模板具体化指定别名:
14.5 总结
C++提供了几种重用代码的手段。第1 3 章介绍的公有继承能够建立 is-a 关系,这样派生类可以重用基类的代码。私有继承和保护继承也使得能够重用基类的代码,但建立的是 has-a 关系。使用私有继承时,基类的公有成员和保护成员将成为派生类的私有成员;使用保护继承时,基类的公有成员和保护成员将成为派生类的保护成员。无论使用哪种继承,基类的公有接口都将成为派生类的内部接口。这有时候被称为继承实现,但并不继承接口,因为派生类对象不能显式地使用基类的接口。因此,不能将派生对象看作是一种基类对象。由于这个原因,在不进行显式类型转换的情况下,基类指针或引用将不能指向派生类对象。 还可以通过开发包含对象成员的类来重用类代码。这种方法被称为包含、层次化或组合,它建立的也是 has-a 关系。与私有继承和保护继承相比,包含更容易实现和使用,所以通常优先采用这种方式。然而,私有继承和保护继承比包含有一些不同的功能。例如,继承允许派生类访问基类的保护成员;还允许派生类重新定义从基类那里继承的虚函数。因为包含不是继承,所以通过包含来重用类代码时,不能使用这些功能。另一方面,如果需要使用某个类的几个对象,则用包含更适合。例如,State 类可以包含一组 County 对象。 多重继承(MI)使得能够在类设计中重用多个类的代码。私有 MI 或保护 MI 建立 has-a 关系,而公有 MI 建立 is-a 关系。MI 会带来一些问题,即多次定义同一个名称,继承多个基类对象。可以使用类限定符来解决名称二义性的问题,使用虚基类来避免继承多个基类对象的问题。但使用虚基类后,就需要为编写构造函数初始化列表以及解决二义性问题引入新的规则。 类模板使得能够创建通用的类设计,其中类型(通常是成员类型)由类型参数表示。典型的模板如下:
|
|
其中,T 是类型参数,用作以后将指定的实际类型的占位符(这个参数可以是任意有效的 C++名称,但通常使用 T 和T ype)。在这种环境下,也可以使用 typename 代替 class: template <typename T> class Rev {...};
类定义(实例化)在声明类对象并指定特定类型时生成。例如,下面的声明导致编译器生成类声明,用声明中的实际类型 short 替换模板中的所有类型参数 T: class Ic<short> sic;
这里,类名为 Ictemplate class Ic<int>;
在这种情况下,编译器将使用通用模板生成一个 int 具体化——Ic
|
|
这样,下面这样的声明将为 chic 使用专用定义,而不是通用模板: class Ic<char *> chic;
类模板可以指定多个泛型,也可以有非类型参数: template <class T, class TT, int n> class Pals {...};
下面的声明将生成一个隐式实例化,用d ouble 代替 T,用s tring 代替 TT,用6 代替 n:
Pals<double, string, 6> mix;
类模板还可以包含本身就是模板的参数:
template < template <typename T> class CL, typename U, int z> class Trophy {...};
其中 z 是一个 int值,U 为类型名,CL 为一个使用 templatetemplate <class T> Pals<T, T, 10> {...};
template <class T, class TT> Pals<T, TT, 10> {...};
template <class T> Pals<T, T*, 10> {...};
第一个声明为两个类型相同,且n 的值为 6 的情况创建了一个具体化。同样,第二个声明为 n 等于 100 的情况创建一个具体化;第三个声明为第二个类型是指向第一个类型的指针的情况创建了一个具体化。
模板类可用作其他类、结构和模板的成员。
所有这些机制的目的都是为了让程序员能够重用经过测试的代码,而不用手工复制它们。这样可以简化编程工作,提供程序的可靠性。
14.6 复习题
14.7 编程练习
第1 5 章 友元、异常和其他
- 本章内容包括:
- 友元类。
- 友元类方法。
- 嵌套类。
- 引发异常、try 块和 catch块。
- 异常类。
- 运行阶段类型识别(RTTI)。
- dynamic_cast 和t ypeid。
- static_cast、const_cast 和r eiterpret_cast。
15.1 友元
本书前面的一些示例将友元函数用于类的扩展接口中,类并非只能拥有友元函数,也可以将类作为友元。在这种情况下,友元类的所有方法都可以访问原始类的私有成员和保护成员。另外,也可以做更严格的限制,只将特定的成员函数指定为另一个类的友元。哪些函数、成员函数或类为友元是由类定义的,而不能从外部强加友情。因此,尽管友元被授予从外部访问类的私有部分的权限,但它们并不与面向对象的编程思想相悖;相反,它们提高了公有接口的灵活性。
15.1.1 友元类
15.1.2 友元成员函数
15.1.3 其他友元关系
15.1.4 共同的友元
15.2 嵌套类
在C++中,可以将类声明放在另一个类中。在另一个类中声明的类被称为嵌套类(nested class),它通过提供新的类型类作用域来避免名称混乱。包含类的成员函数可以创建和使用被嵌套类的对象;而仅当声明位于公有部分,才能在包含类的外面使用嵌套类,而且必须使用作用域解析运算符。
15.2.1 嵌套类和访问权限
有两种访问权限适合于嵌套类。首先,嵌套类的声明位置决定了嵌套类的作用域,即它决定了程序的哪些部分可以创建这种类的对象。其次,和其他类一样,嵌套类的公有部分、保护部分和私有部分控制了对类成员的访问。
- 作用域
- 如果嵌套类是在另一个类的私有部分声明的,则只有后者知道它。
- 如果嵌套类是在另一个类的保护部分声明的,则它对于后者来说是可见的,但是对于外部世界则是不可见的。然而,在这种情况中,派生类将知道嵌套类,并可以直接创建这种类型的对象。
- 如果嵌套类是在另一个类的公有部分声明的,则允许后者、后者的派生类以及外部世界使用它,因为它是公有的。然而,由于嵌套类的作用域为包含它的类,因此在外部世界使用它时,必须使用类限定符。
- 访问控制
- 类可见后,起决定作用的将是访问控制。对嵌套类访问权的控制规则与对常规类相同。
- 类声明的位置决定了类的作用域或可见性。类可见后,访问控制规则(公有、保护、私有、友元)将决定程序对嵌套类成员的访问权限。
- 作用域
15.2.2 模板中的嵌套
15.3 异常
程序有时会遇到运行阶段错误,导致程序无法正常地运行下去。例如,程序可能试图打开一个不可用的文件,请求过多的内存,或者遭遇不能容忍的值。通常,程序员都会试图预防这种意外情况。C++异常为处理这种情况提供了一种功能强大而灵活的工具。
15.3.1 调用 abort( )
Abort( )函数的原型位于头文件 cstdlib(或s tdlib.h)中,其典型实现是向标准错误流(即c err 使用的错误流)发送消息 abnormal program termination(程序异常终止),然后终止程序。它还返回一个随实现而异的值,告诉操作系统(如果程序是由另一个程序调用的,则告诉父进程),处理失败。abort( )是否刷新文件缓冲区(用于存储读写到文件中的数据的内存区域)取决于实现。如果愿意,也可以使用 exit( ),该函数刷新文件缓冲区,但不显示消息。
15.3.2 返回错误码
一种比异常终止更灵活的方法是,使用函数的返回值来指出问题。
15.3.3 异常机制
下面介绍如何使用异常机制来处理错误。C++异常是对程序运行过程中发生的异常情况(例如被 0除)的一种响应。异常提供了将控制权从程序的一个部分传递到另一部分的途径。
- 对异常的处理有 3 个组成部分:
- 引发异常;
- 使用处理程序捕获异常;
- 使用 try块。
- throw 语句实际上是跳转,即命令程序跳到另一条语句。throw 关键字表示引发异常,紧随其后的值(例如字符串或对象)指出了异常的特征。执行 throw 语句类似于执行返回语句,因为它也将终止函数的执行;但t hrow 不是将控制权返回给调用程序,而是导致程序沿函数调用序列后退,直到找到包含 try 块的函数。
- 程序使用异常处理程序(exception handler)来捕获异常,异常处理程序位于要处理问题的程序中。catch 关键字表示捕获异常。处理程序以关键字 catch 开头,随后是位于括号中的类型声明,它指出了异常处理程序要响应的异常类型;然后是一个用花括号括起的代码块,指出要采取的措施。catch 关键字和异常类型用作标签,指出当异常被引发时,程序应跳到这个位置执行。异常处理程序也被称为 catch块。
- try 块标识其中特定的异常可能被激活的代码块,它后面跟一个或多个 catch块。try 块是由关键字 try 指示的,关键字 try 的后面是一个由花括号括起的代码块,表明需要注意这些代码引发的异常。
- 执行完 try 块中的语句后,如果没有引发任何异常,则程序跳过 try 块后面的 catch块,直接执行处理程序后面的第一条语句。
- 对异常的处理有 3 个组成部分:
15.3.4 将对象用作异常类型
通常,引发异常的函数将传递一个对象。这样做的重要优点之一是,可以使用不同的异常类型来区分不同的函数在不同情况下引发的异常。另外,对象可以携带信息,程序员可以根据这些信息来确定引发异常的原因。同时,catch 块可以根据这些信息来决定采取什么样的措施。
15.3.5 异常规范和 C++11
C++11 仍然处于标准之中,但以后可能会从标准中剔除,因此不建议您使用它。然而,C++11 确实支持一种特殊的异常规范:您可使用新增的关键字 noexcept 指出函数不会引发异常。 还有运算符 noexcept( ),它判断其操作数是否会引发异常,详情请参阅附录 E。
15.3.6 栈解退
假设 try 块没有直接调用引发异常的函数,而是调用了对引发异常的函数进行调用的函数,则程序流程将从引发异常的函数跳到包含 try 块和处理程序的函数。这涉及到栈解退(unwinding the stack)。程序进行栈解退以回到能够捕获异常的地方时,将释放栈中的自动存储型变量。如果变量是类对象,将为该对象调用析构函数。
15.3.7 其他异常特性
- 虽然 throw-catch 机制类似于函数参数和函数返回机制,但还是有些不同之处。
- throw 语句将控制权向上返回到第一个这样的函数:包含能够捕获相应异常的 try-catch 组合。
- 另一个不同之处是,引发异常时编译器总是创建一个临时拷贝,即使异常规范和 catch 块中指定的是引用。
- 如果有一个异常类继承层次结构,应这样排列 catch块:将捕获位于层次结构最下面的异常类的 catch 语句放在最前面,将捕获基类异常的 catch 语句放在最后面。
- 虽然 throw-catch 机制类似于函数参数和函数返回机制,但还是有些不同之处。
15.3.8 exception 类
C++异常的主要目的是为设计容错程序提供语言级支持,即异常使得在程序设计中包含错误处理功能更容易,以免事后采取一些严格的错误处理方式。异常的灵活性和相对方便性激励着程序员在条件允许的情况下在程序设计中加入错误处理功能。总之,异常是这样一种特性:类似于类,可以改变您的编程方式。 较新的 C++编译器将异常合并到语言中。 为支持该语言,exception 头文件(以前为 exception.h 或e xcept.h)定义了 exception类,C++可以把它用作其他异常类的基类。代码可以引发 exception 异常,也可以将 exception 类用作基类。有一个名为 what( )的虚拟成员函数,它返回一个字符串,该字符串的特征随实现而异。然而,由于这是一个虚方法,因此可以在从 exception 派生而来的类中重新定义它;
- C++库定义了很多基于 exception 的异常类型。
- stdexcept 异常类 头文件 stdexcept 定义了其他几个异常类。首先,该文件定义了 logic_error 和r untime_error类,它们都是以公有方式从 exception 派生而来的;这两个新类被用作两个派生类系列的基类。异常类系列 logic_error 描述了典型的逻辑错误。总体而言,通过合理的编程可以避免这种错误,但实际上这些错误还是可能发生的。
- bad_alloc 异常和 new 对于使用 new 导致的内存分配问题,C++的最新处理方式是让 new 引发 bad_alloc 异常。头文件 new 包含 bad_alloc 类的声明,它是从 exception 类公有派生而来的。但在以前,当无法分配请求的内存量时,new 返回一个空指针。
- 空指针和 new
很多代码都是在 new 在失败时返回空指针时编写的。为处理 new 的变化,有些编译器提供了一个标记(开关),让用户选择所需的行为。当前,C++标准提供了一种在失败时返回空指针的 new,其用法如下:
int * pa = new (std::nowthrow) int[500];
- C++库定义了很多基于 exception 的异常类型。
15.3.9 异常、类和继承
异常、类和继承以三种方式相互关联。首先,可以像标准 C++库所做的那样,从一个异常类派生出另一个;其次,可以在类定义中嵌套异常类声明来组合异常;第三,这种嵌套声明本身可被继承,还可用作基类。
15.3.10 异常何时会迷失方向
- 异常被引发后,在两种情况下,会导致问题。
- 首先,如果它是在带异常规范的函数中引发的,则必须与规范列表中的某种异常匹配(在继承层次结构中,类类型与这个类及其派生类的对象匹配),否则称为意外异常(unexpected exception)。
- 如果异常不是在函数中引发的(或者函数没有异常规范),则必须捕获它。如果没被捕获(在没有 try 块或没有匹配的 catch 块时,将出现这种情况),则异常被称为未捕获异常(uncaught exception)。
- 异常被引发后,在两种情况下,会导致问题。
15.3.11 有关异常的注意事项
从前面关于如何使用异常的讨论可知,应在设计程序时就加入异常处理功能,而不是以后再添加。这样做有些缺点。例如,使用异常会增加程序代码,降低程序的运行速度。异常规范不适用于模板,因为模板函数引发的异常可能随特定的具体化而异。异常和动态内存分配并非总能协同工作。总之,虽然异常处理对于某些项目极为重要,但它也会增加编程的工作量、增大程序、降低程序的速度。另一方面,不进行错误检查的代价可能非常高。
15.4 RTTI
RTTI 是运行阶段类型识别(Runtime Type Identification)的简称。RTTI 旨在为程序在运行阶段确定对象的类型提供一种标准方式。
15.4.1 RTTI 的用途
在处理一些信息后,选择一个类,并创建这种类型的对象,然后返回它的地址,而该地址可以被赋给基类指针。如何知道指针指向的是哪种对象呢?
15.4.2 RTTI 的工作原理
- C++有3 个支持 RTTI 的元素。
- 如果可能的话,dynamic_cast 运算符将使用一个指向基类的指针来生成一个指向派生类的指针;否则,该运算符返回 0——空指针。 dynamic_cast 运算符是最常用的 RTTI 组件,它不能回答“指针指向的是哪类对象”这样的问题,但能够回答“是否可以安全地将对象的地址赋给特定类型的指针”这样的问题。
- typeid 运算符返回一个指出对象的类型的值。 typeid 运算符使得能够确定两个对象是否为同种类型。它与 sizeof 有些相像,可以接受两种参数: 类名; 结果为对象的表达式。typeid 运算符返回一个对 type_info 对象的引用。
- type_info 结构存储了有关特定类型的信息。 type_info 是在头文件 typeinfo(以前为 typeinfo.h)中定义的一个类。type_info 类重载了= =和!=运算符,以便可以使用这些运算符来对类型进行比较。
- 只能将 RTTI 用于包含虚函数的类层次结构,原因在于只有对于这种类层次结构,才应该将派生对象的地址赋给基类指针。
- 如果发现在扩展的 if else 语句系列中使用了 typeid,则应考虑是否应该使用虚函数和 dynamic_cast。
- C++有3 个支持 RTTI 的元素。
15.5 类型转换运算符
对于这种松散情况,Stroustrop 采取的措施是,更严格地限制允许的类型转换,并添加 4 个类型转换运算符,使转换过程更规范:
- dynamic_cast;该运算符的用途是,使得能够在类层次结构中进行向上转换(由于 is-a 关系,这样的类型转换是安全的),而不允许其他转换。
static_cast <type-name > (expression)
- const_cast;const_cast 运算符用于执行只有一种用途的类型转换,即改变值为 const 或v olatile,如果类型的其他方面也被修改,则上述类型转换将出错。也就是说,除了 const 或v olatile 特征(有或无)可以不同外,type_name 和e xpression 的类型必须相同。
- static_cast;static_cast 运算符的语法与其他类型转换运算符相同,仅当 type_name 可被隐式转换为 expression 所属的类型或 expression 可被隐式转换为 type_name 所属的类型时,上述转换才是合法的,否则将出错。
- reinterpret_cast;reinterpret_cast 运算符用于天生危险的类型转换。它不允许删除 const,但会执行其他令人生厌的操作。有时程序员必须做一些依赖于实现的、令人生厌的操作,使用 reinterpret_cast 运算符可以简化对这种行为的跟踪工作。该运算符的语法与另外 3 个相同。
15.6 总结
友元使得能够为类开发更灵活的接口。类可以将其他函数、其他类和其他类的成员函数作为友元。在某些情况下,可能需要使用前向声明,需要特别注意类和方法声明的顺序,以正确地组合友元。 嵌套类是在其他类中声明的类,它有助于设计这样的助手类,即实现其他类,但不必是公有接口的组成部分。 C++异常机制为处理拙劣的编程事件,如不适当的值、I/O 失败等,提供了一种灵活的方式。引发异常将终止当前执行的函数,将控制权传给匹配的 catch块。catch 块紧跟在 try 块的后面,为捕获异常,直接或间接导致异常的函数调用必须位于 try 块中。这样程序将执行 catch 块中的代码。这些代码试图解决问题或终止程序。类可以包含嵌套的异常类,嵌套异常类在相应的问题被发现时将被引发。函数可以包含异常规范,指出在该函数中可能引发的异常;但C++11 摒弃了这项功能。未被捕获的异常(没有匹配的 catch 块的异常)在默认情况下将终止程序,意外异常(不与任何异常规范匹配的异常)也是如此。 RTTI(运行阶段类型信息)特性让程序能够检测对象的类型。dynamic_cast 运算符用于将派生类指针转换为基类指针,其主要用途是确保可以安全地调用虚函数。Typeid 运算符返回一个 type_info 对象。可以对两个 typeid 的返回值进行比较,以确定对象是否为特定的类型,而返回的 type_info 对象可用于获得关于对象的信息。 与通用转换机制相比,dynamic_cast、static_cast、const_cast 和r einterpret_cast 提供了更安全、更明确的类型转换。
15.7 复习题
15.8 编程练习
第1 6 章 string 类和标准模板库
- 本章内容包括:
- 标准 C++ string类。
- 模板 auto_ptr、unique_ptr 和s hared_ptr。
- 标准模板库(STL)。
- 容器类。
- 迭代器。
- 函数对象(functor)。
- STL 算法。
- 模板 initializer_list。
16.1 string 类
string 类是由头文件 string 支持的(注意,头文件 string.h 和c string 支持对 C-风格字符串进行操纵的 C 库字符串函数,但不支持 string类)。要使用类,关键在于知道它的公有接口,而s tring 类包含大量的方法,其中包括了若干构造函数,用于将字符串赋给变量、合并字符串、比较字符串和访问各个元素的重载运算符以及用于在字符串中查找字符和子字符串的工具等。
16.1.1 构造字符串
先来看 string 的构造函数。毕竟,对于类而言,最重要的内容之一是,有哪些方法可用于创建其对象。程序清单 16.1 使用了 string 的7 个构造函数(用c tor 标识,这是传统 C++中构造函数的缩写)。使用构造函数时都进行了简化,即隐藏了这样一个事实:string 实际上是模板具体化 basic_string
的一个 typedef,同时省略了与内存管理相关的参数(这将在本章后面和附录 F 中讨论)。size_type 是一个依赖于实现的整型,是在头文件 string 中定义的。string 类将 string::npos 定义为字符串的最大长度,通常为 unsigned int 的最大值。另外,表格中使用缩写 NBTS(null-terminated string)来表示以空字符结束的字符串——传统的 C 字符串。 Table 3: string 类的构造函数构造函数 描述 string(const char * s) 将s tring 对象初始化为 s 指向的 NBTS string(size_type n, char c) 创建一个包含 n 个元素的 string 对象,其中每个元素都被初始化为字符 c string(const string & str) 将一个 string 对象初始化为 string 对象 str(复制构造函数) string( ) 创建一个默认的 sting 对象,长度为 0(默认构造函数) string(const char * s, size_type n) 将s tring 对象初始化为 s 指向的 NBTS 的前 n 个字符,即使超过了 NBTS 结尾 template string(Iter begin, Iter end) 将s tring 对象初始化为区间[ begin, end)内的字符,其中 begin 和e nd 的行为就像指针,用于指定位置,范围包括 begin 在内,但不包括 end string(const string & str, string size_type pos = 0, size_type n = npos) 将一个 string 对象初始化为对象 str 中从位置 pos 开始到结尾的字符,或从位置 pos 开始的 n 个字符 string(string && str) noexcept 这是 C++11 新增的,它将一个 string 对象初始化为 string 对象 str,并可能修改 str(移动构造函数) string(initializer_list il) 这是 C++11 新增的,它将一个 string 对象初始化为初始化列表 il 中的字符 16.1.2 string 类输入
- string 版本的 getline( )函数从输入中读取字符,并将其存储到目标 string中,直到发生下列三种情况之一:
- 到达文件尾,在这种情况下,输入流的 eofbit 将被设置,这意味着方法 fail( )和e of( )都将返回 true;
- 遇到分界字符(默认为\ n),在这种情况下,将把分界字符从输入流中删除,但不存储它;
- 读取的字符数达到最大允许值(string::npos 和可供分配的内存字节数中较小的一个),在这种情况下,将设置输入流的 failbit,这意味着方法 fail( )将返回 true。
- string 版本的 getline( )函数从输入中读取字符,并将其存储到目标 string中,直到发生下列三种情况之一:
16.1.3 使用字符串
String 类对全部 6 个关系运算符都进行了重载。如果在机器排列序列中,一个对象位于另一个对象的前面,则前者被视为小于后者。如果机器排列序列为 ASCII码,则数字将小于大写字符,而大写字符小于小写字符。对于每个关系运算符,都以三种方式被重载,以便能够将 string 对象与另一个 string 对象、C-风格字符串进行比较,并能够将 C-风格字符串与 string 对象进行比较。
Table 4: 重载的 find( )方法方 法 原 型 描 述 size_type find(const string & str, size_type pos = 0)const 从字符串的 pos 位置开始,查找子字符串 str。如果找到,则返回该子字符串首次出现时其首字符的索引;否则,返回 string :: npos size_type find(const char * s, size_type pos = 0)const 从字符串的 pos 位置开始,查找子字符串 s。如果找到,则返回该子字符串首次出现时其首字符的索引;否则,返回 string :: npos size_type find(const char * s, size_type pos = 0, size_type n) 从字符串的 pos 位置开始,查找 s 的前 n 个字符组成的子字符串。如果找到,则返回该子字符串首次出现时其首字符的索引;否则,返回 string :: npos size_type find(char ch, size_type pos = 0)const 从字符串的 pos 位置开始,查找字符 ch。如果找到,则返回该字符首次出现的位置;否则,返回 string :: npos string 库还提供了相关的方法:rfind( )、find_first_of( )、find_last_of( )、find_first_not_of( )和f ind_last_not_of( ),它们的重载函数特征标都与 find( )方法相同。rfind( )方法查找子字符串或字符最后一次出现的位置;find_first_of( )方法在字符串中查找参数中任何一个字符首次出现的位置。
16.1.4 string 还提供了哪些功能
string 库提供了很多其他的工具,包括完成下述功能的函数:删除字符串的部分或全部内容、用一个字符串的部分或全部内容替换另一个字符串的部分或全部内容、将数据插入到字符串中或删除字符串中的数据、将一个字符串的部分或全部内容与另一个字符串的部分或全部内容进行比较、从字符串中提取子字符串、将一个字符串中的内容复制到另一个字符串中、交换两个字符串的内容。这些函数中的大多数都被重载,以便能够同时处理 C-风格字符串和 string 对象。
16.1.5 字符串种类
本节将 string 类看作是基于 char 类型的。事实上,正如前面指出的,string 库实际上是基于一个模板类的:
template<class charT, class traits = char _traits<charT>, class Allocator = allocator<charT> > basic_string {...};
16.2 智能指针模板类
智能指针是行为类似于指针的类对象,但这种对象还有其他功能。本节介绍三个可帮助管理动态内存分配的智能指针模板。常规指针,不是有析构函数的类对象。如果它是对象,则可以在对象过期时,让它的析构函数删除指向的内存。这正是 auto_ptr、unique_ptr 和s hared_ptr 背后的思想。模板 auto_ptr 是C++98 提供的解决方案,C++11 已将其摒弃,并提供了另外两种解决方案。
16.2.1 使用智能指针
这三个智能指针模板(auto_ptr、unique_ptr 和s hared_ptr)都定义了类似指针的对象,可以将 new 获得(直接或间接)的地址赋给这种对象。当智能指针过期时,其析构函数将使用 delete 来释放内存。因此,如果将 new 返回的地址赋给这些对象,将无需记住稍后释放这些内存:在智能指针过期时,这些内存将自动被释放。所有智能指针类都一个 explicit 构造函数,该构造函数将指针作为参数。因此不需要自动将指针转换为智能指针对象
auto_ptr<string> ps(new strings);
16.2.2 有关智能指针的注意事项
- 实际上有 4种,但本书不讨论 weak_ptr。
- 两个指针将指向同一个 string 对象。这是不能接受的,因为程序将试图删除同一个对象两次——一次是 ps 过期时,另一次是 vocation 过期时。要避免这种问题,方法有多种。
- 定义赋值运算符,使之执行深复制。这样两个指针将指向不同的对象,其中的一个对象是另一个对象的副本。
- 建立所有权(ownership)概念,对于特定的对象,只能有一个智能指针可拥有它,这样只有拥有对象的智能指针的构造函数会删除该对象。然后,让赋值操作转让所有权。这就是用于 auto_ptr 和u nique_ptr 的策略,但u nique_ptr 的策略更严格。
- 创建智能更高的指针,跟踪引用特定对象的智能指针数。这称为引用计数(reference counting)。例如,赋值时,计数将加 1,而指针过期时,计数将减 1。仅当最后一个指针过期时,才调用 delete。这是 shared_ptr 采用的策略。
- 使用 new 分配内存时,才能使用 auto_ptr 和s hared_ptr,使用 new [ ]分配内存时,不能使用它们。不使用 new 分配内存时,不能使用 auto_ptr 或s hared_ptr;不使用 new 或n ew []分配内存时,不能使用 unique_ptr。
16.2.3 unique_ptr 为何优于 auto_ptr
- 程序试图将一个 unique_ptr 赋给另一个时,如果源 unique_ptr 是个临时右值,编译器允许这样做;如果源 unique_ptr 将存在一段时间,编译器将禁止这样做
- 相比于 auto_ptr,unique_ptr 还有另一个优点。它有一个可用于数组的变体。别忘了,必须将 delete 和n ew 配对,将d elete []和n ew [ ]配对。模板 auto_ptr 使用 delete 而不是 delete [ ],因此只能与 new 一起使用,而不能与 new [ ]一起使用。但u nique_ptr 有使用 new [ ]和d elete [ ]的版本
16.2.4 选择智能指针
- 如果程序要使用多个指向同一个对象的指针,应选择 shared_ptr。模板 shared_ptr 包含一个显式构造函数,可用于将右值 unique_ptr 转换为 shared_ptr。shared_ptr 将接管原来归 unique_ptr 所有的对象。
- 如果程序不需要多个指向同一个对象的指针,则可使用 unique_ptr。如果函数使用 new 分配内存,并返回指向该内存的指针,将其返回类型声明为 unique_ptr 是不错的选择。
16.3 标准模板库
STL 提供了一组表示容器、迭代器、函数对象和算法的模板。容器是一个与数组类似的单元,可以存储若干个值。STL 容器是同质的,即存储的值的类型相同;算法是完成特定任务(如对数组进行排序或在链表中查找特定值)的处方;迭代器能够用来遍历容器的对象,与能够遍历数组的指针类似,是广义指针;函数对象是类似于函数的对象,可以是类对象或函数指针(包括函数名,因为函数名被用作指针)。STL 使得能够构造各种容器(包括数组、队列和链表)和执行各种操作(包括搜索、排序和随机排列)。
16.3.1 模板类 vector
在计算中,矢量(vector)对应数组,存储了一组可随机访问的值,所以 vector 类提供了与第 14 章介绍的 valarray 和A rrayTP 以及第 4 章介绍的 array 类似的操作,即可以创建 vector 对象,将一个 vector 对象赋给另一个对象,使用[ ]运算符来访问 vector 元素。要使类成为通用的,应将它设计为模板类,STL 正是这样做的——在头文件 vector(以前为 vector.h)中定义了一个 vector 模板。 要创建 vector 模板对象,可使用通常的< type>表示法来指出要使用的类型。另外,vector 模板使用动态内存分配,因此可以用初始化参数来指出需要多少矢量
16.3.2 可对矢量执行的操作
所有的 STL 容器都提供了一些基本方法,其中包括 size( )——返回容器中元素数目、swap( )——交换两个容器的内容、begin( )——返回一个指向容器中第一个元素的迭代器、end( )——返回一个表示超过容器尾的迭代器。 什么是迭代器?它是一个广义指针。事实上,它可以是指针,也可以是一个可对其执行类似指针的操作——如解除引用(如o perator*( ))和递增(如o perator++( ))——的对象。通过将指针广义化为迭代器,让S TL 能够为各种不同的容器类(包括那些简单指针无法处理的类)提供统一的接口。每个容器类都定义了一个合适的迭代器,该迭代器的类型是一个名为 iterator 的t ypedef,其作用域为整个类。
- push_back( )是一个方便的方法,它将元素添加到矢量末尾。这样做时,它将负责内存管理,增加矢量的长度,使之能够容纳新的成员。
- erase( )方法删除矢量中给定区间的元素。它接受两个迭代器参数,这些参数定义了要删除的区间。
- insert( )方法的功能与 erase( )相反。它接受 3 个迭代器参数,第一个参数指定了新元素的插入位置,第二个和第三个迭代器参数定义了被插入区间,该区间通常是另一个容器对象的一部分。
16.3.3 对矢量可执行的其他操作
- for_each( )函数可用于很多容器类,它接受 3 个参数。前两个是定义容器中区间的迭代器,最后一个是指向函数的指针(更普遍地说,最后一个参数是一个函数对象,函数对象将稍后介绍)。for_each( )函数将被指向的函数应用于容器区间中的各个元素。被指向的函数不能修改容器元素的值。可以用 for_each( )函数来代替 for 循环。
- Random_shuffle( )函数接受两个指定区间的迭代器参数,并随机排列该区间中的元素。与可用于任何容器类的 for_each 不同,该函数要求容器类允许随机访问,vector 类可以做到这一点。
- sort( )函数也要求容器支持随机访问。该函数有两个版本,第一个版本接受两个定义区间的迭代器参数,并使用为存储在容器中的类型元素定义的<运算符,对区间中的元素进行操作。另一种格式的 sort( )。它接受 3 个参数,前两个参数也是指定区间的迭代器,最后一个参数是指向要使用的函数的指针(函数对象),而不是用于比较的 operator<( )。返回值可转换为 bool,false 表示两个参数的顺序不正确。operator<( )函数将按 rating 进行排序,而W orseThan( )将它们视为相同。第一种排序称为全排序(total ordering),第二种排序称为完整弱排序(strict weak ordering)。在全排序中,如果 a<b 和b<a 都不成立,则a 和b 必定相同。在完整弱排序中,情况就不是这样了。它们可能相同,也可能只是在某方面相同,如W orseThan( )示例中的 rating 成员。所以在完整弱排序中,只能说它们等价,而不是相同。
16.3.4 基于范围的 for 循环(C++11)
基于范围的 for 循环是为用于 STL 而设计的。在这种 for 循环中,括号内的代码声明一个类型与容器存储的内容相同的变量,然后指出了容器的名称。接下来,循环体使用指定的变量依次访问容器的每个元素。
16.4 泛型编程
有了一些使用 STL 的经验后,来看一看底层理念。STL 是一种泛型编程(generic programming)。面向对象编程关注的是编程的数据方面,而泛型编程关注的是算法。它们之间的共同点是抽象和创建可重用代码,但它们的理念绝然不同。 泛型编程旨在编写独立于数据类型的代码。在C++中,完成通用程序的工具是模板。当然,模板使得能够按泛型定义函数或类,而S TL 通过通用算法更进了一步。模板让这一切成为可能,但必须对元素进行仔细地设计。
16.4.1 为何使用迭代器
理解迭代器是理解 STL 的关键所在。模板使得算法独立于存储的数据类型,而迭代器使算法独立于使用的容器类型。因此,它们都是 STL 通用方法的重要组成部分。泛型编程旨在使用同一个 find 函数来处理数组、链表或任何其他容器类型。即函数不仅独立于容器中存储的数据类型,而且独立于容器本身的数据结构。模板提供了存储在容器中的数据类型的通用表示,因此还需要遍历容器中的值的通用表示,迭代器正是这样的通用表示。
- 迭代器应具备哪些特征呢?下面是一个简短的列表。
- 应能够对迭代器执行解除引用的操作,以便能够访问它引用的值。即如果 p 是一个迭代器,则应对* p 进行定义。
- 应能够将一个迭代器赋给另一个。即如果 p 和q 都是迭代器,则应对表达式 p=q 进行定义。
- 应能够将一个迭代器与另一个进行比较,看它们是否相等。即如果 p 和q 都是迭代器,则应对 p= =q 和p!=q 进行定义。
- 应能够使用迭代器遍历容器中的所有元素,这可以通过为迭代器 p 定义++p 和p++来实现。
- 为区分++运算符的前缀版本和后缀版本,C++将o perator++作为前缀版本,将o perator++(int)作为后缀版本;其中的参数永远也不会被用到,所以不必指定其名称。
- 对迭代器的要求变成了对容器类的要求。
- 首先,每个容器类(vector、list、deque等)定义了相应的迭代器类型。对于其中的某个类,迭代器可能是指针;而对于另一个类,则可能是对象。不管实现方式如何,迭代器都将提供所需的操作,如*和++(有些类需要的操作可能比其他类多)。
- 其次,每个容器类都有一个超尾标记,当迭代器递增到超越容器的最后一个值后,这个值将被赋给迭代器。每个容器类都有 begin( )和e nd( )方法,它们分别返回一个指向容器的第一个元素和超尾位置的迭代器。每个容器类都使用++操作,让迭代器从指向第一个元素逐步指向超尾位置,从而遍历容器中的每一个元素。
来总结一下 STL 方法。首先是处理容器的算法,应尽可能用通用的术语来表达算法,使之独立于数据类型和容器类型。为使通用算法能够适用于具体情况,应定义能够满足算法需求的迭代器,并把要求加到容器设计上。即基于算法的要求,设计基本迭代器的特征和容器特征。
- 迭代器应具备哪些特征呢?下面是一个简短的列表。
16.4.2 迭代器类型
不同的算法对迭代器的要求也不同。STL 定义了 5 种迭代器,并根据所需的迭代器类型对算法进行了描述。对于这 5 种迭代器,都可以执行解除引用操作(即为它们定义了*运算符),也可进行比较,看其是相等(使用= =运算符,可能被重载了)还是不相等(使用!=运算符,可能被重载了)。如果两个迭代器相同,则对它们执行解除引用操作得到的值将相同。
这5 种迭代器分别是
- 输入迭代器 术语“输入”是从程序的角度说的,即来自容器的信息被视为输入,对输入迭代器解除引用将使程序能够读取容器中的值,但不一定能让程序修改值。因此,需要输入迭代器的算法将不会修改容器中的值。输入迭代器必须能够访问容器中所有的值,这是通过支持++运算符(前缀格式和后缀格式)来实现的。如果将输入迭代器设置为指向容器中的第一个元素,并不断将其递增,直到到达超尾位置,则它将依次指向容器中的每一个元素。顺便说一句,并不能保证输入迭代器第二次遍历容器时,顺序不变。另外,输入迭代器被递增后,也不能保证其先前的值仍然可以被解除引用。基于输入迭代器的任何算法都应当是单通行(single-pass)的,不依赖于前一次遍历时的迭代器值,也不依赖于本次遍历中前面的迭代器值。 注意,输入迭代器是单向迭代器,可以递增,但不能倒退。
- 输出迭代器 STL 使用术语“输出”来指用于将信息从程序传输给容器的迭代器,因此程序的输出就是容器的输入。输出迭代器与输入迭代器相似,只是解除引用让程序能修改容器值,而不能读取。简而言之,对于单通行、只读算法,可以使用输入迭代器;而对于单通行、只写算法,则可以使用输出迭代器。
- 正向迭代器 与输入迭代器和输出迭代器相似,正向迭代器只使用++运算符来遍历容器,所以它每次沿容器向前移动一个元素;然而,与输入和输出迭代器不同的是,它总是按相同的顺序遍历一系列值。另外,将正向迭代器递增后,仍然可以对前面的迭代器值解除引用(如果保存了它),并可以得到相同的值。这些特征使得多次通行算法成为可能。 正向迭代器既可以使得能够读取和修改数据,也可以使得只能读取数据
- 双向迭代器 双向迭代器具有正向迭代器的所有特性,同时支持两种(前缀和后缀)递减运算符。
随机访问迭代器 有些算法(如标准排序和二分检索)要求能够直接跳到容器中的任何一个元素,这叫做随机访问,需要随机访问迭代器。随机访问迭代器具有双向迭代器的所有特性,同时添加了支持随机访问的操作(如指针增加运算)和用于对元素进行排序的关系运算符。
Table 5: 随机访问迭代器操作表 达 式 描 述 a + n 指向 a 所指向的元素后的第 n 个元素 n + a 与a + n 相同 a - n 指向 a 所指向的元素前的第 n 个元素 r += n 等价于 r = r + n r -= n 等价于 r = r – n a[n] 等价于*(a + n) b - a 结果为这样的 n值,即b = a + n a < b 如果 b – a > 0,则为真 a > b 如果 b < a,则为真 a >= b 如果 !( a < b),则为真 a <= b 如果 !( a > b),则为真 其中,X 表示随机迭代器类型,T 表示被指向的类型,a 和b 都是迭代器值,n 为整数,r 为随机迭代器变量或引用。像a+n 这样的表达式仅当 a 和a+n 都位于容器区间(包括超尾)内时才合法。
16.4.3 迭代器层次结构
您可能已经注意到,迭代器类型形成了一个层次结构。正向迭代器具有输入迭代器和输出迭代器的全部功能,同时还有自己的功能;双向迭代器具有正向迭代器的全部功能,同时还有自己的功能;随机访问迭代器具有正向迭代器的全部功能,同时还有自己的功能。根据特定迭代器类型编写的算法可以使用该种迭代器,也可以使用具有所需功能的任何其他迭代器。所以具有随机访问迭代器的容器可以使用为输入迭代器编写的算法。 为何需要这么多迭代器呢?目的是为了在编写算法尽可能使用要求最低的迭代器,并让它适用于容器的最大区间。注意,各种迭代器的类型并不是确定的,而只是一种概念性描述。
Table 6: 迭代器性能迭代器功能 输入 输出 正向 双向 随机访问 ~~ i i~~有 有 有 有 有 解除引用读取 有 无 有 有 有 解除引用写入 无 有 有 有 有 固定和可重复排序 无 无 有 有 有 −−i i−− 无 无 无 有 有 i[n] 无 无 无 无 有 i + n 无 无 无 无 有 i - n 无 无 无 无 有 i += n 无 无 无 无 有 i −= n 无 无 无 无 有 其中,i 为迭代器,n 为整数。
16.4.4 概念、改进和模型
STL 有若干个用 C++语言无法表达的特性,如迭代器种类。因此,虽然可以设计具有正向迭代器特征的类,但不能让编译器将算法限制为只使用这个类。原因在于,正向迭代器是一系列要求,而不是类型。所设计的迭代器类可以满足这种要求,常规指针也能满足这种要求。STL 算法可以使用任何满足其要求的迭代器实现。STL 文献使用术语概念(concept)来描述一系列的要求。因此,存在输入迭代器概念、正向迭代器概念,等等。顺便说一句,如果所设计的容器类需要迭代器,可考虑 STL,它包含用于标准种类的迭代器模板。概念可以具有类似继承的关系。例如,双向迭代器继承了正向迭代器的功能。然而,不能将 C++继承机制用于迭代器。例如,可以将正向迭代器实现为一个类,而将双向迭代器实现为一个常规指针。因此,对C++而言,这种双向迭代器是一种内置类型,不能从类派生而来。然而,从概念上看,它确实能够继承。有些 STL 文献使用术语改进(refinement)来表示这种概念上的继承,因此,双向迭代器是对正向迭代器概念的一种改进。
16.4.5 容器种类
STL 具有容器概念和容器类型。概念是具有名称(如容器、序列容器、关联容器等)的通用类别;容器类型是可用于创建具体容器对象的模板。以前的 11 个容器类型分别是 deque、list、queue、priority_queue、stack、vector、map、multimap、set、multiset 和b itset(本章不讨论 bitset,它是在比特级处理数据的容器);C++11 新增了 forward_list、unordered_map、unordered_multimap、unordered_set 和u nordered_multiset,且不将 bitset 视为容器,而将其视为一种独立的类别。因为概念对类型进行了分类,下面先讨论它们。
容器概念 没有与基本容器概念对应的类型,但概念描述了所有容器类都通用的元素。它是一个概念化的抽象基类——说它概念化,是因为容器类并不真正使用继承机制。换句话说,容器概念指定了所有 STL 容器类都必须满足的一系列要求。 容器是存储其他对象的对象。被存储的对象必须是同一种类型的,它们可以是 OOP 意义上的对象,也可以是内置类型值。存储在容器中的数据为容器所有,这意味着当容器过期时,存储在容器中的数据也将过期(然而,如果数据是指针的话,则它指向的数据并不一定过期)。 不能将任何类型的对象存储在容器中,具体地说,类型必须是可复制构造的和可赋值的。基本类型满足这些要求;只要类定义没有将复制构造函数和赋值运算符声明为私有或保护的,则也满足这种要求。C++11 改进了这些概念,添加了术语可复制插入(CopyInsertable)和可移动插入(MoveInsertable),但这里只进行简单的概述。 基本容器不能保证其元素都按特定的顺序存储,也不能保证元素的顺序不变,但对概念进行改进后,则可以增加这样的保证。所有的容器都提供某些特征和操作。
Table 7: 一些基本的容器特征表 达 式 返 回 类 型 说 明 复 杂 度 X :: iterator 指向 T 的迭代器类型 满足正向迭代器要求的任何迭代器 编译时间 X :: value_type T T 的类型 编译时间 X u; 创建一个名为 u 的空容器 固定 X( ); 创建一个匿名的空容器 固定 X u(a); 调用复制构造函数后 u == a 线性 X u = a; 作用同 X u(a); 线性 r = a; X& 调用赋值运算符后 r == a 线性 (&a)->~X( ) void 对容器中每个元素应用析构函数 线性 a.begin( ) 迭代器 返回指向容器第一个元素的迭代器 固定 a.end( ) 迭代器 返回超尾值迭代器 固定 a.size( ) 无符号整型 返回元素个数,等价于 a.end( )– a.begin( ) 固定 a.swap(b) void 交换 a 和b 的内容 固定 a = = b 可转换为 bool 如果 a 和b 的长度相同,且a 中每个元素都等于(= =为真)b 中相应的元素,则为真 线性 a != b 可转换为 bool 返回!(a= =b) 线性 其中,X 表示容器类型,如v ector;T 表示存储在容器中的对象类型;a 和b 表示类型为 X 的值;r 表示类型为 X&的值;u 表示类型为 X 的标识符(即如果 X 表示 vector
,则u 是一个 vector 对象)。“复杂度”一列描述了执行操作所需的时间。这个表列出了 3 种可能性,从快到慢依次为: 编译时间; 固定时间; 线性时间。如果复杂度为编译时间,则操作将在编译时执行,执行时间为 0。固定复杂度意味着操作发生在运行阶段,但独立于对象中的元素数目。线性复杂度意味着时间与元素数目成正比。即如果 a 和b 都是容器,则a = = b 具有线性复杂度,因为= =操作必须用于容器中的每个元素。实际上,这是最糟糕的情况。如果两个容器的长度不同,则不需要作任何的单独比较。 C++11 新增的容器要求 复制构造和复制赋值以及移动构造和移动赋值之间的差别在于,复制操作保留源对象,而移动操作可修改源对象,还可能转让所有权,而不做任何复制。如果源对象是临时的,移动操作的效率将高于常规复制。第1 8 章将更详细地介绍移动语义。
Table 8: C++11 新增的基本容器要求表 达 式 返 回 类 型 说 明 复 杂 度 X u(rv); 调用移动构造函数后,u 的值与 rv 的原始值相同 线性 X u = rv; 作用同 X u(rv); a = rv; X& 调用移动赋值运算符后,u 的值与 rv 的原始值相同 线性 a.cbegin( ) const_iterator 返回指向容器第一个元素的 const 迭代器 固定 a.cend( ) const_iterator 返回超尾值 const 迭代器 固定 在这个表中,rv 表示类型为 X 的非常量右值,如函数的返回值。另外,在表中,要求 X::iterator 满足正向迭代器的要求,而以前只要求它不是输出迭代器。
序列 可以通过添加要求来改进基本的容器概念。序列(sequence)是一种重要的改进,因为 7 种S TL 容器类型(deque、C++11 新增的 forward_list、list、queue、priority_queue、stack 和v ector)都是序列(本书前面说过,队列让您能够在队尾添加元素,在队首删除元素。deque 表示的双端队列允许在两端添加和删除元素)。序列概念增加了迭代器至少是正向迭代器这样的要求,这保证了元素将按特定顺序排列,不会在两次迭代之间发生变化。array 也被归类到序列容器,虽然它并不满足序列的所有要求。 序列还要求其元素按严格的线性顺序排列,即存在第一个元素、最后一个元素,除第一个元素和最后一个元素外,每个元素前后都分别有一个元素。数组和链表都是序列,但分支结构(其中每个节点都指向两个子节点)不是。 因为序列中的元素具有确定的顺序,因此可以执行诸如将值插入到特定位置、删除特定区间等操作。
Table 9: 序列的要求表 达 式 返 回 类 型 说 明 X a(n, t); 声明一个名为 a 的由 n 个t 值组成的序列 X(n, t) 创建一个由 n 个t 值组成的匿名序列 X a(i, j) 声明一个名为 a 的序列,并将其初始化为区间[ i,j)的内容 X(i, j) 创建一个匿名序列,并将其初始化为区间[ i,j)的内容 a. insert(p, t) 迭代器 将t 插入到 p 的前面 a.insert(p, n, t) void 将n 个t 插入到 p 的前面 a.insert(p, i, j) void 将区间[ i,j)中的元素插入到 p 的前面 a.erase(p) 迭代器 删除 p 指向的元素 a.erase(p, q) 迭代器 删除区间[ p,q)中的元素 a.clear( ) void 等价于 erase(begin( ), end( )) 因为模板类 deque、list、queue、priority_queue、stack 和v ector 都是序列概念的模型,所以它们都支持表所示的运算符。除此之外,这6 个模型中的一些还可使用其他操作。在允许的情况下,它们的复杂度为固定时间。
Table 10: 序列的可选要求表 达 式 返 回 类 型 含 义 容 器 a.front( ) T& *a.begin( ) vector、list、deque a.back( ) T& - -a.end( ) vector、list、deque a.push_front(t) void a.insert(a.begin( ), t) list、deque a.push_back(t) void a.insert(a.end( ), t) vector、list、deque a.pop_front(t) void a.erase(a.begin( )) list、deque a.pop_back(t) void a.erase(- -a.end( )) vector、list、deque a[n] T& *(a.begin( )+ n) vector、deque a.at(t) T& *(a.begin( )+ n) vector、deque 首先,a[n]和a.at(n)都返回一个指向容器中第 n 个元素(从0 开始编号)的引用。它们之间的差别在于,如果 n 落在容器的有效区间外,则a.at(n)将执行边界检查,并引发 out_of_range 异常。其次,可能有人会问,为何为 list 和d eque 定义了 push_front( ),而没有为 vector 定义?假设要将一个新值插入到包含 100 个元素的矢量的最前面。要腾出空间,必须将第 99 个元素移到位置 100,然后把第 98 个元素移动到位置 99,依此类推。这种操作的复杂度为线性时间,因为移动 100 个元素所需的时间为移动单个元素的 100倍。但表 16.8 的操作被假设为仅当其复杂度为固定时间时才被实现。链表和双端队列的设计允许将元素添加到前端,而不用移动其他元素,所以它们可以以固定时间的复杂度来实现 push_front( )。
16.4.4 关联容器
关联容器(associative container)是对容器概念的另一个改进。关联容器将值与键关联在一起,并使用键来查找值。前面说过,对于容器 X,表达式 X::value_type 通常指出了存储在容器中的值类型。对于关联容器来说,表达式 X::key_type 指出了键的类型。 关联容器的优点在于,它提供了对元素的快速访问。与序列相似,关联容器也允许插入新元素,但不能指定元素的插入位置。原因是关联容器通常有用于确定数据放置位置的算法,以便能够快速检索信息。 关联容器通常是使用某种树实现的。树是一种数据结构,其根节点链接到一个或两个节点,而这些节点又链接到一个或两个节点,从而形成分支结构。像链表一样,节点使得添加或删除数据项比较简单;但相对于链表,树的查找速度更快。 STL 提供了 4 种关联容器:set、multiset、map 和m ultimap。前两种是在头文件 set(以前分别为 set.h 和m ultiset.h)中定义的,而后两种是在头文件 map(以前分别为 map.h 和m ultimap.h)中定义的。
- set 最简单的关联容器是 set,其值类型与键相同,键是唯一的,这意味着集合中不会有多个相同的键。确实,对于 set 来说,值就是键。multiset 类似于 set,只是可能有多个值的键相同。STL set 模拟了多个概念,它是关联集合,可反转,可排序,且键是唯一的,所以不能存储多个相同的值。与v ector 和l ist 相似,set 也使用模板参数来指定要存储的值类型
- multimap 与s et 相似,multimap 也是可反转的、经过排序的关联容器,但键和值的类型不同,且同一个键可能与多个值相关联。 基本的 multimap 声明使用模板参数指定键的类型和存储的值类型。
16.4.5 无序关联容器(C++11)
无序关联容器是对容器概念的另一种改进。与关联容器一样,无序关联容器也将值与键关联起来,并使用键来查找值。但底层的差别在于,关联容器是基于树结构的,而无序关联容器是基于数据结构哈希表的,这旨在提高添加和删除元素的速度以及提高查找算法的效率。有4 种无序关联容器,它们是 unordered_set、unordered_multiset、unordered_map 和u nordered_multimap,将在附录 G 更详细地介绍。
16.5 函数对象
很多 STL 算法都使用函数对象——也叫函数符(functor)。函数符是可以以函数方式与( )结合使用的任意对象。这包括函数名、指向函数的指针和重载了( )运算符的类对象(即定义了函数 operator( )( )的类)。
16.5.1 函数符概念
- 正如 STL 定义了容器和迭代器的概念一样,它也定义了函数符概念。
- 生成器(generator)是不用参数就可以调用的函数符。
- 一元函数(unary function)是用一个参数可以调用的函数符。
- 二元函数(binary function)是用两个参数可以调用的函数符。
- 返回 bool 值的一元函数是谓词(predicate);
- 返回 bool 值的二元函数是二元谓词(binary predicate)。
- 正如 STL 定义了容器和迭代器的概念一样,它也定义了函数符概念。
16.5.2 预定义的函数符
STL 定义了多个基本函数符,它们执行诸如将两个值相加、比较两个值是否相等操作。提供这些函数对象是为了支持将函数作为参数的 STL 函数。对于所有内置的算术运算符、关系运算符和逻辑运算符,STL 都提供了等价的函数符。表列出了这些函数符的名称。它们可以用于处理 C++内置类型或任何用户定义类型(如果重载了相应的运算符)。
运算符 相应的函数符 + plus - minus * multiplies % modulus - negate == equal_to != not_equal_to > greater < less >= greater_equal <= less_equal && logical_and 1 logical_or ! logical_not 16.5.3 自适应函数符和函数适配器
实际上 STL 有5 个相关的概念:自适应生成器(adaptable generator)、自适应一元函数(adaptable unary function)、自适应二元函数(adaptable binary function)、自适应谓词(adaptable predicate)和自适应二元谓词(adaptable binary predicate)。 使函数符成为自适应的原因是,它携带了标识参数类型和返回类型的 typedef 成员。这些成员分别是 result_type、first_argument_type 和s econd_argument_type,它们的作用是不言自明的。例如,plus
对象的返回类型被标识为 plus ::result_type,这是 int 的t ypedef。 函数符自适应性的意义在于:函数适配器对象可以使用函数对象,并认为存在这些 typedef 成员。例如,接受一个自适应函数符参数的函数可以使用 result_type 成员来声明一个与函数的返回类型匹配的变量。 STL 提供了使用这些工具的函数适配器类。
16.6 算法
STL 包含很多处理容器的非成员函数,前面已经介绍过其中的一些:sort( )、copy( )、find( )、random_shuffle( )、set_union( )、set_intersection( )、set_difference( )和t ransform( )。可能已经注意到,它们的总体设计是相同的,都使用迭代器来标识要处理的数据区间和结果的放置位置。有些函数还接受一个函数对象参数,并使用它来处理数据。 对于算法函数设计,有两个主要的通用部分。首先,它们都使用模板来提供泛型;其次,它们都使用迭代器来提供访问容器中数据的通用表示。统一的容器设计使得不同类型的容器之间具有明显关系。
16.6.1 算法组
- STL 将算法库分成 4组:
- 非修改式序列操作;非修改式序列操作对区间中的每个元素进行操作。这些操作不修改容器的内容。
- 修改式序列操作;修改式序列操作也对区间中的每个元素进行操作。然而,顾名思义,它们可以修改容器的内容。可以修改值,也可以修改值的排列顺序。
- 排序和相关操作;包括多个排序函数(包括 sort( ))和其他各种函数,包括集合操作。
- 通用数字运算。数字操作包括将区间的内容累积、计算两个容器的内部乘积、计算小计、计算相邻对象差的函数。通常,这些都是数组的操作特性,因此 vector 是最有可能使用这些操作的容器。
- STL 将算法库分成 4组:
16.6.2 算法的通用特征
正如您多次看到的,STL 函数使用迭代器和迭代器区间。从函数原型可知有关迭代器的假设。与I nputIterator 一样,Predicate 也是模板参数名称,可以为 T 或U。然而,STL 选择用 Predicate 来提醒用户,实参应模拟 Predicate 概念。同样,STL 使用诸如 Generator 和B inaryPredicate 等术语来指示必须模拟其他函数对象概念的参数。请记住,虽然文档可指出迭代器或函数符需求,但编译器不会对此进行检查。如果您使用了错误的迭代器,则编译器试图实例化模板时,将显示大量的错误消息。
16.6.3 STL 和s tring 类
string 类虽然不是 STL 的组成部分,但设计它时考虑到了 STL。例如,它包含 begin( )、end( )、rbegin( )和r end( )等成员,因此可以使用 STL 接口。
16.6.4 函数和容器方法
有时可以选择使用 STL 方法或 STL 函数。通常方法是更好的选择。首先,它更适合于特定的容器;其次,作为成员函数,它可以使用模板类的内存管理工具,从而在需要时调整容器的长度。
16.6.5 使用 STL
STL 是一个库,其组成部分被设计成协同工作。STL 组件是工具,但也是创建其他工具的基本部件。使用 STL 时应尽可能减少要编写的代码。STL 通用、灵活的设计将节省大量工作。另外,STL 设计者就是非常关心效率的算法人员,算法是经过仔细选择的,并且是内联的。
16.7 其他库
16.7.1 vector、valarray 和a rray
您可能会问,C++为何提供三个数组模板:vector、valarray 和a rray。这些类是由不同的小组开发的,用于不同的目的。vector 模板类是一个容器类和算法系统的一部分,它支持面向容器的操作,如排序、插入、重新排列、搜索、将数据转移到其他容器中等。而v alarray 类模板是面向数值计算的,不是 STL 的一部分。例如,它没有 push_back( )和i nsert( )方法,但为很多数学运算提供了一个简单、直观的接口。最后,array 是为替代内置数组而设计的,它通过提供更好、更安全的接口,让数组更紧凑,效率更高。Array 表示长度固定的数组,因此不支持 push_back( )和i nsert( ),但提供了多个 STL 方法,包括 begin( )、end( )、rbegin( )和r end( ),这使得很容易将 STL 算法用于 array 对象。
16.7.2 模板 initializer_list(C++11)
模板 initializer_list 是C++11 新增的。您可使用初始化列表语法将 STL 容器初始化为一系列值。通常,考虑到 C++11 新增的通用初始化语法,可使用表示法{}而不是()来调用类构造函数
16.7.3 使用 initializer_list
要在代码中使用 initializer_list 对象,必须包含头文件 initializer_list。这个模板类包含成员函数 begin( )和e nd( ),您可使用这些函数来访问列表元素。它还包含成员函数 size( ),该函数返回元素数。
16.8 总结
C++提供了一组功能强大的库,这些库提供了很多常见编程问题的解决方案以及简化其他问题的工具。string 类为将字符串作为对象来处理提供了一种方便的方法。string 类提供了自动内存管理功能以及众多处理字符串的方法和函数。例如,这些方法和函数让您能够合并字符串、将一个字符串插入到另一个字符串中、反转字符串、在字符串中搜索字符或子字符串以及执行输入和输出操作。 诸如 auto_ptr 以及 C++11 新增的 shared_ptr 和u nique_ptr 等智能指针模板使得管理由 new 分配的内存更容易。如果使用这些智能指针(而不是常规指针)来保存 new 返回的地址,则不必在以后使用删除运算符。智能指针对象过期时,其析构函数将自动调用 delete 运算符。 STL 是一个容器类模板、迭代器类模板、函数对象模板和算法函数模板的集合,它们的设计是一致的,都是基于泛型编程原则的。算法通过使用模板,从而独立于所存储的对象的类型;通过使用迭代器接口,从而独立于容器的类型。迭代器是广义指针。 STL 使用术语“概念”来描述一组要求。例如,正向迭代器的概念包含这样的要求,即正向迭代器能够被解除引用,以便读写,同时能够被递增。概念真正的实现方式被称为概念的“模型”。例如,正向迭代器概念可以是常规指针或导航链表的对象。基于其他概念的概念叫作“改进”。例如,双向迭代器是正向迭代器概念的改进。 诸如 vector 和s et 等容器类是容器概念(如容器、序列和关联容器)的模型。STL 定义了多种容器类模板:vector、deque、list、set、multiset、map、multimap 和b itset;还定义了适配器类模板 queue、priority_queue 和s tack;这些类让底层容器类能够提供适配器类模板名称所建议的特性接口。因此,stack 虽然在默认情况下是基于 vector的,但仍只允许在栈顶进行插入和删除。C++11 新增了 forward_list、unordered_set、unordered_multiset、unordered_map 和u nordered_multimap。 有些算法被表示为容器类方法,但大量算法都被表示为通用的、非成员函数,这是通过将迭代器作为容器和算法之间的接口得以实现的。这种方法的一个优点是:只需一个诸如 for_each( )或c opy( )这样的函数,而不必为每种容器提供一个版本;另一个优点是:STL 算法可用于非 STL 容器,如常规数组、string 对象、array 对象以及您设计的秉承 STL 迭代器和容器规则的任何类。 容器和算法都是由其提供或需要的迭代器类型表征的。应当检查容器是否具备支持算法要求的迭代器概念。例如,for_each( )算法使用一个输入迭代器,所有的 STL 容器类类型都满足其最低要求;而s ort( )则要求随机访问迭代器,并非所有的容器类都支持这种迭代器。如果容器类不能满足特定算法的要求,则可能提供一个专用的方法。例如,list 类包含一个基于双向迭代器的 sort( )方法,因此它可以使用该方法,而不是通用函数。 STL 还提供了函数对象(函数符),函数对象是重载了( )运算符(即定义了 operator( )( )方法)的类。可以使用函数表示法来调用这种类的对象,同时可以携带额外的信息。自适应函数符有 typedef 语句,这种语句标识了函数符的参数类型和返回类型。这些信息可供其他组件(如函数适配器)使用。 通过表示常用的容器类型,并提供各种使用高效算法实现的常用操作(全部是通用的方式实现的),STL 提供了一个非常好的可重用代码源。可以直接使用 STL 工具来解决编程问题,也可以把它们作为基本部件,来构建所需的解决方案。 模板类 complex 和v alarray 支持复数和数组的数值运算。
16.9 复习题
16.10 编程练习
第1 7 章 输入、输出和文件
- 本章内容包括:
- C++角度的输入和输出。
- iostream 类系列。
- 重定向。
- ostream 类方法。
- 格式化输出。
- istream 类方法。
- 流状态。
- 文件 I/O。
- 使用 ifstream 类从文件输入。
- 使用 ofstream 类输出到文件。
- 使用 fstream 类进行文件输入和输出。
- 命令行处理。
- 二进制文件。
- 随机文件访问。
- 内核格式化。
17.1 C++输入和输出概述
正如 C 实现自带了一个标准函数库一样,C++也自带了一个标准类库。首先,标准类库是一个非正式的标准,只是由头文件 iostream 和f stream 中定义的类组成。ANSI/ISO C++委员会决定把这个类正式作为一个标准类库,并添加其他一些标准类,如第 16 章讨论的那些类。本章将讨论标准 C++ I/O。但首先看一看 C++ I/O 的概念框架。
17.1.1 流和缓冲区
C++程序把输入和输出看作字节流。输入时,程序从输入流中抽取字节;输出时,程序将字节插入到输出流中。流充当了程序和流源或流目标之间的桥梁。这使得 C++程序可以以相同的方式对待来自键盘的输入和来自文件的输入。C++程序只是检查字节流,而不需要知道字节来自何方。同理,通过使用流,C++程序处理输出的方式将独立于其去向。因此管理输入包含两步:将流与输入去向的程序关联起来; 将流与文件连接起来。同样,对输出的管理包括将输出流连接到程序以及将输出目标与流关联起来。 通常,通过使用缓冲区可以更高效地处理输入和输出。缓冲区是用作中介的内存块,它是将信息从设备传输到程序或从程序传输给设备的临时存储工具。通常,像磁盘驱动器这样的设备以 512 字节(或更多)的块为单位来传输信息,而程序通常每次只能处理一个字节的信息。缓冲区帮助匹配这两种不同的信息传输速率。输出时,程序首先填满缓冲区,然后把整块数据传输给硬盘,并清空缓冲区,以备下一批输出使用。这被称为刷新缓冲区(flushing the buffer)。
17.1.2 流、缓冲区和 iostream 文件
管理流和缓冲区的工作有点复杂,但i ostream(以前为 iostream.h)文件中包含一些专门设计用来实现、管理流和缓冲区的类。C++98 版本 C++ I/O 定义了一些类模板,以支持 char 和w char_t 数据;C++11 添加了 char16_t 和c har32_t 具体化。通过使用 typedef 工具,C++使得这些模板 char 具体化能够模仿传统的非模板 I/O 实现。
- 下面是其中的一些类:
- streambuf 类为缓冲区提供了内存,并提供了用于填充缓冲区、访问缓冲区内容、刷新缓冲区和管理缓冲区内存的类方法;
- ios_base 类表示流的一般特征,如是否可读取、是二进制流还是文本流等;
- ios 类基于 ios_base,其中包括了一个指向 streambuf 对象的指针成员;
- ostream 类是从 ios 类派生而来的,提供了输出方法;
- istream 类也是从 ios 类派生而来的,提供了输入方法;
- iostream 类是基于 istream 和o stream 类的,因此继承了输入方法和输出方法。
- C++的i ostream 类库管理了很多细节。
- cin 对象对应于标准输入流。在默认情况下,这个流被关联到标准输入设备(通常为键盘)。wcin 对象与此类似,但处理的是 wchar_t 类型。
- cout 对象与标准输出流相对应。在默认情况下,这个流被关联到标准输出设备(通常为显示器)。wcout 对象与此类似,但处理的是 wchar_t 类型。
- cerr 对象与标准错误流相对应,可用于显示错误消息。在默认情况下,这个流被关联到标准输出设备(通常为显示器)。这个流没有被缓冲,这意味着信息将被直接发送给屏幕,而不会等到缓冲区填满或新的换行符。wcerr 对象与此类似,但处理的是 wchar_t 类型。
- clog 对象也对应着标准错误流。在默认情况下,这个流被关联到标准输出设备(通常为显示器)。这个流被缓冲。wclog 对象与此类似,但处理的是 wchar_t 类型。
- 对象代表流——这意味着什么呢?当i ostream 文件为程序声明一个 cout 对象时,该对象将包含存储了与输出有关的信息的数据成员,如显示数据时使用的字段宽度、小数位数、显示整数时采用的计数方法以及描述用来处理输出流的缓冲区的 streambuf 对象的地址。
- 下面是其中的一些类:
17.1.3 重定向
标准输入和输出流通常连接着键盘和屏幕。但很多操作系统(包括 UNIX、Linux 和W indows)都支持重定向,这个工具使得能够改变标准输入和标准输出。cout 代表的标准输出流是程序输出的常用通道。标准错误流(由c err 和c log 代表)用于程序的错误消息。默认情况下,这3 个对象都被发送给显示器。但对标准输出重定向并不会影响 cerr 或c log,因此,如果使用其中一个对象来打印错误消息,程序将在屏幕上显示错误消息,即使常规的 cout 输出被重定向到其他地方。有些操作系统也允许对标准错误进行重定向。例如,在U NIX 和L inux中,运算符 2>重定向标准错误。
17.2 使用 cout 进行输出
正如前面指出的,C++将输出看作字节流(根据实现和平台的不同,可能是 8位、16 位或 32 位的字节,但都是字节),但在程序中,很多数据被组织成比字节更大的单位。因此,ostream 类最重要的任务之一是将数值类型(如i nt 或f loat)转换为以文本形式表示的字符流。
17.2.1 重载的<<运算符
在C++中,与C 一样,<<运算符的默认含义是按位左移运算符(参见附录 E)。显然,这与输出的关系不大。但o stream 类重新定义了<<运算符,方法是将其重载为输出。在这种情况下,<<叫作插入运算符,而不是左移运算符(左移运算符由于其外观(像向左流动的信息流)而获得这种新角色)。
- 插入运算符被重载,使之能够识别 C++中所有的基本类型:
- unsigned char;
- signed char;
- char;
- short;
- unsigned short;
- int;
- unsiged int;
- long;
- unsigned long;
- long long(C++11);
- unsigned long long(C++11);
- float;
- double;
- long double。
- 输出和指针;ostream 类还为下面的指针类型定义了插入运算符函数:
- const signed char *;
- const unsigned char *;
- const char *;
- void * ;对于其他类型的指针,C++将其对应于 void * ,并打印地址的数值表示。如果要获得字符串的地址,则必须将其强制转换为其他类型,
- 拼接输出 插入运算符的所有化身的返回类型都是 ostream &。
- 插入运算符被重载,使之能够识别 C++中所有的基本类型:
17.2.2 其他 ostream 方法
除了各种 operator<<( )函数外,ostream 类还提供了 put( )方法和 write( )方法,前者用于显示字符,后者用于显示字符串。write( )的第一个参数提供了要显示的字符串的地址,第二个参数指出要显示多少个字符。使用 cout 调用 write( )时,将调用 char 具体化,因此返回类型为 ostream &。
17.2.3 刷新输出缓冲区
由于 ostream 类对 cout 对象处理的输出进行缓冲,所以输出不会立即发送到目标地址,而是被存储在缓冲区中,直到缓冲区填满。然而,对于屏幕输出来说,首先填充缓冲区的重要性要低得多。在屏幕输出时,程序不必等到缓冲区被填满。例如,将换行符发送到缓冲区后,将刷新缓冲区。另外,正如前面指出的,多数 C++实现都会在输入即将发生时刷新缓冲区。如果实现不能在所希望时刷新输出,可以使用两个控制符中的一个来强行进行刷新。控制符 flush 刷新缓冲区,而控制符 endl 刷新缓冲区,并插入一个换行符。这两个控制符的使用方式与变量名相同。事实上,控制符也是函数。例如,可以直接调用 flush( )来刷新 cout 缓冲区。然而,ostream 类对<<插入运算符进行了重载,使得下述表达式将被替换为函数调用 flush(cout):
cout << flush
。17.2.4 用c out 进行格式化
ostream 插入运算符将值转换为文本格式。因为每个值的显示宽度都等于它的长度,因此必须显式地在值之间提供空格;否则,相邻的值将不会被分开。
- 在默认情况下,格式化值的方式如下。
- 对于 char值,如果它代表的是可打印字符,则将被作为一个字符显示在宽度为一个字符的字段中。
- 对于数值整型,将以十进制方式显示在一个刚好容纳该数字及负号(如果有的话)的字段中。
- 字符串被显示在宽度等于该字符串长度的字段中。
- 浮点数的默认行为有变化。下面详细说明了老式实现和新实现之间的区别。
- 新式:浮点类型被显示为 6位,末尾的 0 不显示(注意,显示的数字位数与数字被存储时精度没有任何关系)。数字以定点表示法显示还是以科学计数法表示(参见第 3章),取决于它的值。具体来说,当指数大于等于 6 或小于等于− 5时,将使用科学计数法表示。另外,字段宽度恰好容纳数字和负号(如果有的话)。默认的行为对应于带% g 说明符的标准 C 库函数 fprintf( )。
- 老式:浮点类型显示为带 6 位小数,末尾的 0 不显示(注意,显示的数字位数与数字被存储时的精度没有任何关系)。数字以定点表示法显示还是以科学计数法表示(参见第 3章),取决于它的值。另外,字段宽度恰好容纳数字和负号(如果有的话)。
- 修改显示时使用的计数系统
ostream 类是从 ios 类派生而来的,而后者是从 ios_base 类派生而来的。ios_base 类存储了描述格式状态的信息。通过使用控制符(manipulator),可以控制显示整数时使用的计数系统。通过使用 ios_base 的成员函数,可以控制字段宽度和小数位数。由于 ios_base 类是 ostream 的间接基类,因此可以将其方法用于 ostream 对象(或子代),如c out。要控制整数以十进制、十六进制还是八进制显示,可以使用 dec、hex 和o ct 控制符。
hex(cout);
cout << hex;
- 调整字段宽度
可以使用 width 成员函数将长度不同的数字放到宽度相同的字段中,该方法的原型为:
int width();
int width(int i);
第一种格式返回字段宽度的当前设置;第二种格式将字段宽度设置为 i 个空格,并返回以前的字段宽度值。这使得能够保存以前的值,以便以后恢复宽度值时使用。 width( )方法只影响将显示的下一个项目,然后字段宽度将恢复为默认值。由于 width( )是成员函数,因此必须使用对象(这里为 cout)来调用它。C++永远不会截短数据,因此如果试图在宽度为 2 的字段中打印一个 7 位值,C++将增宽字段,以容纳该数据(在有些语言中,如果数据长度与字段宽度不匹配,将用星号填充字段。C/C++的原则是:显示所有的数据比保持列的整洁更重要。C++视内容重于形式)。 - 填充字符
在默认情况下,cout 用空格填充字段中未被使用的部分,可以用 fill( )成员函数来改变填充字符。例如,下面的函数调用将填充字符改为星号:
cout.fill('\*');
。这对于检查打印结果,防止接收方添加数字很有用。注意,与字段宽度不同的是,新的填充字符将一直有效,直到更改它为止。 - 设置浮点数的显示精度
浮点数精度的含义取决于输出模式。在默认模式下,它指的是显示的总位数。在定点模式和科学模式下(稍后将讨论),精度指的是小数点后面的位数。已经知道,C++的默认精度为 6位(但末尾的 0 将不显示)。precision( )成员函数使得能够选择其他值。例如,下面语句将 cout 的精度设置为 2:
cout.precision(2);
和w idth( )的情况不同,但与 fill( )类似,新的精度设置将一直有效,直到被重新设置。 - 打印末尾的 0 和小数点
对于有些输出(如价格或栏中的数字),保留末尾的 0 将更为美观。iostream 系列类没有提供专门用于完成这项任务的函数,但i os_base 类提供了一个 setf( )函数(用于 set 标记),能够控制多种格式化特性。这个类还定义了多个常量,可用作该函数的参数。例如,下面的函数调用使 cout 显示末尾小数点:
cout.setf(iso_base::showpoint);
showpoint 是i os_base 类声明中定义的类级静态常量。类级意味着如果在成员函数定义的外面使用它,则必须在常量名前面加上作用域运算符(::)。因此 ios_base::showpoint 指的是在 ios_base 类中定义的一个常量。 再谈 setf( ) setf( )方法控制了小数点被显示时其他几个格式选项,因此来仔细研究一下它。ios_base 类有一个受保护的数据成员,其中的各位(这里叫作标记)分别控制着格式化的各个方面,例如计数系统、是否显示末尾的 0等。打开一个标记称为设置标记(或位),并意味着相应的位被设置为 1。位标记是编程开关,相当于设置 DIP 开关以配置计算机硬件。例如,hex、dec 和o ct 控制符调整控制计数系统的 3 个标记位。setf( )函数提供了另一种调整标记位的途径。
setf( )函数有两个原型。
fmtflags setf(fmtflags);
其中,fmtflags 是b itmask 类型(参见后面的“注意”)的t ypedef名,用于存储格式标记。该名称是在 ios_base 类中定义的。这个版本的 setf( )用来设置单个位控制的格式信息。参数是一个 fmtflags值,指出要设置哪一位。返回值是类型为 fmtflags 的数字,指出所有标记以前的设置。如果打算以后恢复原始设置,则可以保存这个值。bitmask 类型是一种用来存储各个位值的类型。它可以是整型、枚举,也可以是 STL bitset 容器。这里的主要思想是,每一位都是可以单独访问的,都有自己的含义。iostream 软件包使用 bitmask 来存储状态信息。Table 11: 格式常量常量 含义 ios_base::boolalpha 输入和输出 bool值,可以为 true 或f alse ios_base::showbase 对于输出,使用 C++基数前缀(0,0x) ios_base::showpoint 显示末尾的小数点 ios_base::uppercase 对于 16 进制输出,使用大写字母,E 表示法 ios_base::showpos 在正数前面加上+ fmtflags setf(fmtflags, fmtflags);
函数的这种重载格式用于设置由多位控制的格式选项。第一参数和以前一样,也是一个包含了所需设置的 fmtflags值。第二参数指出要清除第一个参数中的哪些位。ios_base 类定义了可按这种方式处理的 3 组格式标记。每组标记都由一个可用作第二参数的常量和两三个可用作第一参数的常量组成。第二参数清除一批相关的位,然后第一参数将其中一位设置为 1。表列出了用作 setf( )的第二参数的常量的名称、可用作第一参数的相关常量以及它们的含义。调用 setf( )的效果可以通过 unsetf( )消除,后者的原型如下:void unsetf(fmtflags mask);
Table 12: setf(long, long)的参数第二个参数 第一个参数 含义 ios_base::basefield ios_base::dec 使用基数 10 ios_base::oct 使用基数 8 ios_base::hex 使用基数 16 ios_base::floatfield ios_base::fixed 使用定点计数法 ios_base::scientific 使用科学计数法 ios_base::adjustfield ios_base::left 使用左对齐 ios_base::right 使用右对齐 ios_base::internal 符号或基数前缀左对齐,值右对齐 在C++标准中,定点表示法和科学表示法都有下面两个特征: 精度指的是小数位数,而不是总位数; 显示末尾的 0。
标准控制符 使用 setf( )不是进行格式化的、对用户最为友好的方法,C++提供了多个控制符,能够调用 setf( ),并自动提供正确的参数。前面已经介绍过 dec、hex 和o ct,这些控制符(多数都不适用于老式 C++实现)的工作方式都与 hex 相似。
头文件 iomanip 使用 iostream 工具来设置一些格式值(如字段宽度)不太方便。为简化工作,C++在头文件 iomanip 中提供了其他一些控制符,它们能够提供前面讨论过的服务,但表示起来更方便。3 个最常用的控制符分别是 setprecision( )、setfill( )和s etw( ),它们分别用来设置精度、填充字符和字段宽度。与前面讨论的控制符不同的是,这3 个控制符带参数。setprecision( )控制符接受一个指定精度的整数参数;setfill( ) 控制符接受一个指定填充字符的 char 参数;setw( )控制符接受一个指定字段宽度的整数参数。由于它们都是控制符,因此可以用 cout 语句连接起来。这样,setw( )控制符在显示多列值时尤其方便。它对于每一行输出,都多次修改了字段宽度和填充字符,同时使用了一些较新的标准控制符。
- 在默认情况下,格式化值的方式如下。
17.3 使用 cin 进行输入
现在来介绍输入,即如何给程序提供数据。cin 对象将标准输入表示为字节流。通常情况下,通过键盘来生成这种字符流。如果键入字符序列 2011,cin 对象将从输入流中抽取这几个字符。输入可以是字符串的一部分、int值、float值,也可以是其他类型。因此,抽取还涉及了类型转换。cin 对象根据接收值的变量的类型,使用其方法将字符序列转换为所需的类型。 通常,可以这样使用 cin: cin >> value_holder;
其中,value_holder 为存储输入的内存单元,它可以是变量、引用、被解除引用的指针,也可以是类或结构的成员。cin 解释输入的方式取决于 value_holder 的数据类型。这些运算符函数被称为格式化输入函数(formatted input functions),因为它们可以将输入数据转换为目标指定的格式。
- istream类(在i ostream 头文件中定义)重载了抽取运算符>>,使之能够识别下面这些基本类型:
- signed char &;
- unsigned char &;
- char &;
- short &;
- unsigned short &;
- int &;
- unsigned int &;
- long &;
- unsigned long &;
- long long &(C++11);
- unsigned long long &(C++11);
- float &;
- double &;
- long double &。
- 可以将 hex、oct 和d ec 控制符与 cin 一起使用,来指定将整数输入解释为十六进制、八进制还是十进制格式。
istream 类还为下列字符指针类型重载了>>抽取运算符:signed char * ; char * ; unsigned char * 。 对于这种类型的参数,抽取运算符将读取输入中的下一个单词,将它放置到指定的地址,并加上一个空值字符,使之成为一个字符串。
17.3.1 cin>>如何检查输入
不同版本的抽取运算符查看输入流的方法是相同的。它们跳过空白(空格、换行符和制表符),直到遇到非空白字符。即使对于单字符模式(参数类型为 char、unsigned char 或s igned char),情况也是如此,但对于 C 语言的字符输入函数,情况并非如此。在单字符模式下,>>运算符将读取该字符,将它放置到指定的位置。在其他模式下,>>运算符将读取一个指定类型的数据。也就是说,它读取从非空白字符开始,到与目标类型不匹配的第一个字符之间的全部内容。
17.3.2 流状态
我们来进一步看看不适当的输入会造成什么后果。cin 或c out 对象包含一个描述流状态(stream state)的数据成员(从i os_base 类那里继承的)。流状态(被定义为 iostate 类型,而i ostate 是一种 bitmask 类型)由3 个i os_base 元素组成:eofbit、badbit 或f ailbit,其中每个元素都是一位,可以是 1(设置)或0(清除)。当c in 操作到达文件末尾时,它将设置 eofbit;当c in 操作未能读取到预期的字符时(像前一个例子那样),它将设置 failbit。I/O 失败(如试图读取不可访问的文件或试图写入写保护的磁盘),也可能将 failbit 设置为 1。在一些无法诊断的失败破坏流时,badbit 元素将被设置(实现没有必要就哪些情况下设置 failbit,哪些情况下设置 badbit 达成一致)。当全部 3 个状态位都设置为 0时,说明一切顺利。程序可以检查流状态,并使用这种信息来决定下一步做什么。
Table 13: 流状态成员 描述 eofbit 如果到达文件尾,则设置为 1 badbit 如果流被破坏,则设置为 1;例如,文件读取错误 failbit 如果输入操作未能读取预期的字符或输出操作没有写入预期的字符,则设置为 1 goodbit 另一种表示 0 的方法 good() 如果流可以使用(所有的位都被清除),则返回 true eof() 如果 eofbit 被设置,则返回 true bad() 如果 badbit 被设置,则返回 true fail() 如果 badbit 或f ailbit 被设置,则返回 true rdstate() 返回流状态 exceptions() 返回一个位掩码,指出哪些标记导致异常被引发 exceptions(isostate ex) 设置哪些状态将导致 clear()引发异常;例如,如果 ex 是e ofbit,则如果 eofbit 被设置,clear( )将引发异常 clear(iostate s) 将流状态设置为 s;s 的默认值为 0(goodbit);如果( restate( )& exceptions( ))! = 0,则引发异常 basic_ios::failure setstate(iostate s) 调用 clear(rdstate( )| s)。这将设置与 s 中设置的位对应的流状态位,其他流状态位保持不变 17.3.3 其他 istream 类方法
第3 章~第5 章讨论了 get( )和g etline( )方法。您可能还记得,它们提供下面的输入功能: 方法 get(char&)和g et(void)提供不跳过空白的单字符输入功能; 函数 get(char*, int, char)和g etline(char*, int, char)在默认情况下读取整行而不是一个单词。它们被称为非格式化输入函数(unformatted input functions),因为它们只是读取字符输入,而不会跳过空白,也不进行数据转换。
- 单字符输入
在使用 char 参数或没有参数的情况下,get( )方法读取下一个输入字符,即使该字符是空格、制表符或换行符。get(char & ch)版本将输入字符赋给其参数,而g et(void)版本将输入字符转换为整型(通常是 int),并将其返回。
- 成员函数 get(char &) 通过使用 get(ch),代码读取、显示并考虑空格和可打印字符。get(char &)成员函数返回一个指向用于调用它的 istream 对象的引用,这意味着可以拼接 get(char &)后面的其他抽取。只要存在有效输入,cin.get(ch)的返回值都将是 cin,此时的判定结果为 true,因此循环将继续。到达文件尾时,返回值判定为 false,循环终止。
- 成员函数 get(void) get(void)成员函数还读取空白,但使用返回值来将输入传递给程序。get(void)成员函数的返回类型为 int(或某种更大的整型,这取决于字符集和区域)。get(void)的返回类型为 int,这意味着它后面不能跟抽取运算符。然而,由于 cin.get(c1)返回 cin,因此它可以放在 get( )的前面。到达文件尾后(不管是真正的文件尾还是模拟的文件尾),cin.get(void)都将返回值 EOF——头文件 iostream 提供的一个符号常量。
- 采用哪种单字符输入形式 假设可以选择>>、get(char &)或g et(void),应使用哪一个呢?首先,应确定是否希望跳过空白。如果跳过空白更方便,则使用抽取运算符>>。如果希望程序检查每个字符,请使用 get( )方法,在g et( )方法中,get(char &)的接口更佳。get(void)的主要优点是,它与标准 C 语言中的 getchar( )函数极其类似,这意味着可以通过包含 iostream(而不是 stdio.h),并用 cin.get( )替换所有的 getchar( ),用c out.put(ch)替换所有的 putchar(ch),来将 C 程序转换为 C++程序。
- 字符串输入:getline( )、get( )和i gnore( )
接下来复习一下第 4 章介绍的字符串输入成员函数。getline( )成员函数和 get( )的字符串读取版本都读取字符串,它们的函数特征标相同(这是从更为通用的模板声明简化而来的):
istream & get(char*, int, char);
istream & get(char*, int);
istream & getline(char*, int, char);
istream & getline(char*, int);
第一个参数是用于放置输入字符串的内存单元的地址。第二个参数比要读取的最大字符数大 1(额外的一个字符用于存储结尾的空字符,以便将输入存储为一个字符串)。第3 个参数指定用作分界符的字符,只有两个参数的版本将换行符用作分界符。上述函数都在读取最大数目的字符或遇到换行符后为止。get( )和g etline( )之间的主要区别在于,get( )将换行符留在输入流中,这样接下来的输入操作首先看到是将是换行符,而g erline( )抽取并丢弃输入流中的换行符。ignore( )成员函数。该函数接受两个参数:一个是数字,指定要读取的最大字符数;另一个是字符,用作输入分界符。istream & ignore(int = 1, int = EOF);
意外字符串输入 get(char * , int)和g etline( )的某些输入形式将影响流状态。与其他输入函数一样,这两个函数在遇到文件尾时将设置 eofbit,遇到流被破坏(如设备故障)时将设置 badbit。另外两种特殊情况是无输入以及输入到达或超过函数调用指定的最大字符数。对于上述两个方法,如果不能抽取字符,它们将把空值字符放置到输入字符串中,并使用 setstate( )设置 failbit。方法在什么时候无法抽取字符呢?一种可能的情况是输入方法立刻到达了文件尾。对于 get(char * , int)来说,另一种可能是输入了一个空行,有意思的是,空行并不会导致 getline( )设置 failbit。这是因为 getline( )仍将抽取换行符,虽然不会存储它。
Table 14: 输入行为方法 行为 getline(char *, int) 如果没有读取任何字符(但换行符被视为读取了一个字符),则设置 failbit 如果读取了最大数目的字符,且行中还有其他字符,则设置 failbit get(char *, int) 如果没有读取任何字符,则设置 failbit
- 单字符输入
在使用 char 参数或没有参数的情况下,get( )方法读取下一个输入字符,即使该字符是空格、制表符或换行符。get(char & ch)版本将输入字符赋给其参数,而g et(void)版本将输入字符转换为整型(通常是 int),并将其返回。
17.3.4 其他 istream 方法
- 除前面讨论过的外,其他 istream 方法包括 read( )、peek( )、gcount( )和p utback( )。
- read( )函数读取指定数目的字节,并将它们存储在指定的位置中。与g etline( )和g et( )不同的是,read( )不会在输入后加上空值字符,因此不能将输入转换为字符串。read( )方法不是专为键盘输入设计的,它最常与 ostream write( )函数结合使用,来完成文件输入和输出。该方法的返回类型为 istream &
- peek( )函数返回输入中的下一个字符,但不抽取输入流中的字符。也就是说,它使得能够查看下一个字符。假设要读取输入,直到遇到换行符或句点,则可以用 peek( )查看输入流中的下一个字符,以此来判断是否继续读取
- gcount( )方法返回最后一个非格式化抽取方法读取的字符数。这意味着字符是由 get( )、getline( )、ignore( )或r ead( )方法读取的,不是由抽取运算符(>>)读取的,抽取运算符对输入进行格式化,使之与特定的数据类型匹配。
- putback( )函数将一个字符插入到输入字符串中,被插入的字符将是下一条输入语句读取的第一个字符。putback( )方法接受一个 char 参数——要插入的字符,其返回类型为 istream &,这使得可以将该函数调用与其他 istream 方法拼接起来。使用 peek( )的效果相当于先使用 get( )读取一个字符,然后使用 putback( )将该字符放回到输入流中。然而,putback( )允许将字符放到不是刚才读取的位置。
- 除前面讨论过的外,其他 istream 方法包括 read( )、peek( )、gcount( )和p utback( )。
17.4 文件输入和输出
大多数计算机程序都使用了文件。字处理程序创建文档文件;数据库程序创建和搜索信息文件;编译器读取源代码文件并生成可执行文件。文件本身是存储在某种设备(磁带、光盘、软盘或硬盘)上的一系列字节。需要的只是将程序与文件相连的途径、让程序读取文件内容的途径以及让程序创建和写入文件的途径。重定向(本章前面讨论过)可以提供一些文件支持,但它比显式程序中的文件 I/O 的局限性更大。另外,重定向来自操作系统,而非 C++,因此并非所有系统都有这样的功能。
17.4.1 简单的文件 I/O
- 要让程序写入文件,必须这样做:
- 首先应包含头文件 fstream。对于大多数(但不是全部)实现来说,包含该文件便自动包括 iostream 文件,因此不必显示包含 iostream。
- 创建一个 ofstream 对象来管理输出流;
ofstream fout;
- 将该对象与特定的文件关联起来;
fout.open("jar, txt")
ofstream fout("jar.txt")
- 以使用 cout 的方式使用该对象,唯一的区别是输出将进入文件,而不是屏幕。ofstream 类使用被缓冲的输出,因此程序在创建像 fout 这样的 ofstream 对象时,将为输出缓冲区分配空间。当缓冲区填满后,它便将缓冲区内容一同传输给目标文件。由于磁盘驱动器被设计称以大块的方式传输数据,而不是逐字节地传输,因此通过缓冲可以大大提高从程序到文件传输数据的速度。
- 以这种方式打开文件来进行输出时,如果没有这样的文件,将创建一个新文件;如果有这样的文件,则打开文件将清空文件,输出将进入到一个空文件中。
- 读取文件的要求与写入文件相似:
- 首先,当然要包含头文件 fstream。
- 创建一个 ifstream 对象来管理输入流;
- 将该对象与特定的文件关联起来;
- 以使用 cin 的方式使用该对象。
- 输入和输出一样,也是被缓冲的,因此创建 ifstream 对象与 fin 一样,将创建一个由 fin 对象管理的输入缓冲区。与输出一样,通过缓冲,传输数据的速度比逐字节传输要快得多。
- 当输入和输出流对象过期(如程序终止)时,到文件的连接将自动关闭。另外,也可以使用 close( )方法来显式地关闭到文件的连接:
fout.close();
fin.close();
- 要让程序写入文件,必须这样做:
17.4.2 流状态检查和 is_open( )
C++文件流类从 ios_base 类那里继承了一个流状态成员。正如前面指出的,该成员存储了指出流状态的信息:一切顺利、已到达文件尾、I/O 操作失败等。如果一切顺利,则流状态为零(没有消息就是好消息)。其他状态都是通过将特定位设置为 1 来记录的。文件流类还继承了 ios_base 类中报告流状态的方法。可以通过检查流状态来判断最后一个流操作是否成功。对于文件流,这包括检查试图打开文件时是否成功。
17.4.3 打开多个文件
程序可能需要打开多个文件。打开多个文件的策略取决于它们将被如何使用。如果需要同时打开两个文件,则必须为每个文件创建一个流。然而,可能要依次处理一组文件。在这种情况下,可以打开一个流,并将它依次关联到各个文件。这在节省计算机资源方面,比为每个文件打开一个流的效率高。使用这种方法,首先需要声明一个 ifstream 对象(不对它进行初始化),然后使用 open( )方法将这个流与文件关联起来
17.4.4 命令行处理技术
文件处理程序通常使用命令行参数来指定文件。命令行参数是用户在输入命令时,在命令行中输入的参数。C++有一种让在命令行环境中运行的程序能够访问命令行参数的机制,方法是使用下面的 main( )函数:
int main(int argc, char * argv[])
argc 为命令行中的参数个数,其中包括命令名本身。argv 变量为一个指针,它指向一个指向 char 的指针。这过于抽象,但可以将 argv 看作一个指针数组,其中的指针指向命令行参数,argv[0]
是一个指针,指向存储第一个命令行参数的字符串的第一个字符,依此类推。也就是说,argv[0]
是命令行中的第一个字符串,依此类推。17.4.5 文件模式
文件模式描述的是文件将被如何使用:读、写、追加等。将流与文件关联时(无论是使用文件名初始化文件流对象,还是使用 open( )方法),都可以提供指定文件模式的第二个参数。ios_base 类定义了一个 openmode 类型,用于表示模式;与f mtflags 和i ostate 类型一样,它也是一种 bitmask 类型(以前,其类型为 int)。可以选择 ios_base 类中定义的多个常量来指定模式,表列出了这些常量及其含义。C++文件 I/O 作了一些改动,以便与 ANSI C 文件 I/O 兼容。
Table 15: 文件模式常量常量 含义 ios_base::in 打开文件,以便读取 ios_base::out 打开文件,以便写入 ios_base::ate 打开文件,并移到文件尾 ios_base::app 追加到文件尾 ios_base::trunc 如果文件存在,则截短文件 ios_base::binary 二进制文件 17.4.6 随机存取
在最后一个文件示例中,将探讨随机存取。随机存取指的是直接移动(不是依次移动)到文件的任何位置。随机存取常被用于数据库文件,程序维护一个独立的索引文件,该文件指出数据在主数据文件中的位置。这样,程序便可以直接跳到这个位置,读取(还可能修改)其中的数据。如果文件由长度相同的记录组成,这种方法实现起来最简单。每条记录表示一组相关的数据。seekg( )和s eekp( ),前者将输入指针移到指定的文件位置,后者将输出指针移到指定的文件位置(实际上,由于 fstream 类使用缓冲区来存储中间数据,因此指针指向的是缓冲区中的位置,而不是实际的文件)。也可以将 seekg( )用于 ifstream 对象,将s eekp( )用于 oftream 对象。
17.5 内核格式化
iostream族(family)支持程序与终端之间的 I/O,而f stream 族使用相同的接口提供程序和文件之间的 I/O。C++库还提供了 sstream族,它们使用相同的接口提供程序和 string 对象之间的 I/O。也就是说,可以使用于 cout 的o stream 方法将格式化信息写入到 string 对象中,并使用 istream 方法(如g etline( ))来读取 string 对象中的信息。读取 string 对象中的格式化信息或将格式化信息写入 string 对象中被称为内核格式化(incore formatting)。
17.6 总结
流是进出程序的字节流。缓冲区是内存中的临时存储区域,是程序与文件或其他 I/O 设备之间的桥梁。信息在缓冲区和文件之间传输时,将使用设备(如磁盘驱动器)处理效率最高的尺寸以大块数据的方式进行传输。信息在缓冲区和程序之间传输时,是逐字节传输的,这种方式对于程序中的处理操作更为方便。C++通过将一个被缓冲流同程序及其输入源相连来处理输入。同样,C++也通过将一个被缓冲流与程序及其输出目标相连来处理输出。iostream 和f stream 文件构成了 I/O 类库,该类库定义了大量用于管理流的类。包含了 iostream 文件的 C++程序将自动打开 8 个流,并使用 8 个对象管理它们。cin 对象管理标准输入流,后者默认与标准输入设备(通常为键盘)相连;cout 对象管理标准输出流,后者默认与标准输出设备(通常为显示器)相连;cerr 和c log 对象管理与标准错误设备(通常为显示器)相连的未被缓冲的流和被缓冲的流。这4 个对象有都有用于宽字符的副本,它们是 wcin、wcout、wcerr 和w clog。 I/O 类库提供了大量有用的方法。istream 类定义了多个版本的抽取运算符(>>),用于识别所有基本的 C++类型,并将字符输入转换为这些类型。get( )方法族和 getline( )方法为单字符输入和字符串输入提供了进一步的支持。同样,ostream 类定义了多个版本的插入运算符(<<),用于识别所有的 C++基本类型,并将它们转换为相应的字符输出。put( )方法对单字符输出提供了进一步的支持。wistream 和w ostream 类对宽字符提供了类似的支持。 使用 ios_base 类方法以及文件 iostream 和i omanip 中定义的控制符(可与插入运算符拼接的函数),可以控制程序如何格式化输出。这些方法和控制符使得能够控制计数系统、字段宽度、小数位数、显示浮点变量时采用的计数系统以及其他元素。 fstream 文件提供了将 iostream 方法扩展到文件 I/O 的类定义。ifstream 类是从 istream 类派生而来的。通过将 ifstream 对象与文件关联起来,可以使用所有的 istream 方法来读取文件。同样,通过将 ofstream 对象与文件关联起来,可以使用 ostream 方法来写文件;通过将 fstream 对象与文件关联起来,可以将输入和输出方法用于文件。 要将文件与流关联起来,可以在初始化文件流对象时提供文件名,也可以先创建一个文件流对象,然后用 open( )方法将这个流与文件关联起来。close( )方法终止流与文件之间的连接。类构造函数和 open( )方法接受可选的第二个参数,该参数提供文件模式。文件模式决定文件是否被读和/或写、打开文件以便写入时是否截短文件、试图打开不存在的文件时是否会导致错误、是使用二进制模式还是文本模式等。 文本文件以字符格式存储所有的信息,例如,数字值将被转换为字符表示。常规的插入和抽取运算符以及 get( )和g etline( )都支持这种模式。二进制文件使用计算机内部使用的二进制表示来存储信息。与文本文件相比,二进制文件存储数据(尤其是浮点值)更为精确、简洁,但可移植性较差。read( )和w rite( )方法都支持二进制输入和输出。 seekg( )和s eekp( )函数提供对文件的随机存取。这些类方法使得能够将文件指针放置到相对于文件开头、文件尾和当前位置的某个位置。tellg( )和t ellp( )方法报告当前的文件位置。 sstream 头文件定义了 istringstream 和o stringstream类,这些类使得能够使用 istream 和o stream 方法来抽取字符串中的信息,并对要放入到字符串中的信息进行格式化。
17.7 复习题
17.8 编程练习
第1 8 章 探讨 C++新标准
- 本章首先复习前面介绍过的 C++11 功能,然后介绍如下主题:
- 移动语义和右值引用。
- Lambda 表达式。
- 包装器模板 function。
- 可变参数模板。
18.1 复习前面介绍过的 C++11 功能
18.1.1 新类型
C++11 新增了类型 long long 和u nsigned long long,以支持 64位(或更宽)的整型;新增了类型 char16_t 和c har32_t,以支持 16 位和 32 位的字符表示;还新增了“原始”字符串。第3 章讨论了这些新增的类型。
18.1.2 统一的初始化
C++11 扩大了用大括号括起的列表(初始化列表)的适用范围,使其可用于所有内置类型和用户定义的类型(即类对象)。使用初始化列表时,可添加等号(=),也可不添加; 另外,列表初始化语法也可用于 new 表达式中;创建对象时,也可使用大括号(而不是圆括号)括起的列表来调用构造函数;然而,如果类有将模板 std::initializer_list 作为参数的构造函数,则只有该构造函数可以使用列表初始化形式。
- 缩窄 初始化列表语法可防止缩窄,即禁止将数值赋给无法存储它的数值变量。常规初始化允许程序员执行可能没有意义的操作;然而,如果使用初始化列表语法,编译器将禁止进行这样的类型转换,即将值存储到比它“窄”的变量中;但允许转换为更宽的类型。另外,只要值在较窄类型的取值范围内,将其转换为较窄的类型也是允许的。
- std::initializer_list C++11 提供了模板类 initializer_list,可将其用作构造函数的参数,这在第 16 章讨论过。如果类有接受 initializer_list 作为参数的构造函数,则初始化列表语法就只能用于该构造函数。列表中的元素必须是同一种类型或可转换为同一种类型。STL 容器提供了将 initializer_list 作为参数的构造函数;头文件 initializer_list 提供了对模板类 initializer_list 的支持。这个类包含成员函数 begin( )和e nd( ),可用于获悉列表的范围。除用于构造函数外,还可将 initializer_list 用作常规函数的参数。
18.1.3 声明
C++11 提供了多种简化声明的功能,尤其在使用模板时。
- auto 以前,关键字 auto 是一个存储类型说明符(见第 9章),C++11 将其用于实现自动类型推断(见第 3章)。这要求进行显式初始化,让编译器能够将变量的类型设置为初始值的类型;关键字 auto 还可简化模板声明。
- decltype
关键字 decltype 将变量的类型声明为表达式指定的类型。下面的语句的含义是,让y 的类型与 x 相同,其中 x 是一个表达式:
decltype(x) y;
这在定义模板时特别有用,因为只有等到模板被实例化时才能确定类型;decltype 的工作原理比 auto 复杂,根据使用的表达式,指定的类型可以为引用和 const。 - C++11 新增了一种函数声明语法:在函数名和参数列表后面(而不是前面)指定返回类型:
auto f(double, int) -> double;
就常规函数的可读性而言,这种新语法好像是倒退,但让您能够使用 decltype 来指定模板函数的返回类型;这里解决的问题是,必须在参数列表后使用 decltype。这种新语法使得能够这样做。 - 模板别名:using =
对于冗长或复杂的标识符,如果能够创建其别名将很方便。以前,C++为此提供了 typedef:
typedef std::vector<std::string>::iterator itTye;
C++11 提供了另一种创建别名的语法,这在第 14 章讨论过:using itType = std::vector<std::string>::iterator;
差别在于,新语法也可用于模板部分具体化,但t ypedef 不能:template<typename T> using arr12 = std::array<T, 12>;
- nullptr 空指针是不会指向有效数据的指针。以前,C++在源代码中使用 0 表示这种指针,但内部表示可能不同。这带来了一些问题,因为这使得 0 即可表示指针常量,又可表示整型常量。正如第 12 章讨论的,C++11 新增了关键字 nullptr,用于表示空指针;它是指针类型,不能转换为整型类型。为向后兼容,C++11 仍允许使用 0 来表示空指针,因此表达式 nullptr == 0 为t rue,但使用 nullptr 而不是 0 提供了更高的类型安全。例如,可将 0 传递给接受 int 参数的函数,但如果您试图将 nullptr 传递给这样的函数,编译器将此视为错误。因此,出于清晰和安全考虑,请使用 nullptr—如果您的编译器支持它。
18.1.4 智能指针
如果在程序中使用 new 从堆(自由存储区)分配内存,等到不再需要时,应使用 delete 将其释放。C++引入了智能指针 auto_ptr,以帮助自动完成这个过程。随后的编程体验(尤其是使用 STL时)表明,需要有更精致的机制。基于程序员的编程体验和 BOOST 库提供的解决方案,C++11 摒弃了 auto_ptr,并新增了三种智能指针:unique_ptr、shared_ptr 和w eak_ptr,第1 6 章讨论了前两种。 所有新增的智能指针都能与 STL 容器和移动语义协同工作。
18.1.5 异常规范方面的修改
与a uto_ptr 一样,C++编程社区的集体经验表明,异常规范的效果没有预期的好。因此,C++11 摒弃的异常规范。然而,标准委员会认为,指出函数不会引发异常有一定的价值,他们为此添加了关键字 noexcept
18.1.6 作用域内枚举
传统的 C++枚举提供了一种创建名称常量的方式,但其类型检查相当低级。另外,枚举名的作用域为枚举定义所属的作用域,这意味着如果在同一个作用域内定义两个枚举,它们的枚举成员不能同名。最后,枚举可能不是可完全移植的,因为不同的实现可能选择不同的底层类型。为解决这些问题,C++11 新增了一种枚举。这种枚举使用 class 或s truct 定义;新枚举要求进行显式限定,以免发生名称冲突。因此,引用特定枚举时,需要使用 New1::never 和N ew2::never等。更详细的信息请参阅第 10章。
18.1.7 对类的修改
为简化和扩展类设计,C++11 做了多项改进。这包括允许构造函数被继承和彼此调用、更佳的方法访问控制方式以及移动构造函数和移动赋值运算符,
- 显式转换运算符 有趣的是,C++很早就支持对象自动转换。但随着编程经验的积累,程序员逐渐认识到,自动类型转换可能导致意外转换的问题。为解决这种问题,C++引入了关键字 explicit,以禁止单参数构造函数导致的自动转换;C++11 拓展了 explicit 的这种用法,使得可对转换函数做类似的处理(参见第 11章)。
- 类内成员初始化 可使用等号或大括号版本的初始化,但不能使用圆括号版本的初始化。通过使用类内初始化,可避免在构造函数中编写重复的代码,从而降低了程序员的工作量、厌倦情绪和出错的机会。 如果构造函数在成员初始化列表中提供了相应的值,这些默认值将被覆盖。
18.1.8 模板和 STL 方面的修改
- 基于范围的 for 循环 对于内置数组以及包含方法 begin( ) 和e nd( ) 的类(如s td::string)和S TL 容器,基于范围的 for 循环(第5 章和第 16 章讨论过)可简化为它们编写循环的工作。这种循环对数组或容器中的每个元素执行指定的操作
- 新的 STL 容器
C++11 新增了 STL 容器 forward_list、unordered_map、unordered_multimap、unordered_set 和u nordered_multiset(参见第 16章)。容器 forward_list 是一种单向链表,只能沿一个方向遍历;与双向链接的 list 容器相比,它更简单,在占用存储空间方面更经济。其他四种容器都是使用哈希表实现的。 C++11 还新增了模板 array(这在第 4 和1 6 章讨论过)。要实例化这种模板,可指定元素类型和固定的元素数:
std::array<int, 360> ar;
这个模板类没有满足所有的常规模板需求。例如,由于长度固定,您不能使用任何修改容器大小的方法,如p ut_back( )。但a rray 确实有方法 begin( )和e nd( ),这让您能够对 array 对象使用众多基于范围的 STL 算法。 - 新的 STL 方法 C++11 新增了 STL 方法 cbegin( )和c end( )。与b egin( )和e nd( )一样,这些新方法也返回一个迭代器,指向容器的第一个元素和最后一个元素的后面,因此可用于指定包含全部元素的区间。另外,这些新方法将元素视为 const。与此类似,crbegin( )和c rend( )是r begin( )和r end( )的c onst 版本。 更重要的是,除传统的复制构造函数和常规赋值运算符外,STL 容器现在还有移动构造函数和移动赋值运算符。移动语义将在本章后面介绍。
- valarray 升级 模板 valarray 独立于 STL 开发的,其最初的设计导致无法将基于范围的 STL 算法用于 valarray 对象。C++11 添加了两个函数(begin( )和e nd( )),它们都接受 valarray 作为参数,并返回迭代器,这些迭代器分别指向 valarray 对象的第一个元素和最后一个元素后面。这让您能够将基于范围的 STL 算法用于 valarray(参见第 16章)。
- 摒弃 export C++98 新增了关键字 export,旨在提供一种途径,让程序员能够将模板定义放在接口文件和实现文件中,其中前者包含原型和模板声明,而后者包含模板函数和方法的定义。实践证明这不现实,因此 C++11 终止了这种用法,但仍保留了关键字 export,供以后使用。
- 尖括号 为避免与运算符>>混淆,C++要求在声明嵌套模板时使用空格将尖括号分开
18.1.9 右值引用
传统的 C++引用(现在称为左值引用)使得标识符关联到左值。左值是一个表示数据的表达式(如变量名或解除引用的指针),程序可获取其地址。最初,左值可出现在赋值语句的左边,但修饰符 const 的出现使得可以声明这样的标识符,即不能给它赋值,但可获取其地址。C++11 新增了右值引用(这在第 8 章讨论过),这是使用&&表示的。右值引用可关联到右值,即可出现在赋值表达式右边,但不能对其应用地址运算符的值。右值包括字面常量(C-风格字符串除外,它表示地址)、诸如 x + y 等表达式以及返回值的函数(条件是该函数返回的不是引用)。有趣的是,将右值关联到右值引用导致该右值被存储到特定的位置,且可以获取该位置的地址。也就是说,虽然不能将运算符&用于 13,但可将其用于 r1。通过将数据与特定的地址关联,使得可以通过右值引用来访问该数据。
18.2 移动语义和右值引用
18.2.1 为何需要移动语义
这类似于在计算机中移动文件的情形:实际文件还留在原来的地方,而只修改记录。这种方法被称为移动语义(move semantics)。有点悖论的是,移动语义实际上避免了移动原始数据,而只是修改了记录。要实现移动语义,需要采取某种方式,让编译器知道什么时候需要复制,什么时候不需要。这就是右值引用发挥作用的地方。可定义两个构造函数。其中一个是常规复制构造函数,它使用 const 左值引用作为参数,这个引用关联到左值实参,如语句# 1 中的 vstr;另一个是移动构造函数,它使用右值引用作为参数,该引用关联到右值实参,如语句# 2 中a llcaps(vstr)的返回值。复制构造函数可执行深复制,而移动构造函数只调整记录。在将所有权转移给新对象的过程中,移动构造函数可能修改其实参,这意味着右值引用参数不应是 const。
18.2.2 一个移动示例
18.2.3 移动构造函数解析
虽然使用右值引用可支持移动语义,但这并不会神奇地发生。要让移动语义发生,需要两个步骤。首先,右值引用让编译器知道何时可使用移动语义;总之,通过提供一个使用左值引用的构造函数和一个使用右值引用的构造函数,将初始化分成了两组。使用左值对象初始化对象时,将使用复制构造函数,而使用右值对象初始化对象时,将使用移动构造函数。程序员可根据需要赋予这些构造函数不同的行为。
18.2.4 赋值
适用于构造函数的移动语义考虑也适用于赋值运算符。移动赋值运算符删除目标对象中的原始数据,并将源对象的所有权转让给目标。不能让多个指针指向相同的数据,这很重要,因此上述代码将源对象中的指针设置为空指针。 与移动构造函数一样,移动赋值运算符的参数也不能是 const 引用,因为这个方法修改了源对象。
18.2.5 强制移动
C++11 提供了一种更简单的方式—使用头文件 utility 中声明的函数 std::move( )。
18.3 新的类功能
除本章前面提到的显式转换运算符和类内成员初始化外,C++11 还新增了其他几个类功能。
18.3.1 特殊的成员函数
在原有 4 个特殊成员函数(默认构造函数、复制构造函数、复制赋值运算符和析构函数)的基础上,C++11 新增了两个:移动构造函数和移动赋值运算符。这些成员函数是编译器在各种情况下自动提供的。 前面说过,在没有提供任何参数的情况下,将调用默认构造函数。如果您没有给类定义任何构造函数,编译器将提供一个默认构造函数。这种版本的默认构造函数被称为默认的默认构造函数。对于使用内置类型的成员,默认的默认构造函数不对其进行初始化;对于属于类对象的成员,则调用其默认构造函数。 另外,如果您没有提供复制构造函数,而代码又需要使用它,编译器将提供一个默认的复制构造函数;如果您没有提供移动构造函数,而代码又需要使用它,编译器将提供一个默认的移动构造函数。在类似的情况下,编译器将提供默认的复制运算符和默认的移动运算符,最后,如果您没有提供析构函数,编译器将提供一个。 对于前面描述的情况,有一些例外。如果您提供了析构函数、复制构造函数或复制赋值运算符,编译器将不会自动提供移动构造函数和移动赋值运算符;如果您提供了移动构造函数或移动赋值运算符,编译器将不会自动提供复制构造函数和复制赋值运算符。 另外,默认的移动构造函数和移动赋值运算符的工作方式与复制版本类似:执行逐成员初始化并复制内置类型。如果成员是类对象,将使用相应类的构造函数和赋值运算符,就像参数为右值一样。如果定义了移动构造函数和移动赋值运算符,这将调用它们;否则将调用复制构造函数和复制赋值运算符。
18.3.2 默认的方法和禁用的方法
C++11 让您能够更好地控制要使用的方法。假定您要使用某个默认的函数,而这个函数由于某种原因不会自动创建。在这些情况下,您可使用关键字 default 显式地声明这些方法的默认版本;编译器将创建在您没有提供移动构造函数的情况下将自动提供的构造函数。 另一方面,关键字 delete 可用于禁止编译器使用特定方法。例如,要禁止复制对象,可禁用复制构造函数和复制赋值运算符要禁止复制,可将复制构造函数和赋值运算符放在类定义的 private 部分,但使用 delete 也能达到这个目的,且更不容易犯错、更容易理解。关键字 default 只能用于 6 个特殊成员函数,但d elete 可用于任何成员函数。delete 的一种可能用法是禁止特定的转换。
18.3.3 委托构造函数
C++11 允许您在一个构造函数的定义中使用另一个构造函数。这被称为委托,因为构造函数暂时将创建对象的工作委托给另一个构造函数。委托使用成员初始化列表语法的变种
18.3.4 继承构造函数
派生类继承基类的所有构造函数(默认构造函数、复制构造函数和移动构造函数除外),但不会使用与派生类构造函数的特征标匹配的构造函数
18.3.5 管理虚方法:override 和f inal
虚方法对实现多态类层次结构很重要,让基类引用或指针能够根据指向的对象类型调用相应的方法,但虚方法也带来了一些编程陷阱。例如,假设基类声明了一个虚方法,而您决定在派生类中提供不同的版本,这将覆盖旧版本。在C++11中,可使用虚说明符 override 指出您要覆盖一个虚函数:将其放在参数列表后面。如果声明与基类方法不匹配,编译器将视为错误。说明符 final 解决了另一个问题。您可能想禁止派生类覆盖特定的虚方法,为此可在参数列表后面加上 final。说明符 override 和f inal 并非关键字,而是具有特殊含义的标识符。这意味着编译器根据上下文确定它们是否有特殊含义;在其他上下文中,可将它们用作常规标识符,如变量名或枚举。
18.4 Lambda 函数
lambda 函数并不像看起来那么晦涩难懂,它们提供了一种有用的服务,对使用函数谓词的 STL 算法来说尤其如此。
18.4.1 比较函数指针、函数符和 Lambda 函数
在C++11中,对于接受函数指针或函数符的函数,可使用匿名函数定义(lambda)作为其参数。
[](int) {return x % 3 =
0;}= 使用[]替代了函数名(这就是匿名的由来);没有声明返回类型。返回类型相当于使用 decltyp 根据返回值推断得到的,这里为 bool。如果 lambda 不包含返回语句,推断出的返回类型将为 void。仅当 lambad 表达式完全由一条返回语句组成时,自动类型推断才管用;否则,需要使用新增的返回类型后置语法:[](double x) -> double{int y = x; return x-y;}
18.4.2 为何使用 lambda
在C++中引入 lambda 的主要目的是,让您能够将类似于函数的表达式用作接受函数指针或函数符的函数的参数。因此,典型的 lambda 是测试表达式或比较表达式,可编写为一条返回语句。这使得 lambda 简洁而易于理解,且可自动推断返回类型。然而,有创意的 C++程序员可能开发出其他用法。
- 距离 很多程序员认为,让定义位于使用的地方附近很有用。这样,就无需翻阅多页的源代码,另外,如果需要修改代码,涉及的内容都将在附近;而剪切并粘贴代码以便在其他地方使用时,涉及的内容也在一起。从这种角度看,lambda 是理想的选择,因为其定义和使用是在同一个地方进行的;而函数是最糟糕的选择,因为不能在函数内部定义其他函数,因此函数的定义可能离使用它的地方很远。函数符是不错的选择,因为可在函数内部定义类(包含函数符类),因此定义离使用地点可以很近。
- 简洁 从简洁的角度看,函数符代码比函数和 lambda 代码更繁琐。函数和 lambda 的简洁程度相当
- 效率 这三种方法的相对效率取决于编译器内联那些东西。函数指针方法阻止了内联,因为编译器传统上不会内联其地址被获取的函数,因为函数地址的概念意味着非内联函数。而函数符和 lambda 通常不会阻止内联。
- 功能 最后,lambda 有一些额外的功能。具体地说,lambad 可访问作用域内的任何动态变量;要捕获要使用的变量,可将其名称放在中括号内。如果只指定了变量名,如[ z],将按值访问变量;如果在名称前加上&,如[&count],将按引用访问变量。[&]让您能够按引用访问所有动态变量,而[=]让您能够按值访问所有动态变量。还可混合使用这两种方式,例如,[ted, &ed]让您能够按值访问 ted 以及按引用访问 ed,[&, ted]让您能够按值访问 ted 以及按引用访问其他所有动态变量,[=, &ed]让您能够按引用访问 ed 以及按值访问其他所有动态变量。
18.5 包装器
C++提供了多个包装器(wrapper,也叫适配器[ adapter])。这些对象用于给其他编程接口提供更一致或更合适的接口。C++11 提供了其他的包装器,包括模板 bind、men_fn 和r eference_wrapper 以及包装器 function。其中模板 bind 可替代 bind1st 和b ind2nd,但更灵活;模板 mem_fn 让您能够将成员函数作为常规函数进行传递;模板 reference_wrapper 让您能够创建行为像引用但可被复制的对象;而包装器 function 让您能够以统一的方式处理多种类似于函数的形式。
18.5.1 包装器 function 及模板的低效性
请看下面的代码行:
answer = ef(q);
ef 是什么呢?它可以是函数名、函数指针、函数对象或有名称的 lambda 表达式。所有这些都是可调用的类型(callable type)。鉴于可调用的类型如此丰富,这可能导致模板的效率极低。18.5.2 修复问题
18.5.3 其他方式
18.6 可变参数模板
可变参数模板(variadic template)让您能够创建这样的模板函数和模板类,即可接受可变数量的参数。这里介绍可变参数模板函数。
要创建可变参数模板,需要理解几个要点:
- 模板参数包(parameter pack);
- 函数参数包;
- 展开(unpack)参数包;
- 递归。
18.6.1 模板和函数参数包
C++11 提供了一个用省略号表示的元运算符(meta-operator),让您能够声明表示模板参数包的标识符,模板参数包基本上是一个类型列表。同样,它还让您能够声明表示函数参数包的标识符,而函数参数包基本上是一个值列表。其语法如下:
template<typename... Args> void show_list(Args... args) {...}
其中,Args 是一个模板参数包,而a rgs 是一个函数参数包。与其他参数名一样,可将这些参数包的名称指定为任何符合 C++标识符规则的名称。Args 和T 的差别在于,T 与一种类型匹配,而A rgs 与任意数量(包括零)的类型匹配。18.6.2 展开参数包
索引功能在这里不适用,即您不能使用
Args[2]
来访问包中的第三个类型。相反,可将省略号放在函数参数包名的右边,将参数包展开。18.6.3 在可变参数模板函数中使用递归
这里的核心理念是,将函数参数包展开,对列表中的第一项进行处理,再将余下的内容传递给递归调用,以此类推,直到列表为空。与常规递归一样,确保递归将终止很重要。
18.7 C++11 新增的其他功能
18.7.1 并行编程
为解决并行性问题,C++定义了一个支持线程化执行的内存模型,添加了关键字 thread_local,提供了相关的库支持。关键字 thread_local 将变量声明为静态存储,其持续性与特定线程相关;即定义这种变量的线程过期时,变量也将过期。库支持由原子操作(atomic operation)库和线程支持库组成,其中原子操作库提供了头文件 atomic,而线程支持库提供了头文件 thread、mutex、condition_variable 和f uture。
18.7.2 新增的库
- C++11 添加了多个专用库。
- 头文件 random 支持的可扩展随机数库提供了大量比 rand( ) 复杂的随机数工具。例如,您可以选择随机数生成器和分布状态,分布状态包括均匀分布(类似于 rand( ))、二项式分布和正态分布等。
- 头文件 chrono 提供了处理时间间隔的途径。
- 头文件 tuple 支持模板 tuple。tuple 对象是广义的 pair 对象。pair 对象可存储两个类型不同的值,而t uple 对象可存储任意多个类型不同的值。
- 头文件 ratio 支持的编译阶段有理数算术库让您能够准确地表示任何有理数,其分子和分母可用最宽的整型表示。它还支持对这些有理数进行算术运算。
- 在新增的库中,最有趣的一个是头文件 regex 支持的正则表达式库。正则表达式指定了一种模式,可用于与文本字符串的内容匹配。ed、grep 和a wk 等U NIX 工具都使用正则表达式,而解释型语言 Perl 扩展了正则表达式的功能。C++正则表达式库让您能够选择多种形式的正则表达式。
- C++11 添加了多个专用库。
18.7.3 低级编程
低级编程中的“低级”指的是抽象程度,而不是编程质量。低级意味着接近于计算机硬件和机器语言使用的比特和字节。对嵌入式编程和改善操作的效率而言,低级编程很重要。C++11 给低级编程人员提供了一些帮助。变化之一是放松了 POD(Plain Old Data)的要求。另一项修改是,允许共用体的成员有构造函数和析构函数,这让共用体更灵活;但保留了其他一些限制,如成员不能有虚函数。在需要最大程度地减少占用的内存时,通常使用共用体;上述新规则在这些情况下给程序员有更大的灵活性和功能。 C++11 解决了内存对齐问题。计算机系统可能对数据在内存中的存储方式有一定的限制。constexpr 机制让编译器能够在编译阶段计算结果为常量的表达式,让c onst 变量可存储在只读内存中,这对嵌入式编程来说很有用(在运行阶段初始化的变量存储在随机访问内存中)。
18.7.4 杂项
- C99 引入了依赖于实现的扩展整型,C++11 继承了这种传统。在使用 128 位整数的系统中,可使用这样的类型。在C 语言中,扩展类型由头文件 stdint.h 支持,而在 C++中,为头文件 cstdint。
- C++11 提供了一种创建用户自定义字面量的机制:字面量运算符(literal operator)。使用这种机制可定义二进制字面量,如1 001001b,相应的字面量运算符将把它转换为整数值。
- C++提供了调试工具 assert。这是一个宏,它在运行阶段对断言进行检查,如果为 true,则显示一条消息,否则调用 abort( )。断言通常是程序员认为在程序的某个阶段应为 true 的东西。C++11 新增了关键字 static_assert,可用于在编译阶段对断言进行测试。这样做的主要目的在于,对于在编译阶段(而不是运行阶段)实例化的模板,调试起来将更简单。
- C++11 加强了对元编程(metaprogramming)的支持。元编程指的是编写这样的程序,它创建或修改其他程序,甚至修改自身。在C++中,可使用模板在编译阶段完成这种工作。
18.8 语言变化
18.8.1 Boost 项目
最近,Boost 库成了 C++编程的重要部分,给C++11 带来了深远影响。Boost 项目发起于 1998年,当时的 C++库工作小组主席 Beman Dawes 召集其他几位小组成员制定了一项计划,准备在标准委员会的框架外创建新库。该计划的基本理念是,创建一个充当开放论坛的网站,让人发布免费的 C++库。这个项目提供有关许可和编程实践的指南,并要求对提议的库进行同行审阅。其最终的成果是,一系列得到高度赞扬和广泛使用的库。这个项目提供了一个环境,让编程社区能够检验和评估编程理念以及提供反馈。
18.8.2 TR1
TR1(Technical Report 1)是C++标准委员会的部分成员发起的一个项目,它是一个库扩展选集,这些扩展与 C++98 标准兼容,但不是必不可少的。这些扩展是下一个 C++标准的候选内容。TR1 库让 C++社区能够检验其组成部分的价值。当标准委员会将 TR1 的大部分内容融入 C++11时,面对的是众所皆知且经过实践检验的库。 在T R1中,Boost 库占了很大一部分。这包括模板类 tuple 和a rray、模板 bind 和f unction、智能指针(对名称和实现做了一定的修改)、static_assert、regex 库和 random库。另外,Boost 社区和 TR1 用户的经验也导致了实际的语言变更,如异常规范的摒弃和可变参数模板的添加,其中可变参数模板让 tuple 模板类和 function 模板的实现更好了。
18.8.3 使用 Boost
18.9 接下来的任务
18.10 总结
C++新标准新增了大量功能。有些旨在让 C++更容易学习和使用,这包括用大括号括起的统一的列表初始化、使用 auto 自动推断类型、类内成员初始化以及基于范围的 for 循环;而有些旨在增强类设计以及使其更容易理解,这包括默认的和禁用的方法、委托构造函数、继承构造函数以及让虚函数设计更清晰的说明符 override 和f inal。 有几项改进旨在提供程序和编程效率。lambda 表达式比函数指针和函数符更好,模板 function 可用于减少模板实例数量,右值引用让您能够使用移动语义以及实现移动构造函数和移动赋值运算符。 其他改进提供了更佳的工作方式。作用域内枚举让您能够更好地控制枚举的作用域和底层类型;模板 unique_ptr 和s hared_ptr 让您能够更好地处理使用new 分配的内存。 新增的decltype、返回类型后置、模板别名和可变参数模板让模板设计得到了改进。 修改后的共用体和POD 规则、alignof( )运算符、alignas 说明符以及constexpr 机制支持低级编程。 新增了多个库(包括新的STL类、tuple 模板和regex库)为众多常见的编程问题提供了解决方案。 为支持并行编程,新标准还添加了关键字thread_local 和atomic库。 总之,无论对新手还是专家来说,新标准都改善了C++的可用性和可靠性。
18.11 复习题
18.12 编程练习
附录
附录A 计数系统
A.1 十进制数
A.2 八进制整数
A.3 十六进制数
A.4 二进制数
A.5 二进制和十六进制
附录B C++保留字
B.1 C++关键字
B.2 替代标记
B.3 C++库保留名称
B.4 有特殊含义的标识符
附录C ASCII 字符集
附录D 运算符优先级
附录E 其他运算符
E.1 按位运算符
E.1.1 移位运算符
E.1.2 逻辑按位运算符
E.1.3 按位运算符的替代表示
E.1.4 几种常用的按位运算符技术
E.2 成员解除引用运算符
E.3 alignof(C++11)
E.4 noexcept(C++11)
附录F 模板类string
F.1 13 种类型和一个常量
F.2 数据信息、构造函数及其他
F.2.1 默认构造函数
F.2.2 使用C-风格字符串的构造函数
F.2.3 使用部分C-风格字符串的构造函数
F.2.4 使用左值引用的构造函数
F.2.5 使用右值引用的构造函数(C++11)
F.2.6 使用一个字符的n 个副本的构造函数
F.2.7 使用区间的构造函数
F.2.8 使用初始化列表的构造函数(C++11)
F.2.9 内存杂记
F.3 字符串存取
F.4 基本赋值
F.5 字符串搜索
F.5.1 find( )系列
F.5.2 rfind( )系列
F.5.3 find_first_of( )系列
F.5.4 find_last_of( )系列
F.5.5 find_first_not_of( )系列
F.5.6 find_last_not_of( )系列
F.6 比较方法和函数
F.7 字符串修改方法
F.7.1 用于追加和相加的方法
F.7.2 其他赋值方法
F.7.3 插入方法
F.7.4 清除方法
F.7.5 替换方法
F.7.6 其他修改方法:copy( )和swap( )
F.8 输出和输入
附录G 标准模板库方法和函数
G.1 STL 和C++11
G.1.1 新增的容器
G.1.2 对C++98 容器所做的修改
G.2 大部分容器都有的成员
G.3 序列容器的其他成员
G.4 set 和map 的其他操作
G.4 无序关联容器(C++11)
G.5 STL 函数
G.5.1 非修改式序列操作
G.5.2 修改式序列操作
G.5.3 排序和相关操作
G.5.4 数值运算
附录H 精选读物和网上资源
H.1 精选读物
H.2 网上资源
附录I 转换为ISO 标准C++
I.1 使用一些预处理器编译指令的替代品
I.1.1 使用const 而不是#define 来定义常量
I.1.2 使用inline 而不是# define 来定义小型函数
I.2 使用函数原型
I.3 使用类型转换
I.4 熟悉C++特性
I.5 使用新的头文件
I.6 使用名称空间
I.7 使用智能指针
I.8 使用string类
I.9 使用STL