《C++ Primer》学习笔记(四)
第四部分:高级主题
1.tuple 是类似pair的模板。每个pair 的成员类型都不相同,但每个 pair 都恰好有两个成员。每个确定的tuple 类型的成员数目是固定的,但一个 tuple 可以有任意数量的成员。tuple支持的操作如下图:
只有两个 tuple 具有相同数量的成员时,才可以比较它们。而且为了使用tuple的相等或不等运算符,对每对成员使用==运算符必须都是合法的;为了使用关系运算符,对每对成员使用<必须都是合法的(P638)。
2.bitset类是一个类模板,它类似array类,具有固定的大小。当定义一个bitset 时,需要声明它包含多少个二进制位(大小必须是一个常量表达式):bitset<32> bitvec(1U);//32位,低位为1,其他位为0。就像vector包含未命名的元素一样,bitset中的二进制位也是未命名的,可通过位置来访问它们。二进制位的位置是从0开始编号的,编号从0开始的二进制位被称为低位,编号最大的二进制位被称为高位(P641)。bitset初始化方法如下图:
当使用一个整型值来初始化bitset时,此值将被转换为unsigned long long类型并被当作位模式来处理,bitset中的二进制位将是此模式的一个副本。如果bitset的大小大于一个unsigned long long 中的二进制位数,则剩余的高位被置为0。如果bitset 的大小小于一个unsigned long long 中的二进制位数,则只使用给定值中的低位,超出bitset大小的高位被丢弃。可以从一个 string或一个字符数组指针来初始化bitset,两种情况下,字符都直接表示位模式。当使用字符串表示数时,字符串中下标最小的字符对应高位。bitset支持的操作如下图:
3.正则表达式是一种描述字符序列的方法,是一种极其强大的计算工具。C++正则表达式库(RE库)是新标准库的一部分,RE库定义在头文件regex中,它包含多个组件。如下图:
函数regex_match和regex_search 确定一个给定字符序列与一个给定 regex是否匹配,如果整个输入序列与表达式匹配,则regex_match函数返回true;如果输入序列中一个子串与表达式匹配,则regex_search数返回true,regex_search只会寻找第一个与正则表达式匹配的子序列,而sregex_iterator可用来获取所有匹配的子序列(P645)。下图列出了regex的函数的参数(上图中的regex_开头的三个函数),这些函数都返回boo1值,且都被重载了:
regex类的操作如下图:
正则表达式是在运行时,当一个regex对象被初始化或被赋予一个新模式时,才被“编译”的,所以一个正则表达式的语法是否正确是在运行时解析的。如果编写的正则表达式存在错误,则在运行时标准库会抛出一个类型为regex_error的异常。类似标准异常类型,regex_error有一个 what 操作来描述发生了什么错误,regex_error 还有一个名为code的成员,用来返回某个错误类型对应的数值编码,code 返回的值是由具体实现定义的。RE库能抛出的标准错误如下图:
可以搜索多种类型的输入序列,输入可以是普通char数据或wchart数据,字符可以保存在标准库string中或是char数组中(或是宽字符版本,wstring或wchart数组中)。RE为这些不同的输入序列类型都定义了对应的类型。重点在于使用的RE库类型必须与输入序列类型匹配,下图指出了RE库类型与输入序列类型的对应关系:
可以使用sregex_iterator 来获得所有匹配,regex迭代器是一种迭代器适配器,被绑定到一个输入序列和一个regex对象上(P650)。迭代器操作如下图:
*it是一个smatch对象的引用,it->是一个指向smatch对象的指针,smatch操作如下图:
smatch对象中保存了一次匹配的信息,而每次匹配获取的匹配对象的结果包含了各个子表达式的信息。正则表达式中的模式通常包含一个或多个子表达式,一个子表达式是模式的一部分,本身也具有意义,正则表达式语法通常用括号表示子表达式。匹配对象除了提供匹配整体的相关信息外,还提供访问模式中每个子表达式的能力。子匹配是按位置来访问的,第一个子匹配位置为0,表示整个模式对应的匹配,随后是每个子表达式对应的匹配(P654)。这些子匹配对象是用ssub_match类来描述的,其支持的操作如下图:
当希望在输入序列中查找并替换一个正则表达式时,可以调用regex_replace(P657),如下图:
参数mft需传入的匹配和格式化标志的类型为match_flag_type,这些值都定义在名为regex_constants的命名空间中,regex_constants是定义在命名空间std中的命名空间。为了使用regex_constants中的名字,必须在名字前同时加上两个命名空间的限定符:using namespace std::regexconstants;。match_flag_type可取的值如下图:
默认情况下,regex_replace输出整个输入序列,未与正则表达式匹配的部分会原样输出,匹配的部分按格式字符串指定的格式输出,可以通过在regex_replace调用中指定 format_no_copy 来改变这种默认行为(P659)。
4.随机数引擎类和随机数分布类定义在头文件 random 中,它们通过相互协作来生成随机数。一个引擎类可以生成unsigned随机数序列,一个分布类使用一个引擎类生成指定类型的、在给定范围内的、服从特定概率分布的随机数(P660)。标准库定义了多个随机数引擎类,区别在于性能和随机性质量不同,每个编译器都会指定其中一个作为 default_random_engine 类型,此类型一般具有最常用的特性。下图展示了随机数引擎支持的操作:
类似引擎类型,分布类型也是函数对象类。分布类型定义了一个调用运算符,它接受一个随机数引擎作为参数。分布对象使用它的引擎参数生成随机数,并将其映射到指定的分布,随机数发生器指的是分布对象和引擎对象的组合。一个给定的随机数发生器一直会生成相同的随机数序列,一个函数如果定义了局部的随机数发生器,应该将其(包括引擎和分布对象)定义为static的,否则每次调用函数都会生成相同的序列(P662)。可以通过设置一个种子来获得“随机性”,选择一个好的种子,与生成好的随机数所涉及的其他大多数事情相同,是极其困难的。可能最常用的方法是调用系统函数time,这个函数定义在头文件ctime中,它返回从一个特定时刻到当前经过了多少秒。函数time 接受单个指针参数,它指向用于写入时间的数据结构。如果此指针为空,则函数简单地返回时间,如:default_random_engine el(time(0));。分布类型支持的操作如下图:
5.标准库定义了一组操纵符来修改流的格式状态,一个操纵符是一个函数或是一个对象,会影响流的状态,并能用作输入或输出运算符的运算对象。类似输入和输出运算符,操纵符也返回它所处理的流对象,因此可以在一条语句中组合操纵符和数据(P666)。当操纵符改变流的格式状态时,通常改变后的状态对所有后续IO都生效(但也有些操纵符仅仅只决定其后面一个输出的格式,如setw)。常见操纵符如下图:
标准库还提供了一组低层操作,支持未格式化IO,这些操作允许将一个流当作一个无解释的字节序列来处理(P673)。有几个未格式化操作每次一个字节地处理流,它们会读取而不是忽略空白符,如下图:
一些未格式化 IO 操作一次处理大块数据,如下图:
各种流类型通常都支持对流中数据的随机访问(istream和ostream类型通常不支持随机访问),可以重定位流,使之跳过一些数据,首先读取最后一行,然后读取第一行,依此类推。标准库提供了一对函数,来定位(seek)到流中给定的位置,以及告诉(tell)我们当前位置。为了支持随机访问,IO类型维护一个标记来确定下一个读写操作要在哪里进行。它们还提供了两个函数:一个函数通过将标记 seek 到一个给定位置来重定位它;另一个函数tell 我们标记的当前位置。如下图:
从逻辑上讲,只能对istream和派生自istream 的类型 ifstream 和istringstream使用g版本,同样只能对ostream 和派生自ostream的类型ofstream和ostringstream 使用p版本。一个 iostream、fstream或 stringstream 既能读又能写关联的流,因此对这些类型的对象既能使用g版本又能使用p版本(P677)。
6.异常处理机制允许程序中独立开发的部分能够在运行时就出现的问题进行通信并做出相应的处理,异常使得我们能够将问题的检测与解决过程分离开来。程序的一部分负责检测问题的出现,然后解决该问题的任务传递给程序的另一部分。被选中的处理代码是在调用链中与抛出对象类型匹配的最近的处理代码,当执行一个 throw 时,跟在throw后面的语句将不再被执行。相反,程序的控制权从 throw 转移到与之匹配的 catch 模块。该 catch 可能是同一个函数中的局部 catch,也可能位于直接或间接调用了发生异常的函数的另一个函数中(P684),当找不到匹配的catch时,程序将调用标准库函数terminate, terminate负责终止程序的执行过程。查找与throw抛出的异常匹配的catch语句的过程被称为栈展开,在栈展开的过程中,运行类类型的局部对象的析构函数。因为这些析构函数是自动执行的,所以它们不应该抛出异常。一旦在栈展开的过程中析构函数抛出了异常,并且析构函数自身没能捕获到该异常,则程序将被终止。异常对象是一种特殊的对象,编译器使用异常抛出表达式来对异常对象进行拷贝初始化,异常对象位于由编译器管理的空间中,编译器确保无论最终调用的是哪个catch子句都能访问该空间,当异常处理完毕后,异常对象被销毁。当抛出一条表达式时,该表达式的静态编译时类型决定了异常对象的类型。也就是说如果一条throw表达式解引用一个基类指针,而该指针实际指向的是派生类对象,则抛出的对象将被切掉一部分,只有基类部分被抛出(P686)。
7.catch子句中的异常声明看起来像是只包含一个形参的函数形参列表,像在形参列表中一样,如果catch无须访问抛出的表达式的话,可以忽略捕获形参的名字。在搜寻 catch语句的过程中,最终找到的 catch 未必是异常的最佳匹配。相反,挑选出来的应该是第一个与异常匹配的catch语句。因此越是专门的catch 越应该置于整个 catch 列表的前端。因为catch语句是按照其出现的顺序逐一进行匹配的,所以当程序使用具有继承关系的多个异常时必须对catch语句的顺序进行组织和管理,使得派生类异常的处理代码出现在基类异常的处理代码之前,具体匹配规则见P687。有时一个单独的catch语句不能完整地处理某个异常,在执行了某些校正操作之后,当前的catch可能会决定由调用链更上一层的函数接着处理异常。一条catch语句通过重新抛出的操作将异常传递给另外一个catch语句。这里的重新抛出仍然是一条throw语句,只不过不包含任何表达式:throw;,空的throw语句只能出现在catch语句或 catch语句直接或间接调用的函数之内。如果在处理代码之外的区域遇到了空throw语句,编译器将调用terminate。一个重新抛出语句并不指定新的表达式,而是将当前的异常对象沿着调用链向上传递。有时希望不论抛出的异常是什么类型,程序都能统一捕获它们。为了一次性捕获所有异常,可使用省略号作为异常声明,这样的处理代码称为捕获所有异常的处理代码,形如catch(…)。一条捕获所有异常的语句可以与任意类型的异常匹配。catch(…)既能单独出现,也能与其他几个catch语句一起出现。如果 catch(…)与其他几个catch语句一起出现,则catch(…)必须在最后的位置,出现在捕获所有异常语句后面的catch语句将永远不会被匹配。
8.在 C++11新标准中,可以通过提供noexcept说明指定某个函数不会抛出异常,其形式是关键字noexcept紧跟在函数的参数列表后面,用以标识该函数不会抛出异常。对于一个函数来说,noexcept说明要么出现在该函数的所有声明语句和定义语句中,要么一次也不出现(P690)。编译器并不会在编译时检查noexcept说明,因此可能出现这样一种情况:尽管函数声明了它不会抛出异常,但实际上还是抛出了。一旦一个 noexcept 函数抛出了异常,程序就会调用terminate 以确保遵守不在运行时抛出异常的承诺。noexcept说明符接受一个可选的实参,该实参必须能转换为bool类型:如果实参是true,则函数不会抛出异常;如果实参是false,则函数可能抛出异常。如:void recoup(int) noexcept(true);//recoup 不会抛出异常。noexcept运算符是一元运算符,它的返回值是一个boo1类型的右值常量表达式,用于表示给定的表达式是否会抛出异常(常用于检查某个函数是否会抛出异常,注意是运算符不是上面的说明符),和sizeof类似,noexcept也不会求其运算对象的值(P691)。函数指针及该指针所指的函数必须具有一致的异常说明。也就是说,如果为某个指针做了不抛出异常的声明,则该指针将只能指向不抛出异常的函数。相反如果显式或隐式地说明了指针可能抛出异常,则该指针可以指向任何函数,即使是承诺了不抛出异常的函数也可以。如果一个虚函数承诺了它不会抛出异常,则后续派生出来的虚函数也必须做出同样的承诺;与之相反,如果基类的虚函数允许抛出异常,则派生类的对应函数既可以允许抛出异常,也可以不允许抛出异常(P692)。标准库的异常类的层次见P693。
9.命名空间为防止名字冲突提供了更加可控的机制。命名空间分割了全局命名空间,其中每个命名空间是一个作用域。通过在某个命名空间中定义库的名字,库的作者(以及用户)可以避免全局名字固有的限制。一个命名空间的定义包含两部分:首先是关键字namespace,随后是命名空间的名字。在命名空间名字后面是一系列由花括号括起来的声明和定义。只要能出现在全局作用域中的声明就能置于命名空间内,主要包括:类、变量(及其初始化操作)、函数(及其定义)、模板和其他命名空间。和其他名字一样,命名空间的名字也必须在定义它的作用域内保持唯一。命名空间既可以定义在全局作用域内,也可以定义在其他命名空间中,但是不能定义在函数或类的内部(P696)。在不同命名空间内可以有相同名字的成员,定义在某个命名空间中的名字可以被该命名空间内的其他成员直接访问,也可以被这些成员内嵌作用域中的任何单位访问,位于该命名空间之外的代码则必须明确指出所用的名字属于哪个命名空间。命名空间可以定义在几个不同的不连续的部分。在通常情况下,不把#include放在命名空间内部,否则隐含的意思是把头文件中所有的名字定义成该命名空间的成员。全局作用域中定义的名字(即在所有类、函数及命名空间之外定义的名字)也就是定义在全局命名空间中,全局命名空间以隐式的方式声明,并且在所有程序中都存在。全局作用域中定义的名字被隐式地添加到全局命名空间中。嵌套的命名空间同时是一个嵌套的作用域,它嵌套在外层命名空间的作用域中。嵌套的命名空间中的名字遵循的规则与往常类似:内层命名空间声明的名字将隐藏外层命名空间声明的同名成员。在嵌套的命名空间中定义的名字只在内层命名空间中有效,外层命名空间中的代码要想访问它必须在名字前添加限定符。
10.C++11新标准引入了一种新的嵌套命名空间,称为内联命名空间,和普通的嵌套命名空间不同,内联命名空间中的名字可以被外层命名空间直接使用。也就无须在内联命名空间的名字前添加表示该命名空间的前缀,通过外层命名空间的名字就可以直接访问它。定义内联命名空间的方式是在关键字namespace前添加关键字inline。关键字 inline 必须出现在命名空间第一次定义的地方,后续再打开命名空间的时候可以写inline,也可以不写(P699)。
11.未命名的命名空间是指关键字 namespace 后紧跟花括号括起来的一系列声明语句。未命名的命名空间中定义的变量拥有静态生命周期:它们在第一次使用前创建,并且直到程序结束才销毁。一个未命名的命名空间可以在某个给定的文件内不连续,但是不能跨越多个文件。每个文件定义自己的未命名的命名空间,如果两个文件都含有未命名的命名空间,则这两个空间互相无关。在这两个未命名的命名空间中可以定义相同的名字,并且这些定义表示的是不同实体。如果一个头文件定义了未命名的命名空间,则该命名空间中定义的名字将在每个包含了该头文件的文件中对应不同实体。定义在未命名的命名空间中的名字可以直接使用,同样也不能对未命名的命名空间的成员使用作用域运算符。如果未命名的命名空间定义在文件的最外层作用域中,则该命名空间中的名字一定要与全局作用域中的名字有所区别(P700)。
12.可以为命名空间设置别名,命名空间的别名声明以关键字 namespace 开始,后面是别名所用的名字、=符号、命名空间原来的名字以及一个分号,如namespace f=cpp_primer::first(f为命名空间cpp_primer::first的别名)。不能在命名空间还没有定义前就声明别名,否则将产生错误(P702)。一条using 声明语句一次只引入命名空间的一个成员,如:using namespace std::string;。而using 指示以关键字 using 开始,后面是关键字 namespace 以及命名空间的名字。using指示使得某个特定的命名空间中所有的名字都可见,这样就无须再为它们添加任何前缀限定符了,using指示一般被看作是出现在最近的外层作用域中(意思是using指示引入的所有变量被看作是在最近的外层作用域中定义的)(P703)。
13.对命名空间内部名字的查找遵循常规的查找规则:即由内向外依次查找每个外层作用域。外层作用域也可能是一个或多个嵌套的命名空间,直到最外层的全局命名空间查找过程终止。只有位于开放的块中且在使用点之前声明的名字才被考虑(P705)。当给函数传递一个类类型的对象时,除了在常规的作用域查找外还会查找实参类所属的命名空间,这一规则对于传递类的引用或指针的调用同样有效。所以对于move和forward最好使用std::move和std::forward来避免冲突,具体参考P707。一个另外的未声明的类或函数如果第一次出现在友元声明中,则认为它是最近的外层命名空间的成员。
14.using声明或using指示能将某些函数添加到候选函数集,using声明语句声明的是一个名字,而非一个特定的函数,当为函数书写using声明时,该函数的所有版本都被引入到当前作用域中。一个using声明引入的函数将重载该声明语句所属作用域中已有的其他同名函数。如果 using 声明出现在局部作用域中,则引入的名字将隐藏外层作用域的相关声明。如果using声明所在的作用域中已经有一个函数与新引入的函数同名且形参列表相同,则该using声明将引发错误。除此之外,using 声明将为引入的名字添加额外的重载实例,并最终扩充候选函数集的规模(P709)。与 using 声明不同的是,对于using指示来说,引入一个与已有函数形参列表完全相同的函数并不会产生错误,此时只要指明调用的是命名空间中的函数版本还是当前作用域的版本即可。
15.多重继承是指从多个直接基类中产生派生类的能力,多重继承的派生类继承了所有父类的属性。和只有一个基类的继承一样,多重继承的派生列表也只能包含已经被定义过的类,而且这些类不能是final的,对于派生类能够继承的基类个数,C++没有进行特殊规定,但是在某个给定的派生列表中,同一个基类只能出现一次。构造一个派生类的对象将同时构造并初始化它的所有基类子对象,其中基类的构造顺序与派生列表中基类的出现顺序保持一致,而与派生类构造函数初始值列表中基类的顺序无关(P711)。在 C++11新标准中,允许派生类从它的一个或几个基类中继承构造函数,但是如果从多个基类中继承了相同的构造函数(即形参列表完全相同),则程序将产生错误。如果一个类从它的多个基类中继承了相同的构造函数,则这个类必须为该构造函数定义它自己的版本。和往常一样,派生类的析构函数只负责清除派生类本身分配的资源,派生类的成员及基类都是自动销毁的,合成的析构函数体为空,析构函数的调用顺序正好与构造函数相反。
16.在有多个基类时,依然可以令某个可访问基类的指针或引用直接指向一个派生类对象,编译器不会在派生类向基类的几种转换中进行比较和选择,因为在它看来转换到任意一种基类都一样好(P713)。与只有一个基类的继承一样,对象、指针和引用的静态类型决定了能够使用哪些成员。当一个类拥有多个基类时,有可能出现派生类从两个或更多基类中继承了同名成员的情况,此时不加前缀限定符直接使用该名字将引发二义性(P715)。
17.尽管在派生列表中同一个基类只能出现一次,但实际上派生类可以多次继承同一个类。派生类可以通过它的两个直接基类分别继承同一个间接基类,也可以直接继承某个基类,然后通过另一个基类再一次间接继承该类。为了在这种情况下派生类包含多个子对象,可以使用虚继承,虚继承的目的是令某个类做出声明,承诺愿意共享它的基类。其中共享的基类子对象称为虚基类,在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含唯一一个共享的虚基类子对象。指定虚基类的方式是在派生列表中添加关键字virtual,可参考P718。不论基类是不是虚基类,派生类对象都能被可访问基类的指针或引用操作。
18.在虚派生中,虚基类是由最低层的派生类初始化的(最外层)。含有虚基类的对象的构造顺序与一般的顺序稍有区别:首先使用提供给最低层派生类构造函数的初始值初始化该对象的虚基类子部分,接下来按照直接基类在派生列表中出现的次序依次对其进行初始化。一个类可以有多个虚基类,此时这些虚的子对象按照它们在派生列表中出现的顺序从左向右依次构造(P721)。
19.使用new分配内存时实际执行了以下三步:第一步,new表达式调用一个名为operator new(或者operator new[])的标准库函数。该函数分配一块足够大的、原始的、未命名的内存空间以便存储特定类型的对象(或者对象的数组)。第二步,编译器运行相应的构造函数以构造这些对象,并为其传入初始值。第三步,对象被分配了空间并构造完成,返回一个指向该对象的指针。使用delete释放内存时实际执行了以下两步:第一步,对sp所指的对象或者arr所指的数组中的元素执行对应的析构函数。第二步,编译器调用名为operator delete(或者operator delete[])的标准库函数释放内存空间。如果应用程序希望控制内存分配的过程,则它们需要定义自己的operator new函数和operator delete函数。即使在标准库中已经存在这两个函数的定义,仍旧可以定义自己的版本。编译器不会对这种重复的定义提出异议,相反编译器将使用自定义的版本替换标准库定义的版本(标准库定义了operator new函数和operator delete函数的8个重载版本,具体见P727)。自定义的operator new函数和operator delete函数的目的在于改变内存分配的方式,但是不管怎样都不能改变new运算符和delete运算符的基本含义,自定义的operator new函数和operator delete函数的格式要求见P728。定位new形式(如new (place_address) type)的使用见P729,定位new允许我们在一个特定的、预先分配的内存地址上构造对象。
20.运行时类型识别(run-time type identification,RTTI)的功能由两个运算符实现:typeid 运算符,用于返回表达式的类型;dynamic_cast运算符,用于将基类的指针或引用安全地转换成派生类的指针或引用。这两个运算符特别适用于以下情况:想使用基类对象的指针或引用执行某个派生类操作并且该操作不是虚函数。一般来说,只要有可能应该尽量使用虚函数。当操作被定义成虚函数时,编译器将根据对象的动态类型自动地选择正确的函数版本(只有虚函数才支持动态绑定)。typeid 表达式的形式是typeid(e),其中e可以是任意表达式或类型的名字。typeid 运算符可以作用于任意类型的表达式,顶层 const被忽略,如果表达式是一个引用,则typeid返回该引用所引对象的类型。不过当 typeid 作用于数组或函数时,并不会执行向指针的标准类型转换。当运算对象不属于类类型或者是一个不包含任何虚函数的类时,typeid 运算符指示的是运算对象的静态类型。而当运算对象是定义了至少一个虚函数的类的左值时,typeid的结果直到运行时才会求得。注意typeid应该作用于对象,而不是指针(P733)。typeid 操作的结果是一个常量对象的引用,该对象的类型是标准库类型type_info或者 type_info 的公有派生类型。type_info 类定义在typeinfo 头文件中,其支持的操作如下图:
21.枚举类型可以将一组整型常量组织在一起,和类一样,每个枚举类型定义了一种新的类型,枚举属于字面值常量类型。C++包含两种枚举:限定作用域的和不限定作用域的(限定作用域指的是在使用枚举对象中的成员时要使用enumname::member的形式)。定义限定作用域的枚举类型的一般形式是:首先是关键字 enum class(或者等价地使用enum struct),随后是枚举类型名字以及用花括号括起来的以逗号分隔的枚举成员列表,最后是一个分号。定义不限定作用域的枚举类型时省略掉关键字 class或struct,枚举类型的名字是可选的。如果 enum 是未命名的,则只能在定义该enum 时定义它的对象。默认情况下,枚举值从0开始,依次加1,不过也能为一个或几个枚举成员指定专门的值。枚举成员是const,因此在初始化枚举成员时提供的初始值必须是常量表达式。一个不限定作用域的枚举类型的对象或枚举成员自动地转换成整型,因此可以在任何需要整型值的地方使用它们,而限定作用域的枚举类型则不行。尽管每个 enum 都定义了唯一的类型,但实际上 enum 是由某种整数类型表示的。在C++11新标准中,可以在 enum 的名字后加上冒号以及想在该 enum 中使用的类型。默认情况下限定作用域的enum成员类型是 int,对于不限定作用域的枚举类型来说,其枚举成员不存在默认类型(P738)。可以提前声明enum,enum的前置声明(无论隐式地还是显示地)必须指定其成员的大小。不能在同一个上下文中先声明一个不限定作用域的enum名字,然后再声明一个同名的限定作用域的enum。要想初始化一个enum对象,必须使用该enum 类型的另一个对象或者它的一个枚举成员。
22.成员指针是指可以指向类的非静态成员的指针。和其他指针一样,在声明成员指针时也使用*来表示当前声明的名字是一个指针。与普通指针不同的是,成员指针还必须包含成员所属的类。因此必须在*之前添加classname::以表示当前定义的指针可以指向classname的成员。例如: const string Screen::*pdata;//pdata可以指向一个常量(非常量)Screen对象的string成员,当初始化一个成员指针(或者向它赋值)时,需指定它所指的成员,如:pdata =&Screen::contents;。需要注意的是,当初始化一个成员指针或为成员指针赋值时,该指针并没有指向任何数据。成员指针指定了成员而非该成员所属的对象,只有当解引用成员指针时我们才提供对象的信息。与成员访问运算符.和->类似,也有两种成员指针访问运算符:.*和->*,这两个运算符可以解引用指针并获得该对象的成员:Screen myScreen,*pScreen=&myScreen; auto s=myScreen.*pdata;//.*解引用pdata以获得myScreen对象的contents成员,s =pScreen->*pdata; //->*解引用pdata以获得pScreen所指对象的contents成员(P740)。也可以定义指向类的成员函数的指针,具体可参考P741。要想通过一个指向成员函数的指针进行函数调用,必须首先利用.*运算符或->*运算符将该指针绑定到特定的对象上。因此与普通的函数指针不同,成员指针不是一个可调用对象,这样的指针不支持函数调用运算符,可以通过function、mem_fn或bind生成一个可调用对象,具体参考P745。
23.一个类可以定义在另一个类的内部,前者称为嵌套类或嵌套类型。嵌套类的名字在外层类作用域中是可见的,在外层类作用域之外不可见。和其他嵌套的名字一样,嵌套类的名字不会和别的作用域中的同一个名字冲突。嵌套类中成员的种类与非嵌套类是一样的。和其他类类似,嵌套类也使用访问限定符来控制外界对其成员的访问权限。外层类对嵌套类的成员没有特殊的访问权限,同样,嵌套类对外层类的成员也没有特殊的访问权限。嵌套类在其外层类中定义了一个类型成员。和其他成员类似,该类型的访问权限由外层类决定。位于外层类public部分的嵌套类实际上定义了一种可以随处访问的类型:位于外层类 protected部分的嵌套类定义的类型只能被外层类及其友元和派生类访问;位于外层类private部分的嵌套类定义的类型只能被外层类的成员和友元访问。嵌套类可以直接使用外层类的成员,无须对该成员的名字进行限定。外层类的对象和嵌套类的对象没有任何关系,嵌套类的对象只包含嵌套类定义的成员;同样,外层类的对象只包含外层类定义的成员,在外层类对象中不会有任何嵌套类的成员(P749)。
24.联合(union)是一种特殊的类。一个union可以有多个数据成员,但是在任意时刻只有一个数据成员可以有值。在给union 的某个成员赋值之后,该union 的其他成员就变成未定义的状态了。分配给一个union 对象的存储空间至少要能容纳它的最大的数据成员。union不能含有引用类型的成员,除此之外,它的成员可以是绝大多数类型。union可以为其成员指定public、protected和private等保护标记。默认情况下,union的成员都是公有的,这一点与struct 相同。union 可以定义包括构造函数和析构函数在内的成员函数。但是由于union 既不能继承自其他类,也不能作为基类使用,所以在union中不能含有虚函数。和其他内置类型一样,默认情况下union 是未初始化的。如果提供了初始值,则该初始值被用于初始化第一个成员。匿名union是一个未命名的union,对于一个匿名union,可以在其定义所在的作用域中直接访问其成员(P750)。
25.当 union 包含的是内置类型的成员时,可以使用普通的赋值语句改变 union 保存的值。但是对于含有特殊类类型成员的union就没这么简单了。如果想将union的值改为类类型成员对应的值,或者将类类型成员的值改为一个其他值,则必须分别构造或析构该类类型的成员:将union的值改为类类型成员对应的值时,必须运行该类型的构造函数;反之将类类型成员的值改为一个其他值时,必须运行该类型的析构函数。当union 包含的是内置类型的成员时,编译器将按照成员的次序依次合成默认构造函数或拷贝控制成员。但是如果union含有类类型的成员,并且该类型自定义了默认构造函数或拷贝控制成员,则编译器将为union合成对应的版本并将其声明为删除的。如果在某个类中含有一个union成员,而且该union 含有删除的拷贝控制成员,则该类与之对应的拷贝控制操作也将是删除的(P751)。和普通的类类型成员不一样,作为union 组成部分的类成员无法自动销毁。因为析构函数不清楚union存储的值是什么类型,所以它无法确定应该销毁哪个成员。
26.类可以定义在某个函数的内部,称这样的类为局部类。局部类的所有成员(包括函数在内)都必须完整定义在类的内部。在局部类中也不允许声明静态数据成员,因为没法定义这样的成员。局部类不能使用函数作用域中的变量局部类对其外层作用域中名字的访问权限受到很多限制,局部类只能访问外层作用域定义的类型名、静态变量以及枚举成员。如果局部类定义在某个函数内部,则该函数的普通局部变量不能被该局部类使用(P755)。
27.为了支持低层编程,C++定义了一些固有的不可移植的特性。所谓不可移植的特性是指因机器而异的特性,在将含有不可移植特性的程序从一台机器转移到另一台机器上时,通常需要重新编写该程序。类可以将其(非静态)数据成员定义成位域,在一个位域中含有一定数量的二进制位。位域在内存中的布局是与机器相关的。如果可能的话在类的内部连续定义的位域压缩在同一整数的相邻位,从而提供存储压缩。取地址运算符(&)不能作用于位域,因此任何指针都无法指向类的位域(P756)。直接处理硬件的程序常常包含这样的数据元素,它们的值由程序直接控制之外的过程控制。例如,程序可能包含一个由系统时钟定时更新的变量。当对象的值可能在程序的控制或检测之外被改变时,应该将该对象声明为volatile。关键字 volatile 告诉编译器不应对这样的对象进行优化。const和volatile限定符互相没什么影响,某种类型可能既是const的也是volatile的,此时它同时具有二者的属性。volatile用法见P757。C++程序有时需要调用其他语言编写的函数,最常见的是调用C语言编写的函数。像所有其他名字一样,其他语言中的函数名字也必须在C++中进行声明,并且该声明必须指定返回类型和形参列表。对于其他语言编写的函数来说,编译器检查其调用的方式与处理普通 C++函数的方式相同,但是生成的代码有所区别。C++使用链接指示指出任意非C++函数所用的语言。要想把C++代码和其他语言(包括C语言)编写的代码放在一起使用,要求必须有权访问该语言的编译器,并且这个编译器与当前的C++编译器是兼容的。链接指示不能出现在类定义或函数定义的内部,同样的链接指示必须在函数的每个声明中都出现。链接指示包含一个关键字 extern,后面是一个字符串字面值常量以及一个“普通的”函数声明,其中的字符串字面值常量指出了编写函数所用的语言,编译器应该支持对C语言的链接指示。可以令链接指示后面跟上花括号括起来的若干函数的声明,从而一次性建立多个链接。编写函数所用的语言是函数类型的一部分,因此对于使用链接指示定义的函数来说它的每个声明都必须使用相同的链接指示,而且指向其他语言编写的函数的指针必须与函数本身使用相同的链接指示,指向C函数的指针与指向C++函数的指针是不一样的类型(P759)。