第五章 提高类型安全
5.1 强类型枚举
5.1.1 枚举:分门别类与数值的名字
具名枚举类型一般声明类似:enum Gender { Male, Female }。
匿名枚举类型可以使用三种方式实现:
第一种方式时宏,比如
#define Male 0
#define Female 1
宏的弱点在于其定义的知识预处理阶段的名字,会干扰正常代码。
第二种方式时匿名的enum,比如enum { Male, Female};
c++中更受推荐的是第三种方式,静态变量,例如const static int Male = 0;
静态变量能够得到编译时检查,作用域局限于文件内,但是会在目标代码中产生实际的数据,相比而言匿名的枚举似乎更好用。
5.1.2 有缺陷的枚举类型
c/c++的enum有个奇怪的设定,就是具名enum类型的名字和enum的成员的名字都是全局可见的。
上面例子中的两个General都是全局的名字,因此编译器会报错。解决的办法是将它们声明在不同的namespace之下,然后用限定namespace的方式去访问它。
另外,由于c中枚举被设计为常量数值的别名的本性,所以枚举的成员总是可以被隐式地转换为整型。很多时候这是不安全的。为解决这一问题,一般会对枚举类型进行封装。
但是封装过于复杂,封装采用静态成员,enum变成了非POD。传递参数的时候如果参数是结构体,就不能使用寄存器来传参,而整型可以通过寄存器传递。class封装版本的枚举作为函数参数传递,就会带来一定性能损失。
此外,枚举类型所占用的空间大小也是一个不确定的量。因为标准规定,c++枚举所基于的基础类型由编译器具体指定实现。
5.1.3 强类型枚举以及c++11对原有枚举类型的扩展
c++11引入一种新的枚举类型,即枚举类,又称强类型枚举。
声明强类型枚举只需在enum后面加上关键字class,比如:
enum class Type { General, Light, Medium, Heavy };
强类型枚举的几个优势:
1.强作用域,强类型枚举成员的名称不会被输出到其父作用域空间
2.转换限制,强类型枚举成员的值不可以与整型隐式地互相转换。
3.可以指定底层类型。默认底层类型为int,可以在枚举名称后面加上“: type”来显式指定底层类型,type可以试除wchar_t之外的任何整型。
c++11还对原来的枚举进行了扩展。首先底层的基本类型也可以跟强类型枚举一样,显式地由程序员指定。第二则是枚举成员的名字除了会自动输出到父作用域,也可以在枚举类型定义的作用域内有效。这两点都是向后兼容的。
此外在声明强类型枚举的时候,也可以使用enum struct关键字,两者没有任何区别。
匿名的enum class可能什么也做不了。
5.2 堆内存管理:只能指针与垃圾回收
5.2.1 显式内存管理
显式内存管理常见问题:
一野指针:内存单元已被释放,但是之前指向它的指针却还在被使用。
二重复释放。
三内存泄漏。
c++11新保准对智能指针进行了改进,还提供了所谓的最小垃圾回收的支持。
5.2.2 c++11的智能指针
c++98中智能指针通过一个模板类型“auto_ptr”来实现。auto_ptr以对象的方式管理堆分配的内存,并在适当的时间(比如析构),释放所获得的堆内存。这种堆内存管理的方式只需要程序员将new操作返回的指针作为auto_ptr的初始值即可,程序员不用再显式地调用delete。
auto_ptr存在一些缺点,譬如拷贝时候返回一个左值,不能调用delete[]等,所在在c++11标准中废弃了。c++11中改用unique_ptr, shared_ptr及weak_ptr等智能指针来自动回收堆分配的对象。
每个智能指针都重载了*运算符,可以用来访问所分配的堆内存。智能指针析构或者调用reset成员的时候,智能指针能释放其拥有的堆内存。
unique_ptr唯一拥有所指向的对象内存,仅能通过标准库的move函数来转移。unique_ptr是一个删除了拷贝构造函数、保留了移动构造函数的指针封装类型。
shared_ptr允许多个智能指针共享地拥有同一堆分配对象的内存,实现上采用了引用计数。shared_ptr调用reset成员函数只会引起引用计数的降低,而不会导致堆内存的释放。只有在引用计数归零的时候,shared_ptr才会真正释放所占有的堆内存的空间。
weak_ptr可以指向shared_ptr指针所指向的对象内存,却不拥有该内存。使用weak_ptr成员lock,可以返回其指向内存的一个shared_ptr对象,且在所指对象内存已经无效时,返回指针空值,可用于验证shared_ptr的有效性。
5.2.3 垃圾回收的分类
垃圾回收的方式主要分为两大类:
1.基于引用计数的垃圾回收器
优点是简单,不会造成程序暂停,也不会对系统缓存或者交换空间造成冲击。但是难以处理环形引用问题,产生的额外开销也不小。
2.基于跟踪处理的垃圾回收器
跟踪处理的垃圾回收处理机制更为广泛地应用。其基本方法是产生跟踪对象的关系图,然后进行垃圾回收。使用跟踪方式的垃圾回收算法主要有以下几种:
(1)标记清除(mark-sweep)
该算法将程序中正在使用的对象视为根对象,从根对象开始查找它们所引用的堆空间,并在这些堆空间上做标记。当标记结束后,所有被标记的对象就是可达对象(reachable object)或活对象(live Object),而没有被标记的对象就被认为是垃圾,在第二步的清扫阶段就会被回收掉。
这种方法的特点是活的对象不会被移动,但是其存在会出现大量的内存碎片的问题。
(2)标记整理(mark-compact)
该方法标记的方法和标记清除一样,但是标记完之后,不再遍历所有对象清扫垃圾了,而是将活的对象向左靠齐,这就解决了内存碎片的问题。
标记整理的方法有个特点就是移动活的对象,因此相应地,程序中所有对堆内存的引用都必须更新。
(3)标记拷贝(mark-copy)
该算法将堆空间分为两部分:from和to。刚开始系统只从from的堆空间里面分配内存,当from分配满的时候系统就开始垃圾回收:从from对空间里找出所有活的对象,拷贝到to的堆空间里。这样一来,from的堆空间里面就全剩下垃圾了。而对象被拷贝到to里之后,在to里是紧凑排列的。接下来是需要将from和to交换一下角色,接着从新的from里面开始分配。
标记拷贝算法的一个问题是堆的利用率只有一半,而且也需要移动活的对象。这种算法其实是标记整理的另一种实现而已。
5.2.4 c++与垃圾回收
指针的灵活对垃圾回收带来了很大的困扰。被隐藏的指针会导致编译器在分析指针可达性时出错。c++11解决这种问题的方法就是让程序员利用新的接口来提示编译器代码中存在着指针不安全的区域。
5.2.5 c++11与最小垃圾回收支持
c++11为了做到最小的垃圾回收支持,首先对安全的指针进行了定义,也就是c++11中所谓的安全派生(safely derived)的指针。安全派生的指针是指向由new分配的对象或其子对象的指针。安全派生指针的操作包括:
1.在解引用基础上的引用,比如:&*p。
2.定义明确的指针操作,比如:p+1。
3.定义明确的指针转换,比如static_cast<void *>(p)。
4.指针和整型之间的reinterpret_cast,比如reinterpret_cast<intptr_t>(p)。intptr_t是c++11中一个可选择实现的类型,其长度等于平台上指针的长度。
可通过get_pointer_safety函数查询编译器是否支持最小垃圾回收。
如果代码中出现了指针不安全使用的情况,可以通过一些API来通知垃圾回收器不得回收该内存。即需要声明该内存为可达到的:
declare_reachable()显式地通知垃圾回收器某一个对象应被认为可达到的,即使它的所有指针都对回收器不可见。undeclare_reachable()则可以取消这种可达声明。
declare_reachable只需要传入一个简单的void *指针,但undeclare_reachable通常被设计为一个函数模板。
有时候程序员会选择在一大片连续的堆内存上进行指针式操作,为了让垃圾回收器不关心该区域,也可以使用declare_no_pointers及undeclare_no_pointers函数来告诉垃圾回收器该内存区域不存在有效的指针。
5.2.6 垃圾回收的兼容性
必须限制指针的使用或者使用declare_reachable/undeclare_reachable、declare_no_pointers/undeclare_no_pointers来让一些不安全的指针使用免于垃圾回收器的检查。
此外c++标准对指针的垃圾回收支持仅限于系统提供的new操作符分配的内存,而malloc分配的内存则会被认为总是可达的。
更为现实的状况是本书写作时,垃圾回收特性还没有得到任何表一起的支持,即使是所谓的最小垃圾回收。