第五部分
1、用于大型程序的工具
【异常处理】
异常是通过
抛出对象而引发的。该对象的类型决定对应该激活哪个处理代码。执行
throw时,不会执行跟在throw后面的语句,而是将控制从throw转移到匹配的
catch。异常对象通过
复制
被抛出表达式的结果创建,该结果必须是可以复制的类型。
当抛出一个表达式的时候,被抛出对象的
静态编译时类型将决定异常对象的类型。
抛出指针要求在对应处理代码存在的任意地方存在指针所指向的对象。
因异常而退出函数时,编译器保证适当地
撤销局部对象。析构函数应该从不抛出异常。
在发生异常时自动撤销局部对象,因此运行析构函数对应用程序的设计很重要。
catch子句中
异常说明符的类型决定了处理代码能够捕获的异常种类。异常说明符后可跟一个(可选)形参名的类型名。
在查找匹配的catch期间,找到的catch不必是与异常最匹配的catch,而将选中第一个找到的可以处理该异常的catch。异常的类型与catch说明符匹配
可发生的转换有:
1、允许从非const到const的转换。
2、允许从派生类型转换到基类类型。
3、将数组转换为指向数组的指针,将函数转换为指向函数类型的适当指针。
异常对象本身是被抛出对象的副本。是否再次将异常对象复制到catch位置取决于异常说明符类型。(说明符不是引用时,就复制。是引用时,则该形参只是异常对象的另一名字)
带有因
继承而相关的类型的多个catch子句,必须从最低派生类型到最高派生类型排序。
重新抛出是后面不跟类型或表达式的一个throw【
throw;】,空throw语句将重新抛出异常对象,
它只能出现在catch或者从catch调用的函数中。被抛出的异常是
原来的异常对象,而不是catch形参。
当catch形参是基类类型的时候,不能知道由重新抛出的表达式抛出的实际类型,该类型取决于异常对象的动态类型。
捕获所有异常【catch(...)】,与任意类型的异常都匹配,经常与重新抛出表达式结合使用,此时它必须是最后一个,否则,任何跟在它后面的catch子句都将不能被匹配。
为了处理来自
构造函数初始化式的异常,必须将构造函数编写为
函数测试块。
【exception类型】所定义的唯一操作是一个名为what的虚成员,该函数返回const char*对象,它一般返回用来在抛出位置构造异常对象的信息。
通过
定义一个类来封装资源的分配和释放,可以保证正确释放资源。这一技术常称为
“资源分配即初始化”,简称RAII
【auto_ptr类】“资源分配即初始化”技术例子。库类模板
auto_ptr只能用于
管理从new返回的一个对象(即只能保存一个指向对象的指针),它不能管理动态分配的
数组。当auto_ptr对象超出作用域或者另外撤销时,就
自动回收auto_ptr所指向的动态分配对象。(如果通过
常规指针分配内存,而且
在执行delete之前发生异常,就不会自动释放内存。
auto_ptr类是
接受单个类型形参的模板,该类型指定auto_ptr可以绑定的对象的类型。接受指针的构造函数为
explicit构造函数,所以必须使用初始化的直接形式来创建auto_ptr对象,如果不给定初始化式,auto_ptr
对象未绑定,不指向对象,不能解引用。
auto_ptr在保证自动删除auto_ptr对象引用的同时,支持普通指针式行为。
当
复制auto_ptr对象或者将它的
值赋给其他auto_ptr对象时,将基础对象的所有权从原来的auto_ptr对象
转给副本,
原来的auto_ptr对象重置为未绑定状态。
因为复制和赋值是破坏性操作,所以不能将auto_ptr对象存储在标准容器中。(标准容器类要求复制或赋值之后两个对象相等)
使用
get成员返回包含在auto_ptr对象中的基础指针,为了确定auto_ptr是否指向一个对象,可以将get返回值与0比较,不能用get作为创建其他auto_ptr对象的实参。
不能直接将一个地址(或者其他指针)赋给auto_ptr对象,可调用
reset函数来改变指针。
auto_ptr缺陷:
1、不要使用auto_ptr对象保存指向静态分配对象的指针。
2、不要使用两个auto_ptr对象指向同一对象。
3、不要使用auto_ptr对象保存指向动态分配数组的指针。【删除只删除一个对象,使用delete,而数组需要delete [ ]
4、不要将auto_ptr对象存储在容器中。【复制或赋值后右操作数为未绑定状态
【异常说明】
异常说明跟在函数形参表之后,一个异常说明在关键字
throw之后跟着一个由圆括号括住的
异常类型列表,若列表为空,指出函数
不抛出任何异常。若函数没有指定异常说明,则该函数可以
抛出任意类型的异常。
如果函数抛出了
没有在其异常说明中列出的异常,就调用标准库函数
unexpected。默认情况下,unexpected函数调用terminate函数,terminate函数一般终止程序。
基类中虚函数的异常说明,可以与
派生类中对应虚函数的异常说明符不同,但其异常说明必须与对应基类函数的异常说明同样严格,或者比后者更受限。
派生类不能在异常说明列表中增加异常,如果通过基类指针或引用进行函数调用,那么,这些类的用户涉及的应该只是在基类中指定的异常。基类中的异常列表是虚函数的派生类版本可以抛出异常的
超集。
异常说明是函数类型的一部分,所以也可以在
函数指针的定义中提供异常说明。在用另一指针初始化带异常说明的函数指针,或者将后者赋值给函数地址的时候,两个指针的异常说明不必相同,但是源指针的异常说明必须至少与目标指针的一样严格。
【命名空间】
命名空间可以在全局作用域或其他作用域内部定义,但
不能在函数或类内部定义。
一个命名空间的分离部分
可以分散在多个文件中,在不同文本文件中的命名空间定义也是累积的。
因为
全局命名空间是隐含的,它没有名字,所以记号
::member_name引用全局命名空间的成员。
未命名的命名空间在定义时没有给定名字。未命名的命名空间可以在给定文件中
不连续,
但不能跨越文件。如果在文件的
最外层作用域中定义未命名的命名空间,那么未命名的命名空间中的名字必须与全局作用域中定义的
名字不同。如果头文件定义了未命名的命名空间,那么,在每个包含该头文件的文件中,该命名空间中的名字将定义
不同的局部实体。所以未命名的命名空间可
取代文件中的静态声明。【
文件静态:用关键字static声明的
局部于文件的名字。】
命名空间可以通过namespace::
member_name、using声明【using namespace::member_name;一次只引入一个命名空间成员 】、
using指示【using namespace 命名空间名字;使用多个库时,容易出现全局命名空间污染】和
命名空间别名【namespace 别名 = 原名;】使用。
接受类类型形参(或类类型指针及引用形参)的且
与类本身定义在同一命名空间中的函数(包括重载操作符),
在用类类型对象(或类类型的引用及指针)作为实参的时候是可见的。
没有办法编写
using声明来引用
特定函数声明,函数名字的using声明声明了
所有具有该名字的函数。
using指示将命名空间成员提升到外围作用阈。
【多重继承与虚继承】
基类构造函数按照
基类构造函数在
类派生列表中
的出现次序调用【首先运行最终基类的构造函数】
。析构函数总是按构造函数运行的逆序调用。
派生类的指针或引用可以转换为其任意基类的指针或引用。与单继承一样,
用基类的指针或引用只能访问基类中定义(或继承)的成员,不能访问派生类中引用的成员。【
所以基类指针或引用若不强制转换,则只能通过虚函数才能访问到派生类中成员。
当一个类有多个基类的时候,通过所有直接基类
同时进行名字查找。多重继承的派生类有可能从两个或多个基类继承同名成员,对该成员不加限定的使用是二义性的。
虚继承是一种机制,类通过虚继承指出它希望
共享其虚基类的状态。在虚继承下,对给定虚基类,无论该类在派生层次中作为虚基类出现多少次,只继承一个共享的基类子对象。
共享的基类子对象称为虚基类。通过在派生列表中包含
关键字virtual设置虚基类。
在虚派生中,由
最低层派生类的构造函数初始化虚基类。虽然由最底层派生类初始化虚基类,但是任何直接或间接继承虚基类的类一般必须为该基类提供自己的初始化式。
无论虚基类出现在继承层次中任何地方,总是在构造非虚基类之前构造虚基类。
在多重继承层次中作为基类的类通常应该将它们的
析构函数定义为虚函数。
2、特殊工具与技术
【优化内存分配】
分配原始内存时,必须在该内存中构造对象;在释放该内存之前,必须保证适当地撤销这些对象。
C++提供下面两种方法分配和释放未构造的原始内存:
1、allocator类,它提供可感知类型的内存分配。这个类支持一个抽象接口,以分配内存并随后使用该内存保存对象。
2、标准库中的operator new和operator delete,它们分配和释放需要大小的原始的、未知类型化的内存。
allocator类将内存分配和对象构造分开。【并使用复制构造
使用new表达式时实际发生三个步骤:1、调用标准库函数
operator new 分配原始的未进行类型化的内存;2、运行类型的
构造函数;3、返回
对象指针
使用delete表达式时实际发生两个步骤:1、运行类型的
析构函数;2、调用标准库函数
operator delete 释放对象所用内存
一般而言,使用allocator比直接使用operator new和operator delete函数更为类型安全。operator new和operator delete是allocator的allocate和deallocate成员的低级版本,他们都
分配但不初始化内存。
定位new表达式在已分配的原始内存中
初始化一个对象,它与new的其他版本的不同之处在于,
它不分配内存。定位new表达式比allocator类的construct成员更灵活,
它可以使用任何构造函数,并直接建立对象。而construct函数总是使用复制构造函数。
定位new表达式是使用allocator类的construct成员的低级选择,而
使用析构函数的显示调用可作为调用destroy函数的低级选择。显示调用析构函数适当地
清除对象本身,
但是并没有释放对象所占的内存,需要使用operator delete函数来释放指定内存。
【类特定的new和delete】
通过定义自己的名为operator new和operator delete的成员,
类可以管理用于自身类型的内存。类成员
operator new函数必须具有
返回类型void*并接受size_t类型的形参(以字节计算的分配内存量初始化函数)。
operator delete函数具有
返回类型void。它可以接受
单个形参void*也可以接受void*和size_t类型
两个形参。成员new和delete函数必须是
静态的,因为这些函数实际是没有成员数据可操纵的。
【内存分配器基类】
通用策略:预先分配一块原始内存来保存
未构造的对象,
创建新元素的时候,可以在一个预先分配的对象中构造;
释放元素的时候,将它们
放回预先分配对象的块中,而不是将内存实际返回给系统。这种策略常被称为维持一个
自由列表。可以将自由列表实现为
已分配但未构造的对象的链表。
定义内存分配器基类来处理自由列表。
该类只能用于不包含在继承层次中的类型,它没有办法根据对象的实际类型分配不同大小的对象,其自由列表保存单一大小的对象,因此,
它只能用于不作基类使用的类。
用来实例化内存分配器基类的模板类型将是派生类型本身。
【运行时类型识别】RTTI
通过RTTI,程序能够
使用基类的指针或引用来检索这些指针或引用所指对象的实际派生类型。通过下面两个操作符提供RTTI:
1、typeid操作符,返回指针或引用所指对象的实际类型
2、dynamic_cast操作符
,将
基类类型的指针或引用安全地转换为派生类型的指针或引用
。
这些操作符
只为带有一个或多个虚函数的类返回
动态类型信息
,
对于其他类型,返回
静态(即编译时)类型的信息
。
当具有基类的引用或指针,但需要执行不是基类组成部分的派生类操作时,需要
动态强制类型转换。通常,
从基类指针获得派生类行为最好的方法是通过虚函数,当使用虚函数时,编译器自动根据对象的
实际类型选择正确的函数。
【dynamic_cast】
将基类类型对象的引用或指针转换为
同一继承层次中其他类型的引用或指针。该操作符涉及
运行时类型检查,首先
验证被请求的转换是否有效,只有转换有效,操作符才实际进行转换。
可以对值为0的指针应用,其结果为0.
如果
转换到指针类型失败,则其结果是0,如果
转换到引用类型失败,则抛出一个bad_cast类型的异常。
【typeid】
typeid(e)//e是任意表达式或者类型名
如果
操作数不是类类型或者没有虚函数的类,则该操作符指出操作数的
静态类型;如果
操作数是定义了至少一个虚函数的类类型,则在
运行时计算类型。该操作符结果是
type_info的标准库类型的对象引用。type_info类确切的定义随编译器而变化。
只有当typeid的操作数是带虚函数的类类型的对象时,才返回动态类型信息。测试指针返回指针的静态的、编译时类型。
【类成员的指针】
数据成员的指针:【类型 类名::*】
成员函数的指针:【类型 (类名::*)(形参表)】
类型别名(typedef)可以使成员指针更容易阅读。
操作符.*:从对象或引用获取成员
操作符->*:通过对象的指针获取成员
因为
调用操作符()比成员指针操作符优先级高,所以调用成员函数指针时注意括号()的使用。函数表是函数指针的集合,在运行时从中选择给定调用。
T C::*pmem = &C::member;//指向类C中类型为T的成员,并将pmem初始化为C中成员member
classobj.*pmem;
classptr->*pmem;
【嵌套类】
在另一个类内部定义一个类。外围类和嵌套类的对象是
互相独立的。嵌套类的名字
在其外围类的作用域中可见,但在其他类作用域或定义外围类的作用域中
不可见。
在其
类外部定义的嵌套类成员,必须定义在定义外围类的
同一作用域中。在其类外部定义的嵌套类成员,
不能定义在外围类内部,嵌套类的成员不是外围类的成员。
在
外围类外部定义嵌套类时必须用
外围类的名字限定嵌套类的名字,而且必须在外围类的定义体中
声明嵌套类。
嵌套类
静态成员的定义需要放在外层作用域中。
外围作用域的对象与其嵌套类型的对象之间没有联系,
不能使用this指针访问对方成员。
但嵌套类可以直接引用外围类的静态成员、类型名和枚举成员
实例化外围类模板的时候,
不会自动实例化类模板的嵌套类。只有当
在需要完整类类型的情况下使用嵌套类本身的时候,才会实例化嵌套类。
【联合:节省空间的类】
联合(union):一种特殊的类。一个union对象可以有多个数据成员,
但在任何时刻,只有一个成员可以有值。当将一个值赋给union对象的一个成员时,其他所有成员都变为未定义。
默认情况下,union表现的像struct,除非另外指定,否则union的成员都为public。
union不能作为
基类
使用,即其成员函数不能为
虚函数
,并且union不能具有
静态数据成员或引用成员
,不能具有定义了构造函数、析构函数或赋值操作符的
类类型成员。
定义一个单独的对象跟踪union中存储了什么值,这个附加对象称为
union的判别式。
union最经常用作嵌套类型,其中判别式是外围类的一个成员。
匿名联合:不用于定义对象的未命名union。匿名union的成员的名字出现在
外围作用域中。
匿名union不能有私有成员或受保护成员,也不能定义成员函数。
【局部类】
在函数体内部定义的类。一个局部类定义了一个类型,该类型
只在定义它的局部作用域中可见。
局部类的所有成员必须完全定义在类定义体内部。
不允许局部类声明static数据成员,因为没有办法定义它们。
局部类只能访问在外围作用域中定义的
类型名、static变量和枚举成员,不能使用外围函数中定义的
局部变量。
嵌套在局部类中的类本身是一个带有所有附加限制的局部类,嵌套类的所有成员必须在嵌套类本身定义体内部定义。
【固有的不可移植的特征】
【1、算术类型的大小随机器不同而变化。】
【2、位域】
位域:一种特殊的类数据成员,用来保存特定的位数。当程序需要将
二进制数据传递给另一程序或硬件设备的时候,通常使用位域。位域在内存中的布局是机器相关的。
位域必须是整型数据类型,可以是signed或unsigned。通常最好将位域设为unsigned类型。存储在signed类型中的位域的行为由实现定义。
通过在成员名后面接
一个冒号以及指定位数的
常量表达式,指出成员是一个位域。
如果可能,将类中以连续次序定义的位域
压缩到公共整型值中。
【3、volatile限定符】
volatile的确切含义与机器相关,只能通过阅读编译器文档来理解。
可使与硬件接口更容易。
直接处理硬件的程序常具有这样的成员,它们的值由程序本身直接控制之外的过程所控制。
类型限定符。告诉编译器可以在程序的直接控制之外改变一个变量。关键字volatile是给编译器的指示,指出对这样的对象不应该执行优化。
类似const。volatile对象只能调用volatile成员函数。volatile 与const一个重要区别是,不能使用
合成的复制和赋值操作符从volatile对象进行初始化或赋值。它必须定义自己的复制构造函数和赋值操作符版本。
【4、链接指示】
C++使用链接指示指出
任意非C++函数所用的语言。
允许从C++程序调用不同语言编写的函数,所有编译器都支持调用C和C++函数,是否支持任何其他语言随编译器而定。
链接指示有两种形式:
单个的或复合的。链接指示不能出现在类定义或函数定义的内部,
它必须出现在函数的第一次声明上。
第一种形式由
关键字extern后接
字符串字面值,再接“普通“函数声明构成。字符串字面值指出编写函数
所用语言。第二种形式是通过将几个函数的声明放在跟在
链接指示之后的花括号内部,可以给它们设定
相同的链接。
可以将链接指示应用于
头文件。即将
#include指示放在复合链接指示的花括号中。
通过
对函数定义使用链接指示,使得用其他语言编写的程序可以使用C++函数。
链接指示支持语言:【extern "C"】、【extern "Ada" 】、【extern "FORTRAN"】
对链接到C的预处理器支持:当编译C++时,自动定义预处理器名字__cplusplus。
#ifdef
__cplusplus
extern "C"
#endif
int strcmp(const char*, const char*);
C++保持支持的唯一语言是C,
C语言不支持函数重载,在C++程序中,重载C函数很常见,但是,重载集合中的其他函数必须都是C++函数。
C函数的指针与C++函数的指针具有不同的类型,两者不能相互赋值。