C++ 重要知识点总结


1 / 182 C++重要知识点总结 wuliming wuliming_sc@qq.com wuliming_sc@hotmail.com QQ:44384992 2011.01 2 / 182 参考书 http://www.cplusplus.com/reference/ 3 / 182 目 录 参考书 .................................................................................................................................................................................. 2 目 录 .................................................................................................................................................................................. 3 引用与指针的比较 ............................................................................................................................................................. 11 引用的基本概念 ........................................................................................................................................................................................... 11 引用的规则 .................................................................................................................................................................................................. 11 STL 三大关键组件 .............................................................................................................................................................. 13 STL 容器 .............................................................................................................................................................................. 15 PAIR 类型 ........................................................................................................................................................................................................ 15 DEQUE ............................................................................................................................................................................................................. 17 LIST ................................................................................................................................................................................................................. 18 SET、MULTISET.................................................................................................................................................................................................. 18 MAP、MULTIMAP .............................................................................................................................................................................................. 18 HASHTABLE《STL 源码剖析》 .......................................................................................................................................................................... 19 HASH_SET ......................................................................................................................................................................................................... 19 HASH_MAP ....................................................................................................................................................................................................... 19 HASH_MULTISET ................................................................................................................................................................................................. 19 HASH_MULTIMAP ............................................................................................................................................................................................... 19 STL 迭代器 .......................................................................................................................................................................... 20 迭代器的基本概念 ....................................................................................................................................................................................... 20 容器的 ITERATOR 类型 ..................................................................................................................................................................................... 20 迭代器的范围............................................................................................................................................................................................... 20 BEGIN 和 END 操作 ........................................................................................................................................................................................... 20 使迭代器失效的容器操作 ........................................................................................................................................................................... 21 迭代器的自增和解引用运算 ....................................................................................................................................................................... 21 迭代器的算数运算 ....................................................................................................................................................................................... 21 迭代器 CONST_ITERATOR ................................................................................................................................................................................... 22 插入迭代器 .................................................................................................................................................................................................. 22 IOSTREAM 迭代器(暂时略过) .......................................................................................................................................................................... 23 反向迭代器 .................................................................................................................................................................................................. 23 STL 仿函数(函数对象) ........................................................................................................................................................ 24 STL 算法 .............................................................................................................................................................................. 25 4 / 182 BITSET 类型 ......................................................................................................................................................................... 26 BITSET 的定义和初始化 ................................................................................................................................................................................. 26 用 UNSIGNED 值初始化 BITSET 对象 ................................................................................................................................................................. 26 用 STRING 对象初始化 BITSET 对象.................................................................................................................................................................. 26 BITSET 对象上的操作 ..................................................................................................................................................................................... 27 测试整个 BITSET 对象 .................................................................................................................................................................................... 27 对整个 BITSET 对象进行设置 ......................................................................................................................................................................... 27 输出二进制位............................................................................................................................................................................................... 28 标准库 STRING 类型........................................................................................................................................................... 29 STRING 的设计思想 ........................................................................................................................................................................................ 29 STRING 对象的定义和初始化 ......................................................................................................................................................................... 29 GETLINE 函数 ................................................................................................................................................................................................... 29 STRING 的 SIZE、LENGTH 和 EMPTY 操作 ............................................................................................................................................................. 29 STRING::SIZE_TYPE 类型 ..................................................................................................................................................................................... 30 STRINGS 和 C-STRINGS ........................................................................................................................................................................................ 30 将 STRING 置空 ............................................................................................................................................................................................... 30 SUBSTR()获取子字符串................................................................................................................................................................................... 30 STRING 的查找函数 ........................................................................................................................................................................................ 31 标准库 VECTOR 类型 .......................................................................................................................................................... 32 VECTOR 对象的定义和初始化 ........................................................................................................................................................................ 32 VECTOR 的赋值操作 ........................................................................................................................................................................................ 32 VECTOR 中元素的存取 .................................................................................................................................................................................... 32 迭代器相关函数 ........................................................................................................................................................................................... 33 VECTOR 对象的操作 ........................................................................................................................................................................................ 33 VECTOR 容器的自增长 .................................................................................................................................................................................... 34 避免重新分配内存的方法 ........................................................................................................................................................................... 34 INSERT 和 REMOVE 元素 .................................................................................................................................................................................... 35 函数概念详解..................................................................................................................................................................... 36 函数的基本概念详解 ................................................................................................................................................................................... 36 透彻了解内联函数的里里外外《EFFECTIVE C++》第三版条款 30 ................................................................................ 37 传递指向指针的引用 ......................................................................................................................................................... 37 重载函数 ............................................................................................................................................................................ 39 重载与作用域............................................................................................................................................................................................... 39 重载和 CONST 形参 ........................................................................................................................................................................................ 39 基于 CONST 的重载 ........................................................................................................................................................................................ 39 5 / 182 成员函数与作用域、函数重载 ................................................................................................................................................................... 40 避免遮掩继承而来的名称《EFFECTIVE C++》第三版条款 33 ...................................................................................................................... 40 THIS 指针 ............................................................................................................................................................................ 41 THIS 指针的基本概念 .................................................................................................................................................................................... 41 THIS 指针的使用 ............................................................................................................................................................................................ 41 THIS 指针的类型 ............................................................................................................................................................................................ 42 指向函数的指针................................................................................................................................................................. 42 指向函数的指针的类型 ............................................................................................................................................................................... 43 初始化和赋值............................................................................................................................................................................................... 43 调用 .............................................................................................................................................................................................................. 44 函数指针的数组 ........................................................................................................................................................................................... 45 参数和返回类型 ........................................................................................................................................................................................... 46 考虑写出一个不抛出异常的 SWAP 函数《EFFECTIVE C++》条款 25 ............................................................................ 48 成员初始化表..................................................................................................................................................................... 50 构造函数 ............................................................................................................................................................................ 54 构造函数初始化列表 ................................................................................................................................................................................... 54 默认构造函数............................................................................................................................................................................................... 54 单实参构造函数相关的隐式类型转换 ....................................................................................................................................................... 54 派生类构造函数 ........................................................................................................................................................................................... 54 缺省构造函数《深度探索 C++对象模型》 ...................................................................................................................... 56 带有 DEFAULT CONSTRUCTOR 的 MEMBER CLASS OBJECT .......................................................................................................................................... 56 带有 DEFAULT CONSTRUCTOR 的 BASE CLASS ......................................................................................................................................................... 57 带有一个虚函数的 CLASS .............................................................................................................................................................................. 57 带有一个虚基类的 CLASS .............................................................................................................................................................................. 58 总结 .............................................................................................................................................................................................................. 59 复制构造函数..................................................................................................................................................................... 61 复制构造函数的基本概念 ........................................................................................................................................................................... 61 合成的复制构造函数 ................................................................................................................................................................................... 61 定义自己的复制构造函数 ........................................................................................................................................................................... 61 禁止复制 ...................................................................................................................................................................................................... 61 派生类复制构造函数 ................................................................................................................................................................................... 62 复制对象时不要忘记需要复制的每一个成分《EFFECTIVE C++》第三版条款 12 ...................................................................................... 62 复制构造函数《深度探索 C++对象模型》 ..................................................................................................................... 63 6 / 182 DEFAULT MEMBERWISE INITIALIZATION .................................................................................................................................................................... 63 BITWISE COPY SEMANTICS(位逐次拷贝) .............................................................................................................................................................. 64 不要 BITWISE COPY SEMANTICS! ....................................................................................................................................................................... 65 重新设定虚表的指针 ................................................................................................................................................................................... 65 处理 VIRTUAL BASE CLASS SUBOBJECT ................................................................................................................................................................... 66 赋值操作符 ........................................................................................................................................................................ 69 赋值操作符的基本概念 ............................................................................................................................................................................... 69 派生类赋值操作符 ....................................................................................................................................................................................... 69 令赋值操作符返回一个 REFERENCE TO *THIS《EFFECTIVE C++》第三版条款 10 .............................................................................................. 69 在赋值操作符中处理自我赋值《EFFECTIVE C++》第三版条款 11 .............................................................................................................. 70 赋值操作符的注意事项《EFFECTIVE C++》第三版条款 06 .......................................................................................................................... 70 析构函数 ............................................................................................................................................................................ 72 析构函数的基本概念 ................................................................................................................................................................................... 72 派生类析构函数 ........................................................................................................................................................................................... 72 虚析构函数 .................................................................................................................................................................................................. 72 不要在构造函数和析构函数中调用虚函数《EFFECTIVE C++》条款 09 ...................................................................................................... 73 别让异常逃离析构函数《EFFECTIVE C++》第三版条款 07 .......................................................................................................................... 74 STATIC 类成员 ..................................................................................................................................................................... 75 定义 STATIC 成员 ............................................................................................................................................................................................. 75 使用类的 STATIC 成员 ..................................................................................................................................................................................... 75 STATIC 数据成员 .............................................................................................................................................................................................. 76 STATIC 成员函数 .............................................................................................................................................................................................. 77 友元的基本概念................................................................................................................................................................. 79 普通友元函数............................................................................................................................................................................................... 79 友元成员函数............................................................................................................................................................................................... 79 友元类 .......................................................................................................................................................................................................... 79 注意事项 ...................................................................................................................................................................................................... 80 操作符重载 ........................................................................................................................................................................ 81 重载操作符的定义 ....................................................................................................................................................................................... 81 输出操作符<<重载 ....................................................................................................................................................................................... 82 输入操作符>>重载 ....................................................................................................................................................................................... 82 相等操作符 .................................................................................................................................................................................................. 83 关系操作符 .................................................................................................................................................................................................. 83 赋值操作符、复合赋值操作符 ................................................................................................................................................................... 84 下标操作符 .................................................................................................................................................................................................. 84 成员访问操作符 ........................................................................................................................................................................................... 85 自增自减操作符 ........................................................................................................................................................................................... 85 7 / 182 函数调用操作符重载 ................................................................................................................................................................................... 87 类类型和其它类型之间的相互转换 ................................................................................................................................. 88 到类类型的隐式转换 ................................................................................................................................................................................... 88 从类类型到其它类型的转换 ....................................................................................................................................................................... 89 公有、私有和受保护继承 ................................................................................................................................................. 90 基本概念 ...................................................................................................................................................................................................... 90 接口继承和实现继承 ................................................................................................................................................................................... 91 默认继承保护级别 ....................................................................................................................................................................................... 91 友元与继承的关系 ....................................................................................................................................................................................... 91 继承与静态成员 ........................................................................................................................................................................................... 91 虚函数 ................................................................................................................................................................................ 93 派生类和虚函数 ........................................................................................................................................................................................... 93 VIRTUAL 与动态绑定 ....................................................................................................................................................................................... 93 虚析构函数 .................................................................................................................................................................................................. 93 构造函数和赋值操作符不要设置为虚函数 ............................................................................................................................................... 94 构造函数和析构函数中的虚函数 ............................................................................................................................................................... 94 纯虚函数 ...................................................................................................................................................................................................... 94 多重继承 ............................................................................................................................................................................ 96 多重继承的派生类从每个基类中继承状态 ............................................................................................................................................... 96 同名成员函数的二义性 ............................................................................................................................................................................... 97 转换与多个基类 ........................................................................................................................................................................................... 97 多重继承下的虚函数 ................................................................................................................................................................................... 98 多重继承派生类的复制控制 ....................................................................................................................................................................... 99 明智而谨慎地使用多重继承《EFFECTIVE C++》第三版条款 40 ................................................................................................................ 100 虚拟继承 .......................................................................................................................................................................... 101 基本概念 .................................................................................................................................................................................................... 101 虚拟基类声明............................................................................................................................................................................................. 102 虚基类成员的可见性 ................................................................................................................................................................................. 104 特殊的初始化语义 ..................................................................................................................................................................................... 105 如何构造虚继承的对象 ............................................................................................................................................................................. 106 构造函数与析构函数顺序 ......................................................................................................................................................................... 106 嵌套类 .............................................................................................................................................................................. 108 局部类 .............................................................................................................................................................................. 117 RTTI 运行时类型识别...................................................................................................................................................... 119 8 / 182 基本概念 .................................................................................................................................................................................................... 119 DYNAMIC_CAST 操作符 .................................................................................................................................................................................. 119 TYPEID 操作符 .............................................................................................................................................................................................. 122 TYPE_INFO 类................................................................................................................................................................................................. 123 命名的强制类型转换 ....................................................................................................................................................... 126 CONST_CAST ................................................................................................................................................................................................... 126 STATIC_CAST .................................................................................................................................................................................................... 126 DYNAMIC_CAST ................................................................................................................................................................................................ 126 REINTERPRET_CAST ........................................................................................................................................................................................... 127 尽量少做转型动作《EFFECTIVE C++》第三版条款 27 ................................................................................................................................ 127 C++中对象的大小《深度探索 C++对象模型》.............................................................................................................. 128 数据成员的布局《深度探索 C++对象模型》 ................................................................................................................ 133 静态数据成员的存取 ................................................................................................................................................................................. 134 非静态数据成员的存取 ............................................................................................................................................................................. 134 “继承“与数据成员 ................................................................................................................................................................................. 135 指向数据成员的指针《深度探索 C++对象模型》 ........................................................................................................ 143 成员函数《深度探索 C++对象模型》 ............................................................................................................................ 146 非静态成员函数(NONSTATIC MEMBER FUNCTIONS) ..................................................................................................................................... 146 静态成员函数(STATIC MEMBER FUNCTIONS) ............................................................................................................................................... 147 虚拟成员函数(VIRTUAL MEMBER FUNCTIONS) ............................................................................................................................................. 148 多重继承下的 VIRTUAL FUNCTIONS ................................................................................................................................................................. 151 虚继承下的 VIRTUAL FUNCTIONS ..................................................................................................................................................................... 153 指向成员函数的指针《深度探索 C++对象模型》 ........................................................................................................ 154 支持“指向 VIRTUAL MEMBER FUNCTIONS”的指针 ......................................................................................................................................... 154 在多重继承之下,指向 MEMBER FUNCTIONS 的指针 ................................................................................................................................... 155 智能指针 .......................................................................................................................................................................... 156 GOOGLE C++编程风格指南关于智能指针的说明 ....................................................................................................................................... 156 AUTO_PTR ...................................................................................................................................................................................................... 156 SCOPED_PTR ................................................................................................................................................................................................... 157 SHARED_PTR ................................................................................................................................................................................................... 157 AUTO_PTR 解析 ................................................................................................................................................................ 158 AUTO_PTR 的源代码 ...................................................................................................................................................................................... 158 构造函数与析构函数 ................................................................................................................................................................................. 160 9 / 182 拷贝构造与赋值 ......................................................................................................................................................................................... 161 提领操作(DEREFERENCE) ................................................................................................................................................................................ 162 辅助函数 .................................................................................................................................................................................................... 162 特殊转换 .................................................................................................................................................................................................... 163 AUTO_PTR 运用实例(《STL 标准模板库》P47) ........................................................................................................................................ 163 SCOPED_PTR 解析 ............................................................................................................................................................ 165 SCOPED_PTR 的基本概念 ............................................................................................................................................................................... 165 SCOPED_PTR 的成员函数 ............................................................................................................................................................................... 165 SWAP 函数 .................................................................................................................................................................................................... 166 SCOPED_PTR 的用法 ....................................................................................................................................................................................... 166 SCOPED_PTR 和 PIMPL 用法 ............................................................................................................................................................................ 168 SCOPED_PTR 不同于 CONST AUTO_PTR.............................................................................................................................................................. 169 总结 ............................................................................................................................................................................................................ 170 SCOPED_ARRAY ................................................................................................................................................................................................ 170 SHARED_PTR 解析 ............................................................................................................................................................ 171 SHARED_PTR 的基本概念 ............................................................................................................................................................................... 171 SHARED_PTR 的成员函数 ............................................................................................................................................................................... 172 SHARED_PTR 的用法 ....................................................................................................................................................................................... 174 回顾 PIMPL 用法 .......................................................................................................................................................................................... 174 SHARED_PTR 与标准库容器 .......................................................................................................................................................................... 175 SHARED_PTR 与其它资源 .............................................................................................................................................................................. 176 使用定制删除器的安全性 ......................................................................................................................................................................... 177 从 THIS 创建 SHARED_PTR ............................................................................................................................................................................... 178 总结 ............................................................................................................................................................................................................ 179 SHARED_ARRAY ................................................................................................................................................................................................ 179 C++中资源的管理 ............................................................................................................................................................ 180 以对象管理资源《EFFECTIVE C++》第三版条款 13 .................................................................................................................................... 180 在资源管理类中小心 COPYING 行为《EFFECTIVE C++》第三版条款 14 ....................................................................................................... 181 在资源管理类中提供对原始资源的访问《EFFECTIVE C++》第三版条款 15 ............................................................................................ 181 成对使用 NEW 和 DELETE 时要采取相同的形式《EFFECTIVE C++》第三版条款 16 ..................................................................................... 181 以独立语句将 NEWED 对象置入智能指针《EFFECTIVE C++》第三版条款 17 ............................................................................................. 182 10 / 182 11 / 182 引用与指针的比较 引用的基本概念 引用只是它绑定的对象的另外一个名字,作用在引用上的所有操作事实上都是作用在该引用绑定的对象上。以 下 程序中,n 是 m 的一个引用,m 是被引用的对象。 Widget m; Widget &n = m; n 相当于 m 的别名(或者绰号),对 n 的任何操作实际上就是对 m 的操作。例如有人名叫王二,他的绰号是“小 二”。说“小二”怎么怎么的,其实就是对王二说三道四。所以 n 既不是 m 的拷贝,也不是指向 m 的指针,其实 n 就是 m 它自己。 引用的规则 引用的一些规则如下: (1)引用被创建的同时必须被初始化(指针则可以在任何时候被初始化) (2)不能有 NULL 引用,引用必须与合法的存储单元关联(指针则可以是 NULL) (3)一旦引用被初始化,就不能改变引用的关系(指针则可以随时改变所指的对象) (4)引用的主要功能是传递函数的参数和返回值 以下示例程序中,k 被初始化为 i 的引用。语句 k = j 并不能将 k 修改成为 j 的引用,只是把 k 的值改变成为 6。由于 k 是 i 的引用,所以 i 的值也变成了 6。 int i = 5; int j = 6; int &k = i; k = j; // k 和 i 的值都变成了 6; 上面的程序看起来象在玩文字游戏,没有体现出引用的价值。引用的主要功能是传递函数的参数和返回值。C++ 语言中,函数的参数和返回值的传递方式有三种:值传递、指针传递和引用传递。 以下是“值传递”的示例程序。由于 Func1 函数体内的 x 是外部变量 n 的一份拷贝,改变 x 的值不会影响 n, 所 以 n 的值仍然是 0。 void Func1(int x) { x = x + 10; } … int n = 0; Func1(n); cout << “n = ” << n << endl; // n = 0 以下是“指针传递”的示例程序。由于 Func2 函数体内的 x 是指向外部变量 n 的指针,改变该指针的内容将导致 12 / 182 n 的值改变,所以 n 的值成为 10。 void Func2(int *x) { (* x) = (* x) + 10; } … int n = 0; Func2(&n); cout << “n = ” << n << endl; // n = 10 以下是“引用传递”的示例程序。由于 Func3 函数体内的 x 是外部变量 n 的引用,x 和 n 是同一个东西,改变 x 等于改变 n,所以 n 的值成为 10。 void Func3(int &x) { x = x + 10; } … int n = 0; Func3(n); cout << “n = ” << n << endl; // n = 10 对比上述三个示例程序,会发现“引用传递”的性质象“指针传递”,而书写方式象“值传递”。实际上“引用”可以 做的任何事情“指针”也都能够做,为什么还要“引用”这东西?答案是“用适当的工具做恰如其分的工作”。 一般而言,当你需要考虑“不指向任何对象”的可能性时,或者是考虑“在不同时间指向不同对象”的能力时,你 就应该采用指针。前一种情况可以将指针设置为 null,后一种情况可以改变指针所指向的对象。而当你确定“总 是会代表某个对象”,并且“一旦代表了该对象就不再能够改变”,那么就应该选引用。 还有其它情况也需要使用引用,比如当你实现某些操作符的时候。最常见的例子就是 operator[]。这个操作 符很特别地必须返回某种能够被当做赋值对象的东西: vector v(10); v[5] = 10; 如果 operator[]返回的是指针,上述最后一个语句就必须写成这样子: *v[5] = 10; 这不是取下标操作符的自然直观的使用方式,所以,取下标操作符最好返回对象的引用。 备注: 另外可以参考《More Effective C++》条款 1 13 / 182 STL 三大关键组件 STL(标准模板库)是 C++标准程序库的核心,它深刻影响了标准程序库的整体结构。STL 是一个泛型程序库, 提供一系列软件方案,利用先进、高效的算法来管理数据。 若干精心勾画的组件共同合作,构筑起 STL 的基础。这些组件当中,最为关键的是容器、迭代器和算法。 容器: 迭代器: 算法: STL 数据和操作的分离 14 / 182 15 / 182 STL 容器 注意:本章节只提供容器的一个概要性说明,相关知识点的详细总结,参见指定的材料 pair 类型 《C++标准程序库》P33 《C++ PRIMER》第四版特别版 P306 pair 对象的基本概念 需要注意的是,pair 被定义为 struct,而不是 class,这么一来,所有的成员都是 public,我们因此可以 直接存取 pair 中的两个数据成员。 与容器一样,pair 也是一种模板类型。在创建 pair 对象时,必须提供两个类型名:pair 对象所包含的两个 数据成员各自对应的类型名称: pair< string, string > anon; pair< string, int > word_count; pair< string, vector > line; 16 / 182 pair 对象的操作 make_pair 操作 17 / 182 deque 《C++标准程序库》P160 18 / 182 list 《C++标准程序库》P166 set、multiset 《C++标准程序库》P175 《C++ PRIMER》第四版特别版 P319 set 和 multiset 的底层数据结构通常是红黑树。红黑树在改变元素数量和元素查找方面都很出色,它保证节 点安插时最多只会作两个重新连接动作,而且到达某一元素的最长路径深度,最多只是最短路径深度的两倍。 自动排序造成 set 和 multiset 的一个重要限制,你不能直接改变元素值,因为这样会打乱原本正确的顺序。 因此,要改变元素值,必须先删除旧元素,再插入新元素。 map、multimap 《C++标准程序库》P194 《C++ PRIMER》第四版特别版 P309 map 和 multimap 的底层数据结构通常也是红黑树。同样,自动排序这一性质使得 map 和 multimap 身上有了 一条重要的限制:不可以直接改变元素的 key,因为这会破坏正确的排序。要修改元素的 key,也必须先移除 拥有 key 的元素,然后插入新的 key/value 的元素。 19 / 182 hashtable《STL 源码剖析》 《STL 源码剖析》P247 hash_set 《STL 源码剖析》P270 hash_map 《STL 源码剖析》P275 hash_multiset 《STL 源码剖析》P279 hash_multimap 《STL 源码剖析》P282 20 / 182 STL 迭代器 迭代器的基本概念 迭代器(iterator)是用来遍历容器内所有元素的数据类型。标准库为每一种标准容器定义了一种迭代器类型。 迭代器类型提供了比下标操作更通用的方法:所有标准容器类都定义了相应的迭代器类型,而只有少数的容器支 持下标操作。在编写 C++程序时,用迭代器遍历容器是一种更通用的方法,也更加安全。 容器的 iterator 类型 迭代器的范围 C++语言使用一对迭代器标记迭代器的范围。这两个迭代器分别指向同一容器中的两个元素或超出末端的下一个 位置。通常将它们命名为 first 和 last,或者 beg 和 end,用于标记容器中的一段元素范围。该范围内的元 素包括迭代器 first 指向的元素,以及从 first 开始一直到迭代器 last 指向的位置之前的所有元素。此元素 范围称为左闭合区间,其标准表示方式为: [ first, last ) 表示范围从 first 开始,到 last 结束,但是不包括 last。 使用左闭合区间的编程意义: 假设 first 和 last 标记了一个有效的迭代器范围,于是 (1)、 当 first 与 last 相等时,迭代器范围为空 (2)、 当 first 与 last 不等时,迭代器范围至少有一个元素,而且 first 指向该区间的第一个元素。此外, 通过 first 的若干次自增运算,first 最终能够到达 last,使得 first==last begin 和 end 操作 begin 和 end 操作产生指向容器内第一个元素和最后一个元素的下一个位置的迭代器。这两个迭代器用于标记 包含容器内所有元素的迭代器的范围。 c.begin() 返回一个迭代器,指向容器 c 的第一个元素 c.end() 返回一个迭代器,指向容器 c 的最后一个元素的下一个位置 c.rbegin() 返回一个逆序迭代器,指向容器 c 的最后一个元素 c.rend() 返回一个逆序迭代器,指向容器 c 的第一个元素前面的位置 上述每个操作都有两个不同版本:一个是 const 成员,另外一个是非 const 成员。这些操作返回什么类型取决 于容器是否为 const。如果容器不是 const,则这些操作返回 iterator 或 reverse_iterator 类型;如果 21 / 182 容器是 const,则返回类型要加 const_前缀,也就是 const_iterator 和 const_reverse_iterator 类 型。 假设已经声明了一个 vector类型的 ivec 变量,要把所有的元素值重新设置为 0,我们可以用下标操作 和用迭代器操作两种方式来完成: for( vector::size_type ix=0; ix < ivec.size(); ++ix ) ivec[ix] = 0; for( vector::iterator iter = ivec.begin(); iter != ivec.end(); ++iter ) *iter = 0; 使迭代器失效的容器操作 一些容器操作会修改容器的内在状态或移动容器内的元素。这样的操作使所有指向被移动的元素的迭代器失效, 也可能同时使其他迭代器失效。使用无效迭代器是没有定义的,可能会导致与悬垂指针相同的问题。 我们无法检测迭代器是否有效,也无法通过测试来发现迭代器是否已经失效。任何无效迭代器的使用都可能导致 运行时错误,但程序不一定会崩溃,否则检查这种错误也许会容易一些。 迭代器的自增和解引用运算 迭代器的算数运算 22 / 182 迭代器 const_iterator 插入迭代器 23 / 182 iostream 迭代器(暂时略过) 反向迭代器 24 / 182 STL 仿函数(函数对象) 《C++标准程序库》P124 《C++标准程序库》P293 《STL 源码剖析》P413 仿函数的具体概念详解,参照 C++标准程序库对应的章节,下面仅仅罗列仿函数相对于普通函数所具备的优点: 25 / 182 STL 算法 《C++标准程序库》P321~434 《STL 源码剖析》P285 关于各种算法的详细解释,参照上面的这两本书的对应章节 26 / 182 bitset 类型 有些程序要处理二进制位的有序集,每个位可能包含的是 0(关)或 1(开)的值。位是用来保存一组项或条件 的 yes/no 信息(有时也称标志)的简洁方法。标准库提供了 bitset 类使得处理位集合更容易一些。要使用 bitset 类就必须要包含相关的头文件。在本书提供的例子中,假设都使用了 std::bitset 的 using 声明: #include using std::bitset; bitset 的定义和初始化 下表列出了 bitset 的构造函数。类似于 vector,bitset 类是一种类模板;而与 vector 不一样的是 bitset 类型对象的区别仅在其长度而不在其类型。在定义 bitset 时,要明确 bitset 含有多少位,须在尖括号内给 出它的长度值: bitset<32> bitvec; //32 位,全为 0。 这条语句把 bitvec 定义为含有 32 个位的 bitset 对象。位集合的位置编号从 0 开始,因此,bitvec 的位序 是从 0 到 31。以 0 位开始的位串是低阶位(low-order bit),以 31 位结束的位串是高阶位(high-order bit)。 用 unsigned 值初始化 bitset 对象 当用 unsigned long 值作为 bitset 对象的初始值时,该值将转化为二进制的位模式。而 bitset 对象中的 位集作为这种位模式的副本。如果 bitset 类型长度大于 unsigned long 值的二进制位数,则其余的高阶位 置为 0;如果 bitet 类型长度小于 unsigned long 值的二进制位数,则只使用 unsignedlong 值中的低阶 位,超过 bitet 类型长度的高阶位将被丢弃。 在 32 位 unsigned long 的机器上,十六进制值 0xffff 表示为二进制位就是十六个 1 和十六个 0(每个 0xf 可表示为 1111)。可以用 0xffff 初始化 bitset 对象: // bitvec1 is smaller than the initializer bitset<16> bitvec1(0xffff); // bits 0 ... 15 are set to 1 // bitvec2 same size as initializer bitset<32> bitvec2(0xffff); // bits 0 ... 15 are set to 1; 16 ... 31 are 0 // on a 32-bit machine, bits 0 to 31 initialized from 0xffff bitset<128> bitvec3(0xffff); // bits 32 through 127 initialized to zero 用 string 对象初始化 bitset 对象 当用 string 对象初始化 bitset 对象时,string 对象直接表示为位模式。从 string 对象读入位集的顺序 是从右向左: 27 / 182 string strval("1100"); bitset<32> bitvec4(strval); bitvec4 的位模式中第 2 和 3 的位置为 1,其余位置都为 0。如果 string 对象的字符个数小于 bitset 类型 的长度,则高阶位将置为 0。 不一定要把整个 string 对象都作为 bitset 对象的初始值。相反,可以只用某个子串作为初始值: string str("1111111000000011001101"); bitset<32> bitvec5(str, 5, 4); // 4 bits starting at str[5], 1100 bitset<32> bitvec6(str, str.size() - 4); // use last 4 characters bitset 对象上的操作 测试整个 bitset 对象 如果 bitset 对象中有一个或多个二进制位置为 1,则 any 操作返回 true,也就是说,其返回值等于 1;相反, 如果 bitset 对象中的二进制位全为 0,则 none 操作返回 true。 bitset<32> bitvec; // 32 bits, all zero bool is_set = bitvec.any(); // false, all bits are zero bool is_not_set = bitvec.none(); // true, all bits are zero 如果需要知道置为 1 的二进制位的个数,可以使用 count 操作,该操作返回置为 1 的二进制位的个数: size_t bits_set = bitvec.count(); 与 vector 和 string 中的 size 操作一样,bitset 的 size 操作返回 bitset 对象中二进制位的个数,返回 值的类型是 size_t: size_t sz = bitvec.size(); // returns 32 对整个 bitset 对象进行设置 28 / 182 set 和 reset 操作分别用来对整个 bitset 对象的所有二进制位全置 1 和全置 0: bitvec.reset(); // set all the bits to 0. bitvec.set(); // set all the bits to 1 flip 操作可以对 bitset 对象的所有位或个别位按位取反: bitvec.flip(0); // reverses value of first bit bitvec[0].flip(); // also reverses the first bit bitvec.flip(); // reverses value of all bits 输出二进制位 可以用输出操作符输出 bitset 对象中的位模式: bitset<32> bitvec2(0xffff); // bits 0 ... 15 are set to 1; 16 ... 31 are 0 cout << "bitvec2: " << bitvec2 << endl; 输出结果为: bitvec2: 00000000000000001111111111111111 参考资料: 《C++ PRIMER》( 第 4 版·特别版) P88~92 29 / 182 标准库 string 类型 string 的设计思想 C++标准库中的 string class 使你可以将 string 当做一个一般类型而不会令用户感觉有任何问题。你可以像对 待基本类型那样地复制、赋值和比较 string,再也不必担心内存是否足够、占用的内存的实际长度等问题。C++ 标准库对 string 的设计思维就是:让它的行为尽可能像基本类型,不会在操作上引起什么麻烦(至少原则如此)。 头文件包含 #include using std::string; string 对象的定义和初始化 string s1; 默认构造函数,s1 为空串 string s2(s1); 将 s2 初始化为 s1 的一个副本 string s3(“value”); 将 s3 初始化为一个字符串字面值的副本 string s4(n,‘c’); 将 s4 初始化为字符‘c’的 n 个副本 getline 函数 getline 函数接受两个参数:一个输入流对象和一个 string 对象。getline 从输入流的下一行读取,并保存 读取的内容到 string 中,但是不包括换行符。getline 将 istream 参数作为返回值: int main() { string line; while(getline(cin,line)) cout< using std::vector; vector 是一个类模板,我们可以定义保存 string 对象的 vector,或者保存 int 值的 vector,甚至可以定 义保存自定义的类类型对象的 vector。 vector ivec; vector sales_vec; vector 不是一种数据类型,而只是一个类模板,可以用来定义任意多种数据类型。vector 类型的每一种都指 定了其保存元素的类型。比如上面定义的 vector和 vector都是数据类型。 vector 对象的定义和初始化 vector 定义了好几种构造函数,用来定义和初始化 vector 对象: vector v1 vector 保存类型为 T 的对象 vector v2(v1) v2 是 v1 的一个副本 vector v3(n, elem) v2 包含 n 个值为 elem 的元素 vector v4(n) v4 含有值初始化的元素的 n 个副本 vector v5(first,last) v5 以[first,last)迭代器所指区间的元素作为初值 vector 的赋值操作 v1 = v2 将 v2 的全部元素赋值给 v1 v1.assign(n,elem) 复制 n 个 elem,赋值给 v1 v1.assign(first,last) 将迭代器所指区间[first,last)的元素赋值给 v1 v1.swap(v2) 将 v1 和 v2 交换 swap(v1,v2) 同上,此为全局函数 vector 中元素的存取 v.at(idx) 返回 idx 所标示的元素,如果 idx 越界,抛出 out_of_range 异常 v[idx] 返回 idx 所标示的元素,不进行范围检查 v.front() 返回第一个元素,不检查第一个元素是否存在 v.back() 返回最后一个元素,不检查最后一个元素是否存在 调用 operator[]时,必须确保索引有效;调用 front()或 back()时必须确保容器不空,例如: 33 / 182 std::vector ivec; //empty if(ivec.size() > 5) ivec[5] = 100; //ok if( !ivec.empty() ) cout<,>= 保持这些操作符的惯有含义 empty 和 size 操作类似于 string 类型的相关操作。成员函数 size 返回相应 vector 类定义的 size_type 的值。使用 size_type 类型时,必须指出该类型是在哪里定义的。vector 类型总是应该包括 vector 的元素 类型: vector::size_type //ok vector::size_type //error push_back()操作接受一个元素值,并将它作为一个新的元素添加到 vector 对象的后面: string word; vector text; while(cin>>word){ text.push_back(word); } 34 / 182 vector 中的对象是没有命名的,可以按 vector 中对象的位置来访问它们。通常使用下标操作符来提取元素(下 标操作只能用来获取向量中已经存在的元素): for(vector::size_type ix=0; ix < ivec.size(); ++ix) ivec[ix] = 0; //for 循环将 ivec 向量中的元素都设置为 0 vector 容器的自增长 为了使 vector 容器实现快速的内存分配,其实际分配的容量要比当前所需的空间多一些。vector 容器预留了 这些额外的存储区,用于存放新添加的元素。于是,不必每次添加元素的时候都需要重新分配空间。vector 所 分配的额外内存容量的确切数目因库的实现不同而不同。 vector 类提供两个成员函数:capacity 和 reserve,使程序员可与 vector 容器内存分配的实现部分交互 工作。capacity 操作获取在容器需要分配更多的存储空间之前能够存储的元素总数,而 reserve 操作则告诉 vector 容器应该预留多少个元素的存储空间。 弄清楚容器的 capacity 和 size 的区别非常重要。size 指容器当前拥有的元素个数;而 capacity 则指容 器在必须分配新存储空间之前可以存储的元素的总数。下面是一个相关的例子: vector ivec; cout<<”ivec: size: ” <::size_type ix=0; ix != 24; ++ix ) ivec.push_back(ix); cout<<”ivec: size: ” < ivec; ivec.reserve(80); 另外一种避免重新分配内存的方法是,初始化期间就向构造函数传递附加参数,构造出足够的空间。如果给出的 参数是个数值,该数值将成为 vector 的起始大小。 std::vector svec(80); 35 / 182 当然,要获得这种能力,放在 vector 中的元素必须提供一个缺省的构造函数。请注意,如果元素比较复杂, 就算提供了缺省构造函数,初始化操作也是很耗时间的。如果你这么做只是为了保留足够的内存,还不如直接使 用 reserve()。 insert 和 remove 元素 安插和移除元素,都会使作用点之后的各元素的引用、指针和迭代器失效。如果安插操作甚至引起内存重新分配, 那么该容器上所有的引用、指针和迭代器都会失效。 vector 没有提供任何函数可以直接删除与某个值相等的所有元素,这是算法发挥威力的时候。以下语句可以将 所有其值为 val 的元素移除: std::vector ivec; ivec.erase(remove(ivec.begin(),ivec.end(),val), ivec.end()); ) 参考资料: 《C++ PRIMER》 P78~87 《C++ PRIMER》 P264~288 《C++标准程序库—自修教程与参考手册》 P148~159 《STL 源码剖析》 P115~127 36 / 182 函数概念详解 函数的基本概念详解 7.2 函数参数传递 非引用参数 引用参数 应该将不需要修改的引用形参定义为 const 引用。普通的非 const 引用形参在使用时不太灵活。这样的形参既不能用 const 形参初始化,也不能用字面值或产生右值的表达式实参初始化。 容器类型的形参 通常,函数不应该有 vector 或者其它标准库容器类型的形参。调用含有普通的非引用 vector 形参的函数将会复制 vector 的每一个元素。事实上,C++程序员倾向于通过传递指向容器中需要处理的元素的迭代器来传递容器。 数组形参 7.4 函数声明 默认实参 7.5 局部对象 自动对象 静态局部对象 7.6 内联函数 7.7 类的成员函数 7.8 重载函数 7.9 指向函数的指针 关于函数相关的概念,详细全面的解释可以参见《C++ PRIMER》( 第 4 版·特别版)P195~237 后面将重点介绍关于函数的几个重要概念: 透彻了解内联函数的里里外外《Effective C++》第三版条款 30 指向指针的引用 重载函数 this 指针 指向函数的指针 为类实现不抛出异常的 swap 函数 37 / 182 透彻了解内联函数的里里外外《Effective C++》 第三版条款 30 详细解释参考 Effective C++第三版条款 30 传递指向指针的引用 #include #include using namespace std; // int *&lhs 的定义应该从右向左理解: // lhs 是一个引用,与指向 int 的指针相关联。 // 也就是说,lhs 是传递进 ptrswap 函数的指针的别名。 // 注意:不能这样定义:int &*lhs,编译报错提示为:cant declare pointer to “int &” void ptrswap( int *&lhs, int *&rhs ) { int *tmp = lhs; lhs = rhs; rhs = tmp; } int main(int argc, char *argv[]) { int i = 10; int j = 20; int *pi = &i; int *pj = &j; 38 / 182 std::cout<<"before swap: *pi == "<<*pi<<"; *pj =="<<*pj <<";" < #include using namespace std; void ptrswap( int **lhs, int **rhs ) { int *tmp = *lhs; *lhs = *rhs; *rhs = tmp; } int main(int argc, char *argv[]) { int i = 10; int j = 20; int *pi = &i; int *pj = &j; std::cout<<"before swap: *pi == "<<*pi<<"; *pj =="<<*pj <<";" <isbn == rhs.isbn;} 由于 this 是指向 const 对象的指针,const 成员函数不能修改调用该函数的对象。因此,函数 avg_price 和 same_isbn 只能读取而不能修改调用它们的对象的数据成员。 this 指针的使用 42 / 182 在成员函数中,不必显式地使用 this 指针来访问被调用函数所属对象的成员。对这个类的成员的任何没有前缀 的引用,都被假定为通过 this 指针来实现的引用。在上面的函数中,isbn 的用法与 this->isbn 用法是一样 的。 由于 this 指针是隐式定义的,因此不需要在函数形参表中包含 this 指针,实际上,在形参列表中显式地包含 this 指针也是非法的。 在函数体中可以显式地使用 this 指针,如下定义函数 same_isbn 尽管没有必要,但是却是合法的: bool same_isbn(const Sales_item &rhs) const{ return this->isbn == rhs.isbn; } 尽管在成员函数内部显示地引用 this 通常是不必要的,但是有一种情况下必须这样做,当我们需要将一个对象 作为整体引用而不是引用对象的一个成员的时候。最常见的情况是在这样的函数中使用 this,该函数需要返回 对调用该函数的对象的引用,例如: Widget& Widget::get() { //... return *this; } 该成员函数返回类型是 Widget&,指明该成员函数返回调用自己的那个对象。 this 指针的类型 在普通的非 const 成员函数中,this 的类型是一个指向类类型的 const 指针;在 const 成员函数中,this 的类型是一个指向 const 类类型对象的 const 指针。不能从 const 成员函数返回指向类对象的普通引用。 const 成员函数只能返回*this 作为一个 const 引用。 参考资料: 《C++ PRIMER》( 第 4 版·特别版) P224~225 《C++ PRIMER》( 第 4 版·特别版) P376~379 12.2 指向函数的指针 假定我们被要求提供一个如下形式的排序函数: sort( start, end, compare ); start 和end 是指向字符串数组中元素的指针。函数sort()对于start 和end 之间的数组元素进行排序。 compare 定义了比较数组中两个字符串的比较操作。 该怎样实现compare 呢?我们或许想按字典顺序排序数组内的字符串,或许想按长度排序它们,以便将最短的 字符串放在前面,而长的放在后面。解决这种需求的一种策略是将第三个参数compare 设为函数指针,并由它 指定要使用的比较函数。 43 / 182 为简化sort()的用法而又不限制它的灵活性,我们可能希望指定一个缺省的比较函数,以用于大多数的情况。 让我们假设最常见的以字典序排列字符串的情况,缺省实参将指定一个比较操作,它用到了字符串的compare() 函数。我们将考虑怎样用函数指针来实现我们的sort()函数。 指向函数的指针的类型 怎样声明指向函数的指针呢?用函数指针作为实参的参数会是什么样呢?下面是函数lexicoCompare()的定 义,它按字典序比较两个字符串: #include int lexicoCompare( const string &s1, const string &s2 ) { return s1.compare(s2); } 如果字符串s1 和s2 中的所有字符都相等,则lexicoCompare()返回0;否则,如果第一个参数表示的字符串 小于第二个参数表示的字符串,则返回一个负数;如果大于,则返回一个正数。 函数名不是其类型的一部分,函数的类型只由它的返回值和参数表决定。指向lexicoCompare()的指针必须指 向与lexicoCompare()相同类型的函数(带有相同的返回类型和相同的参数表)。让我们试一下: int *pf( const string &, const string & ); // 喔! 差一点 这几乎是正确的。问题是编译器把该语句解释成名为pf 的函数的声明,它有两个参数,并且返回一个int*型 的指针。参数表是正确的,但是返回值不是我们所希望的。解引用操作符* 应与返回类型关联,所以在这种情 况下,是与类型名int 关联,而不是pf。要想让解引用操作符与pf 关联,括号是必需的: int (*pf)( const string &, const string & ); // ok: 正确 这个语句声明了pf 是一个指向函数的指针,该函数有两个参数和int 型的返回值。即指向函数的指针,它与 lexicoCompare()的类型相同。下列函数与lexicoCompare()类型相同,都可以用pf 来指向: int sizeCompare( const string &, const string & ); 但是,calc()和gcd()与前面两个函数的类型不同,不能用Pf 来指: int calc( int , int ); int gcd( int , int ); 可以如下定义pfi,它能够指向这两个函数: int (*pfi)( int, int ); 初始化和赋值 我们知道,不带下标操作符的数组名会被解释成指向首元素的指针。当一个函数名没有被调用操作符修饰时,会 被解释成指向该类型函数的指针。例如,表达式 lexicoCompare; 被解释成类型: int (*)( const string &, const string & ); 的指针。 将取地址操作符作用在函数名上也能产生指向该函数类型的指针。因此,lexicoCompare和&lexioCompare 类型相同。指向函数的指针可如下被初始化: int (*pfi)( const string &, const string & ) = lexicoCompare; 44 / 182 int (*pfi2)( const string &, const string & ) = &lexicoCompare; 指向函数的指针可以如下被赋值: pfi = lexicoCompare; pfi2 = pfi; 只有当赋值操作符左边指针的参数表和返回类型与右边函数或指针的参数表和返回类型完全匹配时,初始化和赋 值才是正确的。如果不匹配,则将产生编译错误消息。在指向函数类型的指针之间不存在隐式类型转换。例如: int calc( int, int ); int (*pfi2s)( const string &, const string & ) = 0; int (*pfi2i)( int, int ) = 0; int main() { pfi2i = calc; // ok pfi2s = calc; // 错误: 类型不匹配 pfi2s = pfi2i; // 错误: 类型不匹配 return 0; } 函数指针可以用0 来初始化或赋值,以表示该指针不指向任何函数。 调用 指向函数的指针可以被用来调用它所指向的函数。调用函数时,不需要解引用操作符。无论是用函数名直接调用 函数,还是用指针间接调用函数,两者的写法是一样的。例如: #include int min( int*, int ); int (*pf)( int*, int ) = min; const int iaSize = 5; int ia[ iaSize ] = { 7, 4, 9, 2, 5 }; int main() { cout << "Direct call: min: " << min( ia, iaSize ) << endl; cout << "Indirect call: min: " << pf( ia, iaSize ) << endl; return 0; } int min( int* ia, int sz ) { int minVal = ia[ 0 ]; for ( int ix = 1; ix < sz; ++ix ) if ( minVal > ia[ ix ] ) minVal = ia[ ix ]; return minVal; 45 / 182 } 调用 pf( ia, iaSize ); 也可以用显式的指针符号写出: (*pf)( ia, iaSize ); 这两种形式产生相同的结果,但是第二种形式让读者更清楚该调用是通过函数指针执行的。 当然,如果函数指针的值为0,则两个调用都将导致运行时刻错误。只有已经被初始化或赋值的指针(引用到一 个函数)才可以被安全地用来调用一个函数。 函数指针的数组 我们可以声明一个函数指针的数组。例如: int (*testCases[10])(); 将testCases 声明为一个拥有10 个元素的数组。每个元素都是一个指向函数的函数指针,该函数没有参数, 返回类型为int。 像数组testCases 这样的声明非常难读,因为很难分析出函数类型与声明的哪部分相关。在这种情况下,使用 typedef 名字可以使声明更为易读。例如: // typedefs 使声明更易读 typedef int (*PFV)(); // 定义函数类型指针的typedef PFV testCases[10]; testCases 的这个声明与前面的等价。 由testCases 的一个元素引用的函数调用如下: const int size = 10; PFV testCases[size]; int testResults[size]; void runtests() { for ( int i = 0; i < size; ++i ) testResults[ i ] = testCases[ i ](); //调用一个数组元素 } 函数指针的数组可以用一个初始化列表来初始化,该表中每个初始值都代表了一个与数组元素类型相同的函数。 例如: int lexicoCompare( const string &, const string & ); int sizeCompare( const string &, const string & ); typedef int ( *PFI2S )( const string &, const string & ); PFI2S compareFuncs[2] = { lexicoCompare, sizeCompare }; 我们也可以声明指向compareFuncs 的指针。这种指针的类型是“指向函数指针数组的指针”。声明如下: PFI2S (*pfCompare)[2] = &compareFuncs; 46 / 182 声明可以分解为: (*pfCompare) 解引用操作符* 把pfCompare 声明为指针,后面的[2]表示pfCompare 是指向两个元素数组的指针: (*pfCompare)[2] typedef PFI2S 表示数组元素的类型,它是指向函数的指针,该函数返回int,有两个const string&型的 参数。数组元素的类型与表达式&lexicoCompare 的类型相同,也与compareFuncs的第一个元素的类型相 同。此外,它还可以通过下列语句之一获得: compareFuncs[0]; (*pfCompare)[0]; 要通过pfCompare 调用lexicoCompare,程序员可用下列语句之一: // 两个等价的调用 pfCompare[0]( string1, string2 ); // 编写 ((*pfCompare)[0])( string1, string2 ); // 显式 参数和返回类型 现在我们回头看一下本节开始提出的问题,在那里给出的任务要求我们写一个排序函数,怎样用函数指针写这个 函数呢?因为函数参数可以是函数指针,所以我们把表示所用比较操作的函数指针作为参数传递给排序函数: int sort( string*, string*, int (*)( const string &, const string & ) ); 我们再次用typedef 名字使sort()的声明更易读: typedef int ( *PFI2S )( const string &, const string & ); int sort( string*, string*, PFI2S ); 因为在多数情况下使用的函数是lexicoCompare(),所以我们让它成为缺省的函数指针参数: int lexicoCompare( const string &, const string & ); int sort( string*, string*, PFI2S = lexicoCompare ); sort()函数的定义可能像这样: void sort( string *s1, string *s2, PFI2S compare = lexicoCompare ) { // 递归的停止条件 if ( s1 < s2 ) { string elem = *s1; string *low = s1; string *high = s2 + 1; for (;;) { while ( compare( *++low, elem ) < 0 && low < s2) ; while ( compare( elem, *--high ) < 0 && high > s1) ; if ( low < high ) low->swap(*high); 47 / 182 else break; } // end, for(;;) s1->swap(*high); sort( s1, high - 1, compare ); sort( high + 1, s2, compare ); } // end, if ( s1 < s2 ) } sort()是C.A.R.Hoare 的快速排序算法的一个实现。让我们详细查看该函数的定义。该函数对s1 和s2 之 间的数组元素进行排序。数组元素的比较通过调用compare 指向的函数来完成。 下面main()的实现用到了我们的排序函数: #include #include // 这些通常应该在头文件中 int lexicoCompare( const string &, const string & ); int sizeCompare( const string &, const string & ); typedef int (*PFI)( const string &, const string & ); void sort( string *, string *, PFI=lexicoCompare ); string as[10] = { "a", "light", "drizzle", "was", "falling","when", "they", "left", "the", "museum" }; int main() { // 调用 sort(), 使用缺省实参作比较操作 sort( as, as + sizeof(as)/sizeof(as[0]) - 1 ); // 显示排序之后的数组的结果 for ( int i = 0; i < sizeof(as)/sizeof(as[0]); ++i ) cout << as[ i ].c_str() << "\n\t"; } 编译并执行程序,生成下列输出: "a" "drizzle" "falling" "left" "light" "museum" "the" "they" "was" "when" 函数参数的类型不能是函数类型,函数类型的参数将被自动转换成该函数类型的指针。例如: // typedef 表示一个函数类型 48 / 182 typedef int functype( const string &, const string & ); void sort( string *, string *, functype ); 编译器把sort()当作已经声明为: void sort( string *, string *, int (*)( const string &, const string & ) ); 上面这两个sort()的声明是等价的。 注意,除了用作参数类型之外,函数指针也可以被用作函数返回值的类型。例如: int (*ff( int ))( int*, int ); 该声明将ff()声明为一个函数,它有一个int 型的参数,返回一个指向函数的指针,类型为: int (*) ( int*, int ); 同样,使用typedef 名字可以使声明更容易读懂。例如,下面的typedef PF 使得我们能更容易地分解出ff() 的返回类型是函数指针: typedef int (*PF)( int*, int ); PF ff( int ); 函数不能声明返回一个函数类型。例如,函数ff()不能如下声明: typedef int func( int*, int ); func ff( int ); // 错误: ff()的返同类型为函数类型 考虑写出一个不抛出异常的 swap 函数 《Effective C++》条款 25 49 / 182 50 / 182 成员初始化表 #include class Account { public: Account(); Account( const char*, double=0.0 ); Account( const string&, double=0.0 ); Account( const Account& ); // ... private: // ... }; //注意:构造函数的初始化列表只在构造函数的定义中指定,而不在声明中指定 inline Account::Account( const char* name, double opening_bal ) : _name( name ), _balance( opening_bal ) { _acct_nmbr = get_unique_acct_nmbr(); } 成员初始化列表跟在构造函数的原型后,以冒号开头。成员名是被指定的,后面是括在括号中的初始值,类似于 函数调用的语法。如果成员是类对象,则初始值变成被传递给适当的构造函数的实参,该构造函数然后被应用在 成员类对象上。在我们的例子中,name被传递给应用在_name上的string构造函数,_balance 用参数 opening_bal 初始化。类似地,下面是另一个双参数Account构造函数: inline Account::Account( const string& name, double opening_bal ) : _name( name ), _balance( opening_bal ) { _acct_nmbr = get_unique_acct_nmbr(); } 在这种情况下,string的拷贝构造函数被调用,把成员类对象_name 初始化成string参数name。 C++新手关注的一个常见问题是,使用初始化列表和在构造函数内使用数据成员的赋值之间有什么区别。例如, 以下代码: inline Account::Account( const char *name, double opening_bal ) : _name( name ), _balance( opening_bal ) { _acct_nmbr = get_unique_acct_nmbr(); } 和 inline Account::Account( const char *name, double opening_bal ) { _name = name; _balance = opening_bal; 51 / 182 _acct_nmbr = get_unique_acct_nmbr(); } 它们的区别是什么? 两种实现的最终结果是一样的。在两个构造函数调用的结束处,三个成员都含有相同的值,区别是成员初始化表 只提供该类数据成员的初始化。在构造函数体内对数据成员设置值是一个赋值操作。区别的重要性取决于数据成 员的类型。 从概念上,我们可以认为构造函数分两个阶段执行:(1)初始化阶段,(2)普通的计算阶段。计算阶段由构造函 数函数体中所有的语句组成。 不管成员是否在构造函数初始化列表中显式初始化,类类型的数据成员总是会在初始化阶段初始化。初始化发生 在计算阶段开始之前。 在构造函数初始化列表中没有显示提及的每个成员,使用与初始化变量相同的规则来进行初始化。运行该类型的 默认构造函数,来初始化类类型的数据成员。内置或复合类型的成员的初始值依赖于对象的作用域:在局部作用 域中这些成员不被初始化,而在全局作用域中,它们被初始化为0。 计算阶段由构造函数体内的所有语句构成。在计算阶段中,数据成员的设置被认为是赋值,而不是初始化。没有 清楚地认识到这个区别是程序错误和低效的常见源泉。 有些成员必须在构造函数初始化列表中进行初始化: (1)const或引用类型的成员,不管是哪种类型,都必须在构造函数初始化列表中进行初始化。 因为这两种对象要在声明后马上初始化,而在构造函数中,做的是对它们的赋值,这样是不被允许的。 (2) 当调用一个base class的constructor,而它拥有一组参数时 (3) 当调用一个member class的constructor,而它拥有一组参数时 我们知道类的对象的初始化其实就是调用它的构造函数完成,如果没有写构造函数,编译器会为你默认生成 一个。如果你自定义了带参数的构造函数,那么编译器将不生成默认构造函数。这样这个类的对象的初始化 必须有参数。如果这样的类的对象来做另外某个类的成员,那么为了初始化这个成员,你必须为这个类的对 象的构造函数传递一个参数。同样,如果你在包含它的这个类的构造函数里用“=”,其实是为这个对象“赋 值”而非“初始化”它。所以一个类里的所有构造函数都是有参数的,那么这样的类如果做为别的类的成员 变量,你必须显式的初始化它,你也只能通过成员初始化列表来完成初始化。 当我们写如下代码: inline Account::Account() { _name = ""; _balance = 0.0; _acct_nmbr = 0; } 则初始化阶段是隐式的,在构造函数体被执行之前,先调用与_name相关联的缺省string构造函数,这意味着 把空串赋给_name的赋值操作是没有必要的。 对于类对象,在初始化和赋值之间的区别是巨大的。成员类对象应该总是在成员初始化表中被初始化,而不是在 构造函数体内被赋值。缺省Account构造函数的更正确的实现如下: inline Account::Account() : _name( string() ) 52 / 182 { _balance = 0.0; _acct_nmbr = 0; } 它之所以更正确,是因为我们已经去掉了在构造函数体内不必要的对_name的赋值。但是,对于缺省构造函数的 显式调用也是不必要的,下面是更紧凑但却等价的实现: inline Account::Account() { _balance = 0.0; _acct_nmbr = 0; } 剩下的问题是,对于两个被声明为内置类型的数据成员,其初始化情况如何?例如,用成员初始化表和在构造函 数体内初始化_balance 是否等价?答案是“不”。对于非类数据成员的初始化或赋值,除了两个例外,两者 在结果和性能上都是等价的。更受欢迎的实现是用成员初始化表: // 更受欢迎的初始化风格 inline Account::Account() : _balanae( 0.0 ), _acct_nmbr( 0 ) { } 两个例外是指任何类型的const 或引用数据成员。const 或引用数据成员必须是在成员初始化表中被初始化, 否则,就会产生编译时刻错误。例如,下列构造函数的实现将导致编译时刻错误: class ConstRef { public: ConstRef( int ii ); private: int i; const int ci; int &ri; }; ConstRef::ConstRef( int ii ) { i = ii; // ok ci = ii; // 错误: 不能给一个 const 赋值 ri = i; // 错误 ri 没有被初始化 } 当构造函数体开始执行时,所有const 和引用的初始化必须都已经发生。因此,只有将它们在成员初始化表中 指定这才有可能。正确的实现如下: // ok: 初始化引用和 const ConstRef::ConstRef( int ii ):ci( ii ), ri( i ) { i = ii; } 每个成员在成员初始化表中只能出现一次,初始化的顺序不是由名字在初始化表中的顺序决定,而是由成员在类 中被声明的顺序决定的。例如,给出下面的Account 数据成员的声明顺序: class Account { public: 53 / 182 // ... private: unsigned int _acct_nmbr; double _balance; string _name; }; 下面的缺省构造函数: inline Account:: Account() : _name( string() ), _balance( 0.0 ), _acct_nmbr( 0 ) {} 的初始化顺序为_acct_nmbr、_balance,然后是_name。 参考资料: 《C++ PRIMER》( 第 4 版·特别版) P387~390 12.4.1 《深度探索 C++对象模型》 54 / 182 构造函数 构造函数初始化列表 参见对应总结条款 默认构造函数 参见对应总结条款 单实参构造函数相关的隐式类型转换 参见对应总结条款 派生类构造函数 《C++ PRIMER》( 第 4 版•特别版) 15.4.2 每个派生类对象由派生类中定义的(非 static)成员加上一个或多个基类子对象构成。这一事实影响着派生类型 对象的构造、复制、赋值和撤销。当构造、复制、赋值和撤销派生类型对象时,也会构造、复制、赋值和撤销这 些基类子对象。 构造函数和复制控制成员不能继承,每个类定义自己的构造函数和复制控制成员。像任何类一样,如果类不定义 自己的默认构造函数和复制控制成员,就将使用合成版本。 派生类的合成默认构造函数与非派生类的构造函数只有一点不同:除了初始化派生类的数据成员以外,它还初始 化派生类对象的基类部分。基类部分由基类的默认构造函数初始化。 除了默认构造函数以外,我们还可以在初始化列表中显式调用基类的构造函数对基类进行初始化: class Bulk_item : public Item_base{ public: Bulk_item( const std::string& book, double sales_price, std::size_t qty = 0, double disc_rate = 0.0 ): Item_base(book, sales_price), min_qty(qty), discount(disc_rate) } 构造函数初始化列表为类的基类和成员提供初始值,它并不指定初始化的执行次序。首先初始化基类,然后根据 声明次序初始化派生类的成员。 一个类只能初始化自己的直接基类。直接基类就是在派生列表中指定的类。如果类 C 从类 B 派生,类 B 从类 A 派生,则 B 是 C 的直接基类。虽然每个 C 类对象包含一个 A 类部分,但 C 的构造函数不能直接初始化 A 部分。 相反,需要类 C 初始化类 B,类 B 的构造函数再初始化类 A。这一限制的原因是,类 B 的作者已经指定了怎样构 55 / 182 造和初始化 B 类型的对象。像类 B 的任何用户一样,类 C 的作者无权改变这个规约。 参考资料: 《C++ PRIMER》( 第 4 版·特别版) P385~396 《C++ PRIMER》( 第 4 版·特别版) P***~*** 56 / 182 缺省构造函数《深度探索 C++对象模型》 首先看看下面一段程序代码: class Foo { public: int val; Foo *pnext; }; void foo_bar() { Foo bar; if( bar.val || bar.pnext )//程序假设 bar 的两个成员都已被清零 //... do something //... } 在上面这个例子中,正确的程序语意是要求 Foo 有一个缺省构造函数,可以将两个 members 初始化为 0。但是 实际情况却不是这样的。程序如果有需要,那是设计程序的人的责任;只有当编译器需要的时候,才会合成出一 个缺省构造函数,并且合成出来的构造函数只执行编译器所需的行动。也就是说,即使有需要为 class Foo 合 成一个缺省构造函数,那个 constructor 也不会将两个数据成员 val 和 pnext 初始化为 0。 C++ Standard[ISO-C++95]的 Section12.1 上这么说: 对于 Class X,如果没有任何用户声明的构造函数,那么会有一个缺省构造函数被暗中声明出来……一个被暗中 声明出来的缺省构造函数将是一个 trivial(浅薄无能,没啥用的) constructor…… C++ Standard 然后开始一一叙述在什么样的情况下这个暗中声明出来的缺省构造函数会被视为 trivial。一 个 nontrivial 缺省构造函数就是编译器需要的那种,必要的话会由编译器合成出来。下面将分别讨论 nontrivial 缺省构造函数的四种情况。 带有 Default Constructor 的 Member class object 如果一个类没有任何构造函数,但是它内含一个 member object,而后者有一个缺省构造函数,那么这个类的 implicit default constructor 就 是“ nontrivial”,编译器需要为此合成一个 default constructor。 不过这个合成操作只有在 constructor 真正需要被调用时才会发生。举个例子,在下面的程序片段中,编译 器为 class Bar 合成一个 default constructor: class Foo { public: Foo(); Foo(int)... }; class Bar { public: Foo foo; char *str; };//内含 Foo 的对象 void foo_bar() { Bar bar; if( str ){ … } //... } 57 / 182 被合成的 Bar default constructor 内含必要的代码,能够调用 class Foo 的缺省构造函数来处理 member object Bar:: foo,但它并不产生任何代码来初始化 Bar:: str。将 Bar:: foo 初始化是编译器的责任, 将 Bar:: str 初始化则是程序员的责任。被合成出来的 default constructor 看起来可能像这样: inline Bar::Bar() { //C++伪码 foo.Foo::Foo(); } 假设程序员经由下面的 default constructor 提供了 str 的初始化操作: Bar::Bar() { str = 0; } 现在程序的需求获得满足了,但是编译器还需要初始化 member object foo。由于缺省构造函数已经被明确 定义出来,编译器没办法合成第二个,于是编译器采取如下行动:“如果 class A 内含一个或一个以上的 member class object,那 么 class A 的每一个 constructor 必须调用每一个 member classes 的缺省构造函数”。 编译器会扩张已存在的 constructors,在其中安插一些代码,使得 user code 在被执行之前,先调用必要 的缺省构造函数。延续前一个例子,扩张后得 constructors 可能像这样: Bar::Bar() { //C++伪码 foo.Foo::Foo(); str = 0; } 如果有多个 class member objects 都要求 constructor 初始化操作,将如何呢?C++语言要求以“members objects 在 class 中的声明次序”来调用各个 constructors。这一点由编译器完成,它为每一个 constructor安插程序代码,以“member声明次序”调用每一个member所关联的default constructors。 这些代码安插 explicit user code 之前。 带有 Default Constructor 的 Base class 类似的道理,如果一个没有任何构造函数的类派生自一个带有缺省构造函数的基类,那么这个派生类的缺省构造 函数被视为 nontrivial ,并因此需要被合成出来。它将调用上一层 base classes 的 default constructor(根据它们的声明次序)。对一个后继派生的 class 而言,这个合成的 constructor 和一个“被 明确提供的 default construnctor”没什么差异。 如果设计者提供多个构造函数,但是其中却没有缺省构造函数呢?编译器会扩张现有的每一个构造函数,将“用 以调用所有必要之 default constructors”的程序代码加进去。它不会合成一个新的缺省构造函数,这是 因为其它由用户提供的缺省构造函数存在的原因。如果同时亦存在着带有缺省构造函数的 member class objects,那些缺省构造函数也会被调用—在所有基类构造函数都被调用之后。 带有一个虚函数的 class 另外有两种情况,需要合成缺省构造函数:  class 声明(或继承)一个虚函数 58 / 182  class 派生自一个继承串链,其中有一个或更多的虚基类 不管那一种情况,由于缺乏由用户声明的构造函数,编译器会详细记录合成一个 default constructor 的必 要信息。以下面这个程序片段为例: class Widget{ public: virtual void flip() = 0; //... } void flip( const Widget & widget ) { widget.flip(); } //假设 Bell 和 Whistle 都派生自 Widget void foo() { Bell b; Whistle w; flip(b); flip(w); } 下面两个扩张操作会在编译期间发生:  一个虚函数表会被编译器产生出来,内放类的虚函数地址  在每一个对象中,一个额外的 vptr(虚函数表指针)会被编译器合成出来,内含相关的虚函数表的地址 此外,widget.flip()会被重新改写,以使用 widget 的 vptr 和 vtbl 中的 flip()条目: ( *widget.vptr[1] )( &widget ) // 1 表示 flip()在虚表中的固定索引 // &widget 代表要交给“被调用的某个 flip()函数实体”的 this 指针 为了让这个机制发挥功效,编译器必须为每一个 Widget 或其派生类的对象的 vptr 设定初值,放置适当的虚表 地址。对于 class 所定义的每一个构造函数,编译器会安插一些代码来做这些事情。对于那些未声明任何构造 函数的类,编译器会为它们合成一个缺省构造函数,以便正确地初始化每一个类对象的 vptr。 带有一个虚基类的 class 虚基类的实现法则在不同的编译器之间有很大的差别。然而,每一种实现方法的共同点在于必须使虚基类在其每 一个派生类对象中的位置,能够于执行期准备妥当。例如下面这段程序代码中: class X { public: int i; }; class A : public virtual X { public : int j; }; class B : public virtual X { public : int double d; }; class C : public A, public B { public : int k; }; //无法在编译时期决定出 pa->X::i 的位置 void foo( const A* pa ){ pa->i =1024; } main() { foo( new A ); 59 / 182 foo( new C ); //... } 编译器无法固定住 foo()之中“经由 pa 而存取的 X::i”的实际偏移位置,因为 pa 的真正类型可以改变。编 译器必须改变“执行存取操作”的那些代码,使 X::i 可以延迟到执行期才决定下来。原先 cfront 的做法是靠 “在派生类对象的每一个虚基类中安插一个指针”来完成。所有“经由引用或指针来存取一个虚基类”的操作都 可以通过相关指针完成。在这个例子中,foo()可以被改写如下,以符合这样的实现策略: void foo( const A* pa ){ pa->__vbcX->i = 1024; } 其中__vbcX 表示编译器所产生的指针,指向虚基类 X。__vbcX(或编译器所产生的某个什么东西)是在类对象 建构期间被完成的。对于类所定义的每一个构造函数,编译器会安插那些“允许每一个虚基类的执行期存取操作” 的代码。如果类没有声明任何构造函数,编译器必须为它合成一个缺省构造函数。 总结 在以上 4 种情况下,会导致编译器必须为未声明构造函数的类合成一个缺省构造函数。C++ Standard 把那些 合成物称为 implicit nontrivial default constructors。被合成出来的 constructor 只能满足编 译器(而非程序)的需要。至于没有存在以上 4 种情况而又没有声明任何构造函数的类,我们说它们拥有的是 implicit tirvial default constructors,它们实际上并不会被合成出来。 在合成的 default constructor 中,只有 base class subobjects 和 member class objects 会被 初始化。所有其它的 nonstatic data member,如整数、整数指针、整数数组等等都不会被初始化。这些初 始化对程序而言或许有必要,但对编译器则并非必要。如果程序需要一个“把某指针设为 0”的缺省构造函数, 那么提供它的人应该是程序员。 C++新手一般有两个常见的误解: 1、任何类如果没有定义缺省构造函数,就回被合成出一个来; 2、编译器合成出来的缺省构造函数会明确设定“class 内每一个 data member 的默认值”。 如上面所阐述的,以上没有一个是正确的。 参考资料: 《深度探索 C++对象模型》P39~47 《C++ PRIMER》( 第 4 版•特别版)P392 60 / 182 参考资料: 《C++ PRIMER》( 第 4 版·特别版) P392 12.4.3 61 / 182 复制构造函数 只有单个形参,而且该形参是对本类类型对象的引用(常用 const 修饰),这样的构造函数称为复制构造函数。 与默认构造函数一样,复制构造函数可由编译器隐式调用。 复制构造函数的基本概念 《C++ PRIMER》( 第 4 版•特别版) 13.1.0 合成的复制构造函数 《C++ PRIMER》( 第 4 版•特别版) 13.1.1 如果我们没有定义复制构造函数,编译器会为我们合成一个。合成复制构造函数的行为是,执行逐个成员初始化, 将新对象初始化为原对象的副本。 所谓“逐个成员”,指的是编译器将现有对象的每个非 static 成员,依次复制到正创建的对象。合成复制构造 函数直接复制内置类型成员的值;类类型成员使用该类的复制构造函数进行复制。数组成员的复制是个例外。虽 然一般不能复制数组,但如果一个类具有数组成员,则合成复制构造函数将复制数组。复制数组时合成复制构造 函数将复制数组的每一个元素。 定义自己的复制构造函数 《C++ PRIMER》( 第 4 版•特别版) 13.1.2 对许多类而言,合成复制构造函数只完成必要的工作。只包含类类型成员或内置类型(不包括指针类型)成员的 类,无须显式地定义复制构造函数,也可以复制。 然而,有些类必须对复制对象时发生的事情加以控制。这样的类经常有一个数据成员是指针,或者有成员表示在 构造函数中分配的其他资源。而另一些类在创建新对象时必须做一些特定的工作。这两种情况下,都必须定义复 制构造函数。 禁止复制 《C++ PRIMER》( 第 4 版•特别版) 13.1.3 《Effective C++》第三版 P38 在某些情况下,需要阻止对对象的复制或者赋值(比如说,该对象是独一无二的)。如果代码里面不明确地声明 复制构造函数和赋值操作符,编译器会为你自动生成一份,于是你的类就支持复制和赋值。我们如何有效地阻止 编译器自动生成复制构造函数和赋值操作符呢? 可以将复制构造函数和赋值操作符声明为 private,只是声明,没有对应的实现代码。因为声明了复制构造函 数和赋值操作符,这就阻止了编译器自动生成对应的复制构造函数和赋值操作符;而令这些函数为 private, 可以阻止类的用户对复制构造函数和赋值操作符的调用。因为只有声明而没有对应的定义,所以类内部的成员函 数或友元函数调用复制构造函数或赋值操作符时,会导致连接错误。 在标准库实现代码中的 ios_base、basic_ios 和 sentry 等等,它们的复制构造函数和赋值操作符就是声明 为 private 而没有定义的。 62 / 182 派生类复制构造函数 《C++ PRIMER》( 第 4 版•特别版) 15.4.3 像任意其它类一样,派生类也可以使用合成复制控制成员。合成操作对对象的基类部分连同派生部分的成员一起 进行复制、赋值和撤销,使用基类的复制构造函数、赋值操作符和析构函数对基类部分进行复制、赋值和撤销。 类是否需要定义复制控制成员完全取决于类自身的直接成员。基类可以定义自己的复制控制而派生类使用合成版 本,反之亦然。 只包含类类型或内置类型数据成员、不包含指针的类一般可以使用合成操作,复制、赋值和撤销这样的成员不需 要特殊控制。具有指针成员的类一般需要定义自己的复制控制来管理这些成员。 如果派生类显式定义自己的复制构造函数,该复制构造函数一般应显式使用基类复制构造函数初始化对象的基类 部分: class Base{...}; class Derived : public Base{ public: Derived( Derived& d ):Base(d)/*其它成员的初始化*/{...} //... } 初始化函数 Base(d)将派生类对象 d 转换为它的基类部分的引用,并调用基类复制构造函数。如果省略基类初 始化函数,如下代码: class Base{...}; class Derived : public Base{ public: Derived( Derived& d )/*其它成员的初始化*/{...} //... } 效果是运行 Base 的默认构造函数初始化对象的基类部分。假定 Derived 成员的初始化从 d 复制对应成员,则 新构造的对象将具有奇怪的配置:它的基类部分保持默认值,而它的派生类部分则是另外一个对象的副本。 复制对象时不要忘记需要复制的每一个成分《Effective C++》第三版条款 12 该条款的详细解释参见 Effective C++的条款 12,再此不做详细解释。 要谨记的是,在实现复制构造函数或赋值操作符的时候,不但要确保对象内所有的成员变量的妥善处理,还要谨 慎的照顾好所有的 base class 部分。 63 / 182 复制构造函数《深度探索 C++对象模型》 有三种情况,会以一个 object 的内容作为另一个 class object 的初值。最明显的一种情况当然就是对一个 object 做明确的初始化操作,像这样: class X{…}; X x; //明确地以一个 object 的内容作为另一个 class object 的初值 X xx = x; 另两种情况是当 object 被当做参数交给某个函数时,以及当函数传回一个 class object 时。 假设 class 设计者明确定义了一个复制构造函数,像下面这样: X::X( const X& x ); Y::Y( const Y& y, int = 0 ); 那么在大部分情况下,当一个 class object 以另一个同类实体作为初值时,上述的复制构造函数就会被调用。 这可能会导致一些临时性对象的产生或程序代码的蜕变。 Default Memberwise Initialization 如果类没有提供一个 explicit copy constructor 又当如何?当 class object 以相同类的另外一个 object 作为初值时,其内部是以所谓的 default memberwise initialization 手法完成的,也就是把 每一个内建的或派生的 data member 的值,从某一个对象拷贝一份到另外一个对象身上。不过它并不会拷贝 其中的 member class object,而是以递归的方式施行 memberwise initialization。例如,考虑下面 这个类声明: class String{ public: //... private: char *str; int len; }; 一个 String object 的 default memberwise initialization 发生在这种情况之下: String noun(“book”); String verb = noun; 其完成方式就好像个别设定每一个 members 一样: //语意相等 verb.str = noun.str; verb.len = noun.len; 如果一个 String object 被声明为另一个 class 的 member,像下面这样: class Word{ public: //... private: int _occurs; 64 / 182 String _word; }; 那么一个 Word object 的 default memberwise initialization 会拷贝其内建的 member _occurs, 然后再在 _word 身上递归实施 memberwise initialization。 就像缺省构造函数一样,C++ Standard 上说,如果类没有声明一个拷贝构造函数,就会有隐含的声明 (implicitly declared)或隐含的定义(implicitly defined)出现。和以前一样,C++ Standard 把复 制构造函数区分为 trivial 和 nontrivial 两种。只有 nontrivial 的实体才会被合成于程序之中。决定一 个拷贝构造函数是否为 trivial 的标准在于 class 是否展现出所谓的“bitwise copy semantics”。 Bitwise Copy Semantics(位逐次拷贝) 在下面的程序片段中: #include "word.h" Word noun("book"); void foo() { Word verb = noun; //... } 很明显 verb 是根据 noun 来初始化。但是在尚未看过 Word 类的声明之前,我们不可能预测这个初始化操作的 程序行为。如果 Word 的设计者定义了一个复制构造函数,verb 的初始化操作就会调用它。但是如果该类没有 定义 explicit copy constructor,那么是否会有一个编译器合成的实体被调用呢?这就得视该 class 是 否展现“bitwise copy semantics”而定。举个例子,已知下面得 class Word 声明: class Word{ public: Word( const char * ); ~Word(){ delete[] str; } //... private: int cnt; char *str; }; 这种情况下,并不需要合成一个缺省拷贝构造函数,因为上述声明展现了“default copy semantics”, 而 verb 的初始化操作也就不需要以一个函数调用收场(当然,该类的定义存在着严重的缺陷)。然而,如果 class object 是这样声明: class Word{ public: Word( const String& ); ~Word(){ } //... private: int cnt; String str; }; 65 / 182 其中 String 声明了一个 explicit copy constructor: class String{ public: String( const char * ); String( const String & ); //... }; 在这个情况下,编译器必须合成一个拷贝构造函数以便调用 member class string object 的拷贝构造函 数: //C++伪码 Inline Word::Word( const Word& wd ) { str.String::String(wd.str); cnt = wd.cnt; } 有一点需要特别注意:在这个被合成出来的拷贝构造函数中,如整数、指针、数组等等的 nonclass members 也都会被复制,正如我们所期待的一样。 不要 Bitwise Copy Semantics! 什么时候一个类不展现“bitwise copy semantics”呢?有以下 4 种情况:  当类内含一个 member object,而后者的 class 声明有一个拷贝构造函数时(不论是被类设计者明确地 声明,或是被编译器合成的)  当类继承自一个基类而后者存在有一个拷贝构造函数时(再次强调,不论是被明确声明或是被合成而得)  当类声明了一个或多个虚函数时  当类派生自一个继承串链,其中有一个或多个虚基类时 前两种情况中,编译器必须将 member 或 base class 的“copy constructors 调用操作”安插到被合成的 拷贝构造函数中。后面两种情况较为复杂一些,接下来将详细地讨论。 重新设定虚表的指针 假设类声明了一个或多个虚函数,编译期间会进行程序扩张操作:  增加一个虚函数表,内含每一个有作用的虚函数的地址  将一个指向虚函数表的指针,安插在每一个类对象中 显然,如果编译器对于每一个新产生的类对象的 vptr 不能成功而正确地设好其初值,将导致错误的结果。因此, 当编译器导入一个 vptr 到 class 中时,该 class 就不再展现 bitwise semantics 了。现在,编译器需要 合成出一个 copy constructor,以便将 vptr 适当地初始化,下面是个例子: 首先,定义两个类,ZooAnimal 和 Bear: class ZooAnimal{ public: ZooAnimal(); virtual ~ZooAnimal(); virtual void animate(); 66 / 182 virtual void draw(); //... private: //ZooAnimal 的 animate()和 draw()所需要的数据 } class Bear : public ZooAnimal{ public: Bear(); void animate(); //虽然没有明写 virtual,它实际上也是 virtual void draw(); //虽然没有明写 virtual,它实际上也是 virtual virtual void dance(); //.... private: //ZooAnimal 的 animate()、draw()和 dance()所需要的数据 } ZooAnimal 的对象以另一个 ZooAnimal 的对象作为初值,或 Bear 的对象以另一个 Bear 的对象作为初值, 都可以直接靠“bitwise copy semantics”完成(除了可能会有的 pointer member 之外,为了简化,这 里不考虑这种情况)。举例: Bear yogi; Bear winnie = yogi; yogi 会被 default Bear consturctor 初始化。而在构造函数中,yogi 的 vptr 被设定指向 Bear class 的 virtual table。因此,把 yogi 的 vptr 值拷贝给 winnie 的 vptr 是安全的。 当一个基类对象以其派生类的对象做初始化操作时,其 vptr 复制操作也必须保证安全,例如: ZooAnimal franny = yogi;//这会发生切割(sliced)行为 franny 的 vptr 不可以被设定为指向 Bear class 的 virtual table。合成出来的 ZooAnimal copy constructor 会明确设定 object 的 vptr 指向 ZooAnimal class 的 virtual table,而不是从右手边 的 class object 中将其 vptr 现值拷贝过来。 处理 Virtual Base Class Subobject 67 / 182 每一个编译器对虚拟继承的支持承诺,都表示必须让派生类对象中的 virtual base class subobject 位 置在执行期就准备妥当。维护“位置的完整性”是编译器的责任。“bitwise copy semantics”可能会破坏 这个位置,所以编译器必须在它自己合成出来的拷贝构造函数中做出仲裁。举个例子,在下面的声明中, ZooAnimal 成为 Raccoon 的一个虚拟基类,同时 RedPanda public 继承自 Raccoon: class Raccoon : public virtual ZooAnimal{ public: Raccoon(){} Raccoon( int val ){} //… private: //… } class RedPanda : public Raccoon{ public: RedPanda(){} RedPanda( int val ){} //… private: //… } 如果以一个 Raccoon object 作为另一个 Raccoon object 的初值,那么“bitwise copy”就绰绰有余了: Raccoon rocky; Raccoon little_critter = rocky; 然而如果企图以一个 RedPanda 对象作为 Raccoon 对象的初值,编译器必须判断“后续当程序员企图存取其 ZooAnimal subobject 时是否能够正确地执行”: RedPanda rocky; Raccoon little_critter = rocky; 在这种情况下,为了完成正确的 little_critter 初值设定,编译器必须合成一个拷贝构造函数,安插一些代 码以设定 virtual base class pointer/offset 的初值,对每一个 members 执行必要的 memberwise 68 / 182 初始化操作,以及执行其它的内存相关操作。 参考资料: 《C++ PRIMER》( 第 4 版·特别版) P406~411 《C++ PRIMER》( 第 4 版·特别版) P494~495 《深度探索 C++对象模型》 P48 ~ 60 69 / 182 赋值操作符 赋值操作符的基本概念 《C++ PRIMER》( 第 4 版•特别版) 13.2 具体一些的概念,也可以参见操作符重载的总结材料 派生类赋值操作符 《C++ PRIMER》( 第 4 版•特别版) 15.4.3.2 赋值操作符通常与复制构造函数类似,如果派生类定义了自己的赋值操作符,则该操作符必须对基类部分进行显 式赋值。 class Base{...}; class Derived : public Base{ public: Derived( Derived& d ):Base(d)/*其它成员的初始化*/{...} Derived &operator=(const Derived &rhs){ if( this != &rhs ){ Base::operator=(rhs); //... } return *this; } } 赋值操作符必须防止自身赋值。 令赋值操作符返回一个 reference to *this《Effective C++》第三版条款 10 class Widget{ public: ... Widget& Widget=( const Widegt &rhs ){ if( this == &rhs ) return *this; .... return *this; } ... } 注意,这只是一个建议,并无强制性。如果不遵循它,代码照样可以编译通过。然而这份协议被所有内置类型和 标准程序库提供的类型如 string、vector 等等遵守。因此除非你有一个标新立异的好理由,否则还是从众吧。 70 / 182 在赋值操作符中处理自我赋值《Effective C++》第三版条款 11 该条款的详细解释,参见 Effective C++的条款 11,并结合条款 29 详细理解 赋值操作符的注意事项《Effective C++》第三版条款 06 71 / 182 72 / 182 析构函数 构造函数的用途是获取资源,例如,构造函数可以分配一个缓冲区或打开一个文件,在构造函数中分配了资源之 后,需要一个对应操作自动回收或释放资源。析构函数就是这样的一个特殊函数,它可以完成所需的资源回收, 作为构造函数的补充。 与复制构造函数和赋值操作符不同,编译器总是会为我们合成一个析构函数。合成析构函数按对象的数据成员创 建时的逆序撤销每个数据成员。对于类类型的每个成员,合成析构函数调用该成员的析构函数来撤销对象。 析构函数是个成员函数,它的名字是在类名字之前加上一个符号(~),它没有返回值,没有形参。因为不能指定 任何形参,所以不能重载析构函数。虽然可以为一个类定义多个构造函数,但是只能提供一个析构函数,用于类 的所有对象的析构。 析构函数的基本概念 《C++ PRIMER》( 第 4 版•特别版) 13.3 派生类析构函数 《C++ PRIMER》( 第 4 版•特别版) 15.4.3.3 析构函数的工作与复制构造函数和赋值操作符不同,派生类析构函数不负责撤销基类对象的成员(也就是在派生 类的析构函数中,不用像构造函数或者复制构造函数那样显式的对基类对象进行设置)。编译器总是显式调用派 生类对象基类部分的析构函数。每个析构函数只负责清除自己的成员: class Derived : public Base{ public: //Base::~base invoked automatically ~Derived(){...} }; 对象的撤销顺序与构造顺序相反:首先运行派生类析构函数,然后按照继承层次依次向上调用各基类析构函数。 虚析构函数 《C++ PRIMER》( 第 4 版•特别版) 15.4.4 《Effective C++》第三版 P40 删除指向动态分配对象的指针时,需要运行析构函数在释放对象的内存之前清除对象。处理继承层次中的对象时, 指针的静态类型可能与被删除对象的动态类型不同,可能会删除实际指向派生类对象的基类类型指针。 如果删除基类指针,则需要运行基类析构函数并清除基类成员,如果对象实际是派生类型的,则没有定义该行为。 要保证运行适当的析构函数,基类中的析构函数必须为虚函数: class Base{ public: //... virtual ~Base(){...} } 73 / 182 class Derived:public Base{ public: //... virtual ~Derived(){...} } 如果析构函数为虚函数,那么通过指针调用时,运行哪个析构函数将因指针所指对象类型的不同而不同: Base *pBase = new Base(); delete pBase; //调用 Base 的析构函数 pBase = new Derived(); delete pBase; //调用 Derived 的析构函数 像其他虚函数一样,析构函数的虚函数性质都将继承。因此,如果层次中基类的析构函数为虚函数,则派生类析 构函数也将是虚函数,无论派生类显示定义析构函数还是使用合成析构函数,派生类析构函数都是虚函数。 即使析构函数没有工作需要做,继承层次的基类也应该定义一个虚析构函数。 任何类只要带有虚函数都几乎可以确定应该有一个 virtual 析构函数。如果类不含虚函数,通常表示它并不希 望被当做一个基类。在不作为基类的类里面将析构函数声明为 virtual 通常是个馊主意(因为虚函数的出现, 会使得对象的大小增加)。 不要在构造函数和析构函数中调用虚函数《Effective C++》条款 09 《C++ PRIMER》( 第 4 版•特别版) 15.4.5 《Effective C++》第三版 P48 构造派生类对象时首先运行基类构造函数初始化对象的基类部分。在执行基类构造函数时,对象的派生类部分是 74 / 182 未初始化的。实际上,此时的对象还不是一个完整的派生类对象。 撤销派生类对象时,首先撤销它的派生类部分,然后按照与构造顺序的逆序撤销它的基类部分。 在这两种情况下,对象都是不完整的。 如果在构造函数或析构函数中调用虚函数,则运行的是为构造函数或析构函数自身类型定义的版本。无论由构造 函数或析构函数直接调用虚函数,或者从构造函数或析构函数所调用的函数间接调用虚函数,都应用这种绑定。 【base class 构造期间虚函数绝不会下降到 derived class 阶层。取而代之的是,对象的作为就像隶属于 base 类型一样。非正式的说法或许比较传神:在 base class 构造期间,虚函数不是虚函数(《Effective C++》 P49)。相同的道理也适用于析构函数。一旦 derived class 析构函数开始执行,对象内的 derived class 成员变量便呈现未定义值,所以 C++视它们仿佛不再存在。进入 base class 析构函数后对象就成为一个 base class 对 象 ,而 C++的任何部分包括虚函数、dynamic_cast 等等也这么看待它(《Effective C++》P50)】 别让异常逃离析构函数《Effective C++》第三版条款 07 析构函数绝对不要抛出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后 吞下它们(不传播)或者干脆结束程序。 具体的讨论,参见《Effective C++》第三版条款 08 75 / 182 static 类成员 非 static 数据成员存在于类类型的每个对象中。static 数据成员独立于该类的任意对象而存在。每个 static 数据成员是与类关联的,并不与该类的具体某个对象相关联。 类也可以定义 static 的成员函数。static 成员函数没有隐含的 this 形参,它可以直接访问所属类的 static 成员,但是不能直接使用非 static 成员。 使用 static 成员而不是全局对象的优点: (1)static 成员的名字在类的作用域中,可以避免和其它类的成员或全局对象名字冲突。 (2)可以实施封装。static 成员可以是私有成员,而全局对象不可以。 (3)通过阅读程序容易看出 static 成员是与特定类关联的。这种可见性可以清晰地显示程序员的意图。 定义 static 成员 在成员声明之前加上关键字 static 将成员设置为 static。static 成员遵循正常的 public/private 访问 规则。考虑一个简单的表示银行账户的类: class Account{ public: void applyint(){amount += amount*interestRate;} static double rate(){ return interestRate; } static void rate(double); private: std::string owner; double amount; static double interestRate; static double initRate(); } 这个类的每个对象具有两个数据成员:owner 和 amount,对象当中不保存 static 数据成员。interestRate 数据成员为 Account 类型的全体对象所共享。 使用类的 static 成员 可以通过作用域操作符从类直接调用 static 成员,或者通过对象、引用或指向该类类型对象的指针间接调用: Account ac1; Account *pac2 = &ac1; double rate; rate = ac1.rate(); rate = pac2->rate(); rate = Account::rate(); 像使用其它成员一样,类成员函数可以不用作用域操作符来引用类的 static 成员: class Account{ public: void applyint(){ amount += amount * interestRate; } 76 / 182 //... private: std::string owner; double amount; static double interestRate; static double initRate(); } static 数据成员 #include using namespace std; using std::string; class Account { friend int main(int argc, char *argv[]); public: void applyint() { amount += amount * interestRate; } double balance() { return amount; } public: static double rate() { return interestRate; } static void rate(double); private: string owner; double amount; static double interestRate; static double initRate(); private: static const string accountType; static const int period = 30; // interest posted every 30 days double daily_tbl[period]; // ok: period is constant expression }; const string Account::accountType("Savings Account"); double Account::interestRate = initRate(); double Account::initRate() { //... } void Account::rate(double newRate) { interestRate = newRate; } static 数据成员必须在类定义体的外部定义(正好一次)。不像普通数据成员,static 成员不是通过类构造函 77 / 182 数进行初始化,而是应该在定义时进行初始化。 static 关键字只能用于类定义体内部的声明中,定义不能标示为 static。 一般而言,类的 static 成员,像普通数据成员一样,不能在类的定义体内部进行初始化。static 数据成员通 常在定义的时候初始化,如上面的例子所示。该规则的一个例外就是,只要初始化静态数据成员的表达式是一个 常量表达式,整型 const static 数据成员就可以在类的定义体中进行初始化: static const int period = 30; static 成员函数 《深度探索 C++对象模型》 P150 静态成员函数由于缺乏 this 指针,因此差不多等同于非成员函数。如果 Point3d::normalize()是一个静 态成员函数,以下两个调用操作: obj.normalize(); ptr->normalize(); 将被转化为一般的 nonmember 函数调用,像这样: //obj.normalize(); normalize__7Point3dSfv(); //ptr->normalize(); normalize__7Point3dSfv(); 静态成员函数的主要特性就是它没有 this 指针,其次要的特性统统根源于这个主要特性:  它不能够直接存取其 class 中的 nonstatic members  它不能够被声明为 const、volatile 或 virtual  它不需要经由 class object 才被调用--虽然大部分时候它是这样被调用的 一个静态成员函数,会被提到 class 声明之外,并给予一个经过“mangling”的适当名称。例如: unsigned int Point3d::object_count() { Return _object_count; } 会被 cfront 转化为: //在 cfront 之下的内部转化结果 unsigned int object_count_5Point3dSFv() { Return _object_count_5Point3d; } 其中 SFv 表示它是个 static member function,拥有一个空白(void)的参数链表。 如果取一个静态成员函数的地址,得到的将是其在内存中的位置,也就是其地址。由于静态成员函数没有 this 指针,所以其地址的类型并不是一个“指向类成员函数的指针”,而是一个“非成员函数指针”。也就是说: &Point3d::object_count(); 会得到一个数值,类型是: unsigned int(*)(); 78 / 182 而不是: unsigned int( Point3d::* )(); 参考资料: 《C++ PRIMER》( 第 4 版·特别版) P398~402 79 / 182 友元的基本概念 采用类的机制后实现了数据的隐藏与封装,类的数据成员一般定义为私有成员,成员函数一般定义为公有的,依 此提供类与外界间的通信接口。但是,有时需要定义一些函数,这些函数不是该类的一部分,但又需要频繁地访 问该类的数据成员,这时可以将这些函数定义为该类的友元函数。 友元可以是普通的非成员函数,或者其他类的成员函数,或者整个类。将一个类设置为友元,友元类的所有成员 函数都可以访问授予友元关系的那个类的非公有成员。 友元的使用提高了程序的运行效率(即减少了类型检查和安全性检查等都需要的时间开销),但它破坏了类的封 装性和隐藏性,使得友元可以访问类的私有成员。 因为友元不是授予友元关系的那个类的成员,所以它们不受其声明出现部分的访问控制的影响。通常,将友元声 明成组地放在类定义的开始或结尾是个好主意。 普通友元函数 普通友元函数是可以直接访问类的私有成员的非成员函数。它是定义在类外的普通函数,它不属于任何类,但需 要在类的定义中加以声明,声明时只需在友元的名称前加上关键字 friend,其格式如下: friend 类型 函数名(形式参数); 友元函数的声明可以放在类的私有部分,也可以放在公有部分,它们是没有区别的,都说明是该类的一个友元函 数。一个函数可以是多个类的友元函数,只需要在各个类中分别声明。 友元成员函数 class Screen{ friend Window_Mgr& Window_Mgr::relocate(Window_Mgr::index, Window_Mgr::index, Screen& ); //... } 当我们成员函数声明为友元时,函数名必须用该函数所属的类名称加以限定。 友元类 友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息(包括私有成员和保护成员)。 当希望一个类可以存取另一个类的私有成员时,可以将该类声明为另一类的友元类。定义友元类的语句格式如下: friend class 类名; 其中:friend 和 class 是关键字,类名必须是程序中的一个已定义过的类。 class Screen{ friend class Window_Mgr; //... } //... 80 / 182 Window_Mgr& Window_Mgr::relocate( Screen::index r, Screen::index c, Screen& s ) { s.height += r; s.width += c; return *this; } //... 缺少友元声明时,上面在 relocate 成员函数里面对 Screen 类的私有数据成员的直接访问将出错。 注意事项 使用友元类时注意: (1) 友元关系不能继承。基类的友元对派生类的成员没有特殊的访问权限;如果基类被授予友元关系,则只有 基类具有特殊访问权限,该基类的派生类不能访问授予基类友元关系的类。 (2) 友元关系是单向的,不具有交换性。若类 B 是类 A 的友元,类 A 不一定是类 B 的友元,要看在类中是否有 相应的声明。 (3) 友元关系不具有传递性。若类 B 是类 A 的友元,类 C 是 B 的友元,类 C 不一定是类 A 的友元,同样要看类 中是否有相应的申明。 参考资料: 《C++ PRIMER》( 第 4 版·特别版) P396~398 12.5 http://www.cppblog.com/twzheng/articles/21020.html 81 / 182 操作符重载 Google C++编码规范关于运算符重载的说明 除少数特定环境外,不要重载运算符 定义: 一个类可以定义诸如 + 和 / 等运算符, 使其可以像内建类型一样直接操作. 优点: 使代码看上去更加直观, 类表现的和内建类型 (如 int) 行为一致. 重载运算符使 Equals(), Add() 等函数名黯然失色. 为 了使一些模板函数正确工作, 你可能必须定义操作符. 缺点: 虽然操作符重载令代码更加直观, 但也有一些不足: 1、混淆视听, 让你误以为一些耗时的操作和操作内建类型一样轻巧. 2、更难定位重载运算符的调用点, 查找 Equals() 显然比对应的 == 调用点要容易的多. 3、有的运算符可以对指针进行操作, 容易导致 bug. Foo + 4 做的是一件事, 而 &Foo + 4 可能做的是完全不同的另一件事. 对于二者, 编译器都不会报错, 使其很难调试; 4、重载还有令你吃惊的副作用. 比如, 重载了 operator& 的类不能被前置声明. 结论: 一般不要重载运算符. 尤其是赋值操作 (operator=) 比较诡异, 应避免重载. 如果需要的话, 可以定义类似 Equals(),CopyFrom()等函数. 然而,极少数情况下可能需要重载运算符以便与模板或“标准”C++ 类互操作(如 operator<<(ostream&, const T&)).只有 被证明是完全合理的才能重载,但你还是要尽可能避免这样做.尤其是不要仅仅为了在 STL 容器中用作键值就重载 operator== 或 operator<;相反,你应该在声明容器的时候,创建相等判断和大小比较的仿函数类型. 有些 STL 算法确实需要重载 operator== 时, 你可以这么做, 记得别忘了在文档中说明原因. 重载操作符的定义 《C++ PRIMER》( 第 4 版•特别版) 14.1 重载操作符是具有特殊名称的函数:保留字 operator 后接需定义的操作符符号。像任意其他函数一样,重载 操作符具有返回类型和形参表,如下语句: Sales_item operator+( const Sales_item &, const Sales_item &); 声明了加号操作符,可用于将两个 Sales_item 对象相加并获取一个 Sales_item 对象的副本。 重载操作符必须具有一个类类型操作数,这条规则强制重载操作符不能重新定义用于内置类型对象的操作符的含 义。大多数重载操作符可以定义为普通非成员函数或类的成员函数,下面是一些指导性原则,有助于决定将操作 符设置为类成员还是普通的非成员函数: (1)赋值(=)、下标([])、 调 用 ( ())和成员访问箭头(->)等操作符必须定义为成员,将这些操作符定义为 非成员函数将在编译时标记为错误。 (2)像赋值一样,复合赋值操作符通常也应该定义为类的成员。与赋值不同的是,不一定非得这么做,如果定义 非成员的复合赋值操作符,不会出现编译错误。 (3)改变对象状态或与给定类型密切联系的其他一些操作符,比如自增、自减、解引用等等,通常定义为类成员。 (4)对称的操作符,如算术操作符,相等操作符,关系操作符,位操作符,最好定义为普通非成员函数。 (5)定义符合标准库 iostream 规范的输入输出操作符的时候,必须使它们成为非成员函数。 82 / 182 重载逗号、取地址、逻辑与、逻辑或等操作符通常不是好的做法。这些操作符具有有用的内置含义,如果我们定 义了自己的版本,就不能再使用这些内置含义了。 当内置操作符和类型上的操作存在逻辑对应关系时,操作符重载最有用。使用重载操作符可以令程序更自然、更 直观,而滥用操作符重载会使我们的类难以理解。当一个重载操作符的含义不明显时,给操作取一个适当的名字 更好。对于很少用的操作,使用命名函数通常比使用操作符更好。 输出操作符<<重载 《C++ PRIMER》( 第 4 版•特别版) 14.2.1 为了与 IO 标准库一致,操作符应接受 ostream&作为第一个形参,对类类型 const 对象的引用作为第二个形 参,并返回对 ostream 形参的引用。 ostream& operator<<( ostream& os, const classType &object ) { //... os<< //... return os; } 第一个形参是对 ostream 对象的引用,在该对象上将产生输出,ostream 为非 const,因为写入到流会改变 流的状态。并且该形参是一个引用,因为不会复制 ostream 对象。 第二个形参一般应是对要输出的类类型的引用。该形参是一个引用以避免复制实参。它可以是 const,因为一 般而言输出一个对象不会改变该对象的状态。并且,使形参为 const 引用,还可以使用同一个定义来输出 const 和非 const 对象。 一般而言,输出操作符应该仅仅输出对象的内容,尽量少对内容进行格式化,最好不要输出换行符,让用户自己 控制输出相关的细节。 当定义符合标准库 iostream 规范的输入输出操作符的时候,必须使它们成为非成员操作符,为什么需要这样 做呢?如果我们将输出操作符定义为类的成员: Sales_item item; item<>重载 《C++ PRIMER》( 第 4 版•特别版) 14.2.2 与输出操作符类似,输入操作符的第一个形参是一个引用,指向它要读的流,并且返回的也是对同一个流的引用。 它的第二个形参是对要读入的对象的非 const 引用,该形参必须为非 const,因为输入操作符的目的是将数据 读入到这个对象之中。更重要但通常重视不够的是,输入操作符必须处理错误和文件结束的可能性。 Sales_item 的输入操作符如下: 83 / 182 istream& operator>>( ostream& in, Sales_item& s ) { double price; in >> s.isbn >> s.units_sold >> price; if( in ) s.revenue = s.units_sold * price; else s = Sales_item(); return in; } 相等操作符 《C++ PRIMER》( 第 4 版•特别版) 14.3.1 通常,C++中的类使用相等操作符表示对象是等价的。它通常比较每个数据成员,如果所有对应成员都相同,则 认为两个对象相等。与这一设计原则一致,Sales_item 的相等操作符应比较 isbn 以及销售数据: inline bool operator==(const Sales_item &lhs, const Sales_item &rhs) { // must be made a friend of Sales_item return lhs.units_sold == rhs.units_sold && lhs.revenue == rhs.revenue && lhs.same_isbn(rhs); } inline bool operator!=(const Sales_item &lhs, const Sales_item &rhs) { return !(lhs == rhs); // != defined in terms of operator== } 这些函数包含如下一些设计原则: (1)如果类定义了==操作符,该操作符的含义是判断两个对象是否包含同样的数据。 (2)如果类具有一个操作,能确定该类型的两个对象是否相等,通常将该函数定义为 operator==而不是定义命 名函数,用户将习惯于用==来比较对象。 (3)如果类定义了 operator==,它也应该定义 operator!=。用户会期待如果可以用某个操作符,则另外一 个也应该存在。 (4)相等和不等操作符一般应该相互联系起来定义,让一个操作符完成比较对象的实际工作,而另外一个操作符 只是调用前者。 关系操作符 《C++ PRIMER》( 第 4 版•特别版) 14.3.2 84 / 182 定义了相等操作符的类一般也应该具有关系操作符。尤其是,因为关联容器和某些算法使用小于操作符,所以定 义 operator<可能会相当实用。 赋值操作符、复合赋值操作符 《C++ PRIMER》( 第 4 版•特别版) 14.4 类赋值操作符接受类类型形参,通常,该形参是对类类型的 const 引用,当然也可以是对类类型的非 const 引 用。如果没有定义赋值操作符,编译器将合成它。类的赋值操作符必须是类的成员,以便编译器可以知道是否需 要合成一个。 可以为一个类定义许多附加的赋值操作符。这些赋值操作符会因右操作数类型的不同而不同。例如,标准库的类 string 定义了 3 个赋值操作符: class string{ public: string& operator=(const string &); //s1 = s2; string& operator=(const char *); //s1 = “str”; string& operator=(char); //s1 = ‘c’; //... } 除了接受 const string&作为右操作数的类赋值操作符之外,string 还定义了接受 C 风格字符串或 char 作 为右操作数的赋值操作符,这些操作符可以这样使用: string car(“BMW”); car = “OOOO”; //string = const char * string model; model = ‘T’; //string = char 一般而言,赋值操作符和复合赋值操作符应该返回左操作数的引用。下面是复合赋值操作符的一个例子: Sales_item& Sales_item::operator+=(const Sales_item& rhs) { units_sold += rhs.units_sold; revenue += rhs.revenue; return *this; } 下标操作符 《C++ PRIMER》( 第 4 版•特别版) 14.5 可以从容器中检索单个元素的容器类一般会定义下标操作符(operator[]),标准库的类 string 和 vector 均是定义了下标操作符的例子。下标操作符必须定义为类成员函数。 定义下标操作符比较复杂的地方在于,它在用作赋值的左右操作数时都应该能表现正常。下标操作符出现在左边, 必须生成左值,可以指定引用作为返回类型而得到左值。只要下标操作符返回引用,就可以用作赋值的任意一方。 定义下标操作符时,一般需要定义两个版本:一个为非 const 成员函数并返回引用;一个为 const 成员函数并 85 / 182 返回 const 引用。应用于 const 对象时,返回值应为 const 引用,因此不能作为被赋值的目标。下面是一个 应用下标操作符的例子: class Foo{ public: int &operator[]( const size_t ); const int &operator[](const size_t) const; //... private: vector data; //... }; int& Foo::operator[](const size_t index) { return data[index]; } const int& Foo::operator[]( const size_t index ) const { return data[index]; } 成员访问操作符 《C++ PRIMER》( 第 4 版•特别版) 14.6 为了支持指针型类,例如迭代器,C++语言允许重载解引用操作符(*)和箭头操作符(->)。解引用操作符和箭头 操作符常用在实现智能指针的类中。 //本条款暂时不做详细展开解释,具体实现可以参照对 auto_ptr 智能指针的源代码详细理解相关的概念。 自增自减操作符 《C++ PRIMER》( 第 4 版•特别版) 14.7 自增和自减操作符经常由诸如迭代器这样的类来实现,这样的类提供类似于指针的行为来访问序列中的元素。例 如,可以定义一个类,该类指向一个数组并为该数组中的元素提供访问检查。下面是相关的例子: class CheckedPtr{ public: // no default constructor; CheckedPtrs must be bound to an object CheckedPtr(int *b, int *e): beg(b), end(e), curr(b) { } // dereference operator int& operator*(); const int& operator*() const; 86 / 182 //后缀式操作符 CheckedPtr operator++(int); CheckedPtr operator--(int); //前缀式操作符 CheckedPtr& operator++(); CheckedPtr& operator--(); private: int* beg; // pointer to beginning of the array int* end; // one past the end of the array int* curr; // current position within the array }; int& CheckedPtr::operator*() { if (curr == end) throw out_of_range("dereference past the end"); return *curr; } const int& CheckedPtr::operator*() const { if (curr == end) throw out_of_range("dereference past the end"); return *curr; } CheckedPtr CheckedPtr::operator++(int) { CheckedPtr ret(*this); // save current value ++*this; // advance one element, checking the increment return ret; // return saved state } CheckedPtr CheckedPtr::operator--(int) { CheckedPtr ret(*this); // save current value --*this; // move backward one element and check return ret; // return saved state } CheckedPtr& CheckedPtr::operator++() { if (curr == end) throw out_of_range("increment past the end of CheckedPtr"); ++curr; // advance current state return *this; 87 / 182 } CheckedPtr& CheckedPtr::operator--() { if (curr == beg) throw out_of_range("decrement past the beginning of CheckedPtr"); --curr; // move current state back one element return *this; } C++语言不要求自增或自减操作符一定作为类的成员,但是,因为这些操作符改变操作对象的状态,所以更加倾 向于将它们作为类的成员。 对于内置类型而言,自增操作符和自减操作符有前缀和后缀两种形式。同时定义前缀式操作符和后缀式操作符存 在一个问题:它们的形参数目和类型相同,普通重载不能区分所定义的是前缀式操作符还是后缀式操作符。为了 解决这一问题,后缀式操作符函数接受一个额外的 int 形参。这个形参不是后缀式操作符的正常工作所需要的, 它的唯一目的是使后缀函数与前缀函数区别开来。 如果想要使用函数调用来调用后缀式操作符,必须给出一个整型实参值: CheckedPtr parr( ia, ia + size ); //ia points to an array of ints parr.operator++(0); //调用后缀式操作符 parr.operator++(); //调用前缀式操作符 函数调用操作符重载 《C++ PRIMER》( 第 4 版•特别版) 14.8 可以为类类型的对象重载函数调用操作符。一般为表示操作的类重载函数调用操作符。例如,可以定义名为 absInt 的结构,该结 构封装将 int 类型的值转换为绝对值的操作: struct absInt{ int operator()( int val ){ return val < 0 ? –val : val; } } 这个类很简单,它定义了一个操作:函数调用操作,该操作符有一个形参并返回形参的绝对值。通过为类类型对象提供一个实参表 而使用调用操作符,所用的方式看起来像一个函数调用: int i = -42; absInt absObj; unsigned int ui = absObj(i); 尽管 absObj 是一个对象而不是函数,我们仍然可以“调用”该对象,其实际效果是运行由 absObj 对象定义的重载函数调用操 作符,该操作符接受一个 int 值并返回它的绝对值。 函数调用操作符必须声明为成员函数。一个类可以定义多个函数调用操作符。由形参的数目或类型加以区别。 定义了函数调用操作符的类,其对象称为函数对象,即它们是行为类似于函数的对象。函数对象相关概念,参照《C++标准程序库》 相关章节。 另外,函数对象相关的概念,参见单独的对函数对象的总结,其总结主要参照《C++标准模板库》。 参考资料: 《C++ PRIMER》( 第 4 版·特别版) P430~448 88 / 182 类类型和其它类型之间的相互转换 C++语言定义了内置类型之间的几个自动转换。也可以定义如何将其他类型的对象隐式转换为我们的类类型,或 者将我们的类类型的对象隐式转换成其它类型。 到类类型的隐式转换 《C++ PRIMER》( 第 4 版•特别版)P393 12.4.4 可以用单个实参来调用的构造函数定义了从形参类型到该类类型的一个隐式转换。 下面是定义了两个构造函数的 Sales_item 版本: class Sales_item{ public: Sales_item(const std::string &book=””):isbn(book),units_sold(0),revenue(0.0){} Sales_item(std::istream &is); //... } 这里的每一个构造函数都定义了一个隐式转换。因此,在期待一个 Sales_item 类型对象的地方,可以使用一 个 string 或者一个 istream: string null_book = “9-999-99999-9”; item.same_isbn(null_book); 这段程序使用一个 string 类型对象作为实参传递给 Sales_item 的 same_isbn 函数。该函数期待一个 Sales_item 对象作为实参。编译器使用接受一个 string 的 Sales_item 构造函数从 null_book 生成一个 新的 Sales_item 对象。新生成的 Sales_item 被传递给 same_isbn 函数。 这样的自动转换行为是否是我们所需要的,依赖于我们认为用户将如何使用该转换。它可能是一个好主意,也可 能带来意想不到的难以排查的错误。 我们可以通过将构造函数声明为 explicit,来防止在需要隐式转换的上下文中使用构造函数: class Sales_item{ public: explicit Sales_item(const std::string &book=””) :isbn(book),units_sold(0),revenue(0.0){} explicit Sales_item(std::istream &is); //... } explicit 关键字只能用于类内部的构造函数声明上。在类的定义体外部所做的定义上不再重复它。现在,两个 构造函数都不能用于隐式地创建对象,这样的使用都不能通过编译: item.same_isbn(null_book); item.same_isbn(cin); 89 / 182 通常,除非有明显的理由想要定义隐式转换,否则,单形参构造函数都应该为 explicit,将构造函数设置为 explicit 可以避免错误,并且当转换有用的时候,用户可以显式地构造对象。 从类类型到其它类型的转换 《C++ PRIMER》( 第 4 版•特别版)P454 14.9 除了定义到类类型的转换之外,我们还可以定义转换操作符,给定类类型的对象,该操作符将产生其他类型的对 象。像其他转换一样,编译器将自动引用这个转换。 转换操作符是一种特殊的类成员函数。它定义将类类型值转变为其他类型值的转换。转换操作符在类定义体内声 明,在保留字 operator 之后跟着转换的目标类型: class SmallInt{ public: SmallInt(int i=0):val(i){ if( i < 0 || i > 255 ) throw std::out_of_range(“bad smallInt initializer.”); } operator int() const { return val; } private: std::size_t val; } 转换函数采用如下通用形式: operator type(); 这里,type 表示内置类型名、类类型名或由类型别名所定义的名字。对任何可作为函数返回类型的类型(除了 void 之外)都可以定义转换函数。一般而言,不允许转换为数组或函数类型,转换为指针类型以及引用类型是 可以的。 转换函数必须是成员函数,不能指定返回类型,并且形参表必须为空。转换函数一般不应该改变被转换的对象, 因此,转换操作符通常应定义为 const 成员。 参考资料: 《C++ PRIMER》( 第 4 版·特别版) P393 《C++ PRIMER》( 第 4 版·特别版) P454 90 / 182 公有、私有和受保护继承 对类所继承的成员的访问由基类中的成员访问级别和派生类派生列表中使用的访问标号共同控制。在 C++中继承 主要有三种关系:public、protected 和 private。这三种继承关系中 public 继承是最为常用的一种继承 关系,private 继承是最少见的继承关系。 基本概念 每个类控制它所定义的成员的访问,派生类可以进一步限制但不能放松对所继承的成员的访问。 基类本身指定对自身成员的最小访问控制。如果成员在基类中为 private,则只有基类和基类的友元可以访问 该成员。派生类不能访问基类的 private 成员,也不能使自己的用户访问 private 成员。如果基类成员为 public 或 protected,则派生列表中使用的访问标号决定该成员在派生类中的访问级别: 如果是公共继承(public inheritance):基类成员保持自己的访问级别:基类的 public 成员为派生类的 public 成员,基类的 protected 成员为派生类的 protected 成员。 如果是受保护继承(protected inheritance):基类的 public 和 protected 成员在派生类中为 protected 成员。这样一来在派生类中同样还是可以调用基类的 protected 和 public 成员,派生类的派生 类就也可以调用被 protected 继承的基类的 protected 和 public 成员。 如果是私有继承(private inheritance):基类中所有成员在派生类中为 private 成员。这样一来虽然派 生类中同样还是可以调用基类的 protected 和 public 成员,但是在派生类的派生类就不可以再调用被 private 继承的基类的成员了。 例如,考虑下面的继承层次: class Base{ public: void basemem(); protected: int i; //... }; struct Public_derived : public Base{ int use_base(){ return i; } } struct Private_derived : private Base{ int use_base(){ return i; } } 无论派生列表中是什么访问标号,所有继承 Base 的类对 Base 中的成员具有相同的访问权限。派生访问标号将 控制派生类的用户对从 Base 继承而来的成员的访问: Base b; Public_derived d1; 91 / 182 Private_derived d2; b.basemem(); //ok d1.basemem(); //ok d2.basemem(); //error:在 Private_derived 派生类中,basemem 是 private 的 Public_derived 和 Private_derived 都继承了 basemem 函数。当进行 public 继承时,该成员保持其 访问标号,所以,d1 可以调用 basemem。在 Private_derived 中,Base 的成员为 private , Private_derived 的用户不能调用 basemem。 接口继承和实现继承 public 派生类继承基类的接口,它具有与基类相同的接口。设计良好的类层次中,public 派生类的对象可以 用在任何需要基类对象的地方。 使用 private 或 protected 派生的类不继承基类的接口,相反,这些派生通常被称为实现继承。派生类在实 现中使用被继承类但继承基类的部分并未成为其接口的一部分。 默认继承保护级别 用 struct 或 class 保留字定义的类具有不同的默认访问级别。同样,默认继承访问级别根据使用哪个保留字 定义派生类也不相同。根据 class 保留字定义的派生类默认具有 private 继承,而用 struct 保留字定义的 类默认具有 public 继承: class Base{...} struct D1: Base{...} //默认 public 继承 class D2: Base{...} //默认 private 继承 一般来说,程序设计中不要依赖于这种默认的继承方式,显式指定相关的继承方式比较清晰明了。 友元与继承的关系 像其他类一样,基类或派生类可以使其它类或函数成为其友元。友元可以访问类的 private 或 protected 数 据。 友元关系不能继承。①基类的友元对派生类的成员没有特殊的访问权限;②如果基类被授予友元关系,则只有基 类具有特殊访问权限,该基类的派生类不能访问授予基类友元关系的类。 如果派生类想要将对自己成员的访问权限授予其基类的友元,派生类必须显式地声明。同样,如果基类和派生类 都需要访问另一个类,那个类必须特地将访问权限授予基类和相关的派生类。 继承与静态成员 如果基类中定义了 static 数据成员,则整个继承层次中只有一个这样的成员。无论从基类派生出多少派生类, 每个 static 成员只有一个实例。static 成员遵循常规访问控制:如果成员在基类中为 private,则派生类 不能访问它。假定可以访问成员,则既可以通过基类访问 static 成员,也可以通过派生类访问 static 成员。 一般而言,既可以使用作用域操作符也可以使用点或箭头成员访问操作符。 struct Base{ static void statmem(); 92 / 182 } struct Derived : Base{ void f( const Derived& ); } void Derived::f( const Derived& derived_obj ){ Base::statmem(); Derived::statmem(); derived_obj.statmem(); statmem(); } 参考资料: 《C++ PRIMER》( 第 4 版·特别版) P482~487 93 / 182 虚函数 基类通常应该将派生类需要重新定义的函数定义为虚函数。 保留字 virtual 的目的是启用动态绑定。成员默认为非虚函数,对非虚函数的调用在编译时确定。为了指明函 数为虚函数,在其返回类型前面加上保留字 virtual。除了构造函数以外,任意非 static 成员函数都可以是 虚函数。保留字 virtual 只在类内部的成员函数声明中出现,不能用在类定义体外部出现的函数定义上。 派生类和虚函数 《C++ PRIMER》( 第 4 版•特别版) 15.2.3 尽管不是必须这么做,派生类一般会重新定义所继承的虚函数。如果派生类没有重定义某个虚函数,则使用基类 中定义的版本。 派生类中虚函数的声明必须与基类中的定义方式完全匹配,但是有一个例外:返回对基类型的引用(或指针)的 虚函数。派生类中的虚函数可以返回基类函数所返回类型的派生类的引用(或指针)。 一旦函数在基类中声明为虚函数,它就一直为虚函数,派生类无法改变该函数为虚函数这一事实。派生类重定义 虚函数时,可以使用 virtual 保留字,但不是必须这么做。 virtual 与动态绑定 《C++ PRIMER》( 第 4 版•特别版) 15.2.4 C++中的函数调用默认不使用动态绑定。要触发动态绑定,必须满足两个条件:第一,只有指定为虚函数的成员 函数才能进行动态绑定,成员函数默认为非虚函数,非虚函数不能进行动态绑定;第二,必须通过基类类型的引 用或指针进行函数调用。 基类类型引用和指针的关键点在于静态类型(在编译时可知的引用类型或指针类型)和动态类型(指针或引用所 绑定的对象的类型,这是仅在运行时可知的)可能不同。引用和指针的静态类型与动态类型可以不同,这是 C++ 用以支持多态性的基石。 通过引用或指针调用虚函数时,编译器将生成代码,在运行时确定调用哪个函数,被调用的是与动态类型相对应 的函数。 只有通过引用或指针调用,虚函数才在运行时确定。非虚函数总是在编译时根据调用该函数的对象、引用或指针 的类型来确定。 虚析构函数 《C++ PRIMER》( 第 4 版•特别版) 15.4.4 删除指向动态分配对象的指针时,需要运行析构函数在释放对象的内存之前清除对象。处理继承层次中的对象时, 指针的静态类型可能与被删除对象的动态类型不同,可能会删除实际指向派生类对象的基类类型指针。 如果删除基类指针,则需要运行基类析构函数并清除基类成员,如果对象实际是派生类型的,则没有定义该行为。 要保证运行适当的析构函数,基类中的析构函数必须为虚函数: class Base{ public: //... 94 / 182 virtual ~Base(){...} } class Derived:public Base{ public: //... virtual ~Derived(){...} } 如果析构函数为虚函数,那么通过指针调用时,运行哪个析构函数将因指针所指对象类型的不同而不同: Base *pBase = new Base(); delete pBase; //调用 Base 的析构函数 pBase = new Derived(); delete pBase; //调用 Derived 的析构函数 像其他虚函数一样,析构函数的虚函数性质都将继承。因此,如果层次中根类的析构函数为虚函数,则派生类析 构函数也将是虚函数,无论派生类显示定义析构函数还是使用合成析构函数,派生类析构函数都是虚函数。 即使析构函数没有工作需要做,继承层次的基类也应该定义一个虚析构函数。 构造函数和赋值操作符不要设置为虚函数 《C++ PRIMER》( 第 4 版•特别版) 15.4.4 在复制控制成员中,只有析构函数应该定义为虚函数,构造函数不能定义为虚函数。构造函数是在对象完全构造 之前运行的,在构造函数运行的时候,对象的动态类型还不完整。 虽然可以在基类中将成员函数 operator=定义为虚函数,但这样做并不影响派生类中使用的赋值操作符。每个 类都有自己的赋值操作符,派生类的赋值操作符有一个与类本身类型相同的形参,该类型必须不同于继承层次中 任意其它类的赋值操作符的形参类型。 将赋值操作符设为虚函数可能会令人混淆,因为虚函数必须在基类和派生类中具有相同的形参。基类赋值操作符 有一个形参是自身类类型的引用,如果该操作符为虚函数,则每个继承于该基类的类都将得到一个虚函数成员, 该成员定义了参数为一个基类对象的 operator=。但是,对派生类而言,这个操作符与赋值操作符是不同的。 构造函数和析构函数中的虚函数 《C++ PRIMER》( 第 4 版•特别版) 15.4.5 构造派生类对象时首先运行基类构造函数初始化对象的基类部分。在执行基类构造函数时,对象的派生类部分是 未初始化的。实际上,此时的对象还不是一个派生类对象。 撤销派生类对象时,首先撤销它的派生类部分,然后按照与构造顺序的逆序撤销它的基类部分。 在这两种情况下,运行构造函数或析构函数的时候,对象都是不完整的。 如果在构造函数或析构函数中调用虚函数,则运行的是为构造函数或析构函数自身类型定义的版本。无论有构造 函数或析构函数直接调用虚函数,或者从构造函数或析构函数所调用的函数间接调用虚函数,都应用这种绑定。 纯虚函数 《C++ PRIMER》( 第 4 版•特别版) 15.6 95 / 182 在函数形参表后面写上“=0”以指定该函数为纯虚函数: class Widget{ public: double func(int i) const = 0; //... } 将函数定义为纯虚函数能够说明,该函数为后代类型提供了可以覆盖的接口,但用户不能创建 Widget 类型的 对象。试图创建抽象基类的对象将发生编译时错误: Widget myWidget; //error: can’t define a Widget object 含有一个或多个纯虚函数的类是抽象基类。我们不能创建抽象基类类型的对象。 参考资料: 《C++ PRIMER》( 第 4 版·特别版) P398~402 《深度探索 C++对象模型》 P150 96 / 182 多重继承 多重继承是从多于一个直接基类派生类的能力,多重继承的派生类继承其所有父类的属性。尽管概念简单,缠绕 多个基类的细节可能会带来错综复杂的设计问题或实现问题。 为支持多继承,一个类的派生表: class Bear : public ZooAnimal { ... }; 被扩展成支持逗号分隔的基类列表: class Panda : public Bear, public Endangered { ... }; 每个被列出的基类还必须指定其访问级别:public、protected 或 private 之一。与单继承一样,只有当 一个类的定义已经出现后,它才能被列在多继承的基类表中。 对于一个派生类的基类的数目,C++没有限制。实际情况下,两个基类是最常见的,一个基类用于表示一个公有 抽象接口,另外一个基类提供私有实现。从三个或者更多个直接基类继承而来的派生类遵循 mixin-based 设计风格,其中每个基类都表示该派生类完整接口的一个方面。 多重继承的派生类从每个基类中继承状态 在多继承下,派生类含有每个基类的一个基类子对象。例如,当我们写: Panda ying_yang(“ying_yang”); 时,ying_yang 由一个 Bear 类子对象(它又含有一个 ZooAnimal 基类子对象)、一个 Endangered 类子 对象,以及在 Panda 类中声明的非静态数据成员组成: 多继承 Panda 层次结构 基类构造函数被调用的顺序以类派生表中声明的顺序为准。例如对 ying_yang 来说,构造函数被调用的顺序 是:Bear 构造函数(因为 Bear 是从 ZooAnimal 派生的,所以在 Bear 构造函数执行之前,ZooAnimal 的 构造函数先被调用),Endangered 构造函数,然后是 Panda 构造函数。 构造函数调用顺序不受基类在成员初始化表中是否存在以及被列出的顺序的影响,也即是说,如果 Bear 缺省 构造函数被隐式调用,没有出现在成员初始化表中,如下所示: // Bear 缺省构造函数在 Endangered 的双参数构造函数之前被调用 Panda::Panda(): Endangered( Endangered::environment,Endangered::critical ) { ... } 那么 Bear 的缺省构造函数仍然在显式列出的双参数 Endangered 构造函数之前被调用。类似地,析构函数调 用顺序总是与构造函数顺序相反。在我们的例子中,析构函数调用顺序是:~Panda()、~Endangered()、 97 / 182 ~Bear(), 最后是~ZooAnimal()。 同名成员函数的二义性 在单继承下,基类的 public 和 protected 成员可以直接被访问,就像它们是派生类的成员一样,对多继承 这也是正确的。但是在多继承下,派生类可以从两个或者更多个基类中继承同名的成员。然而在这种情况下,直 接访问是二义的,将导致编译时刻错误。例如,如果 Bear 和 Endangered 都定义了一个成员函数 print(), 则如下语句: ying_yang.print( cout ); 将导致编译时刻错误,即使这两个通过继承得到的成员函数定义了不同的参数类型: Error: ying_yang.print( cout ) -- ambiguous, one of Bear::print( ostream& ) ndangered::print( ostream&, int ) 原因在于继承得到的成员函数没有构成派生类中的重载函数,因此,对于 print()调用,编译器在解析的时候, 只是使用了针对 print 的名字解析,而不是使用“基于传递给 print()的实际实参类型的重载解“。 可以通过指定使用那个类解决二义性的问题: ying_yang.Endangered::print(cout); 避免潜在二义性最好的方法就是,在解决二义性的派生类中定义函数的一个版本。例如,应该给选择使用哪个 print 版本的 Panda 类一个 print 函数: std::ostream& Panda::print(std::ostream &os) const { Bear::print(os); Endangered::print(os); return os; } 转换与多个基类 在单继承下,如果有必要的话,派生类的指针或引用可以自动被转换成基类的指针或引用。对于多继承,这也是 正确的。例如一个 Panda 指针或引用可以被转换成 ZooAnimal、Bear 或 Endangered 类的指针或引用。例 如: extern void display( const Bear& ); extern void highlight( const Endangered& ); Panda ying_yang(“ying_yang”); display( ying_yang ); // ok highlight( ying_yang ); // ok extern ostream& operator<<( ostream&, const ZooAnimal& ); cout << ying_yang << endl; // ok 但是在多继承下二义转换的可能性非常大。例如,考虑下列两个函数: extern void display( const Bear& ); extern void display( const Endangered&); 98 / 182 用非限定修饰的 Panda 对象调用 display(): Panda ying_yang; display( ying_yang ); // 错误: 二义性 将导致下列一般形式的编译时刻错误: Error: display( ying_yang ) -- ambiguous, one of extern void display( const Bear&); extern void display( const Endangered&); 编译器没有办法区分应该使用哪一个直接基类(编译器不会试图根据派生类转换来区别基类间的转换,转换到每 个基类都一样好)。 多重继承下的虚函数 为了了解多继承怎样影响虚拟函数机制,让我们为每个 Panda 的直接基类定义一组虚拟函数: class Bear : public ZooAnimal { public: virtual ~Bear(); virtual ostream& print( ostream&) const; virtual string isA() const; // ... }; class Endangered { public: virtual ~Endangered(); virtual ostream& print( ostream&) const; virtual void highlight() const; // ... }; 现在我们来定义 Panda,它提供了自己的 print()实例、析构函数,并引入了一个新的虚拟函数 cuddle(): class Panda : public Bear, public Endangered { public: virtual ~Panda(); virtual ostream& print( ostream&) const; virtual void cuddle(); // ... }; 可以直接从 Panda 对象调用的虚拟函数集如下表所示: 99 / 182 虚拟函数名 活动实例 Destructor(构析函数) Panda::~Panda() print(ostream&) const Panda::print(ostream&) isA() const Bear::isA() highlight() const Endangered::highlight() cuddle() Panda::cuddle() 像单继承一样,用基类的指针或引用只能访问基类中定义(或继承)的成员,不能访问派生类中引入的成员。当 一个类继承于多个基类时,那些基类之间没有隐含的关系,不允许使用一个基类的指针访问其它基类的成员。 当用 Panda 类对象的地址初始化或赋值 Bear 或 ZooAnimal 指针或引用时,Panda 接 口 中“ Panda 特有的 部分“以及”Endangered 部分“就都不能再被访问。例如: Bear *pb = new Panda; pb->print( cout ); // ok: Panda::print(ostream&) pb->isA(); // ok: Bear::isA() pb->cuddle(); // 错误: 不是 Bear 接口的部分 pb->highlight(); // 错误: 不是 Bear 接口的部分 delete pb; // ok: Panda::~Panda() 类似地,当用 Panda 类对象的地址初始化或赋值 Endangered 指针或引用时,Panda 接口中“Panda 特有 的部分“以及”Bear 部分“都不能再被访问。例如: Endangered *pe = new Panda; pe->print( cout ); // ok: Panda::print(ostream&) pe->isA(); // 错误: 不是 Endangered 的接口部分 pe->cuddle(); // 错误: 不是 Endangered 的接口部分 pe->highlight(); // ok: Endangered::highlight() delete pe; // ok: Panda::~Panda() 无论我们删除对象所使用的指针类型是什么,虚拟析构函数的处理都是一致的。例如: // ZooAnimal *pz = new Panda; delete pz; // Bear *pb = new Panda; delete pb; // Panda *pp = new Panda; delete pp; // Endangered *pe = new Panda; delete pe; 在上述例子中,析构函数的调用顺序完全相同。析构函数调用顺序与构造函数的顺序相反:Panda 析构函数被 通过虚拟机制调用。在 Panda 析构函数执行之后依次静态调用 Endangered、Bear 和 ZooAnimal 析构函数。 多重继承派生类的复制控制 100 / 182 多重继承的派生类的逐个成员初始化、赋值和析构,表现得与单继承下的一样,使得基类自己的复制构造函数、 赋值操作符或析构函数隐式构造、赋值或撤销每个基类。假定 Panda 类使用默认复制控制成员,ling_ling 的初始化: Panda ying_yang(“ying_yang”); //create a panda object Panda ling_ling = ying_yang; //uses copy constructor 使用默认复制构造函数调用 Bear 复制构造函数,Bear 复制构造函数依次在执行 Bear 复制构造函数之前运行 ZooAnimal 复制构造函数。一旦构造好了 ling_ling 的 Bear 部分,就运行 Endangered 复制构造函数来创 建对象的那个部分。最后,运行 Panda 复制构造函数。 合成的赋值操作符的行为类似于复制构造函数,它首先对对象的 Bear 部分进行赋值,并通过 Bear 对对象的 ZooAnimal 部分进行赋值,然后,对 Endangered 部分进行赋值,最后对 Panda 部分进行赋值。 合成的析构函数撤销 Panda 对象的每个成员,并且按构造次序的逆序为基类部分调用析构函数。 明智而谨慎地使用多重继承《Effective C++》第三版条款 40 详细解释参见 Effective C++ 条款 40 101 / 182 虚拟继承 基本概念 在缺省情况下,C++中的继承是按值组合的一种特殊情况。当我们写: class Bear : public ZooAnimal { ... }; 每个 Bear 类对象都含有其 ZooAnimal 基类子对象的所有非静态数据成员,以及在 Bear 中声明的非静态数 据成员。类似地,当派生类自己也作为一个基类对象时,如: class PolarBear : public Bear { ... }; 则 PolarBear 类对象含有在 PolarBear 中声明的所有非静态数据成员,以及其 Bear 子对象的所有非静态 数据成员和 ZooAnimal 子对象的所有非静态数据成员。 在单继承下,这种由继承支持的、特殊形式的按值组合提供了最有效的、最紧凑的对象表示。 在多继承下,当一个基类在派生层次中出现多次时就会有问题。最主要的实际例子是 iostream 类层次结构, ostream 和istream 类都从抽象ios 基类派生而来,而iostream 类又是从ostream 和istream 派生: class iostream :public istream, public ostream { ... };//非虚拟继承 缺省情况下,每个 iostream 类对象含有两个 ios 子对象:在 istream 子对象中的实例以及在 ostream 子 对象中的实例。这为什么不好?从效率上而言,存储 ios 子对象的两个复本,浪费了存储区,因为 iostream 只 需要一个实例。而且 ios 构造函数被调用了两次,每个子对象一次。更严重的问题是由于两个实例引起的二义 性。例如,任何未限定修饰地访问 ios 的成员都将导致编译时刻错误:到底访问哪个实例?如果 ostream 和 istream 对其 ios 子对象的初始化稍稍不同,会怎样呢?怎样通过 iostream 类保证这一对 ios 值的一致 性?在缺省的按值组合机制下,真的没有好办法可以保证这一点。 在 C++中,通过使用虚继承解决这类问题。虚继承是一种机制,类通过虚继承指出它希望共享其虚基类的状态。 在虚继承下,对给定虚基类,无论该类在派生层次中作为虚基类出现多少次,只继承一个共享的基类子对象。共 享的基类子对象称为虚基类。在虚拟继承下,基类子对象的复制及由此而引起的二义性都被消除了。 istream 和 ostream 类对它们的基类进行虚继承。通过使基类成为虚基类,istream 和 ostream 指定,如 果其它类(比如 iostream)同时继承它们两个,则派生类中只出现它们公共基类的一个副本。通过在派生列表 中包含关键字 virtual 设置虚基类: class istream : public virtual ios{...}; class ostream : virtual public ios{...}; 102 / 182 class iostream : public istream, public ostream{...}; 为了讨论虚拟继承的语法和语义,我们选择用 Panda 类作为教学示例。在动物学领域中,人们对 Panda 属于 浣熊科(Raccoon )还是熊(Bear) 科,已经激烈争论了 100 多年。由于软件设计主要是一种服务性工业, 所以,我们最实际的解决方案是同时从两者派生: class Panda : public Bear,public Raccoon, public Endangered { ... }; 虚拟继承 Panda 层次结构如下图所示: 如果仔细查看上图,我们会注意到虚拟继承的不直观部分:虚拟派生(本例中的 Bear 和 Raccoon) 在先,实 际上应该在后。只有伴随着 Panda 的声明,虚拟继承才是必要的。但是,如果 Bear 和 Raccoon 还没有实现 虚拟派生,则 Panda 类的设计者就不走运了。 这是否意味着,我们应该尽可能地以虚拟方式派生我们的基类,以便层次结构中后续的派生类可能会需要虚拟继 承,是这样吗?不!我们强烈反对,那样做对性能的影响会很严重(而且增加了后续类派生的复杂性)。 实际上,中间基类指出其继承为虚继承的要求很少引起任何问题。通常,使用虚继承的类层次是一次性由一个人 或一个项目组设计的,独立开发的类很少需要其基类中的一个是虚基类,而且新基类的开发者不能改变已经存在 的层次。 虚拟基类声明 通过用关键字 virtual 修正一个基类的声明可以将它指定为被虚拟派生。例如,下列声明使得 ZooAnimal 成 为 Bear 和 Raccoon 的虚拟基类: // 关键字 public 和 virtual 的顺序不重要 class Bear : public virtual ZooAnimal { ... }; class Raccoon : virtual public ZooAnimal { ... }; 指定虚派生只影响从指定了虚基类的类派生的类。virtual 说明符陈述了在后代派生类中共享指定基类的单个 实例的愿望。 即使一个基类是虚拟的,我们仍然可以通过该基类类型的指针或引用,来操纵派生类的对象。例如,尽管 Panda 类将它的 ZooAnimal 部分作为虚基类继承,下面的 Panda 基类转换也可以正确执行: extern void dance( const Bear* ); extern void rummage( const Raccoon* ); extern ostream& operator<<( ostream&, const ZooAnimal& ); int main() 103 / 182 { Panda yin_yang; dance( &yin_yang ); //ok rummage( &yin_yang ); //ok cout << yin_yang; //ok // ... } 如果一个类可以被指定为基类,那么我们就可以将它指定为虚拟基类,而且它可以包含非虚拟基类支持的所有元 素。例如,下面是 ZooAnimal 类声明: #include #include class ZooAnimal; extern ostream& operator<<( ostream&, const ZooAnimal& ); class ZooAnimal { public: ZooAnimal( string name, bool onExhibit, string fam_name ) : _name( name ), _onExhibit( onExhibit), _fam_name( fam_name ) {} virtual ~ZooAnimal(); virtual ostream& print( ostream& ) const; string name() const { return _name; }; string family_name() const { return _fam_name; } // ... protected: bool _onExhibit; string _name; string _fam_name; // ... }; 直接派生类实例的声明和实现与非虚拟派生的情形相同,只是要用到关键字 virtual。例如,下面是 Bear 类 声明: class Bear : public virtual ZooAnimal { public: enum DanceType {two_left_feet, macarena, fandango, waltz }; Bear( string name, bool onExhibit=true ) : ZooAnimal( name, onExhibit, "Bear" ),_dance( two_left_feet ) {} virtual ostream& print( ostream& ) const; void dance( DanceType ); 104 / 182 // ... protected: DanceType _dance; // ... }; 类似地,下面是 Raccoon 类的声明: class Raccoon : public virtual ZooAnimal { public: Raccoon( string name, bool onExhibit=true ) : ZooAnimal( name, onExhibit, "Raccoon" ),_pettable( false ) {} virtual ostream& print( ostream&) const; bool pettable() const { return _pettable; } void pettable( bool petval ) { _pettable = petval; } // ... protected: bool _pettable; // ... }; 虚基类成员的可见性 使用虚基类的多重继承层次比没有虚继承的引起更少的二义性问题。 可以无二义性地直接访问共享虚基类中的成员。同样,如果只沿一个派生路径重定义来自虚基类的成员,则可以 直接访问该重定义成员。在非虚派生情况下,两种访问都可能是二义性的。 假定通过多个派生路径继承名为 X 的成员,有下面三种可能性: 1、如果在每个路径中 X 表示同一虚基类成员,则没有二义性,因为共享该成员的单个实例 2、如果在某个路径中 X 是虚基类的成员,而在另外一个路径中 X 是后代派生类的成员,也没有二义性,特定派 生类实例的优先级高于共享虚基类实例 3、如果沿每个继承路径 X 表示后代派生类的不同成员,则该成员的直接访问是二义性的。像非虚多重继承层次 一样,这种二义性最好用在派生类中提供覆盖实例的类来解决。 让我们重新定义 Bear 类,以提供它自己的 onExhibit()成员函数的实例(原来的 onExhibit()成员实例从 ZooAnimal 继承而来): bool Bear::onExhibit() { ... } 通过 Bear 类对象引用的 onExhibit()现在被解析为 Bear 的实例: Bear winnie( "a lover of honey" ); winnie.onExhibit(); // Bear::onExhibit() 通过 Raccoon 类对象引用的 Raccoon meeko( "a lover of all foods" ); Raccoon meeko( "a lover of all foods" ); 105 / 182 meeko.onExhibit(); // ZooAnimal::onExhibit() 派生类 Panda 从它的两个基类所继承而来的成员可被分为以下三类: 1 ZooAnimal 虚拟基类实例,如 name()和 family_name(),它们没有被 Bear 和 Raccoon 改写。 2 继承自 Raccoon、属于 ZooAnimal 虚拟基类的 onExhibit()实例,以及 Bear 定义的、被改写了的 onExhibit()实例。 3 继承自 ZooAnimal、分别被 Bear 和 Raccoon 特化了的 print()实例。 对于这些继承得到的成员,哪些可以在 Panda 类域中被直接地、无二义地访问?在虚拟派生下,第 1 项和第 2 项的所有成员都可以被直接地、无二义地访问。例如,已知 Panda 类对象: Panda spot( "Spottie" ); 下面的调用 spot.name(); 调用了共享的 ZooAnimal 虚拟基类成员函数 name()。而下面的调用: spot.onExhibit(); 调用了派生的 Bear 成员函数 onExhibit()。 如果在同一派生级别上有两个或多个基类重新定义了一个虚拟基类成员,则在派生类中,它们有相同的优先级。 例如,如果 Raccoon 也定义了一个 onExhibit()成员,则 Panda 需要用适当的类域操作符来限定修饰每个 访问: bool Panda::onExhibit(){ return Bear::onExhibit() && Raccoon::onExhibit() && ! _sleeping; } 特殊的初始化语义 通常,每个类只初始化自己的直接基类。在应用于虚基类的时候,这个初始化策略会失败。如果使用常规规则, 就可能会多次初始化虚基类。类将沿着包含该虚基类的每个继承路径初始化。在 ZooAnimal 示例中,使用常规 规则将导致 Bear 类和 Raccoon 类都试图初始化 Panda 对象的 ZooAnimal 类部分。 为了解决这个重复初始化问题,从具有虚基类的类继承的类对初始化进行特殊处理。在虚派生中,由最底层派生 类的构造函数初始化虚基类。在我们的例子中,当创建 Panda 对象的时候,只有 Panda 构造函数控制怎样初始 化 ZooAnimal 基类。 虽然由最底层派生类初始化虚基类,但是任何直接或间接继承虚基类的类一般也必须为该基类提供自己的初始化 式。只要可以创建虚基类派生类类型的独立对象,该类就必须初始化自己的虚基类。这些初始化式只在创建中间 类型的对象时使用。 在我们的层次中,可以有 Bear、Raccoon 或 Panda 类型的对象。创建 Panda 对象的时候,它是最低层派生 类型并控制共享的 ZooAnimal 基类的初始化;创建 Bear 对象(或 Raccoon 对象)的时候,不涉及更底层的派 生类型。在这种情况下,Bear(或 Raccoon)构造函数像平常一样直接初始化它们的 ZooAnimal 基类: Bear::Bear( std::string name, bool onExhibit ) :ZooAnimal( name, onExhibit, “Bear” ){} Raccoon::Raccoon(std::string name, bool onExhibit) :ZooAnimal( name, onExhibit, “Raccoon” ){} 106 / 182 虽然 ZooAnimal 不是 Panda 的直接基类,但是 Panda 的构造函数也初始化 ZooAnimal 基类: Panda::Panda( string name, bool onExhibit) : ZooAnimal( name, onExhibit, "Panda" ), Bear( name, onExhibit ), Raccoon( name, onExhibit ), Endangered(Endangered::critical), _sleeping(false){} 创建 Panda 对象的时候,这个构造函数初始化 Panda 对象的 ZooAnimal 部分。 如何构造虚继承的对象 让我们看看虚继承情况下怎样构造对象: Bear winnie(“pooh”); Raccoon meeko(“meeko”); Panda yolo(“yolo”); 当创建 Panda 对象的时候: 1、首先使用构造函数初始化列表中指定的初始化式构造 ZooAnimal 部分 2、接下来,构造 Bear 部分。忽略 Bear 的用于 ZooAnimal 构造函数初始化列表的初始化式 3、然后,构造 Raccoon 部分,再次忽略 ZooAnimal 初始化式 4、最后,构造 Panda 部分 如果 Panda 构造函数不显式初始化 ZooAnimal 基类,就使用 ZooAnimal 默认构造函数;如果 ZooAnimal 没有默认构造函数,则代码出错。当编译 Panda 构造函数的定义时,编译器将给出一个错误消息。 构造函数与析构函数顺序 无论虚拟基类出现在继承层次中的哪个位置上,它们都是在非虚拟基类之前被构造。例如,在下面这个有点古怪 的 TeddyBear 派生类中,有两个虚拟基类:直接的 ToyAnimal 实例,以及来自 Bear 的 ZooAnimal 实例: class Character { ... }; class BookCharacter : public Character { ... }; class ToyAnimal { ... }; class TeddyBear : public BookCharacter,public Bear, public virtual ToyAnimal { ... }; 层次结构如下图所示: 107 / 182 编译器按照直接基类在声明中的顺序,来检查虚拟基类的出现情况。在我们的例子中,BookCharacter 的继 承子树首先被检查,然后是 Bear,最后是 ToyAnimal。每个子树按深度优先的顺序被检查。即,查找从树根 类开始,然后向下移动。对于 BookCharacter 子树,先检查 Character,然后是 BookCharacter。对于 Bear 子树而言,则先检查 ZooAnimal,然后是 Bear。 在这个查找算法下,TeddyBear 的虚拟基类构造函数的调用顺序是,先 ZooAnimal,后跟 ToyAnimal。 一旦调用了虚拟基类的构造函数,则非虚拟基类构造函数就按照声明的顺序被调用:先是 BookCharater,然 后是 Bear。在 BookCharacter 构造函数执行之前,它的基类 Character 构造函数先被调用。已知声明: TeddyBear Paddington; 基类构造函数的调用顺序如下: ZooAnimal(); // Bear 的虚拟基类 ToyAnimal(); // 直接虚拟基类 Character(); // BookCharacter 的非虚拟基类 BookCharacter(); // 直接非虚拟基类 Bear(); // 直接非虚拟基类 TeddyBear(); // 最终派生类 这里初始化 ZooAnimal 和 ToyAnimal 是 TeddyBear 的责任,因为它是 Paddington 类对象的最终派生 类。 在合成复制构造函数中使用同样的构造次序,在合成赋值操作符中也是按这个次序给基类赋值。保证调用基类析 构函数的次序与构造函数的调用次序相反。 108 / 182 嵌套类 一个类可以在另一个类中定义,这样的类被称为嵌套类。嵌套类是其外围类的一个成员。嵌套类的定义可以出现 在其外围类的公有、私有或保护区域中。 嵌套类是独立的类,基本上与它们的外围类不相关,因此,外围类和嵌套类的对象是相互独立的。嵌套类型的对 象没有包含外围类所定义的成员,同样,外围类的对象也没有包括嵌套类所定义的成员。 像任何其它类一样,嵌套类使用访问标号控制对自己成员的访问。成员可以声明为 public、priate 和 protected。外围类对嵌套类的成员没有特殊访问权,并且嵌套类对其外围类的成员也没有特殊访问权。 嵌套类定义了其外围类中的一个类型成员。像任何其他成员一样,外围类决定对这个类型的访问。在外围类的 public 部分定义的嵌套类可在任何地方使用的类型;在外围类的 protected 部分定义的嵌套类定义了只能由 外围类、友元或派生类访问的类型;在外围类的 private 部分定义的嵌套类定义了只能被外围类或其友元访问 的类型。 嵌套类的名字不会与外围域中声明的相同名字冲突。例如: class Node { /* ... */ }; class Tree { public: // Node 被封装在 Tree 的域中,在类域中 Tree::Node 隐藏了 ::Node class Node {...}; // ok: 被解析为嵌套类: Tree::Node Node *tree; }; // Tree::Node 在全局域中不可见,Node 被解析为全局的 Node 声明 Node *pnode; class List { public: // Node 被封装在 List 的域中,在类域 List::Node 中隐藏了 ::Node class Node {...}; // ok: 解析为: List::Node Node *list; }; 与非嵌套类一样,嵌套类可以有与自身同样类型的成员: // Not ideal configuration: evolving class definition class List { public: class ListItem { friend class List; // 友元声明 ListItem( int val = 0 ); // 构造函数 ListItem *next; // 指向自己类的指针 109 / 182 int value; }; // ... private: ListItem *list; ListItem *at_end; }; 私有成员是指这样的成员,它只能在该类的成员或友元定义中被访问。除非外围类被声明为嵌套类的友元,否则 它没有权利访问嵌套类的私有成员。这就是为什么 ListItem 把 List 声明为友元的原因:为了允许类 List 的成员定义访问 ListItem 的私有成员。嵌套类也没有任何特权访问其外围类的私有成员。如果我们想授权 ListItem 允许它访问类 List 的私有成员,那么该外围类 List 必须把嵌套类 ListItem 声明为友元。在 前面的例子中 ListItem 不是 List 的友元,所以它不能引用 List 的私有成员。 把类 ListItem 声明为 List 类的公有成员意味着,该嵌套类可以在整个程序中(在类 List 的友元和成员定 义之外)用作类型。例如: // ok: 全局域中的声明 List::ListItem *headptr; 这超出了我们的本意。嵌套类 ListItem 支持 List 类的抽象,我们不希望让 ListItem 类型在整个程序中都 可以被访问。那么,比较好的设计是把 ListItem 嵌套类定义为类 List 的私有成员: class List { public: // ... private: class ListItem { // ... }; ListItem *list; ListItem *at_end; }; 现在,只有 List 的成员和友元的定义可以访问类型 ListItem 。把类 ListItem 的所有成员都声明为公有 的也不再有任何坏处。因为 ListItem 类是 List 的私有成员,所以只有 List 类的友元和成员可以访问 ListItem 的成员。有了这个新的设计,我们就不再需要友元声明了。下面是类 List 的新定义: class List { public: // ... private: // 现在 ListItem 是一个私有的嵌套类型 class ListItem { // 它的成员都是公有的 public: ListItem( int val = 0 ); 110 / 182 ListItem *next; int value; }; ListItem *list; ListItem *at_end; }; 当我们没有在嵌套类体内以 inline 形式定义嵌套类的成员函数时,我们就必须在最外围的类之外定义这些成 员函数。下面是正确的定义方式: // 用外围类名限定修饰嵌套类名 List::ListItem::ListItem( int val ) { value = val; next = 0; } 注意,只有嵌套类名是限定修饰的。第一个限定修饰符 List::指外围类,它限定修饰其后的名字——嵌套类 ListItem。第二个 ListItem 是指构造函数而不是嵌套类。 如果 ListItem 已经声明了一个静态成员,那么它的定义也要放在全局域中。在这样的定义中,静态成员名看 起来如下所示: int List::ListItem::static_mem = 1024; 注意,对于成员函数和静态数据成员而言,不一定只有嵌套类的公有成员,才能在类定义之外被定义。类 ListItem 的私有成员也可以被定义在全局域中。 嵌套类也可以被定义在其外围类之外。例如,Lisiltem 的定义也可以在全局域中被给出,如下: class List { public: // ... private: // 这个声明是必需的 class ListItem; ListItem *list; ListItem *at_end; }; // 用外围类名限定修饰嵌套类名 class List::ListItem { public: ListItem( int val = 0 ); ListItem *next; int value; }; 在全局定义中,嵌套类 ListItem 的名字必须由其外围类 List 的名字限定修饰。注意,在类 List 体内的 ListItem 的声明不能省略。如果嵌套类没有先被声明为其外围类的一个成员,则全局域中的定义不能被指定 为嵌套类。在全局域中定义的嵌套类不一定非得是其外围类的公有成员,也可以是其它类型的成员。 111 / 182 在嵌套类的定义被看到之前,我们只能声明嵌套类的指针和引用。即使类 ListItem 是在全局域中被定义的, List 的数据成员 list 和 at_end 仍然是有效的,因为这两个成员都是指针。如果这两个成员中有一个是对 象而不是指针,那么类 List 的成员声明将会引发一个编译错误。例如: class List { public: // ... private: // 这个声明是必需的 class ListItem; ListItem *list; ListItem at_end; // 错误: 未定义嵌套类 ListItem }; 为什么会希望在类定义之外定义嵌套类呢?或许嵌套类支持外围类的实现细节,我们不想让 List 类的用户看到 ListItem 的细节。因此,我们不愿把嵌套类的定义放在含有 List 类接口的头文件中。于是,我们只能在含 有 List 类及其成员实现的文本文件中给出嵌套类 ListItem 的定义。 嵌套类可以先被声明,然后再在外围类体中被定义。这允许多个嵌套类具有互相引用的成员,例如: class List { public: // ... private: // List::ListItem 的声明 class ListItem; class Ref { ListItem *pli; // pli 类型为: List::ListItem* }; // List::ListItem 的定义 class ListItem { Ref *pref; // pref 的类型为: List::Ref* }; }; 如果类 ListItem 没有在类 Ref 之前先被声明,那么成员 pli 的声明就是错的,因为名字 ListItem 没有 被声明。 嵌套类不能直接访问其外围类的非静态成员,即使这些成员是公有的,任何对外围类的非静态成员的访问都要求 通过外围类的指针、引用或对象来完成。例如: class List { public: int init( int ); private: class ListItem { 112 / 182 public: ListItem( int val = 0 ); void mf( const List & ); int value; int memb; }; }; List::ListItem::ListItem( int val ) { // List::init() 是类 List 的非静态成员 // 必须通过 List 类型的对象或指针来使用 value = init( val ); // 错误: 非法使用 init } 使用类的非静态成员时,编译器必须能够识别出非静态成员属于哪个对象。在类 ListItem 的成员函数中,this 指针只能被隐式地应用在类 ListItem 的成员上,而不是外围类的成员上。由于隐式的 this 指针,我们知道 数据成员 value 指向被凋用构造函数的对象。在 ListItem 的构造函数中的 this 指针的类型是 ListItem*。 而要访问成员 init()所需的是 List 类型的对象或 List*类型的指针。 下面是成员函数 mf()通用引用参数引用 init()。从这里我们能够知道,成员 init()是针对函数实参指定的 对象而被调用的: void List::ListItem::mf( const List &il ) { memb = il.init(); // ok: 通过引用调用 init() } 尽管访问外围类的非静态数据成员需要通过对象、指针或引用才能完成,但是嵌套类可以直接访问外围类的静态 成员、类型名、枚举值(假定这些成员是公有的)。类型名是一个 typedef 名字、枚举类型名、或是一个类名。 例如: class List { public: typedef int (*pFunc)(); enum ListStatus { Good, Empty, Corrupted }; // ... private: class ListItem { public: void check_status(); ListStatus status; // ok pFunc action; // ok // ... }; // ... }; 113 / 182 pFunc 和 ListStatus 都是外围类 List 的域内部的嵌套类型名。这两个名字以及 ListStatus 的枚举值都 可以被用在 ListItem 的域中,这些成员可以不加限定修饰地被引用: void List::ListItem::check_status() { ListStatus s = status; switch ( s ) { case Empty: ... case Corrupted: ... case Good: ... } } 在 ListItem 的域之外,以及在外围类 List 域之外引用外围类的静态成员、类型名和枚举名都要求域解析操 作符,例如: List::pFunc myAction; // ok List::ListStatus stat = List::Empty; // ok 当引用一个枚举值时,我们不能写: List::ListStatus::Empty 这是因为枚举值可以在定义枚举的域内被直接访问。为什么?因为枚举定义并不像类定义一样维护了自己相关的 域。 在嵌套类域中的名字解析 让我们来看看在嵌套类的定义,及其成员定义中的名字解析是怎样进行的。被用在嵌套类的定义中的名字(除了 inline 成员函数定义中的名字和缺省实参的名字之外)其解析过程如下: 1、考虑出现在名字使用点之前的嵌套类的成员声明。 2、如果第 1 步没有成功,则考虑出现在名字使用点之前的外围类的成员声明。 3、如果第 2 步没有成功,则考虑出现在嵌套类定义之前的名字空间域中的声明。 例如: enum ListStatus { Good, Empty, Corrupted }; class List { public: // ... private: class ListItem { public: // 查找: // 1) 在 List::ListItem 中 // 2) 在 List 中 // 3) 在全局域中 ListStatus status; // 引用全局枚举 // ... 114 / 182 }; // ... }; 编译器首先在类 ListItem 的域中查找 ListStatus 的声明。因为没有找到成员声明,所以编译器接着在类 List 的域中查找 ListStatus 的声明。因为在 List 类中也没有找到声明,于是编译器在全局域中查找 ListStatus 的声明。在这三个域中,只有位于 ListStatus 使用点之前的声明才会被编译器考虑。编译器找 到了全局枚举 ListStatus 的声明,它是被用在 Status 声明中的类型。 如果在全局域中,在外围域 List 之外定义嵌套类 ListItem,则 List 类的所有成员都已经被声明完毕,因 而编译器将考虑其所有声明: class List { private: class ListItem; // ... public: enum ListStatus { Good, Empty, Corrupted }; // ... }; class List::ListItem { public: // 查找: // 1) 在 List::ListItem 中 // 2) 在 List 中 // 3) 在全局域中 ListStatus status; // List::ListStatus // ... }; ListItem 的名字解析过程首先在类 ListItem 的域中开始查找。因为没有找到成员声明,所以编译器在类 List 的域内查找 ListStatus 的声明。因为类 List 的完整定义都已经能够看得到,所以这一步查找考虑 List 的所有成员。于是找到 List 中嵌套的 enumListStatus, 尽管它是在 ListItem 之后被声明的。 status 是 List 的 ListStatus 类型的一个枚举对象。如果 List 没有名为 ListStatus 的成员,则名字 查找过程会在全局域中。在嵌套类 ListItem 定义之前查找声明。 被用在嵌套类的成员函数定义中的名字其解析过程如下: 1、首先考虑在成员函数局部域中的声明。 2、如果第 1 步没有成功,则考虑所有嵌套类成员的声明。 3、如果第 2 步没有成功,则考虑所有外围类成员的声明。 4、如果第 3 步没有成功,则考虑在成员函数定义之前的名字空间域中出现的声明。 在下面的代码段中,成员函数 check_status()定义中的 list 引用了哪个声明? class List { public: 115 / 182 enum ListStatus { Good, Empty, Corrupted }; // ... private: class ListItem { public: void check_status(); ListStatus status; // ok // ... }; ListItem *list; // ... }; int list = 0; void List::ListItem::check_status() { int value = list; // 哪个 list? } 很有可能程序员想让 check_status()中的 List 引用全局对象: 1、value 和全局对象 List 的类型都是 int。List::list 成员是指针类型,在没有显式转换的情况它不能 被赋给 value。 2、不允许 ListItem 访问其外围类的私有数据成员,如 List。 3、list 是一个非静态数据成员,在 ListItem 的成员函数中必须通过对象、指针或引用来访问它。 但是,尽管有这些原因,在成员 check_status()中用到的名字 List 仍被解析为类 List 的数据成员 list。 记住,如果在嵌套类 ListItem 的域中没有找到该名字;则在查找全局域之前,下一个要查找的是其外围类的 域。外围类 List 的成员 list 隐藏了全局域中的对象。于是产生一个错误消息,因为在 check_status() 中使用指针 list 是无效的。 只有在名字解析成功之后,编译器才会检查访问许可和类型兼容性。如果名字的用法本身就是错误的,则名字解 析过程将不会再去查找更适合于该名字用法的声明,而是产生一个错误消息。 为了访问全局对象 list, 必须使用全局域解析操作符: void List::ListItem:: check_status() { value = ::list; // ok } 如果成员函数 check_status()被定义成位于 ListItem 类体中的内联函数,则上面所讲到的最后一步修改 会使编译器产生一个错误消息,报告说全局域中的 list 没有被声明。 class List { public: // ... private: class ListItem { public: 116 / 182 // 错误: 没有可见的 ::list 声明 void check_status() { int value = ::list; } // ... }; ListItem *list; // ... }; int list = 0; 全局对象 list 是在类 List 定义之后被声明的,对于在类体中内联定义的成员函数,只考虑在外围类定义之 前可见的全局声明。如果 check_status()的定义出现在List 的定义之后,则编译器考虑在check_status() 定义之前可见的全局声明,于是找到对象 list 的全局声明。 参考资料: 《C++ PRIMER》( 第 4 版·特别版) P658~662 117 / 182 局部类 类也可以定义在函数体内,这样的类被称为局部类。局部类只在定义它的局部域内可见。与嵌套类不同的是,在 定义该类的局部域外没有语法能够引用局部类的成员。因此,局部类的成员函数必须被定义在类定义中。在实际 中,这就把局部类的成员函数的复杂性限制在几行代码中。否则,对读者来说,代码将变得很难理解。 因为没有语法能够在名字空间域内定义局部类的成员,所以也不允许局部类声明静态数据成员。 在局部类中嵌套的类可以在其类定义之外被定义。但是,该定义必须出现在包含外围局部类定义的局部域内。在 局部域定义中的嵌套类的名字必须由其外围类名限定修饰。在外围类中,该嵌套类的声明不能被省略,例如: void foo( int val ) { class Bar { public: int barVal; class nested; // 嵌套类的声明是必需的 }; // 嵌套类定义 class Bar::nested { // ... }; } 外围函数没有特权访问局部类的私有成员。当然,这可以通过使外围函数成为局部类的友元来实现。但是,看起 来,局部类几乎从不需要私有成员。能够访问局部类的程序部分只有很少的一部分。局部类被封装在它的局部域 中,通过信息隐藏进一步封装好像有点太过了。在实际中,很难找到一个理由不把局部类的所有成员都声明为公 有的。 同嵌套类一样,局部类可以访问的外围域中的名字也是有限的。局部类只能访问在外围局部域中定义的类型名、 静态变量以及枚举成员,不能使用定义该类的函数中的变量,例如: int a, val; void foo( int val ) { static int si; enum Loc { a = 1024, b }; class Bar { public: Loc locVal; // ok; int barVal; void fooBar( Loc l = a ) { // ok: Loc::a barVal = val; // 错误: 局部对象 barVal = ::val; // OK: 全局对象 118 / 182 barVal = si; // ok: 静态局部对象 locVal = b; // ok: 枚举值 } }; // ... } 在局部类体内(不包括成员函数定义中的)的名字解析过程是:在外围域中查找出现在局部类定义之前的声明。 在局部类的成员函数体内的名字的解析过程是:在查找外围域之前,首先直找该类的完整域。 还是一样,如果先找到的声明使该名字的用法无效,则不考虑其他声明。即使在 fooBar()中使用 val 是错的, 编译器也不会找到全局变量 val,除非用全局域解析操作符限定修饰 val。 参考资料: 《C++ PRIMER》( 第 4 版·特别版) P665~666 《深度探索 C++对象模型》 P 119 / 182 RTTI 运行时类型识别 基本概念 RTTI(运行时类型识别)允许“用指向基类的指针或引用来操纵对象”的程序能够获取到“这些指针或引用所 指对象“的实际派生类型。在 c++中,为了支持 RTTI,提供了两个操作符: 1、dynamic_cast 操作符,它允许在运行时刻进行类型转换,从而使程序能够在一个类层次结构中安全地转 换类型,把基类指针转换成派生类指针,或把指向基类的左值转换成派生类的引用,当然只有在保证转换能够成 功的情况下才可以。 2、typeid 操作符,它指出指针或引用指向的对象的实际派生类型。 这些操作符只为带有一个或多个虚函数的类返回动态类型信息,对于其他类型,返回静态(既编译时)类型的信 息。对于带虚函数的类,在运行时执行 RTTI 操作符;对于其他类型,在编译时计算 RTTI 操作符。 当具有基类的引用或指针,但需要执行不是基类组成部分的派生类操作的时候,需要动态的强制类型转换。通常, 从基类指针获得派生类行为最好的方法是通过虚函数。当使用虚函数的时候,编译器自动根据对象的实际类型选 择正确的函数。 但是,在某些情况下,不可能使用虚函数。在这些情况下,RTTI 提供了可选的机制。然而,这种机制比使用虚 函数更加容易出错:程序员必须知道应该将对象强制转换为哪种类型,并且必须检查转换是否成功执行了。 dynamic_cast 操作符 在进一步详细了解 dynamic_cast 的行为之前,我们先来了解为什么在 C++程序中用户需要使用 dynamic_cast。 假设我们的程序用类库来表示公司中不同的雇员。这个层次结构中的类都支持某些成员函数, 以计算公司的薪金,例如: class employee { public: virtual int salary(); }; class manager : public employee { public: int salary(); }; class programmer : public employee { public: int salary(); }; void company::payroll( employee *pe ) { // 使用 pe->salary() } 我们的公司有不同类型的雇员。company 成员函数 payroll()的参数是指向 employee(雇员)类的指针, 120 / 182 它可能指向 manager(经理)类,也可能指向 programmer(程序员)类。因为 payroll()调用虚拟函数 salary(),所以,根据 pe 指向的雇员的类型,它分别调用 manager 或 programer 类对应的函数。 假设,employee 类不再能满足我们的需要,我们想要修改它。希望增加一个名为 bonus()的成员函数,在计 算公司的薪金时,能与成员函数 salary()一起被使用。则可以通过向 employee 层次结构中的类增加一个虚 拟成员函数来实现。例如: class employee { public: virtual int salary(); virtual int bonus(); }; class manager : public employee { public: int salary(); }; class programmer : public employee { public: int salary(); int bonus(); }; void company::payroll( employee *pe ) { // 使用 pe->salary() 和 pe->bonus() } 如果 payroll()的参数 pe 指向一个 manager 类型的对象,则调用基类 employee 中定义的虚拟函数 bonus(), 因为 manager 类型的对象没有改写 employee 类中定义的虚拟函数 bonus()。如 果 payroll() 的参数 pe 指向一个 programmer 类型的对象,则调用在 programmer 类中定义的虚拟成员函数 bonus()。 为类层次结构增加虚拟函数时,我们必需重新编译类层次结构中的所有类成员函数。如果我们能够访问类 employee、manager 和 programmer 的成员函数的源代码,那么,就可以增加虚拟函数 bonus()。但这样 做并不总是可能的。如果前面的类层次结构是由第三方库提供商提供的,那么,该提供商可能只提供库中定义类 接口的头文件以及库的目标文件。但他们可能不会提供类成员函数的源代码。在这种情况下,重新编译该层次结 构下的类成员函数是不可能的。 如果我们希望扩展这个类库,则不能增加虚拟成员函数。但我们可能仍然希望增加功能。在这种情况下,就必需 使用 dynamic_cast。dynamic_cast 操作符可用来获得派生类的指针,以便使用派生类的某些细节,这些细 节没有其他办法能够得到。例如,假设我们通过向 programmer 类增加 bonus()成员函数来扩展这个库。我 们可以在 programmer 类定义(在头文件中给出)中增加这个成员函数的声明,并在我们自己的程序文本文件 中定义这个新的成员用数: class employee { public: virtual int salary(); 121 / 182 }; class manager : public employee { public: int salary(); }; class programmer : public employee { public: int salary(); int bonus(); }; 记住,函数 payroll()接收一个指向基类 employee 的指针作为参数。我们可以用 dynamic_cast 操作符 获得派生类 programmer 的指针并用这个指针,调用成员函数 bonus(),如下所示: void company::payroll( employee *pe ) { programmer *pm = dynamic_cast< programmer* >( pe ); // 如果 pe 指向 programmer 类型的一个对象,则 dynamic_cast 成功 // 并且 pm 指向 programmer 对象的开始 if ( pm ) { // 用 pm 调用 programmer::bonus() // 如果 pe 不是指向 programmmer 类型的一个对象,则 dynamic_cast 失败 // 并且 pm 的值为 0 } else { // 使用 employee 的成员函数 } } dynamic_cast 转换 dynamic_cast 支持运行时识别指针或引用所指向的对象。可以使用 dynamic_cast 操作符将基类类型对象 的指针或引用转换为同一继承层次中其它类型的指针或引用。与其他强制类型转换不同,dynamic_cast 涉及 到运行时类型检查。如果绑定到指针或引用上的对象不是目标类型的对象,则转换失败。如果转换到指针类型的 dynamic_cast 失败 ,则 dynamic_cast 的结果是 0 值;如果转换到引用类型的 dynamic_cast 失败,则抛 出一个 bad_cast 类型的异常。 指针的转换: 假设 Base 是至少带有一个虚函数的类,Derived 派生于 Base 类,basePtr 是一个指向 Base 的对象的指针 if( Derived *derivedPtr = dynamic_cast(basePtr) ) { //如果 basePtr 实际指向 Derived 对象,则转换成功,derivedPtr 将初始化为指向 basePtr 所指的 Derived 对象 }else{ //否则,derivedPtr 设置为 0,if 条件判断不成立 } 122 / 182 引用的转换: 因为不存在空引用,所以不可能对引用使用用于指针强制类型转换的检查策略,相反,当转换失败的时候,它抛 出一个 std::bad_cast 异常。重写上面的例子如下: void f( const Base &b ) { try{ const Derived &d = dynamic_cast< const Derived &>(b); }catch(bad_cast){ //... } } typeid 操作符 为 RTTI 提供的第二个操作符是 typeid 操作符。typeid 操作符可以与任何类型的表达式一起使用。内置类型 的表达式以及常量都可以用作 typeid 操作符的操作数。如果操作数不是类类型或者是没有虚函数的类,则 typeid 操作符指出操作数的静态类型;如果操作数是定义了至少一个虚函数的类类型,则返回动态类型信息。 typeid 操作符的结果是名为 type_info 的标准库类型的对象引用。 typeid 最常见的用途是比较两个表达式的类型,或者将表达式的类型与特定类型相比较: Base *bp; Derived *dp; //在运行时比较两个对象 if( typeid(*bp) == typeid(*dp) ){...} //测试表达式的类型是否是特定类型 if(typeid(*bp) == typeid(Derived) ){...} ... 内置类型的表达式和常量可以被用作 typeid 的操作数。当操作数不是类类型时,typeid 操作符会指出操作 数的类型: int iobj; cout << typeid( iobj ).name() << endl; // 打印: int cout << typeid( 8.16 ).name() << endl; // 打印: double 当 typeid 操作符的操作数是类类型,但不是带有虚拟函数的类类型时,typeid 操作符会指出操作数的类型, 而不是底层对象的类型: class Base { /* 没有虚拟函数 */ }; class Derived : public Base { /* 没有虚拟函数 */ }; Derived dobj; Base *pb = &dobj; cout << typeid( *pb ).name() << endl; // 打印: Base typeid 操作符的操作数是 Base 类型的,即表达式*pb 的类型。因为 Base 不是一个带有虚拟函数的类类型, 123 / 182 所以 typeid 的结果指出,表达式的类型是 Base,尽管 pb 指向的底层对象的类型是 Derived。 可以对 typeid 的结果进行比较,例如: #include employee *pe = new manager; employee& re = *pe; if ( typeid( pe ) == typeid( employee* ) ) // true // do something /* if ( typeid( pe ) == typeid( manager* ) ) // false if ( typeid( pe ) == typeid( employee ) ) // false if ( typeid( pe ) == typeid( manager ) ) // false */ if 语句的条件子句比较“在一个表达式上应用 typeid 操作符的结果“和”用在类型名操作数上的 typeid 操 作符的结果“。注意比较: typeid( pe ) == typeid( employee* ) 的结果为 true。这使得习惯写: // 调用虚拟函数 pe->salary(); 的用户有些吃惊,它导致调用 manager 派生类的函数 salary()。typeid(pe)与虚拟函数调用机制不同。这 是因为操作数 pe 是一个指针,而不是一个类类型。为了要获取到派生类类型,typeid 的操作数必须是一个类 类型(带有虚拟函数)。表达式 typeid(pe)指出 pe 的类型,即指向 employee 的指针。它与表达式 typeid(employee*)相等,而其他比较的结果都是 false。 当表达式*pe 被用在 typeid 上时,结果指出 pe 实际指向的对象的类型: typeid( *pe ) == typeid( manager ) // true typeid( *pe ) == typeid( employee ) // false 在这两个比较中,因为*pe 是一个类类型的表达式,该类带有虚拟函数,所以 typeid 的结果指出操作数所指 的实际对象的类型,即 manager。 typeid 操作符也可以被用在引用上,例如: typeid( re ) == typeid( manager ) // true typeid( re ) == typeid( employee ) // false typeid( &re ) == typeid( employee* ) // true typeid( &re ) == typeid( manager* ) // false 在前两个比较中,操作数 re 是带有虚拟函数的类类型,因此 typeid 操作数的结果指出 re 指向的底层对象的 类型。在后两个比较中,操作数&re 是一个类型指针。因此 typeid 操作符的结果指出操作数的类型,即 employee*。 typeid 操作符实际上返回一个类型为 type_info 的类对象。type_info 类类型被定义在头文件 中,它的类接口描述了我们可以对 typeid 操作符的结果做什么操作。我们将在下一小节看到这 个接口。 type_info 类 124 / 182 type_info 类的确切定义是与编译器实现相关的,但是这个类的某些特性对每个 C++程序却都是相同的: class type_info { // 依赖于编译器的实现 private: type_info( const type_info& ); type_info& operator=( const type_info& ); public: virtual ~type_info(); int operator==( const type_info& ) const; int operator!=( const type_info& ) const; const char * name() const; }; 因为 type_info 类的拷贝构造函数和赋值赋值操作符都是私有成员,所以用户不能在自己的程序中定义 type_info 对象,例如: #include // 错误: 没有缺省构造函数 type_info t1; // 错误: 拷贝构造函数是 private 的 type_info t2 ( typeid( unsigned int ) ); 在程序中创建 type_info 对象的惟一途径是使用 typeid 操作符。 该类还有重载的比较操作符。这些操作符允许比较两个 type_info 对象,因此允许比较“用 typeid 操作符 获得的结果“,如上节所示: typeid( re ) == typeid( manager ) // true typeid( *pe ) != typeid( employee ) // false 函数 name()返回一个 C 风格字符串,它是 type_info 对象所表示的类型的名字。该函数可以被用在我们的 程序中,如下所示: #include int main() { employee *pe = new manager; // 输出: "manager" cout << typeid( *pe ).name() << endl; } name 函数为 type_info 对象所表示的类型的名字返回 C 风格的字符串。返回字符串的格式取决于编译器,无 须与程序中使用的类型名字完全匹配。对 name 返回值的唯一保证是,它为每个类型返回唯一的字符串。虽然如 此,仍然可以使用 name 成员来显示 type_info 对象的名字,下面是相关的一个例子程序: #include class Base{ int ibase; }; class Derived : public Base{ 125 / 182 int iDerived; }; int main(int argc, char *argv[]) { int iobj; std::cout<< typeid(8.16).name()<(expression); 其中 cast-name 为 const_cast、static_cast、dynamic_cast、reinterpret_cast 之一,type 为转换的目标类型, expression 则是被强制转换的值。强制转换的类型指定了在 expression 上执行某种特定类型的转换。 const_cast const_cast,转换掉表达式的 const 性质。例如,假设有函数 string_copy,我们对其唯一的 char*类型的参数 只读不写。在访问该函数时,最好的选择是修改它让它接受 const char*类型的参数。如果不行,可以通过 const_cast 用一个 const 值调用 string_copy 函数: const char *pc_str; char *pc = string_copy( const_cast(pc_str) ); 这 4 种强制类型转换,只有 const_cast 才能将 const 性质转换掉。 static_cast 编译器隐式执行的任何类型转换都可以用 static_cast 显式完成: double b = 97.0; char ch = static_cast(b); 当需要将一个较大的算数类型赋值给较小的类型时,使用强制类型非常有用。 dynamic_cast 《C++PRIMER》第四版 P647 dynamic_cast 支持运行时识别指针或引用所指向的对象。可以使用 dynamic_cast 操作符将基类类型对象的指 针或引用转换为同一继承层次中其它类型的指针或引用。与其他强制类型转换不同,dynamic_cast 涉及到运行 时类型检查。如果绑定到指针或引用上的对象不是目标类型的对象,则转换失败。如果转换到指针类型的 dynamic_cast 失败,则 dynamic_cast 的结果是 0 值;如果转换到引用类型的 dynamic_cast 失败,则抛出一 个 bad_cast 类型的异常。 指针的转换: 假设 Base 是至少带有一个虚函数的类,Derived 派生于 Base 类,basePtr 是一个指向 Base 的对象的指针 if( Derived *derivedPtr = dynamic_cast(basePtr) ) { //如果 basePtr 实际指向 Derived 对象,则转换成功,derivedPtr 将初始化为指向 basePtr 所指的 Derived 对象 }else{ //否则,derivedPtr 设置为 0,if 条件判断不成立 } 127 / 182 引用的转换: 因为不存在空引用,所以不可能对引用使用用于指针强制类型转换的检查策略,相反,当转换失败的时候,它抛 出一个 std::bad_cast 异常。重写上面的例子如下: void f( const Base &b ) { try{ const Derived &d = dynamic_cast< const Derived &>(b); }catch(bad_cast){ //... } } reinterpret_cast reinterpret_cast 通常为操作数的位模式提供较低层次的重新解释。操作符修改了操作数类型,但仅仅是重新解释 了给出的对象的比特模型而没有进行二进制转换。reinterpret_cast 本质上依赖于机器。为了安全地使用 reinterpret_cast,要求程序员完全理解转换所涉及到的数据类型,以及编译器实现强制类型转换的细节。 表达式 reinterpret_cast(expression ) 能够用于诸如 char* 到 int* ,或者 One_class* 到 Unrelated_class*等类似这样的转换,因此可能是不安全的。 尽量少做转型动作《Effective C++》第三版条款 27 参考资料: 《C++ PRIMER》( 第 4 版·特别版) P158~160 《C++ PRIMER》( 第 4 版·特别版) P647~648 128 / 182 C++中对象的大小《深度探索 C++对象模型》 需要多少内存才能表现一个 class object?一般而言要有:  其非静态数据成员的总和大小;  加上任何由于字节对齐需要而填充上去的空间(可能存在与 members 之间,也可能存在于集合体边界);  加上为了支持 virtual 而由内部产生的任何额外负担。 C++中实际对象的大小,不同编译器的实现是不一样的,以下的讨论基于 virtual studio 2008,对于其他编译 的可能出现的结果也做了分析和猜测。在反推不同编译器实现的 C++对象的大小时,对齐是一个很重要也容易 被遗忘的问题。 class A{}; 看上去一个空的类 A 事实上并不是空的,它有一个隐含的 1byte,那是被编译器安插进去的一个 char,这 使得 这个 class 的两个 objects 得以在内存中配置独一无二的地址: A a,b; If ( &a == &b ) cerr<<”yipes!”< using std::cout; using std::endl; class A{}; class B:public virtual A{}; class C:public virtual A{}; class D:public B,public C{}; class E { int i; }; class F { double d; }; class G { double num; char in; 131 / 182 }; class H { int num; double in; }; class I { int num; double in; public: virtual ~I(){}; }; class J { double num; int in; public: virtual ~J(){}; }; class K { int i; int k; public: virtual ~K(){}; }; class L { int i; int j; L(){}; public: float ll(int i) { return 0.0; } static double hhh(int i) { return 0.0; } virtual ~L(){}; virtual void ji(){}; }; int main() { cout <<"A "< *freeList; float y; static const int chunkSize = 250; float z; } 非静态数据成员在 class object 中的排列顺序将和其被声明的顺序一样,任何中间介入的静态数据成员如 freeList 和 chunkSize 都不会被放进对象布局中。在上述例子中,每一个 Point3d 对象由三个 float 组 成,次序是 x、y、z。静态数据成员存放在程序的 data segment 中,和个别的 class object 无关。 C++标准要求,在同一个 access section(也就是 private、public、protected 等区段)中,members 的排列只须符合“较晚出现的 members 在 class object 中有较高的地址”这一条即可。也就是说,各个 members 并不一定得连续排列。什么东西可能会介于被声明的 members 之间呢?比如说 members 的边界调整 时需要填充的一些字节等等。 同时,编译器还可能会合成一些内部使用的 data members,以支持整个对象模型。vptr 就是这样的东西, 当前所有的编译器都把它安插在每一个“内含 virtual function 的 class”的 object 内。vptr 会被放 在什么位置呢?传统上它被放在所有明确声明的 members 的最后,不过如今也有一些编译器把 vptr 放在 class object 的最前端。C++ standard 允许编译器把这些内部产生出来的 members 自由放在任何位置上。 C++标准也允许编译器将多个 access sections之中的 data members 自由排列,不必在乎它们出现在 class 声明中的次序。也就是说,下面这样的声明中: class Point3d{ public: //… private: float x; static List *freeList; private: float y; static const int chunkSize = 250; private: float z; } 其 class object 的大小和组成和我们先前声明的那个相同,但是 members 的排列次序则视编译器而定。编 134 / 182 译器可以随意把 y 或 z 或其它什么东西放在第一个,不过大部分的编译器都没有这样做。当前各家编译器都是 把一个以上的 access sections 连锁在一起,依照声明次序,成为一个连续的区块。access sections 的 多少,不会招来额外的负担。例如,在一个 section 中声明 8 个 members,或是在 8 个 sections 中总共声 明 8 个 members,得到的 object 大小是一样的。 静态数据成员的存取 Static data members,按照其字面意思,被编译器提出到 class 之外,并被视为一个 global 变量(但只 在 class 生命范围之内可见)。每一个 member 的存取许可(private 或 protected 或 public),以及与 class 的关联,并不会导致任何空间上或执行时间上的额外负担。 每一个 static data member 只有一个实体,存放在程序的 data segment 之中。每次程序取用 static data member,就会被内部转化为对该唯一的 extern 实体的直接操作: //origin.chunkSize = 250; Point3d::chunkSize = 250; //pt->chunkSize = 250; Point3d::chunkSize = 250; 从指令执行的观点来看,这是 C++语言中“通过一个指针和通过一个对象来存取 member,结论完全相同”的唯 一一种情况。这是因为“经由 member selection operators 对一个 static data member 进行存取操 作“只是语法上都一种便宜行事而已。member 其实不在 class object 之中,因此存取 static members 并不需要通过 class object。 如果 chunkSize 是从一个复杂继承关系中继承而来都 member,又当如何呢?或许它是一个“virtual base class 的 virtual base class“(或其它同等复杂的继承结构)的 member 也说不定。即使这样的情况, 也是无关紧要的,程序之中对于 static members 还是只有唯一的一个实体,而其存取路径依然是那么直接。 若取一个静态数据成员的地址,会得到一个指向其数据类型的指针,而不是一个指向其 class member 的指针, 因为 static member 并不内含在一个 class object 之中。例如: &Point3d:: chunkSize; 会获得类型如下都内存地址: const int* 如果有两个 classes,每一个都声明了一个 static member freeList,那么当它们都被放在程序的 data segment 时,就会导致名称冲突。编译器的解决办法是暗中对每一个 static data member 编码(这种手法 被称为:name-mangling),以获得一个独一无二的程序识别代码。 非静态数据成员的存取 非静态数据成员直接存放在每一个 class object 之中。除非经由明确的(explicit)或暗喻的(implicit) class object,没有办法直接存取它们。只要程序员在一个 member function 中直接处理一个 nonstatic data member,所谓“implicit class object”就会发生。例如下面这段代码: Point3d Point3d::translate( const Point3d &pt ){ 135 / 182 x += pt.x; y += pt.y; z += pt.z; } 表面上所看到的对于 x、y、z 的直接存取,事实上是经由一个“implicit class object“( 由 this 指针 表达)来完成,事实上这个函数的参数为: //member function 的内部转化 Point3d Point3d::translate( Point3d * const this, const Point3d &pt ){ this->x += pt.x; this->y += pt.y; this->z += pt.z; } 欲对一个非静态数据成员进行存取操作,编译器需要把 class object 的起始地址加上 data member 的偏移 量。比如: origin._y = 0.0; 地址&origin._y 将等于: &origin + (&Point3d::_y - 1); 要注意这里都有减 1 的操作。指向数据成员的指针,其 offset 值总是被加上 1,这样可以使编译系统区分出“没 有指向任何数据成员的指针”和“指向第一个数据成员的指针”这两种情况。 每一个非静态数据成员的偏移量(offset)在编译时期即可获知,甚至如果 member 属于一个 base class subobject 也是一样,因此,存取一个非静态数据成员,其效率和存取一个 C struct member 或一个 nonderived class 的 member 也是一样的。 现在我们看看虚拟继承。虚拟继承将为“经由 base class subobject“存取 class members 导入一层新 的间接性,譬如: Point3d *pt3d; Pt3d->_x = 0.0; 其执行效率在_x 是一个 struct member、一个 class member、单一继承、多重继承的情况下都完全相同。 但如果_x 是一个 virtual base class 的 member,存取速度会慢一些。 “继承“与数据成员 只要继承不要多态(Inheritance without Polymorphism) 假设有如下三个类及其继承关系: class Concreate1{ public: //... private: int val; char bit1; }; class Concreate2 : public Concrete1{ 136 / 182 public: //... private: char bit2; } class Concreate3 : public Concrete2{ public: //... private: char bit3; } Concreate1、Concreate2、Concreate3 的对象布局情况: C++语言保证”出现在派生类中的 base class subobject 有其完整原样性“。Concrete1 内含两个 members: val 和 bit1,加起来 5bytes。而一个 Concreate1 object 实际上用掉 8bytes,包括填充用的 3bytes, 以使 object 能够符合一部机器的 word 边界。一般而言,边界调整(alignment)是由处理器来决定的。 然而,Concreate2 的 bit2 实际上却是被放在填补空间所用的 3bytes 之后,于是其大小变成 12bytes,而 不是 8bytes,其中 6bytes 浪费在填补空间上。相同的道理使得 Concreate3 object 的大小是 16bytes, 其中 9bytes 用于填补空间。 加上多态(adding Polymorphism) 假设我们要处理一个坐标点,而不打算在乎它是一个 Point2d 或 Point3d 实例,那么我们需要在继承关系中 提供一个 virtual function 接口: class Point2d{ public: 137 / 182 Point2d(float x=0.0, float y=0.0) : _x(x),_y(y){}; virtual float z(){return 0.0;} virtual void z(float){} operate+=(const Point2d &rhs){ _x += rhs.x(); _y += rhs.y(); } protected: float _x; float _y; } class Point3d : public Point2d{ public: Point3d(float x=0.0, float y=0.0, float z=0.0) : Point2d(x,y),_z(z){}; virtual float z(){ return _z; } virtual void z(float newZ){ _z = newZ; } operate+=(const Point2d &rhs){ Point2d::operator+=(rhs); _z += rhs.z(); } protected: float _z; } 只有当我们以多态的方式来处理 2d 或 3d 坐标点时,在设计之中导入一个 virtual 接口才显得合理。也就是 说,写下这样的代码: void foo( Point2d &p1, Point2d &p2 ){ //… P1 += p2; //… } 其中 p1 和 p2 可能是 2d 也可能是 3d 坐标点,这并不是以前任何设计所能支持的。这样的弹性,当然正是面向 对象程序设计的中心。同时,支持这样的弹性,也给我们的 Point2d class 带来空间和存取时间的额外负担:  导入一个和 Point2d 有关的 virtual table,用来存放它所声明的每一个 virtual functions 的地 址。  在每一个 class object 中导入一个 vptr,提供执行期的链接,使每一个 object 能够找到相应的 virtual table。  加强 constructor,使它能够为 vptr 设定初值,让它指向 class 所对应的 virtual table。这可能 意味着在 derived class 和每一个 base class 的 constructor 中,重新设定 vptr 的值。其情况视 编译器的优化的积极性而定。  加强 destructor,使它能够抹消“指向 class 之相关 virtual table“的 vptr。要知道,vptr 很 可能已经在 derived class destructor 中被设定为 derived class 的 virtual table 地址。记 住,desturctor 的调用次序是反向的:从 derived class 到 base class。 138 / 182 下图显示了Point2d 和Point3d加上了virtual function之后的继承布局。此图把vptr放在base class 的尾端: 多重继承(Multiple Inheritance) 多重继承不像单一继承,不容易模塑出其模型。多重继承的复杂度在于derived class和其上一个base class 乃至于上上个 base class…之间的“非自然“关系。例如,考虑下面这个多重继承所获得的 class Vertex3d: class Point2d{ public: //... protected: float _x; float _y; } class Point3d : public Point2d{ public: //... ptotected: float _z; } class Vertex{ public: //... protected: Vertex *next; } class Vertex3d : public Point3d, public Vertex{ public: //... protected: float mumble; 139 / 182 } 至此,Point2d、Point3d、Vertex、Vertex3d 的继承关系如下图所示: 多重继承的主要问题发生于 derived class objects 和其第二或后继的 base class objects 之间的转 换。对于一个多重派生对象,将其地址指定给“最左端 base class 的指针”,情况将和单一继承时相同,因为 二者都指向相同的起始地址。至于第二个或后继的 base class 的地址指定操作,则需要对地址进行调整:加 上(或减去,如果 downcast 的话)介于中间的 base class subobject 大小,例如: Vertex3d v3d; Vertex *pv; Point2d *p2d; Point3d *p3d; 那么下面这个指定操作: pv = &v3d; 需要这样的内部转化: //虚拟 C++码 pv = (Vertex *)( ((char *)&v3d) + sizeof( Point3d ) ) 而下面都指定操作: p2d = &v3d; p3d = &v3d; 都只需要简单地拷贝其地址就可以了。下面是该多重继承的数据布局示意图: 140 / 182 虚拟继承(Virtual Inheritance) 下图是 Point2d、Point3d、Vertex、Vertex3d 的继承体系: class Point2d{ public: //... protected: float _x; float _y; }; class Vertex : public virtual Point2d{ public: //... protected: 141 / 182 Vertex *next; }; class Point3d : public virtual Point2d{ public: //... protected: float _z; }; class Vertex3d: public Vertex, public Point3d{ public: //... protected: float mumble; }; 在存取派生类的共有的虚拟基类的时候,cfront 编译器会在每一个派生类对象中安插一些指针,每个指针指向 一个 virtual base class。要存取继承得来的 virtual base class members,可以使用相关指针间接 完成。 上面这种实现方式的一个缺点是:每一个对象必须针对每一个 virtual base class 背负一个额外的指针。 然而理想情况下我们希望 class object 有固定的负担,不因为其 virtual base classes 的数目而有所 变化。该如何解决这个问题呢?virtual table offset strategy 采用了另外一种实现策略:在 virtual function table 中放置 virtual base class 的 offset(而不是地址),将 virtual base class offset 和 virtual function entries 混在一起。virtual function table 可经由正值或负值来索引:如果 是正值,很显然就索引到 virtual functions;如果是负值,则索引到 virtual base class offsets。 142 / 182 一般而言,虚基类最有效的一种运用形式就是:一个抽象的虚基类,其中没有任何数据成员。 参考资料: 《深度探索 C++对象模型》 143 / 182 指向数据成员的指针《深度探索 C++对象模型》 指向数据成员的指针,是一个有点神秘又颇有用处的语言特性,特别是如果你需要详细调查 class members 的底层布局的话。这样的调查可以用于决定 vptr 是放在 class 的起始处或者尾端。另外一个用途是可以用来 决定 class 中的 access sections 的次序。 考虑下面的 Point3d 声明。其中有一个 virtual function,一个 static data member,以及三个坐标: class Point3d{ public: virtual ~Point3d(); //… protected: static Point3d origin; float x,y,z; } 每一个 Point3d 的对象含有三个坐标值,依次为 x、y、z,以及一个 vptr。至于静态数据成员 origin,将 被放在 class object 之外。唯一可能因编译器不同而不同的是 vptr 的位置。C++标准允许 vptr 被放在对 象中的任何位置:在起始处,在尾端,或者是在各个 members 之间。然而实际上,所有编译器不是把 vptr 放 在对象的头部,就是放在对象的尾部。 那么,取某个坐标成员的地址,代表什么意思呢?例如,以下操作所得到的值代表什么: &Point3d:: z; 上述操作将得到 z 坐标在 class object 中的偏移量(offset)。最低限度其值将是 x 和 y 的大小总和,因为 C++语言要求同一个 access level 中的 members 的排列次序应该和其声明次序相同。在一台 32 位机器上, 每一个 float 是 4 个字节,所以我们应该期望刚才获得的值要不是 8,就是 12(在 32 位机器上,一个 vptr 是 4 个字节)。 然而,这样的期望还少了 1 个字节。对于 C 和 C++程序员来说,这多少算是个有点年代的错误了。如果 vptr 放在对象的末尾,则三个坐标值在对象布局中的偏移量分别为 0、4、8;如果 vptr 放在对象的开头,则三个坐 标值在对象布局中的偏移量分别为 4、8、12。然而你若去取 data members 的地址,传回的值总是多 1,也 就是 1、5、9 或 5、9、12 等等。 #include class Point3d{ public: virtual ~Point3d(){}; //… public://如果换成 private 或者 protected,则报错 static Point3d origin; float x; float y; float z; }; 144 / 182 int main() { printf("&Point3d::x = %p\n", &Point3d::x); printf("&Point3d::y = %p\n", &Point3d::y); printf("&Point3d::z = %p\n", &Point3d::z); std::cout<<"&Point3d::x = "<<&Point3d::x<normalize(); 时,会发生什么事情呢?其中的 Point3d:: normalize()定义如下: Point3d Point3d::normalize() const{ register float mag = magnitude(); Point3d normal; normal._x = _x/mag; normal._y = _y/mag; normal._z = _z/mag; return normal; } 而其中的 Point3d::magnitude()又定义如下: float Point3d::magnitude() const{ return sqrt( _x*_x + _y*_y + _z*_z ); } 答案是:需要视实际情况而定,C++支持三种类型的成员函数:static、nonstatic、virtual,每一种被调 用的方式都不相同。 非静态成员函数(Nonstatic Member Functions) C++的设计准则就是:非静态成员函数至少必须和一般的非成员函数有相同的效率。也就是说,如果我们要在以 下两个函数之间做选择: float magnitude3d( const Point3d *_this ){…} float Point3d::magnitude3d() const{…} 那么,选择成员函数不应该带来什么额外负担。这是因为编译器内部已经将“member 函数实体”转换为对等的 “nonmember 函数实体”。 举个例子,下面是 magnitude()的一个 nonmember 定义: float magnitude3d( const Point3d *_this ){ return sqrt( _this->_x*_this->_x + _this->_y*_this->_y + _this->_z*_this->_z ); } 乍看之下似乎非成员函数比较没有效率,它间接地经由参数取用坐标成员,而成员函数却是直接取用坐标成员。 147 / 182 然而实际上成员函数被内化为非成员的形式,下面就是转化步骤: 1、改写函数的 signature 以安插一个额外的参数到成员函数中,用以提供一个存取管道,使 class object 得以调用该函数。该额外参数被称为 this 指针: Point3d Point3d::magnitude( Point3d *const this ) 如果 member function 是 const,则变成: Point3d Point3d::magnitude( const Point3d *const this ) 2、将每一个“对非静态数据成员的存取操作”改为经由 this 指针来存取: { return sqrt( this->_x*this->_x + this->_y*this->_y + this->_z*this->_z ); } 3、将成员函数重新写成一个外部函数。对函数名称进行“mangling”处理,使它在程序中独一无二: extern magnitude__7Point3dFv( register Point3d *const this ); 现在这个函数已经转换好了,而其每一个调用操作也都必须转换。于是: ”obj.magnitude();”变成了:”magnitude__7Point3dFv(&obj);” ”ptr->magnitude();”变成了:”magnitude__7Point3dFv(ptr);” 前面提及的 normalize() 函数会被转化为下面的形式,其中假设已经声明有一个 Point3d copy constructor,而 named returned value(NRV)的优化也已施行: void magnitude__7Point3dFv( register const Point3d *const this, Point3d &__result ) { Register float mag = this->magnitude(); __result.Point3d::Point3d(); __result.x = this->_x/mag; __result.y = this->_y/mag; __result.z = this->_z/mag; } 静态成员函数(Static Member Functions) 静态成员函数由于缺乏 this 指针,因此差不多等同于非成员函数。如果 Point3d::normalize()是一个静 态成员函数,以下两个调用操作: obj.normalize(); ptr->normalize(); 将被转化为一般的 nonmember 函数调用,像这样: //obj.normalize(); normalize__7Point3dSfv(); //ptr->normalize(); normalize__7Point3dSfv(); 静态成员函数的主要特性就是它没有 this 指针,其次要的特性统统根源于这个主要特性: 148 / 182  它不能够直接存取其 class 中的 nonstatic members  它不能够被声明为 const、volatile 或 virtual  它不需要经由 class object 才被调用--虽然大部分时候它是这样被调用的 一个静态成员函数,会被提到 class 声明之外,并给予一个经过“mangling”的适当名称。例如: unsigned int Point3d::object_count() { Return _object_count; } 会被 cfront 转化为: //在 cfront 之下的内部转化结果 unsigned int object_count_5Point3dSFv() { Return _object_count_5Point3d; } 其中 SFv 表示它是个 static member function,拥有一个空白(void)的参数链表。 如果取一个静态成员函数的地址,得到的将是其在内存中的位置,也就是其地址。由于静态成员函数没有 this 指针,所以其地址的类型并不是一个“指向类成员函数的指针”,而是一个“非成员函数指针”。也就是说: &Point3d::object_count(); 会得到一个数值,类型是: unsigned int(*)(); 而不是: unsigned int( Point3d::* )(); 虚拟成员函数(Virtual Member Functions) 虚函数的一般实现模型是:每一个类有一个虚表,内含该类之中各虚函数的地址,然后每一个对象有一个 vptr, 指向虚表的所在。在这一小节,将根据单一继承、多重继承和虚拟继承等各种情况,从细节上探讨该实现方式。 在单一继承的情况下,一个 class 只会有一个 virtual table,每一个 table 内含其对应的 class object 中所有 active virtual function 函数实体的地址。这些 active virtual function 包括:  这个类所定义的函数实体,它会改写一个可能存在的 base class virtual function 函数实体。  继承自 base class 的函数实体,这是在 derived class 决定不改写 virtual function 时才会出现 的情况  一个 pure_virtual_called()函数实体,它既可以扮演 pure virtual function 的空间保卫角色, 也可以当做执行期异常处理函数。 149 / 182 每一个虚函数都被指派一个固定的索引值,这个索引在整个继承体系中保持与特定的 virtual function 的 关联。例如,在我们的 Point class 体系中: class Point{ public: virtual ~Point(); virtual Point& mult(float) = 0; //... float x() const { return _x; } virtual float y() const { return 0; } virtual float z() const { return 0; } //... protected: Point( float x = 0.0 ); float _x; } virtual destructor 被赋值 slot1,而 mult()被赋值 slot2。此例并没有 mult()的函数定义,所以 pure_virtual_called()的函数地址会被放在 slot2 中。如果该函数意外地被调用,通常的操作是结束掉 这个程序。y()被赋值 slot3 而 z()被赋值 slot4。X()的 slot 是多少?答案是没有,因为它并不是虚函数。 在上图中,可以清楚地看到相关的内存布局及其 virtual table。 当一个类派生于 Point 时,会发生什么事情?例如,类 Point2d: class Point2d:public Point{ public: Point2d( float x=0.0, float y=0.0 ):Point(x),_y(y){} ~Point2d(); Point2d& mult(float); 150 / 182 //... float x() const { return _x; } float y() const { return 0; } //... protected: float _x; } 一共有三种可能性:  它可以继承 base class 所声明的 virtual functions 的函数实体。正确地说,是该函数实体的地址 会被拷贝到派生类的 virtual table 相对应的 slot 之中。  它可以使用自己的函数实体。这表示它自己的函数实体地址必须放在对应的 slot 之中。  它可以加入一个新的 virtual function。这时候 virtual table 的尺寸会增大一个 slot,而新的函 数实体地址会被放进该 slot 之中。 Point2d 的 virtual table 在 slot1 中指出 destructor,在 slot2 中指出 mult()(取代 pure virtual function)。它自己的 y()函数实体地址放进 slot3,继承自 Point 的 z()函数实体地址则放在 slot4。 类似情况,Point3d 派生自 Point2d,如下: class Point3d : public Point2d{ public: Point3d( float x=0.0, float y=0.0, float z=0.0 ):Point2d(x,y),_z(z){} ~Point3d(); Point3d& mult(float); //... float z() const { return _z; } //... protected: float _z; } 其 virtual table 中的 slot1 放置 Point3d 的析构函数,slot2 放置 Point3d::mult()函数地址。Slot3 放置继承自 Point2d 的 y()函数地址,slot4 放置自己的 z()函数地址。 现在,如果有如下的语句: ptr->z(); 那么,如何有足够的知识在编译时期设定 virtual function 的调用呢?  一般而言,我们并不知道ptr 所指对象的真正类型。然而,我们知道,经由 ptr 可以存取到该对象的 virtual table。  虽然不知道哪一个 z()函数实体会被调用,但我们知道每一个 z()函数地址都被放在 slot4。 这些信息使得编译器可以将该调用转化为: ( *ptr->vptr[4] )( ptr ); 在这个转化中,vptr 表示编译器所安插的指针,指向 virtual table;4 表示 z()被赋值的 slot 编号。唯 151 / 182 一一个在执行期才能知道的东西是:slot4 所指的到底是哪一个 z()函数实体? 在一个单一继承体系中,vritual function 机制的行为十分良好,不但有效率而且很容易塑造出模型来。但 是在多重继承和虚拟继承之中,对 virtual functions 的支持就没有那么美好了。 多重继承下的 Virtual Functions 在多重继承中支持 virtual functions,其复杂度围绕在第二个以及后继的基类身上,以及“必须在执行期 调整 this 指针”这一点上。以下面的 class 体系为例: class Base1{ public: Base1(); virtual ~Base1(); virtual void speakclearly(); virtual Base1 *clone() const; protected: float data_Base1; }; class Base2{ public: Base2(); virtual ~Base2(); virtual void mumble(); virtual Base2 *clone() const; protected: float data_Base2; }; class Derived : public Base1,public Base2{ public: Derived(); virtual ~Derived(); virtual Derived *clone() const; protected: float data_Derived; }; 该多重继承体系的虚表布局情况如下所示: 152 / 182 首先,把一个从堆中配置而得的 Derived 对象的地址,指定给一个 Base2 指针: Base2 *pbase2 = new Derived; 新的 Derived 对象的地址必须调整,以指向其 Base2 subobject。编译时期会产生以下的代码: Derived *temp = new Derived; Base2 *pbase2 = temp ? temp + sizeof(Base1) : 0; 当程序员要删除 pbase2 所指的对象时: delete pbase2; 指针必须再次被调整,以便再一次指向 Derived 对象的起始处。 一般规则是,经由指向“第二或后继之 base class”的指针(或引用)来调用 derived class virtual function,那么该调用操作所需“必要的 this 指针调整”操作,必须在执行期完成。也就是说,offset 的 大小,以及把 offset 加到 this 指针上头的那一小段程序代码,必须由编译器在某个地方插入。 调整 this 指针的另外一个负担是,由于两种不同的可能:(1)经由 derived class(或第一个 base class) 调用,(2)经由第二个(或其后继)base class 调用,同一函数在虚表中可能需要多笔对应的 slots。例如: Base1 *pbase1 = new Derived; Base2 *pbase2 = new Derived; //… delete pbase1; delete pbase2; 虽然两个 delete 导致相同的 Derived destructor,但它们需要两个不同的 virtual table slots:  pbase1 不需要调整 this 指针(因为 Base1 已经指向 Derived 对象都起始处)。其 virtual table slot 需放置真正的 destructor 地址。  pbase2 需要调整 this 指针,其 virtual table slot 需要相关的 thunk 地址。 153 / 182 Thunk 解释:所谓 thunk 是一小段 assembly 代码,用来(1)以适当的 offset 值调整 this 指针,(2)跳到 virtual function 去。例如,经由一个 Base2 指针调用 Derived destructor,其相关的 thunk 可能看 起来是下面这个样子: //虚拟 C++代码 pbase2_dtor_thunk: this += sizeof( base1 ); Derived::~Derived( this ); Thunk 技术允许 virtual table slot 继续内含一个简单的指针,因此多重继承下不需要任何空间上的额外 负担。slots 中的地址可以直接指向 virtual function,也可以指向一个相关的 thunk(如果需要调整 this 指针的话)。 在多重继承下,一个 derived class 内含 n-1 个额外的 virtual tables,n 表示其上一层 base classes 的数目。对于本例而言,会有两个 virtual table 被编译器产生出来:  一个主要实体,与 Base1(最左端 base class)共享;  一个次要实体,与 Base2(第二个 base class)有关。 针对每一个 virtual table,Derived 对象中有对应的 vptr。vptrs 将在 constructor(s)中被设立初值 (经由编译器所产生出来的码)。 虚继承下的 Virtual Functions 《深入探索 C++对象模型》P168~169 参考资料: 《深度探索 C++对象模型》 154 / 182 指向成员函数的指针《深度探索 C++对象模型》 取一个非静态成员函数的地址,如果该函数是 nonvirtual,则得到的结果是它在内存中真正的地址。然而这 个值也不是完全的,它也需要被绑定于某个 class object 的地址上,才能够通过它调用该函数。所有的非静 态成员函数都需要对象的地址(以参数 this 指出)。 一个指向成员函数的指针,其声明语法如下所示: double //return type (Point::* //class the function is member pmf) // name of pointer to member (); // argument list 然后我们可以这样定义并初始化该指针: double (Point::*coord)() = &Point::x; 也可以这样指定其值: coord = &Point::y; 想调用它,可以这样做: (origin.*coord)();或 (ptr->*coord)(); 指向 member function 的指针的声明语法,以及指向“member selection 运算符”的指针,其作用是作 为 this 指针的空间保留者。这也就是为什么 static member functions(没有 this 指针)的类型为“函数 指针”,而不是“指向 member function 之指针”的原因。使用一个成员函数的指针,如果并不用于虚函数、 多重继承、虚基类等情况的话,并不会比使用一个“nonmember function 指针”的成本更高。 支持“指向 virtual member functions”的指针 注意下面的程序片段: float (Point::*pmf)() = &Point::z; Point *ptr = new Point3d; pmf,一个指向成员函数的指针,被设置为 Point::z()(一个虚函数)的地址。如果我们直接经由 ptr 调用 z(): ptr->z(); 则被调用的是 Point3d::z()。但如果我们从 pmf 间接调用 z()呢? (ptr->*pmf)(); 仍然是 Point::z()被调用吗?也就是说,虚拟机制仍然能够在使用“指向成员函数的指针”的情况下正常运 行吗?如何实现的呢? 面对一个虚函数,其地址在编译时期是未知的,所能知道的仅仅是虚函数在其相关的虚表中的索引值。也就是说, 对一个虚成员函数取其地址,所能获得的只是一个索引值。假设我们有以下的 Point 声明: Class Point{ public: virtual ~Point(); float x(); float y(); 155 / 182 virtual float z(); //… } 然后取 destructor 的地址: &Point:: ~Point; 得到的结果是 1。取 x()或 y()的地址: &Point::x(); &Point::y(); 得到的是函数在内存中的地址,因为它们不是 virtual。取 z()的地址: &Point::z(); 得到的结果是 2,通过 pmf 来调用 z(),会被内部转化为一个编译时期的式子,一般形式如下: ( *ptr->vptr[(int)pmf] )( ptr ); 对一个指向成员函数的指针评估求值,会因为该值有两种意义而复杂化,其调用操作也将有别于常规调用操作。 pmf 的内部定义,也就是: float (Point::*pmf)(); 必须允许该函数能够寻址出 nonvirtual x()和 virtual z()两个成员函数,这两个成员函数有着相同的原 型,只不过其中一个代表内存地址,另外一个代表在对应虚表中的索引值。因此,编译器必须定义 pmf 使它能 够(1)含有两种数值,(2)更重要的是其数值可以被区别代表内存地址还是虚表中的索引值。 在 cfront2.0 的非正式版本中,这两个值被内含在一个普通的指针内。它使用如下技巧: (((int)pmf) & ~127) ? (*pmf)(ptr) //non-virtual invocation : ( *ptr->vptr[(int)pmf] )( ptr ); //virtual invocation 这种实现技巧必须假设继承体系中最多只能够有 128 个虚函数。这并不是我们所希望的,但却证明是可行的。 在多重继承之下,指向 Member Functions 的指针 《深入理解 C++对象模型》P178~180 参考资料: 《深度探索 C++对象模型》 156 / 182 智能指针 Google C++编程风格指南关于智能指针的说明 如果确实需要使用智能指针的话,scoped_ptr 完全可以胜任。在非常特殊的情况下,例如对 STL 容器中对象, 你应该只使用 std::tr1::shared_ptr,任何情况下都不要使用 auto_ptr。 “智能”指针看上去是指针,其实是附加了语义的对象。以 scoped_ptr 为例,scoped_ptr 被销毁时,删除 了它所指向的对象。shared_ptr 也是如此,而且,shared_ptr 实现了引用计数,所以最后一个 shared_ptr 对象析构时, 如果检测到引用次数为 0,就会销毁所指向的对象。 一般来说,我们倾向于设计对象隶属明确的代码,最明确的对象隶属是根本不使用指针,直接将对象作为一个域 (field)或局部变量使用。另一种极端是引用计数指针不属于任何对象,这样设计的问题是容易导致循环引用 或其他导致对象无法删除的诡异条件,而且在每一次拷贝或赋值时连原子操作都会很慢。 虽然不推荐这么做,但有些时候,引用计数指针是最简单有效的解决方案。 Google C++编程风格指南,相关文档常见下面的链接: http://code.google.com/p/google-styleguide/ http://code.google.com/p/zh-google-styleguide/ auto_ptr 与引用计数型智能指针不同的,auto_ptr 要求其对“裸”指针的完全占有性。也就是说一个”裸“指针不能同 时被两个以上的 auto_ptr 所拥有。在拷贝构造或赋值操作时,我们必须作特殊的处理来保证这个特性。 auto_ptr 的做法是“所有权转移”,即拷贝或赋值的源对象将失去对“裸”指针的所有权。 在使用 auto_ptr 时,有几点需要注意的地方: 1) auto_ptr 是这样一种指针,它是“它所指向的对象”的拥有者,当作为对象拥有者的 auto_ptr 被摧毁 时,该对象也将遭到摧毁。auto_ptr 要求一个对象只能有一个拥有者,绝对不要出现多个 auto_ptr 同时拥 有同一个对象的情况,像这样: int* p = new int(0); auto_ptr ap1(p); auto_ptr ap2(p); 因为 ap1 与 ap2 都认为指针 p 是归它管的,在析构时都试图删除 p,两次删除同一个对象的行为在 C++标准中 是未定义的。所以我们必须防止这样使用 auto_ptr。 2) 因为一个 auto_ptr 被拷贝或被赋值后,其已经失去对原对象的所有权,这个时候,对这个 auto_ptr 的 解引用操作是不安全的。如下: int* p = new int(0); auto_ptr ap1(p); auto_ptr ap2 = ap1; cout<<*ap1; //错误,此时 ap1 只剩一个 null 指针在手了 3) auto_ptr 并不满足 stl 标准容器对元素的最基本要求。因为在拷贝和赋值动作之后,原本的 auto_ptr 和新产生的 auto_ptr 并不相等。在拷贝和赋值过后,原来的 auto_ptr 会交出拥有权,而不是拷贝给新的 auto_ptr。因此绝对不要将 auto_ptr 作为标准容器的元素。 157 / 182 2) 并不存在针对 array 而设计的 auto_ptr,考虑下面这种用法: int* pa = new int[10]; auto_ptr ap(pa); 因为 auto_ptr 的析构函数中删除指针用的是 delete,而不是 delete [],所以我们不应该用 auto_ptr 来 管理一个数组指针。 scoped_ptr scoped_ptr 有着与 std::auto_ptr 类似的特性,而最大的区别在于在于对拥有权的处理。auto_ptr 在复 制时会从源 auto_ptr 自动交出拥有权,而 scoped_ptr 则不允许被复制或被赋值!scoped_ptr 拥有它所指 向的资源的所有权,并永远不会放弃这个所有权。 scoped_ptr 的用法与普通的指针没什么区别;最大的差别在于你不必再记得在指针上调用 delete,还有复 制是不允许的。典型的指针操作(operator* 和 operator->)都被重载了,并提供了和裸指针一样的语法。 用 scoped_ptr 和用裸指针一样快,也没有大小上的增加,因此它们可以广泛使用。使用 boost::scoped_ptr 时,包含头文件"boost/scoped_ptr.hpp". 在声明一个 scoped_ptr 时,用被指物的类型来指定类模板的 参数。例如,以下是一个包含 std::string 指针的 scoped_ptr: boost::scoped_ptr p(new std::string("Hello")); 当 scoped_ptr 被销毁时,它对它所拥有的指针调用 delete 。 shared_ptr shared_ptr 可以从一个裸指针、另一个 shared_ptr、一 个 std::auto_ptr、或者一个 boost::weak_ptr 构造。还可以传递第二个参数给 shared_ptr 的构造函数,它被称为删除器(deleter)。删除器稍后会被调用, 来处理共享资源的释放。这对于管理那些不是用 new 分配也不是用 delete 释放的资源时非常有用。 引用计数智能指针是非常重要的工具。Boost 的 shared_ptr 提供了坚固而灵活的解决方案,它已被广泛用 于多种环境下。需要在使用者之间共享对象是常见的,shared_ptr 让使用者无需知道也在使用共享对象的其 它对象,并让它们无需担心在没有对象引用时的资源释放。这对于 Boost 的智能指针类而言是最重要的。通过 使用自定义删除器,几乎所有资源类型都可以存入 shared_ptr。这 使 得 shared_ptr 成为处理资源管理的通 用类,而不仅仅是处理动态分配对象。 在以下情况时使用 shared_ptr : 当有多个使用者使用同一个对象,而没有一个明显的拥有者时 当要把指针存入标准库容器时 当要传送对象到库或从库获取对象,而没有明确的所有权时 当管理一些需要特殊清除方式的资源时 158 / 182 auto_ptr 解析 auto_ptr 是当前 C++标准库中提供的一种智能指针,或许相对于 boost 库提供的一系列眼花缭乱的智能指针, 这个不怎么智能的智能指针难免会黯然失色。诚然,auto_ptr 有这样那样的不如人意,以至于程序员必须像使 用”裸“指针那样非常小心地使用它才能保证不出错,以至于它甚至无法适用于同是标准库中的那么多的容器和 一些算法,但即使如此,我们仍然不能否认这个小小的 auto_ptr 所蕴含的价值与理念。 auto_ptr 的源代码 下面将列出 auto_ptr 的源代码,并详细讲解每一部分。因为标准库中的代码要考虑不同编译器支持标准的不 同而插入了不少预编译判断,而且命名可读性不是很强,这里我用了 Nicolai M. Josuttis(《The C++ standard library》的作者)写的一个 auto_ptr 的版本,并做了少许格式上的修改以易于分析阅读: namespace std { // auxiliary type to enable copies and assignments (now global) template struct auto_ptr_ref { Y* yp; auto_ptr_ref (Y* rhs):yp(rhs) { } }; template class auto_ptr { private: T* ap; // refers to the actual owned object (if any) public: typedef T element_type; // 构造函数 explicit auto_ptr(T* ptr = 0) throw() : ap(ptr) {} // 析构函数 ~auto_ptr() throw() { delete ap; } // 拷贝构造函数 auto_ptr(auto_ptr& rhs) throw() : ap(rhs.release()) { } template auto_ptr(auto_ptr& rhs) throw() : ap(rhs.release()) { 159 / 182 } // 赋值操作符 auto_ptr& operator=(auto_ptr& rhs) throw() { reset(rhs.release()); return *this; } template auto_ptr& operator=(auto_ptr& rhs) throw() { reset(rhs.release()); return *this; } // value access T* get() const throw() { return ap; } T& operator*() const throw() { return *ap; } T* operator->() const throw() { return ap; } // release ownership T* release() throw() { T* tmp(ap); ap = 0; return tmp; } // reset value void reset (T* ptr=0) throw() { if (ap != ptr) { delete ap; ap = ptr; } } /* special conversions with auxiliary type to enable copies and assignments*/ auto_ptr(auto_ptr_ref rhs) throw() : ap(rhs.yp) { } auto_ptr& operator=(auto_ptr_ref rhs) throw() { // new reset(rhs.yp); return *this; 160 / 182 } template operator auto_ptr_ref() throw() { return auto_ptr_ref(release()); } template operator auto_ptr() throw() { return auto_ptr(release()); } }; } 构造函数与析构函数 备注:C++支持两种初始化变量的方式:复制初始化和直接初始化;复制初始化语法用等号,直接初始化则是把 初始化式放在括号中: int ival(1024); //直接初始化 int ival = 1024; //复制初始化 auto_ptr<>不允许使用一般指针惯用的初始化方式,你必须直接初始化(《stl 标准程序库》p40): std::auto_ptr ptr1(new ClassA); //OK std::auto_ptr ptr2 = new ClassA; //ERROR 只有 auto_ptr 可以拿来当做另外一个 auto_ptr 的初始值,普通指针是不行的(《stl 标准程序库》p41): std::auto_ptr ptr; ptr = new ClassA; //ERROR ptr = std::auto_ptr(new ClassA); //ok delete old object and own new 在使用 auto_ptr 时,有几点需要注意的地方: 1) auto_ptr 是这样一种指针,它是“它所指向的对象”的拥有者,当身为对象拥有者的 auto_ptr 被摧毁 时,该对象也将遭到摧毁。auto_ptr 要求一个对象只能有一个拥有者,绝对不应该出现多个 auto_ptr 同时 拥有一个对象的情况,像这样: int* p = new int(0); auto_ptr ap1(p); auto_ptr ap2(p); 因为 ap1 与 ap2 都认为指针 p 是归它管的,在析构时都试图删除 p,两次删除同一个对象的行为在 C++标准中 是未定义的。所以我们必须防止这样使用 auto_ptr。 2) 并不存在针对 array 而设计的 auto_ptr,考虑下面这种用法: int* pa = new int[10]; auto_ptr ap(pa); 因为 auto_ptr 的析构函数中删除指针用的是 delete,而不是 delete [],所以我们不应该用 auto_ptr 来 管理一个数组指针。 161 / 182 3) 构造函数的 explicit 关键词有效阻止从一个“裸”指针隐式转换成 auto_ptr 类型。 4) 因为 C++保证删除一个空指针是安全的,所以我们没有必要把析构函数写成: ~auto_ptr() throw() { if(ap) delete ap; } 拷贝构造与赋值 与引用计数型智能指针不同的,auto_ptr 要求其对“裸”指针的完全占有性。也就是说一个”裸“指针不能同 时被两个以上的 auto_ptr 所拥有。在拷贝构造或赋值操作时,我们必须作特殊的处理来保证这个特性。 auto_ptr 的做法是“所有权转移”,即拷贝或赋值的源对象将失去对“裸”指针的所有权,所以,与一般拷贝 构造函数、赋值函数不同,auto_ptr 的拷贝构造函数、赋值函数的参数为引用而不是常量引用(const reference).当然,一个 auto_ptr 也不能同时拥有两个以上的“裸”指针,所以,拷贝或赋值的目标对象将 先释放其原来所拥有的对象。下面就 auto_ptr 拥有权转移的情况举例进行说明: 拷贝构造函数: std::auto_ptr ptr1(new ClassA); //initialize an auto_ptr with a new object std::auto_ptr ptr2(ptr1); //copy th auto_ptr //transfers ownership from ptr1 to ptr2 在第一个语句中,ptr1 拥有那个 new 出来的对象。在第二个语句中,拥有权从 ptr1 转移到 ptr2。此 后 ptr2 就拥有了那个 new 出来的对象,而 ptr1 不再拥有它。这样,对象就只会被 delete 一次--在 ptr2 销毁的时 候。 赋值操作符(赋值函数): std::auto_ptr ptr1(new ClassA); //initialize an auto_ptr with a new object std::auto_ptr ptr2(new ClassA); //create another auto_ptr ptr2 = ptr1; //assign the auto_ptr //delete object owned by ptr2 //transfers ownership from ptr1 to ptr2 在这里,赋值动作将拥有权从 ptr1 转移到 ptr2,于是,ptr2 拥有了先前被 ptr1 所拥有的那个对象。如果 ptr2 被赋值前正拥有另外一个对象,赋值动作发生时会调用 delete,将该对象删除。 这里的注意点是: 1) 因为一个 auto_ptr 被拷贝或被赋值后,其已经失去对原对象的所有权,这个时候,对这个 auto_ptr 的 解引用操作是不安全的。如下: int* p = new int(0); auto_ptr ap1(p); auto_ptr ap2 = ap1; cout<<*ap1; //错误,此时 ap1 只剩一个 null 指针在手了 这种情况较为隐蔽的情形出现在将 auto_ptr 作为函数参数按值传递,因为在函数调用过程中在函数的作用域 中会产生一个局部对象来接收传入的 auto_ptr(拷贝构造),这样,传入的实参 auto_ptr 就失去了其对原对 162 / 182 象的所有权,而该对象会在函数退出时被局部 auto_ptr 删除。如下: void f(auto_ptr ap){cout<<*ap;} auto_ptr ap1(new int(0)); f(ap1); cout<<*ap1; //错误,经过 f(ap1)函数调用,ap1 已经不再拥有任何对象了。 因为这种情况太隐蔽,太容易出错了,所以 auto_ptr 作为函数参数按值传递是一定要避免的。或许大家会想 到用 auto_ptr 的指针或引用作为函数参数或许可以,但是仔细想想,我们并不知道在函数中对传入的 auto_ptr 做了什么,如果当中某些操作使其失去了对对象的所有权,那么这还是可能会导致致命的执行期错误。 也许,用 const reference 的形式来传递 auto_ptr 会是一个不错的选择。 2)我们可以看到复制构造函数提供了一个成员模板,使得可通过类型自动转换,构造出合适的 auto_ptr。例 如,根据一个派生类的对象,构造出一个基类对象的 auto_ptr。同样,赋值操作符也提供了一个成员模板,使 得可通过类型自动转换,赋值给合适的 auto_ptr。例如,将一个派生类的对象,赋值给一个基类对象的 auto_ptr。 class base{...}; class derived: public base{...}; 那么下列代码就可以通过,实现从 auto_ptr到 auto_ptr的隐式转换,因为 derived* 可以转换成 base*类型 auto_ptr apbase = auto_ptr(new derived); 3) auto_ptr 不满足 stl 容器对元素的要求。 auto_ptr 并不满足 stl 标准容器对元素的最基本要求。因为在拷贝和赋值动作之后,原本的 auto_ptr 和新 产生的auto_ptr并不相等。在拷贝和赋值过后,原来的auto_ptr会交出拥有权,而不是拷贝给新的auto_ptr。 因此绝对不要将 auto_ptr 作为标准容器的元素。 提领操作(dereference) 提领操作有两个操作,一个是返回其所拥有的对象的引用,另一个是则实现了通过 auto_ptr 调用其所拥有的 对象的成员。如: struct A{ … } auto_ptr apa(new A); (*apa).f(); apa->f(); 当然, 我们首先要确保这个智能指针确实拥有某个对象,否则,这个操作的行为即对空指针的提领是未定义的。 辅助函数 1) get 用来显式的返回 auto_ptr 所拥有的对象指针。我们可以发现,标准库提供的 auto_ptr 既不提供从 “裸”指针到 auto_ptr 的隐式转换(构造函数为 explicit),也不提供从 auto_ptr 到“裸”指针的隐式转 换,从使用上来讲可能不那么的灵活,考虑到其所带来的安全性还是值得的。 2) release,用来转移所有权。 3) reset,用来接收所有权,如果接收所有权的 auto_ptr 如果已经拥有某对象,必须先释放该对象。 163 / 182 特殊转换 auto_ptr 中剩余的部分(辅助型别 auto_ptr_ref 及其相关函数)涉及非常精致的技巧,使我们得以拷贝和 赋值 non-const auto_ptrs,却不能拷贝和赋值 const auto_ptr(更加详细的说明,参考《stl 标准模 板库》P55)。 auto_ptr 运用实例(《STL 标准模板库》p47) 下面的一个例子展示了 auto_ptr 转移拥有权的行为: #include #include using namespace std; /* define output operator for auto_ptr * - print object value or NULL */ template ostream& operator<<(ostream& strm, const auto_ptr& p)//参数 p 是常量引用,所以不发生拥有权转移 { // does p own an object ? if (p.get() == NULL) { strm << "NULL"; // NO: print NULL } else { strm << *p; // YES: print the object } return strm; } int main() { auto_ptr p(new int(42)); auto_ptr q; cout << "after initialization:" << endl; cout << " p: " << p << endl; cout << " q: " << q << endl; q = p; cout << "after assigning auto pointers:" << endl; cout << " p: " << p << endl; cout << " q: " << q << endl; *q += 13; // change value of the object q owns p = q; cout << "after change and reassignment:" << endl; cout << " p: " << p << endl; 164 / 182 cout << " q: " << q << endl; } 输出结果为: after initialization: p: 42 q: NULL after assigning auto pointers: p: NULL q: 42 after change and reassignment: p: 55 q: NULL 参考资料: 《STL 标准程序库》P38-58 165 / 182 scoped_ptr 解析 《超越 C++标准库-Boost 库导论》 头文件: "boost/scoped_ptr.hpp" scoped_ptr 的基本概念 boost::scoped_ptr 用于确保动态分配的对象能够被正确地删除。scoped_ptr 有着与 std::auto_ptr 类似的特性,而最大的区别在于它不能转让所有权,然而 auto_ptr 却可以。事实上,scoped_ptr 永远不能 被复制或被赋值!scoped_ptr 拥有它所指向的资源的所有权,并永远不会放弃这个所有权。scoped_ptr 的 这种特性改进了代码的表示方式,我们可以根据需要选择最合适的智能指针(scoped_ptr 或 auto_ptr)。 要决定使用std::auto_ptr还是 boost::scoped_ptr, 就要考虑转移所有权是不是你想要的智能指针的一 个特性。如果不是,就用 scoped_ptr。它是一种轻量级的智能指针;使用它不会使你的程序变大或变慢。它 只会让你的代码更安全,更好维护。下面是 scoped_ptr 的摘要,以及其成员的简要描述: namespace boost { template class scoped_ptr : noncopyable { public: explicit scoped_ptr(T* p = 0); ~scoped_ptr(); void reset(T* p = 0); T& operator*() const; T* operator->() const; T* get() const; void swap(scoped_ptr& b); }; template void swap(scoped_ptr & a, scoped_ptr & b); } scoped_ptr 的成员函数 explicit scoped_ptr(T* p=0) 构造函数,存储 p 的一份拷贝。注意,p 必须是用 operator new 分配的,或者是 null. 在构造的时候,不 要求 T 必须是一个完整的类型。当指针 p 是调用某个分配函数的结果而不是直接调用 new 得到的时候很有用: 因为这个类型不必是完整的,只需要类型 T 的一个前向声明就可以了。这个构造函数不会抛出异常。 ~scoped_ptr() 删除指针所指向的对象。类型 T 在被销毁时必须是一个完整的类型。如果 scoped_ptr 在它被析构时并没有保 存资源,它就什么都不做。这个析构函数不会抛出异常。 void reset(T* p=0); 166 / 182 重置一个 scoped_ptr 就是删除它已保存的指针,如果它有的话,并重新保存 p. 通常,资源的生存期管理应 该完全由 scoped_ptr 自己处理,但是在极少数时候,资源需要在 scoped_ptr 的析构之前释放,或者 scoped_ptr 要处理它原有资源之外的另外一个资源。这时,就可以用 reset,但一定要尽量少用它。(过多 地使用它通常表示有设计方面的问题) 这个函数不会抛出异常。 T& operator*() const; 该运算符返回一个智能指针中存储的指针所指向的对象的引用。由于不允许空的引用,所以解引用一个拥有空指 针的 scoped_ptr 将导致未定义行为。如果不能肯定所含指针是否有效,就用函数 get 替代解引用。这个函数 不会抛出异常。 T* operator->() const; 返回智能指针所保存的指针。如果保存的指针为空,则调用这个函数会导致未定义行为。如果不能肯定指针是否 空的,最好使用函数 get。这个函数不会抛出异常。 T* get() const; 返回保存的指针。应该小心地使用 get,因为它可以直接操作裸指针。但是,get 使得你可以测试保存的指针 是否为空。这个函数不会抛出异常。get 通常在调用那些需要裸指针的函数时使用。 operator unspecified_bool_type() const 返回 scoped_ptr 是否为非空。返回值的类型是未指明的,但这个类型可被用于 Boolean 的上下文(boolean context)中。在 if 语句中最好使用这个类型转换函数,而不要用 get 去测试 scoped_ptr 的有效性 void swap(scoped_ptr& b) 交换两个 scoped_ptr 的内容。这个函数不会抛出异常。 swap 函数 template void swap(scoped_ptr& a,scoped_ptr& b) 这个函数提供了交换两个 scoped pointer 的内容的更好的方法。之所以说它更好,是因为 swap(scoped1,scoped2) 可以更广泛地用于很多指针类型,包括裸指针和第三方的智能指针。 scoped1.swap(scoped2) 则只能用于它的定义所在的智能指针,而不能用于裸指针。 scoped_ptr 的用法 scoped_ptr 的用法与普通的指针没什么区别;最大的差别在于你不必再记得在指针上调用 delete,还有复 制是不允许的。典型的指针操作(operator* 和 operator->)都被重载了,并提供了和裸指针一样的语法。 用 scoped_ptr 和用裸指针一样快,也没有大小上的增加,因此它们可以广泛使用。使用 boost::scoped_ptr 时,包含头文件"boost/scoped_ptr.hpp". 在声明一个 scoped_ptr 时,用被指物的类型来指定类模板的 参数。例如,以下是一个包含 std::string 指针的 scoped_ptr: boost::scoped_ptr p(new std::string("Hello")); 当 scoped_ptr 被销毁时,它对它所拥有的指针调用 delete 。 让我们看一个程序,它使用 scoped_ptr 来管理 std::string 指针。注意这里没有对 delete 的调用,因为 scoped_ptr 是一个自动变量,它会在离开作用域时被销毁: #include "boost/scoped_ptr.hpp" #include 167 / 182 #include int main() { { boost::scoped_ptr p(new std::string("Use scoped_ptr often.")); // 打印字符串的值 if (p) std::cout << *p << '\n'; // 获取字符串的大小 size_t i=p->size(); // 给字符串赋新值 *p="Acts just like a pointer"; } // 这里 p 被销毁,并删除 std::string } 这段代码中有几个地方值得注明一下。首先,scoped_ptr 可以测试其有效性,就象一个普通指针那样,因为 它提供了隐式转换到一个可用于布尔表达式的类型的方法。其次,可以象使用裸指针那样调用被指物的成员函数, 因为重载了 operator->. 第三,也可以和裸指针一样解引用 scoped_ptr,这归功于 operator*的重载。 这些特性正是 scoped_ptr 和其它智能指针的用处所在,因为它们和裸指针的不同之处在于对生存期管理的语 义上,而不在于语法上。 scoped_ptr 与 auto_ptr 间的区别主要在于对拥有权的处理。auto_ptr 在复制时会从源 auto_ptr 自动 交出拥有权,而 scoped_ptr 则不允许被复制。看看下面这段程序,它把 scoped_ptr 和 auto_ptr 放在一 起,你可以清楚地看到它们有什么不同。 void scoped_vs_auto() { using boost::scoped_ptr; using std::auto_ptr; scoped_ptr p_scoped(new std::string("Hello")); auto_ptr p_auto(new std::string("Hello")); p_scoped->size(); p_auto->size(); scoped_ptr p_another_scoped=p_scoped; auto_ptr p_another_auto=p_auto; p_another_auto->size(); (*p_auto).size(); } 这个例子不能通过编译,因为 scoped_ptr 不能被复制构造或被赋值。auto_ptr 既可以复制构造也可以赋值, 这意味着它把所有权从 p_auto 转移给了 p_another_auto, 在赋值后 p_auto 将只剩下一个空指针。这可 168 / 182 能会导致令人不快,就象你试图把 auto_ptr 放入容器内时所发生的那样。如果我们删掉对 p_another_scoped 的赋值,程序就可以编译了,但它的运行结果是不可预测的,因为它解引用了 p_auto 里 的空指针(*p_auto). 由于 scoped_ptr::get 会返回一个裸指针,所以就有可能对 scoped_ptr 做一些有害的事情,其中有两件是 你尤其要避免的。第一,不要删除这个裸指针。因为它会在 scoped_ptr 被销毁时再一次被删除。第二,不要 把这个裸指针保存到另一个 scoped_ptr (或其它任何的智能指针)里。因为这样也会两次删除这个指针,每个 scoped_ptr 一次。简单地说,尽量少用 get, 除非你要使用那些要求你传送裸指针的遗留代码! scoped_ptr 和 Pimpl 用法 scoped_ptr 可以很好地用于许多以前使用裸指针或 auto_ptr 的地方,如在实现 pimpl 用法时。[4]pimpl 用法背后的思想是把客户与所有关于类的私有部分的知识分隔开。由于客户是依赖于类的头文件的,头文件中的 任何变化都会影响客户,即使仅是对私有段或保护段的修改。pimpl 用法隐藏了这些细节,方法是将私有数据 和函数放入一个单独的类中,并保存在一个实现文件中,然后在头文件中对这个类进行前向声明并保存一个指向 该实现类的指针。类的构造函数分配这个 pimpl 类,而析构函数则释放它。这样可以消除头文件与实现细节的 相关性。我们来构造一个实现 pimpl 用法的类,然后用智能指针让它更为安全。 // pimpl_sample.hpp #if !defined (PIMPL_SAMPLE) #define PIMPL_SAMPLE class pimpl_sample { struct impl; // 译者注:原文中这句在 class 之外,与下文的实现代码有矛盾 impl* pimpl_; public: pimpl_sample(); ~pimpl_sample(); void do_something(); }; #endif 这是 pimpl_sample 类的接口。struct impl 是一个前向声明,它把所有私有成员和函数放在另一个实现文 件中。这样做的效果是使客户与 pimpl_sample 类的内部细节完全隔离开来。 // pimpl_sample.cpp #include "pimpl_sample.hpp" #include #include struct pimpl_sample::impl { void do_something_() { std::cout << s_ << "\n"; } std::string s_; 169 / 182 }; pimpl_sample::pimpl_sample() : pimpl_(new impl) { pimpl_->s_ = "This is the pimpl idiom"; } pimpl_sample::~pimpl_sample() { delete pimpl_; } void pimpl_sample::do_something() { pimpl_->do_something_(); } 看起来很完美,但并不是的。这个实现不是异常安全的!原因是 pimpl_sample 的构造函数有可能在 pimpl 被构造后抛出一个异常。在构造函数中抛出异常意味着已构造的对象并不存在,因此在栈展开时将不会调用它的 析构函数。这样就意味着分配给 pimpl_指针的内存将泄漏。然而,有一样简单的解决方法:用 scoped_ptr 来解救! class pimpl_sample { struct impl; boost::scoped_ptr pimpl_; ... }; 让 scoped_ptr 来处理隐藏类 impl 的生存期管理,并从析构函数中去掉对 impl 的删除(它不再需要,这要感 谢 scoped_ptr),这样就做完了。但是,你必须记住要手工定义析构函数;原因是在编译器生成隐式析构函数 时,类 impl 还是不完整的,所以它的析构函数不能被调用。如果你用 auto_ptr 来保存 impl, 你可以编译, 但也还是有这个问题,但如果用 scoped_ptr, 你将收到一个错误提示。 要注意的是,如果你使用 scoped_ptr 作为一个类的成员,你就必须手工定义这个类的复制构造函数和赋值操 作符。原因是 scoped_ptr 是不能复制的,因此聚集了它的类也变得不能复制了。 最后一点值得注意的是,如果 pimpl 实例可以安全地被多个封装类(在这里是 pimpl_sample)的实例所共享, 那么用 boost::shared_ptr 来管理 pimpl 的生存期才是正确的选择。用 shared_ptr 比用 scoped_ptr 的优势在于,不需要手工去定义复制构造函数和赋值操作符,而且可以定义空的析构函数,shared_ptr 被设 计为可以正确地用于未完成的类。 scoped_ptr 不同于 const auto_ptr 留心的读者可能已经注意到auto_ptr可以几乎象scoped_ptr一样地工作,只要把auto_ptr声明为const: const auto_ptr no_transfer_of_ownership(new A); 它们很接近,但不是一样。最大的区别在于 scoped_ptr 可以被 reset, 在需要时可以删除并替换被指物。而 对于 const auto_ptr 这是不可能的。另一个小一点的区别是,它们的名字不同:尽管 const auto_ptr 意 思上和 scoped_ptr 一样,但它更冗长,也更不明显。当你的词典里有了 scoped_ptr,你就应该使用它,因 为它可以更清楚地表明你的意图。如果你想说一个资源是要被限制在作用域里的,并且不应该有办法可以放弃它 的所有权,你就应该用 boost::scoped_ptr. 170 / 182 总结 使用裸指针来写异常安全和无错误的代码是很复杂的。使用智能指针来自动地把动态分配对象的生存期限制在一 个明确的范围之内,是解决这种问题的一个有效方法,并且提高了代码的可读性、可维护性和质量。scoped_ptr 明确地表示被指物不能被共享和转移。正如你所看到的,std::auto_ptr 可以从另一个 auto_ptr 那里窃取 被指物,那怕是无意的,这被认为是 auto_ptr 的最大缺点。正是这个缺点使得 scoped_ptr 成为 auto_ptr 最好的补充。当一个动态分配的对象被传送给 scoped_ptr, 它就成为了这个对象的唯一的拥有者。因为 scoped_ptr 几乎总是以自动变量或数据成员来分配的,因此它可以在离开作用域时正确地销毁对象,从而在 执行流由于返回语句或异常抛出而离开作用域时,也总能释放它所管理的内存。 在以下情况时使用 scoped_ptr : 在可能有异常抛出的作用域里使用指针 函数里有几条控制路径 动态分配对象的生存期应被限制于特定的作用域内 异常安全非常重要时(总应如此!) scoped_array 头文件: "boost/scoped_array.hpp" 需要动态分配数组时,通常最好用 std::vector 来实现,但是有两种情形看起来用数组更适合: 一种是为了 优化,用 vector 多少有一些额外的内存和速度开销;另一种是为了某种原因,要求数组的大小必须是固定的。 动态分配的数组会遇到与普通指针一样的危险,并且还多了一个(也是最常见的一个),那就是错误调用 delete 操作符而不是 delete[]操作符来释放数组。我曾经在你想象不到的地方见到过这个错误,那也是它常被用到的 地方,就是在你自己实现的容器类里!scoped_array 为数组做了 scoped_ptr 为单个对象指针所做的事情: 它负责释放内存。区别只在于 scoped_array 是用 delete[] 操作符来做这件事的。 scoped_array 是一个单独的类而不是 scoped_ptr 的一个特化,其原因是,因为不可能用元编程技术来区分 指向单个对象的指针和指向数组的指针。不管如何努力,也没有人能发现一种可靠的方法,因为数组太容易退化 为指针了,这使得没有类型信息可以表示它们是指向数组的。结果,只能由你来负责,使用 scoped_array 而 不是 scoped_ptr,就如你必须用 delete[]操作符而不是用 delete 操作符一样。这样的好处是 scoped_array 负责为你处理释放内存的事情,而你则告诉 scoped_array 我们要处理的是数组,而不是裸 指针。 scoped_array 与 scoped_ptr 非常相似,不同的是它提供了 operator[] 来模仿一个裸数组。 scoped_array 是比普通的动态分配数组更好用。它处理了动态分配数组的生存期管理问题,就如 scoped_ptr 管理对象指针的生存期一样。但是记住,多数情况下应该使用 std::vector,它更灵活、更强 大。只有当你需要确保数组的大小是固定的时候,才使用 scoped_array 来替代 std::vector. 171 / 182 shared_ptr 解析 《超越 C++标准库-Boost 库导论》 头文件: "boost/shared_ptr.hpp" shared_ptr 的基本概念 几乎所有稍微复杂点的程序都需要某种形式的引用计数智能指针。这些智能指针让我们不再需要为了管理被两个 或多个对象共享的对象的生存期而编写复杂的逻辑。当引用计数降为零,没有对象再需要这个共享的对象时,这 个对象就自动被销毁了。引用计数智能指针可以分为侵入式(intrusive)和非侵入式(non-intrusive)两类。 前者要求它所管理的类提供明确的函数或数据成员用于管理引用计数。这意味着在类的设计时就必须预见到它将 与一个侵入式的引用计数智能指针一起工作,或者重新设计它。非侵入式的引用计数智能指针对它所管理的类没 有任何要求。引用计数智能指针拥有与它所存指针有关的内存的所有权。 被管理的类可能拥有一些特性使得它更应该与引用计数智能指针一起使用。例如,它的复制操作很昂贵,或者它 所代表的有些东西必须被多个实例共享,这些特性都值得去共享所有权。还有一种情形是共享的资源没有一个明 确的拥有者。使用引用计数智能指针可以在需要访问共享资源的对象之间共享资源的所有权。引用计数智能指针 还让你可以把对象指针存入标准库的容器中而不会有泄漏的风险,特别是在面对异常或要从容器中删除元素的时 候。如果你把指针放入容器,你就可以获得多态的好处,可以提高性能(如果复制的代价很高的话),还可以通 过把相同的对象放入多个辅助容器来进行特定的查找。 在你决定使用引用计数智能指针后,你应该选择侵入式的还是非侵入式的?非侵入式智能指针几乎总是更好的选 择,由于它们的通用性、不需要修改已有代码,以及灵活性。你可以对你不能或不想修改的类使用非侵入式的引 用计数智能指针。而把一个类修改为使用侵入式引用计数智能指针的常见方法是从一个引用计数基类派生。这种 修改可能比你想象的更昂贵。至少,它增加了相关性并降低了重用性。它还增加了对象的大小,这在一些特定环 境中可能会限制其可用性。 shared_ptr 可以从一个裸指针、另一个 shared_ptr、一 个 std::auto_ptr、或者一个 boost::weak_ptr 构造。还可以传递第二个参数给 shared_ptr 的构造函数,它被称为删除器(deleter)。删除器稍后会被调用, 来处理共享资源的释放。这对于管理那些不是用 new 分配也不是用 delete 释放的资源时非常有用(稍后将看到 创建自定义删除器的例子)。shared_ptr 被创建后,它就可象普通指针一样使用了,除了一点,它不能被显式 地删除。 以下是 shared_ptr 的部分摘要;最重要的成员和相关普通函数被列出,随后是简单的讨论。 namespace boost { template class shared_ptr { public: template explicit shared_ptr(Y* p); template shared_ptr(Y* p,D d); ~shared_ptr(); 172 / 182 shared_ptr(const shared_ptr & r); template explicit shared_ptr(const weak_ptr& r); template explicit shared_ptr(std::auto_ptr& r); shared_ptr& operator=(const shared_ptr& r); void reset(); T& operator*() const; T* operator->() const; T* get() const; bool unique() const; long use_count() const; operator unspecified_bool_type() const; void swap(shared_ptr& b); }; template shared_ptr static_pointer_cast(const shared_ptr& r); } shared_ptr 的成员函数 template explicit shared_ptr(Y* p); 这个构造函数获得给定指针 p 的所有权。参数 p 必须是指向 Y 的有效指针。构造后引用计数设为 1。唯一从 这个构造函数抛出的异常是 std::bad_alloc (仅在一种很罕见的情况下发生,即不能获得引用计数器所需的 自由空间)。 template shared_ptr(Y* p,D d); 这个构造函数带有两个参数。第一个是 shared_ptr 将要获得所有权的那个资源,第二个是 shared_ptr 被销 毁时负责释放资源的一个对象,所存储的资源将以 d(p)的形式传给那个对象。因此 p 的值是否有效取决于 d。 如果引用计数器不能分配成功,shared_ptr 抛出一个类型为 std::bad_alloc 的异常。 shared_ptr(const shared_ptr& r); r 中保存的资源被新构造的 shared_ptr 所共享,引用计数加一。这个构造函数不会抛出异常。 template explicit shared_ptr(const weak_ptr& r); 从一个 weak_ptr 构造 shared_ptr。这使得 weak_ptr 的使用具有线程安全性,因为指向 weak_ptr 参数 的共享资源的引用计数将会自增(weak_ptr 不影响共享资源的引用计数)。如果 weak_ptr 为空 (r.use_count()==0), shared_ptr 抛出一个类型为 bad_weak_ptr 的异常。 template shared_ptr(std::auto_ptr& r); 这个构造函数从一个 auto_ptr 获取 r 中保存的指针的所有权,方法是保存指针的一份拷贝并对 auto_ptr 调 用 release。构造后的引用计数为 1。而 r 当然就变为空的。如果引用计数器不能分配成功,则抛出 std::bad_alloc 。 173 / 182 ~shared_ptr(); shared_ptr析构函数对引用计数减一。如果计数为零,则保存的指针被删除。删除指针的方法是调用operator delete 或者,如果程序中给定了一个执行删除操作的自定义删除器对象,就把保存的指针作为唯一参数调用 这个对象。析构函数不会抛出异常。 shared_ptr& operator=(const shared_ptr& r); 赋值操作共享 r 中的资源,并停止对原有资源的共享。赋值操作不会抛出异常。 void reset(); reset 函数用于停止对保存指针的所有权的共享。共享资源的引用计数减一。 T& operator*() const; 这个操作符返回对已存指针所指向的对象的一个引用。如果指针为空,调用 operator* 会导致未定义行为。 这个操作符不会抛出异常。 T* operator->() const; 这个操作符返回保存的指针。这个操作符与 operator*一起使得智能指针看起来象普通指针。这个操作符不会 抛出异常。 T* get() const; get 函数是当保存的指针有可能为空时(这时 operator* 和 operator-> 都会导致未定义行为)获取它的最 好办法。注意,你也可以使用隐式布尔类型转换来测试 shared_ptr 是否包含有效指针。这个函数不会抛出异 常。 bool unique() const; 这个函数在 shared_ptr 是它所保存指针的唯一拥有者时返回 true ;否则返回 false。 unique 不会抛出 异常。 long use_count() const; use_count 函数返回指针的引用计数。它在调试的时候特别有用,因为它可以在程序执行的关键点获得引用计 数的快照。小心地使用它,因为在某些可能的 shared_ptr 实现中,计算引用计数可能是昂贵的,甚至是不行 的。这个函数不会抛出异常。 operator unspecified_bool_type() const; 这是个到 unspecified_bool_type 类型的隐式转换函数,它可以在 Boolean 上下文中测试一个智能指针。 如果 shared_ptr 保存着一个有效的指针,返回值为 True;否则为 false。注意,转换函数返回的类型是不 确定的。把返回类型当成 bool 用会导致一些荒谬的操作,所以典型的实现采用了 safe bool idiom, 它很 好地确保了只有可适用的 Boolean 测试可以使用。这个函数不会抛出异常。 void swap(shared_ptr& b); 这可以很方便地交换两个 shared_ptr。swap 函数交换保存的指针(以及它们的引用计数)。这个函数不会抛 出异常。 普通函数: template shared_ptr static_pointer_cast(const shared_ptr& r); 要对保存在 shared_ptr 里的指针执行 static_cast,我们可以取出指针然后强制转换它,但我们不能把它 存到另一个 shared_ptr 里;新的 shared_ptr 会认为它是第一个管理这些资源的。解决的方法是用 static_pointer_cast. 使用这个函数可以确保被指物的引用计数保持正确。static_pointer_cast 不 174 / 182 会抛出异常。 shared_ptr 的用法 下面是一个简单易懂的例子,有两个类 A 和 B, 它们共享一个 int 实例。使用 boost::shared_ptr, 你需 要包含 "boost/shared_ptr.hpp". #include "boost/shared_ptr.hpp" #include class A { boost::shared_ptr no_; public: A(boost::shared_ptr no) : no_(no) {} void value(int i) { *no_=i; } }; class B { boost::shared_ptr no_; public: B(boost::shared_ptr no) : no_(no) {} int value() const { return *no_; } }; int main() { boost::shared_ptr temp(new int(14)); A a(temp); B b(temp); a.value(28); assert(b.value()==28); } 类 A 和 B 都保存了一个 shared_ptr. 在创建 A 和 B 的实例时,shared_ptr temp 被传送到它们 的构造函数。这意味着共有三个 shared_ptr:a, b, 和 temp,它们都指向同一个 int 实例。如果我们用 指针来实现对同一个 int 实例的共享,那么 A 和 B 表明找出它们所共享的对象何时(是否)被释放非常困难。在 这个例子中,直到 main 的结束,引用计数为 3,当所有 shared_ptr 离开了作用域,计数将达到 0,而最后 一个智能指针将负责删除共享的 int. 回顾 Pimpl 用法 前一节展示了使用 scoped_ptr 的 pimpl 用法,如果使用这种用法的类是不允许复制的,那么 scoped_ptr 在保存 pimpl 的动态分配实例时它工作得很好。但是这并不适合于所有想从 pimpl 用法中获益的类型(注意, 你还可以用 scoped_ptr,但必须手工实现复制构造函数和赋值操作符)。对于那些可以处理共享的实现细节 的类,应该用 shared_ptr。当 pimpl 的所有权被传递给一个 shared_ptr, 复制和赋值操作都是免费的。 175 / 182 你可以回忆起,当使用 scoped_ptr 去处理 pimpl 类的生存期时,对封装类的复制是不允许的,因为 scoped_ptr 是不可复制的。这意味着要使这些类支持复制和赋值,你必须手工定义复制构造函数和赋值操作 符。当使用 shared_ptr 去处理 pimpl 类的生存期时,就不再需要用户自己定义复制构造函数了。注意,这 时 pimpl 实例是被该类的多个对象所共享,因此如果规则是每个 pimpl 实例只能被类的一个实例使用,你还是 要手工编写复制构造函数。解决的方法和我们在 scoped_ptr 那看到的很相似,只是把 scoped_ptr 换成了 shared_ptr。 shared_ptr 与标准库容器 把对象直接存入容器中有时会有些麻烦。以值的方式保存对象意味着使用者将获得容器中的元素的拷贝,对于那 些复制是一种昂贵的操作的类型来说可能会有性能的问题。此外,有些容器,特别是 std::vector, 当你加 入元素时可能会复制所有元素,这更加重了性能的问题。最后,传值的语义意味着没有多态的行为。如果你需要 在容器中存放多态的对象而且你不想切割它们,你必须用指针。如果你用裸指针,维护元素的完整性会非常复杂。 从容器中删除元素时,你必须知道容器的使用者是否还在引用那些要删除的元素,不用担心多个使用者使用同一 个元素。这些问题都可以用 shared_ptr 来解决。下面是如何把共享指针存入标准库容器的例子。 #include "boost/shared_ptr.hpp" #include #include class A { public: virtual void sing()=0; protected: virtual ~A() {}; }; class B : public A { public: virtual void sing() { std::cout << "Do re mi fa so la"; } }; boost::shared_ptr createA() { boost::shared_ptr p(new B()); return p; } int main() { typedef std::vector< boost::shared_ptr > container_type; typedef container_type::iterator iterator; container_type container; for (int i=0;i<10;++i) { container.push_back(createA()); } 176 / 182 std::cout << "The choir is gathered: \n"; iterator end=container.end(); for (iterator it=container.begin();it!=end;++it) { (*it)->sing(); } } 这里有两个类, A 和 B, 各有一个虚拟成员函数 sing。B 从 A 公有继承而来,并且如你所见,工厂函数 createA 返回一个动态分配的B的实例,包装在shared_ptr里 。在 main里, 一个包含shared_ptr 的 std::vector 被放入 10 个元素,最后对每个元素调用 sing。如果我们用裸指针作为元素,那些对象需要 被手工删除。而在这个例子里,删除是自动的,因为在 vector 的生存期中,每个 shared_ptr 的引用计数都 保持为 1;当 vector 被销毁,所有引用计数器都将变为零,所有对象都被删除。 上面的例子示范了一个强有力的技术,它涉及 A 里面的 protected 析构函数。因为函数 createA 返回的是 shared_ptr, 因此不可能对 shared_ptr::get 返回的指针调用 delete 。这意味着如果为了向某个需 要裸指针的函数传送裸指针而从 shared_ptr 中取出裸指针的话,它不会由于意外地被删除而导致灾难。那么, 又是如何允许 shared_ptr 删除它的对象的呢? 这是因为指针指向的真正类型是 B; 而 B 的析构函数不是 protected 的。这是非常有用的方法,用于给 shared_ptr 中的对象增加额外的安全性。 shared_ptr 与其它资源 有时你会发现你要把 shared_ptr 用于某个特别的类型,它需要其它清除操作而不是简单的 delete. shared_ptr 可以通过自定义删除器来支持这种需要。那些处理象 FILE*这样的操作系统句柄的资源通常要使 用象 fclose 这样的操作来释放。要在 shared_ptr 里使用 FILE* ,我们要定义一个类来负责释放相应的资 源。 class FileCloser { public: void operator()(FILE* file) { std::cout << "The FileCloser has been called with a FILE*, " "which will now be closed.\n"; if (file!=0) fclose(file); } }; 这是一个函数对象,我们用它来确保在资源要释放时调用 fclose 。下面是使用 FileCloser 类的示例程序: int main() { std::cout << "shared_ptr example with a custom deallocator.\n"; { FILE* f=fopen("test.txt","r"); if (f==0) { std::cout << "Unable to open file\n"; throw "Unable to open file"; } 177 / 182 boost::shared_ptr my_shared_file(f, FileCloser()); // 定位文件指针 fseek(my_shared_file.get(),42,SEEK_SET); } std::cout << "By now, the FILE has been closed!\n"; } 注意,在访问资源时,我们需要对 shared_ptr 使用 &* 用法, get, 或 get_pointer。(请注意最好使用 &*. 另两个选择不太清晰) 这个例子还可以更简单,如果我们在释放资源时只需要调用一个单参数函数的话,就根 本不需要创建一个自定义删除器类型。上面的例子可以重写如下: { FILE* f=fopen("test.txt","r"); if (f==0) { std::cout << "Unable to open file\n"; throw file_exception(); } boost::shared_ptr my_shared_file(f,&fclose); // 定位文件指针 fseek(&*my_shared_file,42,SEEK_SET); } std::cout << "By now, the FILE* has been closed!\n"; 自定义删除器在处理需要特殊释放程序的资源时非常有用。由于删除器不是 shared_ptr 类型的一部分,所以 使用者不需要知道关于智能指针所拥有的资源的任何信息(当然除了如何使用它!)。例如,你可以使用对象池, 定制删除器只需简单地把对象返还到池中。或者,一个 singleton 对象应该使用一个什么都不做的删除器。 使用定制删除器的安全性 我们已经看到对基类使用 protected 析构函数有助于增加使用 shared_ptr 的类的安全性。另一个达到同样 安全级别的方法是,声明析构函数为 protected (或 private) 并使用一个定制删除器来负责销毁对象。这 个定制删除器必须是它要删除的类的友元,这样它才可以工作。封装这个删除器的好方法是把它实现为私有的嵌 套类,如下例所示: #include "boost/shared_ptr.hpp" #include class A { class deleter { public: void operator()(A* p) { delete p; } }; friend class deleter; public: virtual void sing() { 178 / 182 std::cout << "Lalalalalalalalalalala"; } static boost::shared_ptr createA() { boost::shared_ptr p(new A(),A::deleter()); return p; } protected: virtual ~A() {}; }; int main() { boost::shared_ptr p=A::createA(); } 注意,我们在这里不能使用普通函数来作为 shared_ptr 的工厂函数,因为嵌套的删除器是 A 私有的。使 用这个方法,用户不可能在栈上创建 A 的对象,也不可能对 A 的指针调用 delete 。 从 this 创建 shared_ptr 有时候,需要从 this 获得 shared_ptr,即是说,你希望你的类被 shared_ptr 所管理,你需要把"自身" 转换为 shared_ptr 的方法。看起来不可能?好的,解决方案来自于我们即将讨论的另一个智能指针 boost::weak_ptr. weak_ptr 是 shared_ptr 的一个观察者;它只是安静地坐着并看着它们,但不会影 响引用计数。通过存储一个指向 this 的 weak_ptr 作为类的成员,就可以在需要的时候获得一个指向 this 的 shared_ptr。为了你可以不必编写代码来保存一个指向 this 的 weak_ptr,接着又从 weak_ptr 获得 shared_ptr,Boost.Smart_ptr 为此提供了一个助手类,称为 enable_shared_from_this. 只要简单 地让你的类公有地派生自 enable_shared_from_this,然后在需要访问管理 this 的 shared_ptr 时,使 用函数 shared_from_this 就行了。下面的例子示范了如何使用 enable_shared_from_this : #include "boost/shared_ptr.hpp" #include "boost/enable_shared_from_this.hpp" class A; void do_stuff(boost::shared_ptr p) { ... } class A : public boost::enable_shared_from_this { public: void call_do_stuff() { do_stuff(shared_from_this()); } }; int main() { boost::shared_ptr p(new A()); 179 / 182 p->call_do_stuff(); } 这个例子还示范了你要用 shared_ptr 管理 this 的情形。类 A 有一个成员函数 call_do_stuff 需要调用 一个普通函数 do_stuff, 这个普通函数需要一个类型为 boost:: shared_ptr的参数。现在,在 A::call_do_stuff 里, this 不过是一个 A 指针, 但由于 A 派生自 enable_shared_from_this, 调 用 shared_from_this 将返回我们所要的 shared_ptr 。在 enable_shared_from_this 的成员函数 shared_from_this 里,内部存储的 weak_ptr 被转换为 shared_ptr, 从而增加了相应的引用计数,以 确保相应的对象不会被删除。 总结 引用计数智能指针是非常重要的工具。Boost 的 shared_ptr 提供了坚固而灵活的解决方案,它已被广泛用 于多种环境下。需要在使用者之间共享对象是常见的,而且通常没有办法通知使用者何时删除对象是安全的。 shared_ptr 让使用者无需知道也在使用共享对象的其它对象,并让它们无需担心在没有对象引用时的资源释 放。这对于 Boost 的智能指针类而言是最重要的。你会看到 Boost.Smart_ptr 中还有其它的智能指针,但这 一个肯定是你最想要的。通过使用自定义删除器,几乎所有资源类型都可以存入 shared_ptr。这使得 shared_ptr 成为处理资源管理的通用类,而不仅仅是处理动态分配对象。与裸指针相比,shared_ptr 会有 一点点额外的空间代价。我还没有发现由于这些代价太大而需要另外寻找一个解决方案的情形。不要去创建你自 己的引用计数智能指针类。没有比使用 shared_ptr 智能指针更好的了。 在以下情况时使用 shared_ptr : 当有多个使用者使用同一个对象,而没有一个明显的拥有者时 当要把指针存入标准库容器时 当要传送对象到库或从库获取对象,而没有明确的所有权时 当管理一些需要特殊清除方式的资源时[9] shared_array 头文件: "boost/shared_array.hpp" shared_array 用于共享数组所有权的智能指针。它与 shared_ptr 的关系就如 scoped_array 与 scoped_ptr 的关系。shared_array 与 shared_ptr 的不同之处主要在于它是用于数组的而不是用于单个 对象的。在我们讨论 scoped_array 时,我提到过通常 std::vector 是一个更好的选择。但 shared_array 比 vector更有价值,因为它提供了对数组所有权的共享。shared_array 的接口与 shared_ptr 非常相似, 差别仅在于增加了一个下标操作符,以及不支持定制删除器。 由于一个指向 std::vector 的 shared_ptr 提供了比 shared_array 更多的灵活性,所以我们就不对 shared_array 的用法进行讨论了。如果你发现自己需要 boost::shared_array, 可以参考一下在线文档。 180 / 182 C++中资源的管理 以对象管理资源《Effective C++》第三版条款 13 把资源放进对象内,我们便可以依赖 C++的“析构函数自动调用机制”确保资源被释放。 181 / 182 在资源管理类中小心 copying 行为《Effective C++》第三版条款 14 在资源管理类中提供对原始资源的访问《Effective C++》第三版条款 15 成对使用 new 和 delete 时要采取相同的形式《Effective C++》第三版条款 16 即将被删除的那个指针,所指的是单一对象还是一个数组?这个是必须明确的问题。因为单一对象的内存布局一 般不同于数组的内存布局。更明确的说,数组所用的内存通常还包括“数组大小”的记录,以便 delete 知道 需要调用多少次析构函数,而单一对象的内存则没有这笔记录。直观的,可以把两种不同的内存布局想象如下: 其中 n 是对象数组的大小。当然,这只是一个示例,编译器不需要非得这么实现,虽然实际上许多编译器确实 是这样做的。 182 / 182 以独立语句将 newed 对象置入智能指针《Effective C++》第三版条款 17
还剩181页未读

继续阅读

pdf贡献者

andykobe

贡献于2013-03-21

下载需要 15 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!