《C++ Primer 第五版》读书笔记 - 第四部分

高级主题

第17章 标准库特殊设施

tuple类型

bitset类型

定义和初始化bitset

string的下标编号习惯与bitset恰好相反:string中下标最大的字(最右字符)用来初始化bitset中的低位(下标为0的二进制位)。

bitset操作

正则表达式

使用正则表达式库

一个正则表达式的语法是否正确是在运行时解析的。

如果我们编写的正则表达式存在错误,则在运行时标准库会抛出一个类型为regex_error的异常。类似标准异常类型,regex_error有个what操作来描述发生了什么错误。regex_error还有一个名为code的成员,用来返回某个错误类型对应的数值编码。

匹配与Regex迭代器类型

使用子表达式

使用regex_replace

随机数

定义在头文件random中的随机数库通过一组协作的类来解决这些问题:随机数引擎类和随机数分布类。一个引擎类可以生成unsigned随机数序列,一个分布类使用一个引擎类生成指定类型的、在给定范围内的、服从特定概率分布的随机数。

随机数引擎和分布

随机数引擎是函数对象类,它们定义了一个调用运算符,该运算符不接受参数并返回一个随机unsigned整数。我们可以通过调用一个随机数引擎对象来生成原始随机数。

分布类型也是函数对象类。分布类型定义了一个调用运算符,它接受一个随机数引擎作为参数。分布对象使用它的引擎参数生成随机数,并将其映射到指定的分布。

其他随机数分布

我们可以定义一个uniform_real_distribution类型的对象,并让标准库来处理从随机整数到随机浮点数的映射。在定义对象时,我们指定最小值和最大值。

每个分布模板都有一个默认模板实参。生成浮点值的分布类型默认生成double值,而生成整型值的分布默认生成int值。由于分布类型只有一个模板参数,因此当我们希望使用默认随机数类型时要记得在模板名之后使用空尖括号。

bernoulli_distribution是一个普通类,而非模板。此分布总是返回一个bool值。它返回true的概率是一个常数,此概率的默认值是0.5。

IO库再探

格式化输入与输出

标准库定义了一组操纵符来修改流的格式状态。一个操纵符是一个函数或是一个对象,会影响流的状态,并能用作输入或输出运算符的运算对象。类似输入和输出运算符,操纵符也返回它所处理的流对象,因此我们可以在一条语句中组合操纵符和数据。

当操纵符改变流的格式状态时,通常改变后的状态对所有后续IO都生效。

未格式化的输入/输出操作

一般情况下,在读取下一个值之前,标准库保证我们可以退回最多一个值。

这些函数返回一个int的原因是:可以返回文件尾标记。标准库使用负值表示文件尾,这样就可以保证与任何合法字符的值都不同。

应该在任何后续未格式化输入操作之前调用gcount。特别是,将字符退回流的单字符操作也属于未格式化输入操作。如果在调用gcount之前调用了peekungetputback,则gcount的返回值为0。

流随机访问

虽然标准库为所有流类型都定义了seektell函数,但它们是否会做有意义的事情依赖于流绑定到哪个设备。在大多数系统中,绑定到cincoutcerrclog的流不支持随机访问。

fstreamstringstream类型可以读写同一个流。在这些类型中,有单一的缓冲区用于保存读写的数据,同样,标记也只有一个,表示缓冲区中的当前位置。标准库将gp版本的读写位置都映射到这个单一的标记。

第18章 用于大型程序的工具

异常处理

抛出异常

当执行一个throw时,跟在throw后面的语句将不再被执行。相反,程序的控制权从throw转移到与之匹配的catch模块。该catch可能是同一个函数中的局部catch,也可能位于直接或间接调用了发生异常的函数的另一个函数中。

控制权从一处转移到另处,这有两个重要的含义:

  • 沿着调用链的函数可能会提早退出。
  • 一旦程序开始执行异常处理代码,则沿着调用链创建的对象将被销毁。

异常对象是一种特殊的对象,编译器使用异常抛出表达式来对异常对象进行拷贝初始化。因此,throw语句中的表达式必须拥有完全类型。而且如果该表达式是类类型的话,则相应的类必须含有一个可访问的析构函数和一个可访问的拷贝或移动构造函数。如果该表达式是数组类型或函数类型,则表达式将被转换成与之对应的指针类型。

异常对象位于由编译器管理的空间中,编译器确保无论最终调用的是哪个catch子句都能访问该空间。当异常处理完毕后,异常对象被销毁。

抛出一个指向局部对象的指针几乎肯定是一种错误的行为。

当我们抛出一条表达式时,该表达式的静态编译时类型决定了异常对象的类型。

捕获异常

catch子句中的异常声明看起来像是只包含个形参的函数形参列表。像在形参列表中一样,如果catch无须访问抛出的表达式的话,则我们可以忽略捕获形参的名字。

声明的类型决定了处理代码所能捕获的异常类型。这个类型必须是完全类型,它可以是左值引用,但不能是右值引用。

当进入一个catch语句后,通过异常对象初始化异常声明中的参数。

如果catch的参数是基类类型,则我们可以使用其派生类类型的异常对象对其进行初始化。

另一方面,如果catch的参数是基类的引用,则该参数将以常规方式绑定到异常对象上。异常声明的静态类型将决定catch语句所能执行的操作。如果catch的参数是基类类型,则catch无法使用派生类特有的任何成员。

越是专门的catch越应该置于整个catch列表的前端。 因为catch语句是按照其出现的顺序逐一进行匹配的,所以当程序使用具有继承关系的多个异常时必须对catch语句的顺序进行组织和管理,使得派生类异常的处理代码出现在基类异常的处理代码之前。

除了一些极细小的差別之外,要求异常的类型和catch声明的类型是精确匹配的:

  • 允许从非常量向常量的类型转换。
  • 允许从派生类向基类的类型转换。
  • 数组被转换成指向数组(元素)类型的指针,函数被转成指向该函数类型的指针。

除此之外,包括标准算术类型转换和类类型转换在内,其他所有转换规则都不能在匹配catch的过程中使用。

在执行了某些校正操作之后,当前的catch可能会决定由调用链更上一层的函数接着处理异常。一条catch语句通过重新抛出的操作将异常传递给另外一个catch语句。这里的重新抛出仍然是一条throw语句,只不过不包含任何表达式。

空的throw语句只能出现在catch语句或catch语句直接或间接调用的函数之内。如果在处理代码之外的区域遇到了空throw语句,编译器将调用terminate

一个重新抛出语句并不指定新的表达式,而是将当前的异常对象沿着调用链向上传递。

很多时候,catch语句会改变其参数的内容。如果在改变了参数的内容后catch语句重新抛出异常,则只有当catch异常声明是引用类型时我们对参数所做的改变才会被保留并继续传播。

为了一次性捕获所有异常,我们使用省略号作为异常声明,这样的处理代码称为捕获所有异常的处理代码,形如catch(...)。一条捕获所有异常的语句可以与任意类型的异常匹配。

函数try语句块与构造函数

要想处理构造函数初始值抛出的异常,我们必须将构造函数写成函数try语句块的形式。函数try语句块使得一组catch语句既能处理构造函数体(或析构函数体),也能处理构造函数的初始化过程(或析构函数的析构过程)。关键字try出现在表示构造函数初始值列表的冒号以及构造函数体的花括号之前。

noexcept异常说明

通过提供npexcept说明指定某个函数不会抛出异常。其形式是关键字noexcept紧跟在函数的参数列表后面。

对于一个函数来说,noexcept说明要么出现在该函数的所有声明语句和定义语句中,要么一次也不出现。该说明应该在函数的尾置返回类型之前。我们也可以在函数指针的声明和定义中指定noexcept。在typedef或类型别名中则不能出现noexcept。在成员函数中,noexcept说明符需要跟在const及引用限定符之后,而在finaloverride或虚函数的=0之前。

一旦一个noexcept函数抛出了异常,程序就会调用terminate以确保遵守不在运行时抛出异常的承诺。

noexcept说明符接受一个可选的实参,该实参必须能转换为bool类型:如果实参是true,则函数不会抛出异常;如果实参是false,则函数可能抛出异常。

noexcept运算符是一个一元运算符,它的返回值是一个bool类型的右值常量表达式,用于表示给定的表达式是否会抛出异常。noexcept不会求其运算对象的值。

noexcept有两层含义:当跟在函数参数列表后面时它是异常说明符;而当作为noexcept异常说明的bool实参出现时,它是一个运算符。

函数指针及该指针所指的函数必须具有一致的异常说明。也就是说,如果我们为某个指针做了不抛出异常的声明,则该指针将只能指向不抛出异常的函数。相反,如果我们显式或隐式地说明了指针可能抛出异常,则该指针可以指向任何函数,即使是承诺了不抛出异常的函数也可以.

如果一个虚函数承诺了它不会抛出异常,则后续派生出来的虚函数也必须做出同样的承诺;与之相反,如果基类的虚函数允许抛出异常,则派生类的对应函数既可以允许抛出异常,也可以不允许抛出异常。

当编译器合成拷贝控制成员时,同时也生成一个异常说明。如果对所有成员和基类的所有操作都承诺了不会抛出异常,则合成的成员是noexcept的。如果合成成员调用的任意一个函数可能抛出异常,则合成的成员是 noexcept(false)。而且,如果我们定义了一个析构函数但是没有为它提供异常说明,则编译器将合成一个。合成的异常说明将与假设由编译器为类合成析构函数时所得的异常说明一致。

异常类层次

类型exception仅仅定义了拷贝构造函数、拷贝赋值运算符、一个虚析构函数和个名为what的虚成员。其中what函数返回一个const char*,该指针指向一个以null结尾的字符数组,并且确保不会抛出任何异常。

exceptionbad_castbad_alloc定义了默认构造函数。类runtime_errorlogic_error没有默认构造函数,但是有一个可以接受C风格字符串或者标准库string类型实参的构造函数,这些实参负责提供关于错误的更多信息。在这些类中,what负责返回用于初始化异常对象的信息。因为what是虚函数,所以当我们捕获基类的引用时,对what函数的调用将执行与异常对象动态类型对应的版本。

命名空间

只要能出现在全局作用域中的声明就能置于命名空间内,主要包括:类、变量(及其初始化操作)、函数(及其定义)、模板和其他命名空间

命名空间可以定义在几个不同的部分。

在通常情况下,我们不把#include放在命名空间内部。如果我们这么做了,隐含的意思是把头文件中所有的名字定义成该命名空间的成员。

和定义在类外部的类成员一样,一且看到含有完整前缀的名字,我们就可以确定该名字位于命名空间的作用域内。

模板特例化必须定义在原始模板所属的命名空间中。和其他命名空间名字类似,只要我们在命名空间中声明了特例化,就能在命名空间外部定义它了。

和普通的嵌套命名空间不同,内联命名空间中的名字可以被外层命名空间直接使用。也就是说,我们无须在内联命名空间的名字前添加表示该命名空间的前缀,通过外层命名空间的名字就可以直接访问它。定义内联命名空间的方式是在关键字namespace前添加关键字inline

关键字inline必须出现在命名空间第一次定义的地方,后续再打开命名空间的时候可以写inline,也可以不写。

未命名的命名空间是指关键字namespace后紧跟花括号括起来的一系列声明语句。未命名的命名空间中定义的变量拥有静态生命周期:它们在第一次使用前创建,并且直到程序结束才销毁。

一个未命名的命名空间可以在某个给定的文件内不连续,但是不能跨越多个文件。每个文件定义自己的未命名的命名空间,如果两个文件都含有未命名的命名空间,则这两个空间互相无关。在这两个未命名的命名空间中可以定义相同的名字,并且这些定义表示的是不同实体。如果一个头文件定义了命名的命名空间,则该命名空间中定义的名字将在每个包含了该头文件的文件中对应不同实体。

定义在未命名的命名空间中的名字可以直接使用;同样的,我们也不能对未命名的命名空间的成员使用作用域运算符。未命名的命名空间中定义的名字的作用域与该命名空间所在的作用域相同。

在文件中进行静态声明的做法已经被C++标准取消了,现在的做法是使用未命名的命名空间。

使用命名空间成员

命名空间的别名声明以关键字namespace开始,后面是别名所用的名字、=符号、命名空间原来的名字以及一个分号。不能在命名空间还没有定义前就声明别名,否则将产生错误。命名空间的别名也可以指向一个嵌套的命名空间。

using声明的名字的作用域与using声明语句本身的作用域一致,从效果上看就好像using声明语句为命名空间的成员在当前作用域内创建了一个别名一样。

对于using声明来说,我们只是简单地令名字在局部作用域内有效。相反,using指示是令整个命名空间的所有内容变得有效。通常情况下,命名空间中会含有一些不能出现在局部作用域中的定义,因此,using指示一般被看作是出现在最近的外层作用域中。

头文件如果在其顶层作用域中含有using指示或using声明,则会将名字注入到所有包含了该头文件的文件中。通常情况下,头文件应该只负责定义接口部分的名字,而不定义实现部分的名字。因此,头文件最多只能在它的函数或命名空间内使用using指示或using声明。

类、命名空间与作用域

当我们给函数传递一个类类型的对象时,除了在常规的作用域找外还会查找实参类所属的命名空间。这一例外对于传递类的引用或指针的调用同样有效。查找规则的这个例外允许概念上作为类接口一部分的非成员函数无须单独的using声明就能被程序使用。

当类声明了一个友元时,该友元声明并没有使得友元本身可见。然而,一个另外的未声明的类或函数如果第一次出现在友元声明中,则我们认为它是最近的外层命名空间的成员。

重载与命名空间

using声明语句声明的是一个名字,而非一个特定的函数。

一个using声明引入的函数将重载该声明语句所属作用域中己有的其他同名函数。如果using声明出现在局部作用域中,则引入的名字将隐藏外层作用域的相关声明。如果using声明所在的作用域中己经有一个函数与新引入的函数同名且形参列表相同,则该using声明将引发错误。除此之外,using声明将为引入的名字添加额外的重载实例,并最终扩充候选函数集的规模。

对于using指示来说,引入一个与已有函数形参列表完全相同的函数并不会产生错误。此时,只要我们指明调用的是命名空间中的函数版本还是当前作用域的版本即可。

多重继承与虚继承

多重继承

对于派生类能够继承的基类个数,C++没有进行特殊规定;但是在某个给定的派生列表中,同一个基类只能出现一次。

派生类的构造函数初始值列表将实参分别传递给每个直接基类。其中基类的构造顺序与派生列表中基类的出现顺序保持一致,而与派生类构造函数初始值列表中基类的顺序无关。

允许派生类从它的一个或几个基类中继承构造函数。但是如果从多个基类中继承了相同的构造函数(即形参列表完全相同), 则程序将产生错误。

类型转换与多个基类

编译器不会在派生类向基类的几种转换中进行比较和选择,因为在它看来转换到任意一种基类都一样好。

多重继承下的类作用域

当一个类拥有多个基类时,有可能出现派生类从两个或更多基类中继承了同名成员的情况。此时,不加前限定符直接使用该名字将引发二义性。

虚继承

虚继承的目的是令某个类做出声明,承诺愿意共享它的基类。其中,共享的基类子对象称为虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含唯一一个共享的虚基类子对象。

虚派生只影响从指定了虚基类的派生类中进一步派生出的类,它不会影响派生类本身。

我们指定虚基类的方式是在派生列表中添加关键字virtual

因为在每个共享的虚基类中只有唯一一个共享的子对象,所以该基类的成员可以被直接访问,并且不会产生二义性。此外,如果虚基类的成员只被一条派生路径覆盖,则我们仍然可以直接访问这个被覆盖的成员。但是如果成员被多于一个基类覆盖,则一般情况下派生类必须为该成员自定义一个新的版本。

构造函数与虚继承

在虚派生中,虚基类是由最低层的派生类初始化的。

含有虚基类的对象的构造顺序与一般的顺序稍有区别:首先使用提供给最低层派生类构造函数的初始值初始化该对象的虚基类子部分,接下来按照直接基类在派生列表中出现的次序依次对其进行初始化。

虚基类总是先于非虚基类构造,与它们在继承体系中的次序和位置无关。

一个类可以有多个虚基类。此时,这些虚的子对象按照它们在派生列表中出现的顺序从左向右依次构造。

第19章 特殊工具与技术

控制内存分配

重载new和delete

当我们使用一条new表达式时实际执行了三步操作。第一步,new表达式调用一个名为operator new(或者operator new[])的标准库函数。该函数分配一块足够大的、原始的、未命名的内存空间以便存储特定类型的对象(或者对象的数组)。第二步,编译器运行相应的构造函数以构造这些对象,并为其传入初始值。第三步,对象被分配了空间并构造完成,返回一个指向该对象的指针。

当我们使用一条delete表达式删除一个动态分配的对象时实际执行了两步操作。第一步,对sp所指的对象或者arr所指的数组中的元素执行对应的析构函数。第二步,编译器调用名为operator delete(或者operator delete[])的标准库函数释放内存空间。

应用程序可以在全局作用域中定义operator new函数和operator delete函数,也可以将它们定义为成员函数。当编译器发现一条new表达式或delete表达式后,将在程序中查找可供调用的operator函数。如果被分配(释放)的对象是类类型,则编译器首先在类及其基类的作用域中査找。此时如果该类含有operator new成员或operator delete成员,则相应的表达式将调用这些成员。否则,编译器在全局作用域査找匹配的函数。此时如果编译器找到了用户自定义的版本,则使用该版本执行new表达式或delete表达式;如果没找到,则使用标准库定义的版本。

标准库定义了operator new函数和operator delete函数的8个重载版本。其中前4个版本可能抛出bad_alloc异常,后4个版本则不会抛出异常:

1
2
3
4
5
6
7
8
9
void *operator new(size t);
void *operator new[](size t);
void *operator delete(void*)noexcept;
void *operator delete[](void*) noexcept;

void *operator new(size_t, nothrow t&)noexcept;
void *operator new[](size_t, nothrow t&)noexcept
void *operator delete(void*, nothrow t&)noexcept;
void *operator delete[](void*, nothrow t&)noexcept;

类型nothrow_t是定义在new头文件中的一个struct,在这个类型中不包含任何成员。new头文件还定义了一个名为nothrowconst对象,用户可以通过这个对象请求new的非抛出版本。与析构函数类似,operator delete也不允许抛出异常。当我们重载这些运算符时,必须使用noexcept异常说明符指定其不抛出异常。

应用程序可以自定义上面函数版本中的任意一个,前提是自定义的版本必须位于全局作用域或者类作用域中。当我们将上述运算符函数定义成类的成员时,它们是隐式静态的。因为operator new用在对象构造之前而operator delete用在对象销毁之后,所以这两个成员(newdelete)必须是静态的,而且它们不能操纵类的任何数据成员。

对于operator new函数或者operator new[]函数来说,它的返回类型必须是void*,第一个形参的类型必须是size_t且该形参不能含有默认实参。当我们为一个对象分配空间时使用operator new;为一个数组分配空间时使用operator new[]。当编译器调用operator new时,把存储指定类型对象所需的字节数传给size_t形参;当调用operator new[]时,传入函数的则是存储数组中所有元素所需的空间。

如果我们想要自定义operator new函数,则可以为它提供额外的形参。此时,用到这些自定义函数的new表达式必须使用new的定位形式将实参传给新增的形参。尽管在一般情况下我们可以自定义具有任何形参的 operator new,但是下面这个函数却无论如何不能被用户重载

1
void *operator new(size t, void*); //不允许重新定义这个版本

对于operator delete函数或者operator delete[]函数来说,它们的返回类型必须是void,第一个形参的类型必须是void*。执行一条delete表达式将调用相应的operator函数,并用指向待释放内存的指针来初始化void*形参。

当我们将operator deleteoperator delete[]定义成类的成员时,该函数可以包含另外一个类型为size_t的形参。此时,该形参的初始值是第一个形参所指对象的字节数。size_t形参可用于删除继承体系中的对象。如果基类有一个虚析构函数,则传递给operator delete的字节数将因待删除指针所指对象的动态类型不同而有所区別。而且,实际运行的operator delete函数版本也由对象的动态类型决定。

定位new表达式

allocator不同的是,对于operator new分配的内存空间来说我们无法使用construct函数构造对象。相反,我们应该使用new的定位new形式构造对象。

我们可以使用定位new传递一个地址,此时定位new的形式如下所示:

1
2
3
4
new (place_address) type
new (place_address) type (initializers)
new (place address) type [size]
new (place address) type [size] { bracedinitializer list}

其中place_address必须是一个指针,同时在initializers中提供一个(可能为空的)以逗号分隔的初始值列表,该初始值列表将用于构造新分配的对象。

当仅通过一个地址值调用时,定位new使用operator new(size_t, void*)“分配”它的内存。这是一个我们无法自定义的operator new版本。该函数不分配任何内存,它只是简单地返回指针实参;然后由new表达式负责在指定的地址初始化对象以完成整个工作。事实上,定位new允许我们在一个特定的、预先分配的内存地址上构造对象.

我们传给construct的指针必须指向同一个allorator对象分配的空间,但是传给定位new的指针无须指向operator new分配的内存。传给定位new表达式的指针甚至不需要指向动态内存。

和调用destroy类似,调用析构函数可以清除给定的对象但是不会释放该对象所在的空间。如果需要的话,我们可以重新使用该空间

运行时类型识别

运行时类型识别的功能由两个运算符实现:

  • typeid运算符,用于返回表达式的类型。
  • dynamic_cast运算符,用于将基类的指针或引用安全地转换成派生类的指针或引用。

当我们将这两个运算符用于某种类型的指针或引用,并且该类型含有虚函数时,运算符将使用指针或引用所绑定对象的动态类型。

这两个运算符特别适用于以下情況:我们想使用基类对象的指针或引用执行某个派生类操作并且该操作不是虚函数。一般来说,只要有可能我们应该尽量使用虚函数。当操作被定义成虚函数时,编译器将根据对象的动态类型自动地选择正确的函数版本。

然而,并非任何时候都能定义一个虚函数。假设我们无法使用虚函数,则可以使用个RTTI运算符。

dynamic_cast运算符

dynamic_cast运算符的使用形式如下所示:

1
2
3
dynamic_cast<type*>(e)
dynamic_cast<type&>(e)
dynamic_cast<type&&>(e)

其中,type必须是一个类类型,并且通常情况下该类型应该含有虚函数。在第一种形式中,e必须是一个有效的指针;在第二种形式中,e必须是一个左值;在第三种形式中,e不能是左值。

在上面的所有形式中,e的类型必须符合以下三个条件中的任意一个:e的类型是目标type的公有派生类、e的类型是目标type的公有基类或者e的类型就是目标type的类型。如果符合,则类型转换可以成功。否则,转换失败。

如果一条dynamic_cast语句的转换目标是指针类型并且失败了,则结果为0。如果转换目标是引用类型并且失败了, 则抛出一个bad_cast异常。

typeid运算符

typeid表达式的形式是typeid(e),其中e可以是任意表达式或类型的名字。typeid操作的结果是一个常量对象的引用,该对象的类型是标准库类型type_info或者type_info的公有派生类型。type_info类定义在typeinfo头文件中。

typeid运算符可以作用于任意类型的表达式。和往常一样,顶层const被忽略,如果表达式是一个引用,则 typeid返回该引用所引对象的类型。不过当typeid作用于数组或函数时,并不会执行向指针的标准类型转换。当运算对象不属于类类型或者是一个不包含任何函数的类时,typeid运算符指示的是运算对象的静态类型。而当运算对象是定义了至少个虚函数的类的左值时,typeid的结果直到运行时才会求得。

typeid是否需要运行时检查决定了表达式是否会被求值。只有当类型含有虚函数时, 编译器才会对表达式求值。反之,如果类型不含有虚函数,则typeid返回表达式的静态类型。

如果表达式的动态类型可能与静态类型不同,则必须在运行时对表达式求值以确定返回的类型。这条规则适用于typeid(*p)的情况。如果指针p所指的类型不含有虚函数, 则p不必非得是一个有效的指针。否则,p将在运行时求值,此时p必须是一个有效的指针。如果p是一个空指针,则typeid(*p)将抛出一个名为bad_typeid的异常。

使用RTTI

在某些情况下RTTI非常有用,比如当我们想为具有继承关系的类实现相等运算符时。对于两个对象来说,如果它们的类型相同并且对应的数据成员取值相同,则我们说这两个对象是相等的。在类的继承体系中,每个派生类负责添加自己的数据成员,因此派生类的相等运算符必须把派生类的新成员考虑进来。

type_info类

我们无法定义或拷贝type_info类型的对象,也不能为type_info类型的对象赋值。创建type_info对象的唯一途径是使用typeid运算符。

对于name返回值的唯一要求是,类型不同则返回的字符串必须有所区别。

枚举类型

定义限定作用域的枚举类型的一般形式是:首先是关键字enum class(或者等价地使用enum struct),随后是枚举类型名字以及用花括号括起来的以逗号分隔的枚举成员列表,最后是一个分号。

定义不限定作用域的枚举类型时省略掉关键字class(或struct),枚举类型的名字是可选的。

如果enum是未命名的,则我们只能在定义该enum时定义它的对象。

在限定作用域的枚举类型中,枚举成员的名字遵循常规的作用域准则,并且在枚举类型的作用域外是不可访问的。与之相反,在不限定作用域的枚举类型中,枚举成员的作用域与枚举类型本身的作用域相同。

在初始化枚举成员时提供的初始值必须是常量表达式。每个枚举成员本身就是一条常量表达式,我们可以在任何需要常量表达式的地方使用枚举成员。

要想初始化enum对象或者为enum对象赋值,必须使用该类型的一个枚举成员或者该类型的另一个对象。

一个不限定作用域的枚举类型的对象或枚举成员自动地转换成整型。

我们可以在enum的名字后加上冒号以及我们想在该enum中使用的类型。

如果我们没有指定enum的潜在类型,则默认情况下限定作用域的enum成员类型是int。对于不限定作用域的枚举类型来说,其枚举成员不存在默认类型,我们只知道成员的潜在类型足够大,肯定能够容纳枚举值。如果我们指定了枚举成员的潜在类型(包括对限定作用域的enum的隐式指定),则一旦某个枚举成员的值超出了该类型所能容纳的范围,将引发程序错误。

enum的前置声明(无论隐式地还是显示地)必须指定其成员的大小。

因为不限定作用域的enum未指定成员的默认大小,因此每个声明必须指定成员的大小。对于限定作用域的enum来说,我们可以不指定其成员的大小,这个值被隐式地定义成int

和其他声明语句一样,enum的声明和定义必须匹配。

要想初始化一个enum对象,必须使用该enum类型的另一个对象或者它的一个枚举成员。因此,即使某个整型值恰好与枚举成员的值相等,它也不能作为函数的enum实参使用。

尽管我们不能直接将整型值传给enum形参,但是可以将一个不限定作用域的枚举类型的对象或枚举成员传给整型形参。此时,enum的值提升成int或更大的整型,实际提升的结果由枚举类型的潜在类型决定。

类成员指针

类的静态成员不属于任何对象,因此无须特殊的指向静态成员的指针,指向静态成员的指针与普通指针没有什么区别。

成员指针的类型囊括了类的类型以及成员的类型。当初始化一个这样的指针时,我们令其指向类的某个成员,但是不指定该成员所属的对象:直到使用成员指针时,才提供成员所属的对象。

数据成员指针

和其他指针一样,在声明成员指针时我们也使用*来表示当前声明的名字是一个指针。与普通指针不同的是,成员指针还必须包含成员所属的类。在*前添加classname::以表示当前定义的指针可以指向classname的成员。

当我们初始化一个成员指针或为成员指针赋值时,该指针并没有指向任何数据。成员指针指定了成员而非该成员所属的对象,只有当解引用成员指针时我们才提供对象的信息。

两种成员指针访问运算符:.*->*,这两个运算符使得我们可以解引用指针并获得该对象的成员。

成员函数指针

成员函数和指向该成员的指针之间不存在自动转换规则。

因为函数调用运算符的优先级較高,所以在声明指向成员函数的指针并使用这样的指针进行函数调用时,括号必不可少:(C::*p)(parms)(obj.*p)(args)

将成员函数用作可调用对象

如我们所知,要想通过一个指向成员函数的指针进行函数调用,必须首先利用.*运算符或->*运算符将该指针绑定到特定的对象上。因此与普通的函数指针不同,成员指针不是一个可调用对象,这样的指针不支持函数调用运算符。

从指向成员函数的指针获取可调用对象的一种方法是使用标准库模板function

通过使用标准库功能mem_fn来让编译器负责推断成员的类型。和function一样,mem_fn也定义在functional头文件中,并且可以从成员指针生成一个可调用对象;和function不同的是,mem_fn可以根据成员指针的类型推断可调用对象的类型,而无须用户显式地指定。

mem_fn生成的可调用对象可以通过对象调用,也可以通过指针调用。

我们还可以使用bind从成员函数生成一个可调用对象。

嵌套类

嵌套类是一个独立的类,与外层类基本没什么关系。特别是,外层类的对象和嵌套类的对象是相互独立的。在嵌套类的对象中不包含任何外层类定义的成员;类似的,在外层类的对象中也不包含任何嵌套类定义的成员。

union:一种节省空间的类

union不能含有引用类型的成员,除此之外,它的成员可以是绝大多数类型。在C++11新标准中,含有构造函数或析构函数的类类型也可以作为union的成员类型。union可以为其成员指定publicprotectedprivate等保护标记。默认情况下,union的成员都是公有的,这一点与struct相同。union可以定义包括构造函数和析构函数在内的成员函数。但是由于union既不能继承自其他类,也不能作为基类使用,所以在union中不能含有函数。

匿名union是一个未命名的union,并且在右花括号和分号之间没有任何声明。一旦我们定义了一个匿名union,编译器就自动地为该union创建一个未命名的对象。

在匿名union的定义所在的作用域内,该union的成员都是可以直接访问的。

union包含的是内置类型的成员时,我们可以使用普通的赋值语句改变union保存的值。但是对于含有特殊类类型成员的union就没这么简单了。如果我们想将union的值改为类类型成员对应的值,或者将类类型成员的值改为一个其他值,则必须分别构造或析构该类类型的成员;当我们将union的值改为类类型成员对应的值时,必须运行该类型的构造函数;反之,当我们将类类型成员的值改为一个其他值时,必须运行该类型的析构函数。

union包含的是内置类型的成员时,编译器将按照成员的次序依次合成默认构造函数或拷贝控制成员。但是如果union含有类类型的成员,并且该类型自定义了默认构造函数或拷贝控制成员,则编译器将为union合成对应的版本并将其声明为删除的。

为了追踪union中到底存储了什么类型的值,我们通常会定义一个独立的对象,该对象称为union的判别式。我们可以使用判别式辨认union存储的值。

局部类

局部类的所有成员(包括函数在内)都必须完整定义在类的内部。

在局部类中不允许声明静态数据成员。

局部类只能访问外层作用域定义的类型名、静态变量以及枚举成员。如果局部类定义在某个函数内部,则该函数的普通局部变量不能被该局部类使用。

固有的不可移植的特性

位域

类可以将其(非静态)数据成员定义成位域,在一个位域中含有一定数量的二进制位。

位域的类型必须是整型或枚举类型。因为带符号位域的行为是由具体实现确定的,所以在通常情况下我们使用无符号类型保存一个位域。位域的声明形式是在成员名字之后紧跟一个冒号以及一个常量表达式,该表达式用于指定成员所占的二进制位数。

取地址运算符(&)不能作用于位域,因此任何指针都无法指向类的位域。

volatile限定符

当对象的值可能在程序的控制或检测之外被改变时,应该将该对象声明为volatile。关键字volatile告诉编译器不应对这样的对象进行优化。

constvolatile限定符互相没什么影响。

也可以将成员函数定义成volatile的。只有volatile的成员函数才能被volatile的对象调用。

const一样,我们只能将一个volatile对象的地址(或者拷贝一个指向volatile类型的指针)赋给一个指向volatile的指针。同时,只有当某个引用是volatile的时,我们オ能使用一个volatile对象初始化该引用。

我们不能使用合成的拷贝/移动构造函数及赋值运算符初始化volatile对象或从volatile对象赋值。

链接指示:extern "C"

链接指示可以有两种形式:单个的或复合的。链接指示不能出现在类定义或函数定义的内部。同样的链接指示必须在函数的每个声明中都出现。

链接指示的第一种形式包含一个关键字extern,后面是一个字符串字面值常量以及一个“普通的”函数声明。 其中的字符串字面值常量指出了编写函数所用的语言。

我们可以令链接指示后面跟上花括号括起来的若干函数的声明,从而一次性建立多个链接。花括号的作用是将适用于该链接指示的多个声明聚合在一起,否则花括号就会被忽略,花括号中声明的函数名字就是可见的,就好像在花括号之外声明的一样。多重声明的形式可以应用于整个头文件。

当一个#include指示被放置在复合链接指示的花括号中时,头文件中的所有普通函数声明都被认为是由链接指示的语言编写的。链接指示可以嵌套,因此如果头文件包含带自带链接指示的函数,则该函数的链接不受影响。

指向其他语言编写的函数的指针必须与函数本身使用相同的链接指示。

指向C函数的指针与指向C++函数的指针是不一样的类型。一个指向C函数的指针不能用在执行初始化或赋值操作后指向C++函数,反之亦然。

当我们使用链接指示时,它不仅对函数有效,而且对作为返回类型或形参类型的函数指针也有效。

《C++ Primer 第五版》读书笔记 - 第四部分

https://blog.xqmmcqs.com/《CPP Primer 第五版》读书笔记 - 第四部分/

作者

xqmmcqs

发布于

2021-03-08

更新于

2022-01-18

许可协议

评论

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×