1. 控制内存分配
1. new和delete
new表达式三步走:
==>调用名为operator new
(对应对象)或operator new[]
(对应对象的数组)的标准库函数,该函数分配足够大的、原始的、未命名的内存空间来存储特定的对象或是对象的数组。
==>编译器允许对应的构造函数完成对象的构造,并传入初始值。
==>对象被分配空间,返回指向该对象的指针。
delete两步走:
==>对指针所指向对象,或是所指向的数组的元素调用析构函数
==>编译器调用operator delete
(对应对象)或operator delete[]
(对应对象的数组)
需要重载new和delete就意味着需要自定义operator new 和operator delete。
如果被分配的对象是类类型,则按照查找类内名字的过程查找。即先找本类,再找基类,再找全局的过程。
operator new和operator delete有8个重载版本:
nothrow_t
是在new
头文件内定义的一个struct
,没有任何成员,单纯为了和抛出异常的版本重载开。
同时,在new
头文件还定义了一个名为nothrow
的nothrow_t
类型的const
对象。这个对象用于作为实参,来调用不会抛出异常的版本。
将上述函数重载为类成员时,隐式作为静态成员,因为他们两一个用在类对象调用之前,一个用在类对象销毁之后。上述函数作为类成员不能操纵类的数据成员。
1.1.1 new重载
对于operator new
和operator new[]
,第一个形参都必须是size_t
类型的,对于前者,该形参指出了对象的大小;对于后者,该形参指出了整个数组的大小,即元素数目×元素大小。
二者都返回void*。
自定义operator new
可以为它提供额外形参,使用提供了额外形参的new表达式需要用定位new的形式传递这些参数。
要注意,不能重载下述operator new
:
调用operator new
和operator new[]
,第一个形参都必须是size_t
类型的,对于前者,该形参指出了对象的大小;对于后者,该形参指出了整个数组的大小,即元素数目×元素大小。
二者都返回void*。
1.1.2 delete重载
对于operator delete
和operator delete[]
,第一个形参都必须是void*
类型的,二者返回值都是void*。
当operator delete
和operator delete[]
作为类成员函数时,可包含额外的类型为size_t
的形参,指出第一个形参所指对象的字节数。
这一点在删除继承体系的对象时十分有效,如果基类有虚析构函数,则传递给operator delete
的字节数size_t
和调用的operator delete
版本将随删除指针的动态类型不同而不同。
事实上,我们无法自定义new和delete的形为,只能改变其底层实现operate new
或operate delete
的内存分配方式。
1.2 定位new表达式
operator new类似于allocator的allocate成员,只负责分配空间,不负责初始化。
类似allocator的construct成员,使用new的定位new形式构造对象。定位new形式如下:
place_address是一个指针,initializers提供初始值列表。
当参数仅为一个指针时,定位new使用operator new(size_t,void*) (即之前不让自定义的operator new版本)作为分配空间的函数,根据所提供的地址创建对象并使用它初始化给定的内存。
这个过程中,定位new不分配任何内存(所以也无需释放内存),只是创建对象并把place_address转化为void*传递给operator new,由operator new在给定地址初始化对象以完成整个工作。
new和construct很相似,但是construct接收的指针必须指向同一个alloctator对象分配的空间。而传给定位new的指针无需指向operator new分配的内存。
析构函数会销毁对象,但是不会释放内存!!
2. 运行时类型识别
当把typeid和dynamic_cast作用于某种类型的指针或引用,使用的是所指/所绑定对象的动态类型。
用于使用基类指针/引用调用在无法定义成虚函数的派生类操作。
2.1 dynamic_cast
type为类类型(通常情况应该含有虚函数)。
在第一种形式中,e为有效指针。在第二种形式种,e为左值,在第三种形式中,e不可为左值 。
dynamic_cast完成基类向派生类的强制类型转换,当基类中至少含有一个虚函数时,检查指针或引用所绑定的对象类型,如果相同,转换成功,否则转换失败。
如果转换目标是指针,且转换失败,返回0。如果转换目标是引用,且转换失败,则抛出bad_cast异常。
示例,Derived是Base的公有派生类,且Base类至少含有一个虚函数:
如果基类指针bp指向的不是派生类对象,转换就会发生错误,dp为0,进入Base对象处理部分,也就是根据绑定的对象类型选择执行的函数体。完成和动态类型绑定一样的功能。
将绑定派生类对象的基类指针/引用转化为派生类指针,可以访问到派生类独有成员。这也是很有用的。
2.2 typeid
typeid使用形式为:typeid(e)
e为任意表达式或类型名。返回一个类型为type_info或其派生类的常量对象的引用。
返回时,顶层const被忽略。不会将数组转为指针。
当e是包含虚函数的类的左值时,typeid的结果直到运行时才可知,编译器才会对该表达式求值。其他情况都是得到e的静态类型,编译器也无需求值。
例子:
2.3 type_info
type_info类在typeinfo
头文件中,且至少有如下操作:
type_info类没有默认构造函数,拷贝和移动构造函数被定义为删除的。所以无法拷贝和给type_info对象赋值,只能通过typeid创建type_info对象。
2.3 枚举类型
枚举类型是将一组整形常量组织在一起。枚举属于字面值常量类型。
每个枚举定义了一种枚举类型,可以用于定义其它的成员。
枚举分为限定作用域的枚举和不限定作用域的枚举。
2.3.1 限定作用域的枚举
限定作用域的枚举:
关键字enum class + 枚举名 + 花括号括起来,逗号分开的枚举成员
枚举成员在枚举作用域外(即花括号外)不可访问。若需要访问,则要使用枚举名::枚举成员名
的形式。
2.3.1 不限定作用域的枚举
不限定作用域的枚举:
省去class,枚举名字也是可选的,如果没有枚举名字,则必须在定义enum时定义它的对象(在花括号内提供声明列表,下文的=是指定值)
枚举成员的作用域和枚举本身的作用域相同。
例子:
2.3.2 枚举类型
默认情况下,枚举从0开始,依次加1。也可指定专门的值。
如果枚举成员部分指定了值,则未指定值的枚举成员的值由其前面的成员值加1得到。
枚举的值不唯一。
枚举成员是const的,因此初始化枚举成员需要使用常量表达式,即每个枚举成员都是常量表达式。所以可以在使用常量表达式的地方使用枚举。
有名字的枚举可以定义对应枚举类型的成员。且该成员只能用枚举成员或者同样都是由该枚举定义的对象初始化。
在函数传参时,成员的枚举类型和同值的内置类型也是明显区分的:
不能使用和枚举成员同潜在类型的值传给枚举类型对象,只可用枚举成员和该枚举类型的另一个对象。但可以把不限定作用域的枚举类型对象值传给对应潜在类型的变量。此时该对象的值会被提升至int或更大的整形(只会是整形,之后交给类型转换),实际提升多少由枚举的潜在类型决定。
不限定作用域的枚举类型对象或枚举成员会自动转为int。
限定作用域的则不会自动转换,还是枚举类型。
可以在enum的名字后加上:类型
来表示该枚举的成员用什么类型表示。如:
默认情况下,限定作用域的枚举成员是由int
表示的。
不限定作用域的枚举类型的成员不存在默认类型,只知道其潜在类型足够大容纳枚举值。
如果指定了枚举成员的潜在类型,一旦某个成员的值超出了该类型所能表示的范围,则会引发程序错误。
2.3.3 枚举类型的前置声明
enum的前置声明必须隐式或显式的指定其成员大小:
enum的声明必须和定义匹配,如下:
2.4 类成员指针
成员指针是指指向类的非静态成员的指针,一般情况下,指针是指向对象的,而成员指针指向的是对象的成员。
静态成员不属于任何对象,无需特殊指针,直接使用普通指针即可。
成员指针类型包括类的类型和所指成员的类型,初始化成员指针,令其指向类的成员,但是不指定成员所属对象,直到使用指针时才提供成员所属对象。
2.4.1 数据成员指针
需要用类名::
表示指针是一个指向该类的成员指针。
用&
作用于类成员(而非内存中的对象)令成员指针指向非特定Screen对象的成员。
只有当解引用时才提供具体Screen对象的信息。
类的访问控制对成员指针同样生效,对私有成员的成员指针的使用必须在类成员或者友元内部,不能直接获得私有数据成员的指针。
为了在类外使用私有成员的成员指针,可定义一个返回成员指针的函数,从它返回的指针访问私有成员。如下:
获取返回的成员指针:
使用返回的成员指针:
2.4.2 成员函数指针
pmf2是指向一个接收两个形参的get成员函数的成员指针。注意pmf2旁边的括号必不可少。
在使用成员函数指针时,不会自动将函数指向指针,需要显式使用&。
成员函数指针的使用和数据成员指针一样,用->*
或者.*
,只是需要一个实参列表:
注意成员指针外层的括号必不可少:
可为成员指针定义类型别名,简化表达,更易理解,此时可以省略成员指针名字,只需提供别名:
可以将成员函数指针作为参数传入,或者作为函数返回值,定义:
使用:
2.5 将成员函数用作可调用对象
成员指针不是可调用对象。可使用标准库模板function,定义在头文件functional内,从指向成员函数的指针获取可调用对象。
empty是一个接受string参数并返回bool的函数。
事实上,上述的function将成员函数"翻译"成如下语句,即通过传入的对象以及成员函数指针完成调用:
使用function需要知道成员函数的调用形式,如参数、返回值,men_fn则可以自己推断,men_fn也是定义在头文件functional内:
使用men_fn生成的可调用对象既可以通过对象调用,也可以通过指针调用:
除此以外,还可以通过bind生成一个可调用对象:
和men_fn类似,bind生成的可调用对象的第一个参数可以是对象,也可以是指针,即可以是string指针,也可以是string的引用。
3. 嵌套类
一个类可以嵌套在另一个类的内部。前者称为嵌套类或者嵌套类型。嵌套类是一个独立的和外层类没有关系的类(除了名字查找时有区别)。二者拥有各自的成员,定义各自的对象。
嵌套类的名字(不是类成员名字)在外层类是可见的,也仅在外层类可见。外层类要访问嵌套类成员也需要遵循访问控制规则:
==>public:可随意访问嵌套类。
==>protected:只能被外层类以及其友元和派生类访问。
==>private:只能被外层类的成员和友元访问。
嵌套类可直接使用外层类的成员,他看起来就和外层类的某个成员一样。
总而言之,嵌套类可以视作外围类的友元,外围类对嵌套类没有访问特权。
嵌套类示例,由于嵌套类是外层类的类型成员,所以要先声明后使用,也就是定义得在后面:
嵌套类必须声明在类的内部,但是定义可以在类的内部或者外部,和成员函数一样,在外部定义嵌套类需要以外层类的名字限定嵌套类。
在类外定义嵌套类的成员需要加 外层类::嵌套类::成员
。
嵌套类的静态成员需要定义在外层类之外。
嵌套类的名字查找和一般类名字查找一样,只是额外需要查找它的外层类。
4. union:一种节省空间的类
一个union可以有多个数据成员,但在任意时刻只能有一个数据成员有值,其他成员都是未定义的状态。
union是一种类,可以含构造函数和析构函数在内的析构函数。但是union不能作为基类。
union的成员类型可以是大多数类型,而不能是引用类型。含有构造函数和析构函数的类类型也可以作为union的成员,还可以为其指定访问控制符,默认情况下是public
的。
4.1 定义union
union的定义:
可以像初始化聚合类一样列表初始化一个union:
通过成员访问运算符访问union成员:
为某个union成员赋值会使得其他成员都是未定义状态。
4.2 匿名union
匿名union是指union关键字后没跟union名的union版本。
匿名union不能定义对象,不能包含成员函数,不能包含protected和private的成员。
定义匿名union后会为该union创建一个未命名对象,所以在Union作用域内可直接使用union成员。
4.3 类和union
union会为内置类型的成员按照次序依次合成默认构造函数或者拷贝控制成员。
为自定义了默认构造函数和拷贝控制成员的类合成对应的版本并声明为删除的。(所以,此时用户可以自定义拷贝控制函数,而不使用合成的)
如果某个类包含union成员,该union含有删除的拷贝控制成员,则该类与之对应的拷贝控制操作也是删除的。(即union中含有自定义拷贝控制成员的类,所以包含union的类也应该自定义拷贝控制成员或默认构造函数)/类包含的union包含有另一个类/
新标准允许含有定义了构造函数或拷贝控制成员的类类型成员。当把union的值改成类类型成员对应的值时,必须运行该类型的构造函数。当把类类型成员的值改成一个其他值时,必须调用析构函数。
从上面可以看出,当union包含有类类型的成员时,对于union的管理是很复杂的,可以将union作为成员嵌入到另一个类中,用该类管理和控制union的类成员进行相关的转换。
该类的用于追踪union中存储值类型的一个成员称为判别式。如下:
如果union存储的是int,enum的值就是INT,依次类推。
由类的赋值运算符为union成员赋值,所以需要在重载复制运算符时增加对union类类型成员的管理:
其他值->类类型值:析构
类类型值->其他值:构造(注意定位new的小细节)
自定义拷贝控制成员(如果有需要的话)的过程也和上述差不多,需要根据操作数类型完成不同种类的操作。
5. 局部类
定义在函数内部的类称为局部类。局部类定义的类型只在定义它的作用域内可见,所以局部类成员必须完整在类内定义。
局部类只能访问外层作用域定义的类型名、静态变量、以及枚举成员。
也就是说函数的局部变量不能被该局部类所使用。
外层函数只能访问局部类的公有成员,或者把外层函数声明为友元。
局部类的名字查找和普通查找一样,先类内,然后去外层函数作用域,然后再往外层函数所在的作用域。
在局部类里嵌套一个嵌套类,此时嵌套类的定义(只是类的定义)可以出现在嵌套类作用域外,和局部类相同的作用域内。局部类的嵌套类也是局部类,需要遵循局部类的规矩(所有成员在类内定义)。
6. 不可移植特性
不可移植特性是指因机器而异的特性,将具有该特性的程序从一台机器转移到另一台机器需要重新编写程序。
6.1 位域
类可以将非静态数据成员定义成位域,一个位域含有一定数量的二进制位,位域把数据以位的形式紧凑的分配并位位提供名字,以允许程序员对此结构的位进行操作。
位域的类型必须是整形或者枚举类型,在成员名字后加上冒号以及常量表达式来声明位域,即指定:
如上,mode位域占两个二进制位,modified位域占1个二进制位等等。连续定义的位域会压缩在同一整数的相临位。
无法用取地址运算符作用于位域。
可使用常规访问类成员的方式访问位域,用内置的位运算符操作超过一位的位域:
定义了位域成员的类,也会定义一组内联成员函数检验或设置位域的值。
6.2 volatile限定符
当对象的值可能在程序的控制或检测之外改变时(就是说要假设随时都会变),应将对象声明位volatile
,通知编译器不要对这样的对象进行优化。
volatile变量使用场景:
1)并行设备的硬件寄存器(如:状态寄存器)
2)一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)
3)多线程应用中被几个任务共享的变量
volatile和const限定符互相不影响。可以同时具有二者属性。volatile和指针也有类似于const和指针之间的关系,如下:
只能将volatile对象的地址赋给指向volatile的指针。同样的,只有当引用是volatile时,才能将其绑定到volatole对象。
不能用合成的拷贝/移动构造函数以及赋值运算符初始化volatile对象和给volatile对象赋值。因为合成版本的成员接受的参数是非volatile的常量引用,自然不能把非volatile的引用绑定到volatile对象上。
如果希望拷贝、移动、赋值volatile对象,需要自定义这些拷贝控制成员。如下:
6.3 链接指示:“extern C”
C++通过链接指示指出任意非C++函数所用的语言。
链接指示包含关键字extern,和一个指示编写函数所用语言的字符串字面值常量,以及一个函数声明。
除了单个函数的声明,还可以用花括号括起来若干函数的声明,一次性建立起链接。
更进一步,花括号内可以作用于整个头文件,使得头文件中所有普通函数声明都认为是由链接指示的语言编写的:
对于使用了链接指示的函数来说,它的每个声明都必须具有相同的链接指示。
指向某个函数的指针的链接指示必须和函数本身的链接指示相同。如下:
链接指示不同的两个函数指针不能相互赋值:
链接指针对整个函数都有效,包括函数的形参和返回值:
通过类型别名以及链接指示,可以给C++函数传入指向C的函数指针:
通过将C++函数用链接指示为C语言程序,可将C++导出到C语言内:
目标语言能不能重载对链接指示存在约束:
也就是说,如果一组重载函数有一个是C函数,其他的必须都是C++的,因为C语言不支持重载,重名函数会出问题。