<!-- @page { size: 21.59cm 27.94cm; margin: 2cm } P { margin-bottom: 0.21cm } -->
第 16 章 模板与泛型编程
16.1
函数模板:模板形参表不能为空。模板形参可以是类型形参,跟在 class或 typename后面;也可以是表示常量表达式的非类型形参
inline函数模板: inline放在模板形参表后面
模板也像函数或类一样,可以只声明不定义
在模板内部定义类型成员:在成员名字前加上 typename关键字
非类型模板形参:在调用函数时非类型形参将用值代替,值的类型在模板形参表中指定。非类型形参是模板内部的常量值
编写模板代码时,对模板实参类型的要求尽可能少是很有益的
使用类模板时,必须指定模板实参;使用函数模板时,编译器为我们推断模板实参
16.2
模板实例化:接受 const引用或指针的函数可以用非 const引用或指针来调用,无须产生新的实例化。如果函数接受非引用类型,形参类型和实参类型都忽略 const,既无论传递 const或非 const对象给接受非引用类型的函数,都使用相同的实例化。如果模板形参不是引用类型,则对数组或函数类型的实参应用常规指针转换。数组实参当作指向第一个元素的指针,函数实参当作指向函数类型的指针
获取函数模板实例化的地址的时候,上下文必须是这样的:它允许为每个模板形参确定唯一的类型或值
可以使用函数模板对函数指针进行初始化或赋值,这时编译器使用指针的类型实例化该模板
template<typename T> int compare(const T&,const T&);
int (*pf1)(const int&,const int&)=compare; //ok
指定显式模板实参:
template<typename T1,typename T2,typename T3>
T1 sum(T2,T3);
long val3=sum< long>(i,lng);
16.3
模板编译模型:包含编译模型、分别编译模型
包含编译模型:所有编译器都支持。在 hpp头文件中定义模板,最后添加一条 #include指示引入模板的 cpp实现文件。头文件中最好要有头文件保护符
分别编译模型:在模板的实现文件中用 export关键字导出,而在模板定义的头文件中不必 export。在一个程序中,一个 模板只能定义为导出一次
函数模板:一般在实现文件中指定为 export,在定义的头文件不必 export
类模板:内联成员函数与类模板定义一起放在头文件中,非内联成员函数和 static数据成员的定义放在实现文件中。 在实现文件中应该导出类模板,以便让编译器了解要记住该模板定义。如果只想导出个别成员,则非导出成员的定义放在头文件中,实现文件中只在被导出的成员定义上指定 export。
16.4
在类模板的作用域内部,可以用它的非限定名字引用该类
类模板定义中的复制控制成员(默认构造函数、复制构造函数、赋值操作符、析构函数)可以省略模板形参,但一般不建议这样做
类模板成员函数的实现:必须以关键字 template开头,后接类的模板形参表;必须指出这它是哪个类的成员;类名必须包含其模板形参
类模板的成员函数本身也是函数模板,传递的实参能够进行常规转换
类模板的成员函数只有为程序所用才进行实例化。定义模板类型对象时,导致实例化类模板,并且也会实例化其对应的构造函数,以及该构造函数调用的任意成员
类模板的指针定义不会对类进行实例化,只有用到这样的指针时(如用指针来调用成员函数)才会对类进行实例化
非类型模板实参必须是编译时常量表达式
非模板类或非模板函数可以是类模板的友元;友元可以是类模板或函数模板,这时模板的所有实例都是友元;类也可以设定特定的模板实例为友元,这时模板实参放在紧接模板名的后面
注意:编译器将友元声明也当作类或函数的声明对待,因此不需要再次声明
成员模板不能为虚
在类模板的外部实现模板时,必须包含两个模板形参表,先是类的模板形参表,后是成员自己的模板形参表。
类模板的 static成员:模板的每个实例化都有自己的 static成员。每个实例化表示不同的类型,所以给定实例化的对象都共享一个 static成员
16.6
函数模板的特化: template后接一对空的尖括号 <>,再接模板名和一对尖括号,尖括号中指定特化的模板形参,后面接着是函数形参表和函数体。这样遇到相应的实参类型时调用特化版本,否则就调用泛型版本。一般先定义模板的泛型版本,接着定义其特化版本。
如果缺少特化语法 template<>,结果是声明该函数的重载非模板版本,这里实参与形参之间可以使用常规转换。而在模板特化版本的调用中,实参类型必须与特化版本函数的形参类型完全匹配,否则编译将为实参从泛型模板实例化一个相应的类型
最佳实践:与其他函数声明一样,应在一个头文件中包含模板特化的声明,然后使用该特化的每个源文件包含该头文件
声明模板特化时,如果可以从函数形参表推断模板实参,则可以省略模板名后面的模板实参
类模板的特化:与函数模板特化类似,特化的模板形参放在类名后
特化可以定义与模板本身完全不同的成员。但一般类模板特化应该与它所特化的模板定义相同的接口,否则当用户试图使用未定义的成员时会感到奇怪
特化成员而不特化类:因为类的成员函数本质上是函数模板,故可以在成员函数的实现中用 template<>,函数名后接特化形参,这样就只特化了相应的成员
类模板的部分特化:若类模板有多个模板形参,可以特化某些模板形参。特化版本与通用版本可以有不同的成员。
当声明了部分特化时,编译器将为实例化选择最特化的模板定义,否则就选择通用模板定义
16.7
函数模板可以重载:可以定义有相同名字但形参数目或类型不同的多个函数模板,也可以定义与函数模板有相同名字的普通非模板函数
函数核匹配:当都完全匹配时(无任何转换),普通函数优于同名的函数模板
最佳实践:定义函数模板特化,几乎总是比重载为非模板版本要好
总结:
函数模板、类模板、模板形参(类型形能、非类型形参)、模板的实例化、模板实参推断、模板的显示实参、模板编译模型(包含编译模型、分别编译模型)、模板中的友元声明、成员模板、类模板的 static成员、泛型句柄类、函数模板的特化与重载、类模板的特化、类模板的部分特化
第 17 章 用于大型程序的工具
内容:异常处理、命名空间、多重继承
17.1
异常机制:可以将程序的错误检测部分与错误处理部分分离
抛出类类型的异常:异常是可传给非引用形参的任意类型的对象,这说明必须能够复制该类型的对象
隐式(即由系统抛出)或显示的 throw会初始化一个异常对象,它由编译器管理,并能驻留在任意 catch都可以访问的空间中。异常对象将给对应的 catch,并且完全处理了异常之后撤销
异常对象通过复制 throw表达式的结果而创建,该表达式结果必须是可以复制的类型
被抛出对象的静态编译时类型决定异常对象的类型。因此若基类类型指针指向派生类异常对象,则这个对象看作是基类类型,将被分割,只抛出基类部分。因此我们一般不抛出指针,尤其不应该抛出指向局部对象的指针
range_error r(“error”);
throw r; //r的类型是 range_error
exception *p=&r;
throw *p; //抛出的异常对象 *p,与 p的静态类型相匹配,为 exception类型
栈展开:抛出异常时,会沿着函数调用链向上寻找 catch块,退出每个函数时,会此函数释放局部对象所用的内存并运行类类型局部对象的析构函数。如果找不到匹配的 catch,程序就调用 terminate函数
若一个块用 new动态分配了内存,该块异常而退出,则栈展开期间不会删除该指针
在为某个异常进行栈展开时,析构函数如果又抛出异常,将会导致调用标准库 terminate函数,一般 terminate函数将调用 abort函数(在 <cstdlib>中定义),强制退出整个程序。因此析构函数应该从不抛出异常
在查找匹配的 catch时,将选中第一个找到的可以处理异常的 catch(不一定是整个 catch链中最匹配的那个)
异常与 catch中的形参说明符匹配规则:允许从非 const到 const的转换;允许从派生类到基类类型的转换;将数组或函数转换为相应的指针
进入 catch时,用抛出的异常对象初始化 catch的形参。基类的异常说明符可以用于捕获派生类型的异常对象,这时由于静态类型是基类, catch不能使用派生类特有的任何成员
最佳实践:带当然 catch的形参说明符也可以是引用。通常如果 catch子句处理因继承而相关的类型异常,它就应该将自己的形定义为引用。这时 catch对象就直接访问异常对象(而不是其副本),不受静态类型的影响
有因继承而相关的类型的多个 catch子句,必须从最低派生类型到最高派生类型排序
若 catch不能完全处理一个异常,则可用空 throw语句重新抛出异常对象,向上传递。注意重新抛出的异常是原来的异常对象,而不是 catch形参。
空 throw语句:用于重新抛出异常,只能出现在 catch或者在 catch处调用的函数中
捕获所有异常:用 catch(...){/*code*/},它通常与重新抛出表达式结合使用, catch完成可做的局部工作,然后重新抛出异常
构造函数内部的 catch子句不能处理初始化列表中发生的异常。这时要编写成函数测试块的形式,即将初始化列表及函数体一起放在 try测试块中,这时后面的 catch子句即可以处理初始化列表中的异常,也可以处理构造函数函数体中的异常
exception类定义的唯一操作是 what虚成员,因为是虚函数,如果捕获了基类引用,对 what函数调用将执行动态类型的版本
标准库异常类的层次:
exception类:派生 bad_cast,bad_alloc,runtime_error,logic_error类
runtime_error类:派生 overflow_error,underflow_error,range_error类
logic_error类:派生 domain_error,invalid_argument,out_of_range,length_error类
编写异常安全的代码:由于发生异常时,分配的动态资源不会释放,导致内存泄漏,因此通常用类来管理资源的分配和回收,这种技术叫做“资源分配即初始化”( RAII)
用类管理资源分配:设计一个类来封装资源的分配和释放。 这个资源管理类在构造函数中分配资源(返回的指针为此类的成员),在析构函数中释放资源。因此需要资源时创建该类的对象即可,用对象的成员来引用所分配的资源。当发生异常时该对象的析构函数会被调用,保证资源得到释放。
另一种方法:不用资源管理类,把可能发生异常的代码放在 try测试块中,在 catch块中释放之前分配的任何动态资源
最佳实践:可能存在异常的程序以及分配资源的程序应该使用类来管理那些资源,这可以保证如果发生异常就释放资源
auto_ptr<T>类:在 <memory>中定义,只能用于管理从 new返回的一个对象(保存其指针),不能用于指向动态分配的数组。当 auto_ptr被复制或赋值时,有不寻常的行为,因此不能将 auto_ptr存储在标准库容器中
操作:
auto_ptr<T> ap; auto_ptr<T> ap(p); auto_ ptr<T> ap1(ap2); ap1=ap2; ~ap; *ap; ap->; ap.reset(p); ap.release(); ap.get()
auto_ptr是可以保存任何类型指针的模板,其接受指针构造函数为 explicit的,故必须使用直接初始化
auto_ptr缺陷:
( 1)不要使用 auto_ptr对象保存指向静态分配对象的指针
( 2)永远不要使用两个 auto_ptr对象指向同一对象
( 3)不要使用 auto_ptr对象保存指向动态分配数组的指针。因为它释放对象使用 delete操作符,而不是 delete[]操作符
( 3)不要将 auto_ptr对象存储在容器中
函数的异常说明:是函数原型的一部分。没有异常说明的函数可以抛出任意异常,而空说明指示函数不抛出任何异常:
void no_problem() throw();
如果函数抛出了没有在其异常说明中列出的异常,就用标准库函数 unexpected,默认情况下 unexpected调用 terminate函数,终止程序
注意:不可能在编译时知道函数是否抛出异常以及会抛出哪些异常。因此,在编译时编译器不能也不会试图验证异常说明
在 const成员函数声明中,异常说明跟在 const限定符之后
标准异常类 logic_error的析构函数有空 throw()说明符,因此我们要从它派生子类时,析构函数也必须承诺不抛出任何异常
派生类虚函数的异常说明必须与对应基类虚函数的异常说明同样严格,或者比后者更受限
17.2
每个命名空间是一个作用域
全局命名空间:在任意类、函数或命名空间外部声明的名字。记号 ::member_name引用全局命名空间的成员
未命名的命名空间: namespace后跟花括号,没有名字。与其他命名空间不同,未命名的命名空间的定义局部于特定文件,从不跨越多个文本文件。它通常用于声明局部于文件的实体,其中的名字可直接使用(不能使用作用域操作符)。其中的变量程序开始时创建,在程序结束之前一直存在
未命名的命名空间可以嵌套在另一命名空间中
若头文件定义了未命名的命名空间,则在每个包含该头文件的文件中,该命名空间中的名字将定义不同的局部实体
最佳实践:除了在函数或其他作用域内部,头文件不应该包含 using指示或 using声明
using声明:从声明点开始,直到包含该 using声明的作用域末尾,名字都是可见的。外部作用域中定义的同名实体被屏蔽
一个命名空间可以有许多别名,所有别名以及原来的命名空间名字都可以互换使用
using指示:在 using指示的作用域处注入命名空间的所有名字
两个不同命名空间中的函数不能互相重载
有类类型形参的函数的名字查找会包括定义形参类型的命名空间,因此有时函数名前可以不加命名空间限定符
using声明或 using指示使得命名空间中的成员在当前作用域中可见,因此与当前作用域中的函数形成重载关系
为了提供命名空间中的模板的特化版本,必须保证这个特化版本也在这个命名空间中,否则该特化与原模板就不同名了(因为前面有不同的限定符)
17.3
多重继承:各基类子对象按照在类派生列表中的出现次序构造。析构函数链则总是按构造函数运行顺序的逆序调用
派生类的指针或引用可以转换为其任意基类的指针或引用。这时只能访问相应基类部分,不允许使用一个基类指针访问其他基类成员
若所有基类的析构函数都是虚函数,则无论通过哪种指针删除对象,虚析构函数的处理都是一致的,即按构造函数运行顺序的逆序调用
多重继承派生类的复制控制:与单继承一样,使用基类自己的复制构造函数、赋值操作符或析构函数隐式构造、赋值或撤销相应基类部分
名字查找:先在派生类中找,再在各基类中找,若从多个基类继承了同名成员,对该成员不加限定的使用是二义性的。即使继承的两个同名函数有不同的形参表,也会产生错误;即使函数在一个类中私有在另一个类中非私有,也是错误的。
避免用户级的二义性:在派生类中定义函数的一个版本,由它来选择调用从基类中继承的哪个版本
在多重继承下,一个基类可以在派生层次中出现多次。如 iostream派生自 ostream和 istream,而 ostream和 istream均派生自基类 ios,这样 iostream两次继承了 ios。
可见,派生类中可以有同一基类的多个子对象。
虚继承:使用 virtual来继承基类,即“虚派生”这个基类,这样继承了一个虚基类,无论该基类在派生层次中作为虚基类出现多少次,只继承一个共享的基类子对象
从多个派生路径继承 X成员:若每个路径中 X表式同一虚基类成员,则无二义性。若一个路径中 X是虚基类成员,另一个路径中 X是后代派生类成员,则特定派生类实例优先级高于共享虚基类实例。若各个路径上 X均为各后代派生类成员,则直接访问 X具有二义性
虚基类的初始化:如果虚基类的两个直接子类都试图初始化虚基类部分,导致重复初始化,因此在虚派生中,由最低层派生类的构造函数初始化虚基类,如果此派生类不显示初始化虚基类,就使用虚基类的默认构造函数,若没有默认构造函数,则编译出错
无论虚基类出现在继承层次中任何地方,总是在构造非虚基类之前构造虚基类
只有最低层派生类可以初始化虚基类,中间基类中出现的虚基类初始化被忽略
多重继承中,作为基类通常应用将析构函数定义为虚函数
总结:
异常处理、命名空间、 using声明和 using指示、多重继承、虚继承
第 18 章 特殊工具与技术
18.1
new操作符:分配内存并构造相应类型的一个对象
delete操作符:运行析构函数撤销对象,并释放内存
分配和释放原始的未初始化的内存: allocator类, operator new, operator delete
在原始的内存中构造和撤销对象: allocator类的 construct和 destroy函数、定位 new表达式接受原始内存并在该空间中初始化对象、调用对象的析构函数、算法 uninitialized_fill和 uninitialized_copy
标准 allocator类与定制算法: allocator<t> a; a.allocate(n); a.deallocate(p,n); a.construct(p,t); a.destroy(p); uninitialized_copy(b,e,b2); uninitialized_fill(b,e,t); uninitialized_fill_n(b,e,t,n);
dealloate期待指向由 allocate分配的空间的指针,传给 deallocate一个零指针是不合法的
标准库函数 operator new和 operator delete:与其他 operator函数不同,它们没有重载 new或 delete表达式,实际上我们不能重定义 new和 delete表达式的行为。只是这两个函数与 new或 delete表达式同名(前面加了 operator)。它们类似于 allocator类的 allocate和 deallocate成员函数,分配但不初始化内存。它们有两个重载版本:
void *operator new(size_t); //返回的 void*指针要用 static_cast<T*>转换成对象类型
void *operator new[](size_t);
void operator delete(void*); //形参也是 void*指针
void operator delete[](void*);
最佳实践:一般而言,使用 allocator比直接使用 operator new和 operator delete函数更为类型安全
定位 new表达式:不分配内存,而是接受指向已分配但未构造的内存指针,并在该内存中初始化一个对象
new (place_address) type
new (place_address) type (initializer-list)
注意:定位 new表达式更灵活,它可以使用任何构造函数,并直接建立对象。 construct函数总是使用复制构造函数
显式调用析构函数:我们编写的析构函数通常是由系统来调用的(比如 delete表达式的第一步)。我们也可以在对象上显式地调用析构函数,这会清除对象本身,但不会释放内存,该内存还可重用
内存分配与释放的管理:
( 1)标准库函数 operator new和 operator delete是 allocator类的 allocate和 deallocate成员的低级选择,它们都只分配或释放内存,并不运行构造函数或析构函数。
( 2)定位 new表达式和显式调用析构函数是 allocator类的 construct和 destroy成员的低级选择,它们都只调用构造函数初始化对象或调用析构函数清除对象,并不分配内存或释放内存
( 3)即分配内存又运行构造函数: new表达式。会先调用 operator new分配内存,再运行构造函数初始化对象,返回对象的指针
既运行析构函数又释放内存: delete表达式。会先运行析构函数,再调用 operator delete释放内存
类的成员 new和 delete函数:类可以定义(或继承)自己的 operator new、 operator new[]和 operator delete、 operator delete[]成员函数,这时其原型必须与标准库版本一样( delete及 delete[]版本也可以再增加一个 size_t的形参),编译器会使用类定义的这些版本为对象分配和释放内存,否则调用标准库版本
类的成员 new和 delete函数默认为 static的,因为它们在构造对象之前或撤销对象之后运行,没有非静态的成员数据可操纵
若类有自己的 new和 delete版本,又希望调用标准库版本来创建或释放该类对象,可以在 new或 delete表达式前加全局作用域符 ::。一般 new使用了哪种版本, delete也要使用相应的版本,以保持一致
对象初始化:
Type *ps=new Type(a); //用 type的带 a的类型形参的构造函数创建对象,返回指针并赋给 ps
const Type *ps2=new const Type(a); //创建一个 const Type对象,故要用 const Type指针
//指向它
Type *ps3=new Type[20]; //创建有 20个对象的数组, ps3指向其首地址,这里数组元素为类类型,
//会使用默认构造函数初始化,故 Type必须要有默认构造函数
18.2
运行时类型识别 RTTI: typeid操作符返回指针或引用所指对象的实际类型。 dynamic_cast操作符将基类类型的指针或引用安全地转换为派生类型的指针或引用。
这些操作符只为带虚函数的类返回动态(即运行时)类型信息,对于基他类型,返回静态(即编译时)类型信息
最佳实践:通常从基类指针获得派生类行为的最好方法是通过虚函数,因此定义和使用虚函数比直接动态强制类型转换要好得多。一般在无法为基类增加虚函数时使用 dynamic_cast
dynamic_cast<T>:若转换指针类型失败则结果为 0值,转换引用类型失败则抛出 bad_cast异常。因此一般把转换语句放在 if语句中,若失败则执行 else语句
bad_cast异常:在 <typeinfo>中定义
typeid操作符:操作数是对象本身,返回 type_info类型对象的引用, type_info类在 <typeinfo>中定义。
type(*p):若 p的类型不是类类型或没有虚函数,则返回 p的静态类型;若是有虚函数的类类型,则返回动态类型信息;若有虚函数而 p值为 0,则抛出 bad_typeid异常
type_info类:操作 t1==t2, t1!=t2, t.name(), t1.before(t2)
18.3
类成员指针:指向类成员的指针变量,指针符号 *位于类作用域符和成员之间,它包含了类的类型和成员的类型信息。成员指针只应用于类的非 static成员,因为 static类成员不是任何对象的组成部分,故 static成员指针是普通指针
string Screen::*ps; //声明的指针指向 Screen类的 string成员
ps=&Screen::contents; //用类成员初始化指针
char (Screen::*pmf)() const=&Screen::get; //pmf指针指向 Screen类无形参的,返回 char
// 值的 const成员函数
注意:调用操作符优先级高于成员指针操作符,故要用括号
为成员指针使用类型别名:
typedef char (Screen::*Action)(Screen::index,Screen::index) const;
Action get=&Screen::get; //Action是指针类型
成员指针解引用操作符 .*:获取对象的成员,左操作数是类类型对象或对象指针,右操作数是该类型的成员指针
成员指针箭头操作符 ->*:获取对象的成员,左操作符是对象指针,右操作数是该类型的成员指针
也可以定义类成员指针数组
18.4
嵌套在模板内部的类是模板:因为类模板的所有成员隐含地是模板
嵌套类的成员不是外围类的成员。嵌套类可以直接引用外围类的静态成员、类型名和枚举成员
实例化外围类模板时,不会自动实例化嵌套类模板,像成员函数一样,必须在使用时才会实例化
18.5
联合 union:也定义了一个新的类型。可有多个数据成员,但在任何时候只有一个成员可以有值。为 union对象分配的存储量至少与包含其最大数据成员的一样多, union对象的大小在编译时是固定的
union类型:成员默认为 public,也可以定义成员函数,但是 union不能作为基类使用,故成员函数不能虚的。 union没有静态数据成员、引用成员或类类型成员
像内置类型一样, union对象默认是未初始化的
避免通过错误成员访问 union值的最佳办法是:定义一个单独的对象跟踪 union中存储了什么值,这个附加对象称为 union的判别式
匿名联合:不用于定义对象的未命名 union,其不能有私有成员或受保护成员,也不能定义成员函数
18.6
局部类:定义在函数体内的类
局部类的所有成员(包括函数)必须完全定义在类定义体内部,因此,局部类远不如嵌套类有用
不允许局部类声明 static数据成员
局部类只能访问外围函数中定义的类型名、 static变量枚举成员,不能使用外围函数中的变量
局部类可以将外围函数设为友元,这样外围函数可以访问其私有成员,否则不能
18.7
固有的不可移植的特性:位域、 volatile关键字、链接指示
位域:类数据成员,用于保存特定的位数,必须是整型,可以是 signed或 unsigned。位域在内存中的布局与机器相关
typedef unsigned int Bit;
class File{
Bit mode:2; //mode位域有两个位
Bit modified:1;
Bit prot_owner:3:
//...
};
地址操作符 &不能应用于位域,不能有引用位域的指针,位域也不能是类的静态成员
当可以用编译器的控制或检测之外的方式(依赖于硬件系统)改变对象值时,应该将对象声明为 volatile。 Volatile的使用方式与 const一样
volatile与 const的一个区别:合成的复制控制成员接受 const形参,不能将 volatile对象传递给普通引用或 const引用。若类希望复制 volatile对象,必须定义自己的复制构造函数和赋值操作符版本
链接指示:调用其他语言编写的函数库。如常用的 extern “C”
extern “C”{ //指明要使用下面的用 C编写的函数库
int strcmp(const char*,const char*);
char strcat(char*,const char*);
}
extern “C” double calc(double);
C++ 保证能够支持的语言是 C ,其他语言是否支持由编译器而定
C函数的指针与 C++函数的指针具有不同的类型,不能将 C函数的指针初始化或赋值为 C++函数的指针,反之亦然
总结:
优化内存分配的技术( allocator类、 operator new和 operator delete库函数、定位 new表达式、显示调用析构函数、算法 uninitialized_fill和 uninitialized_copy、类的成员 new和 delete函数)、运行时类型识别 RTTI( typeid操作符、 dynamic_cast操作符、 type_info类)、类成员指针(操作符 .*和 ->*)、嵌套类、联合 union及匿名联合、局部类、固有的不可移植的特征(位域、 volatile修饰符、链接指示 extern “C”)