第十八章 特殊工具与技术
18.1 优化内存分配
C++的内存分配是一种类型化操作:new为特定类型分配内存,并在新分配的内存中构造该类型的一个对象,new表达式自动运行合适的构造函数来初始化每个动态分配的类类型对象
18.1.1 C++中的内存分配
对未构造的内存中的对象进行赋值而不是初始化,其行为是未定义的,赋值涉及删除现存对象,如果没有现存对象,赋值操作符中的动作就会有灾难性效果
C++提供下面两种方法分配和释放未构造的原始内存:1、allocator类,它提供可感知类型的内存分配;2、标准库中的operator new和operator delete,它们分配和释放需要大小的原始的、未类型化的内存 (现代C++程序一般应该使用allocator类来分配内存,它更安全更灵活,但是在构造对象的时候,用new表达式比allocator::construct成员更灵活,有几种情况下必须使用new)
18.1.2 allocator类
allocator类是一个模板,它提供类型化的内存分配以及对象构造与撤销,allocator类将内存分配和对象构造分开,但是,它分配的内存是未构造的,allocator的用户必须分别construct和destroy放置在该内存中的对象
#include <iostream> using namespace std; class B{ int x; public: B():x(100) { } B(int x ):x(x ) { } ~B() { cout << "~B()"; } void printX(){ cout << x; } }; int main() { //分配内存与对象构造分离 allocator<B> al; B* bp = al.allocate(5 ); //分配原始的未构造的内存,以保存B类型的5个对象 //--------------- al.construct( bp ); //构造一个对象 //100 //al.construct( bp, 120 ); //构造一个对象 //120 bp[0].printX( ); al.destroy(bp ); //删除对象 // 内存还在 //--------------- al.deallocate(bp,5 ); //释放内存 return 0; }
18.1.3 operator new函数 和 operator delete函数
当使用new表达式的时候,实际上发生三个步骤:首先,该表达式调用名为operator new的标准库函数,分配足够大的原始的未类型化的内存,以保存指定类型的一个对象;接下来,运行该类型的一个构造函数,用指定初始化构造对象;最后,返回指向新分配并构造的对象的指针
当使用delete表达式删除动态分配对象的时候,发生两个步骤:首先,对指针指向的对象运行适当的析构函数;然后通过调用名为operator delete的标准库函数释放该对象所用内存
因为new(或delete)表达式与标准库函数同名,所以二者容易混淆
allocate成员分配类型化的内存,所以使用它的程序可以不必计算以字节为单位的所需内存量,它们也可以避免对operator new的返回值进行强制类型转换,类似地,deallocate释放特定类型的内存,也不必转换为void*
18.1.4 定位new表达式
定位new表达式(不分配内存)使我们能够 在特定的、预分配的内存地址 构造一个对象,定位new表达式的形式是:new(place_address) type; new(place_address) type(initializer-list);,其中place_address必须是一个指针,而initializer-list提供了(可能为空)初始化列表,以便在构造新分配的对象时使用,在复制构造函数是私有的情况下,使用定位new表达式(直接构造对象,不使用复制构造,定位new表达式构造的对象,显式调用析构函数清除对象)
18.1.5 显式析构函数的调用
显式调用析构函数的效果是适当地清除对象本身,但是,并不释放对象所占的内存,如果需要,可以重用该内存空间,调用operator delete函数不会运行析构函数,它只释放指定的内存
18.1.6 类特定的new和delete
优化new和delete的行为的时候,只需要定义operator new 和 operator delete的新版本,new和delete表达式自己照管对象的构造和撤销,类成员operator new 函数必须具有返回类型void*并接受size_t类型的形参;类成员operator delete函数必须具有返回类型void,它可定义为接受单个void*类型形参,也可以定义为接受两个形参,即void*和size_t形参(除非类是某继承层次的一部分,否则形参size_t不是必须的 )
成员new和delete函数必须是静态的,new和delete只能直接访问所属类的静态成员
也可以定义成员operator new[]和operator delete[]来管理类类型的数组,如果这些operator函数存在,编译器就使用它们代替全局版本
如果类定义了自己的成员new和delete,类的用户就可以通过使用全局作用域确定操作符,强制new或delete表达式使用全局的库函数,如果用new表达式调用全局operator new函数分配内存,则delete表达式也应调用全局operator delete函数
18.1.7 一个内存分配器基类
希望为自己的类型使用自由列表分配策略的类将继承这个内存分配器基类,通过继承,这些类可使用这个内存分配器基类的operator new 和 operator delete定义,以及表示自由列表所需的数据成员
18.2 运行时类型识别
通过 运行时类型识别(RTTI),程序能够使用基类的指针或引用来检索这些指针或引用所指对象的实际派生类型
通过下面两个操作符提供RTTI:typeid操作符,返回指针或引用所指对象的实际类型;dynamic_cast操作符,将基类类型的指针或引用安全地转换为派生类型的指针或引用,这些操作符只为带有一个或多个虚函数的类返回动态类型信息,对于其他类型返回静态(即编译时)类型的信息,对于带虚函数的类,在运行时执行RTTI操作符,但对于其他类型,在编译时计算RTTI操作符
18.2.1 dynamic_cast操作符
可以使用dynamic_cast操作符将 基类类型对象的引用或指针 转换为同一继承层次中其他类型的引用或指针,与其他强制类型转换不同,dynamic_cast涉及运行时类型检查,如果绑定的引用或指针的对象不是目标类型的对象,则dynamic_cast失败
#include <iostream> using namespace std; class B{ public: virtual void f() { } //基类必须带有一个或多个虚函数 }; class D:public B{ }; int main() { B *pb = new B(); cout << ( dynamic_cast<D*>(pb) ); //运行时检查,转换到指针类型的dynamic_cast失败,则dynamic_cast的结果是0值 try{ //转换到引用类型的dynamic_cast失败,则抛出一个bad_cast类型的异常 (dynamic_cast<D&>(*pb ) ); }catch(exception &e ){ cout << e.what(); //std::bad_cast } return 0; }
与dynamic_cast一起使用的指针必须是有效的--它必须为0或指向一个对象,可以对值为0的指针应用dynamic_cast,结果是0
18.2.2 typeid操作符
typeid操作符使程序能够问一个表达式是什么类型,typeid操作符可以与任何类型的表达式一起使用:内置类型的表达式以及常量都可以用作typeid操作符的操作数,如果操作数不是类类型或是没有虚函数的类,则typeid操作符指出操作数的静态类型;如果操作数是定义了至少一个虚函数的类类型,则在运行时计算类型
typeid最常见的用途是比较两个表达式的类型,或者将表达式的类型与特定类型相比较,typeid操作符的结果是type_info的标准库的对象引用,必须包含库头文件 typeinfo,只有当typeid的 操作数是带虚函数的类类型的对象 的时候,才返回动态类型信息,测试指针返回指针的静态、编译时类型
#include <iostream> using namespace std; #include <typeinfo> //typeid 头文件 class B{ public: virtual void f() { } //基类必须带有一个或多个虚函数 }; class D:public B{ }; int main() { B *pb = new B(); B *pd = new D(); if(typeid(*pb)==typeid(*pd) ){ //注意:比较的是对象!!! //比较指针,返回的是静态的、编译时的类型 cout << "=="; //不带虚函数 == } return 0; }
类类型指针p的值为0,p的类型是带有虚函数的类类型,还是不带虚函数的类类型的区别:
#include <iostream> using namespace std; #include <typeinfo> //typeid 头文件 class B{ public: virtual void f() { } //带有一个或多个虚函数 }; /* class D:public B{ }; */ int main() { B *p = NULL; //指针p的值为0,且p的类型是带有虚函数的类型 try{ cout << typeid(*p).name(); //会抛出异常 //如果p的类型不是带虚函数的类型,编译器不计算*p,使用p的静态类型 }catch(exception &e){ cout << e.what(); //std::bad_typeid } return 0; }
18.2.3 RTTI (Run-Time Type Identification) 的使用
类型相同,比较数据成员是否相同,实现相等操作符
#include <iostream> using namespace std; #include <typeinfo> //typeid 头文件 class B{ friend bool operator==(const B &lhs, const B &rhs); int x; public: B(int x ):x(x ) { } virtual bool equal(const B& b) const { return x == b.x; //比较数据成员 } }; class D:public B{ friend bool operator==(const B &lhs, const B &rhs); int y; public: D(int x, int y ):B(x), y(y) { } virtual bool equal(const B& b) const { if(const D* dp = dynamic_cast<const D*>(&b ) ){ //基类的引用的地址转为本类指针 return y == dp->y; //比较数据成员 }else{ return false; } } }; bool operator==(const B &lhs, const B &rhs){ return typeid(lhs)==typeid(rhs) && lhs.equal(rhs); //类型相同,比较数据成员是否相同 } int main() { B b1(100),b2(100),b3(200); D d1(100,111),d2(100,111),d3(100,222); cout << (b1==b2 ) << "," << (b1==b3 ) << endl; cout << (d1==d2) << "," << (d1==d3) << endl; cout << (b1==d1); return 0; }
18.2.4 type_info类
type_info类的确切定义随编译器而变化,标准操作:t1==t2、t1!=t2、t.name()、t1.before(t2),程序中创建type_info对象的唯一方法是使用typeid操作符
18.3 类成员的指针
成员指针(pointer to memeber),包含类的类型 以及 成员的类型,包括数据成员指针、成员函数指针,成员指针只应用于类的非static成员,static成员指针是普通指针
18.3.1 成员指针定义
数据成员指针
class Screen{ public: typedef std::string::size_type index; char get()const; char get(index ht, index wd ) const; private: std::string contents; //数据成员指针:string Screen::* //string Screen::* ps_Screen = &Screen::contents; index cursor; //Screen::index Screen::* //Screen::index Screen::* pindex = &Screen::cursor; index height, width; //pindex = &Screen::height; //pindex = &Screen::width; };
成员函数指针,必须在三个方面与它所指函数的类型相匹配:函数形参 的类型和数目,包括成员是否是const,返回类型, 所属类的类型
char get()const; //成员函数指针 char (Screen::*)()const // char (Screen::*pmf1)()const = &Screen::get; char get(index ht, index wd ) const; //成员函数指针 char (Screen::*)(Screen::index,Screen::index)const // char (Screen::*pmf2)(Screen::index,Screen::index)const = &Screen::get;
为 成员指针 使用 类型别名
typedef char (Screen::*Action)(Screen::index,Screen::index)const; //函数指针的类型别名 Action Action get = &Screen::get;
可以使用成员指针类型来声明函数形参和函数返回类型
Screen& action(Screen&, Action = &Screen::get );
18.3.2 使用类成员的指针
成员指针 解引用操作符 ,.* 和 ->* ,将成员指针绑定到实际对象,操作符的左操作数必须是类类型的对象或类类型的指针,右操作数是该类的成员指针
成员指针解引用操作符(.* )从 对象或引用 获取成员,成员指针解引用操作符(->*)通过 对象的指针 获取成员
函数指针 和 成员函数指针 的公共用途是,将它们 存储在函数表中,函数表是函数指针的集合,在运行时从中选择给定调用
class Screen{ public: //定义成员函数指针类型 typedef Screen& (Screen::*Action)(); //Action是可以指向,任何表示方向的成员函数指针 static Action Menu[]; //成员函数指针表 public: enum Direction { HOME, FORWARD, BACK, UP, DOWN }; Screen& move( Direction cm ){ //cm作为Menu的索引 (this->*Menu[cm] )( ); //this对象通过成员函数指针调用表示方向的成员函数 return *this; } }; //成员函数指针表初始化 Screen::Action Screen::Menu[] = { &Screen::home, &Screen::forword, &Screen::back, &Screen::up, &Screen::down };
测试代码
#include <iostream> using namespace std; class Screen{ private: Screen& home(){ cout << "home" << endl; return *this; } Screen& forword(){ cout << "forword" << endl; return *this; } Screen& back(){ cout << "back" << endl; return *this; } Screen& up(){ cout << "up" << endl; return *this; } Screen& down(){ cout << "down" << endl; return *this; } private: typedef Screen& (Screen::*Action)(); //Action是可以指向,任何表示方向的成员函数 static Action Menu[]; //成员函数指针表 public: enum Direction { HOME, FORWARD, BACK, UP, DOWN }; Screen& move( Direction cm ){ //cm作为Menu的索引 (this->*Menu[cm] )( ); //this对象通过成员函数指针调用表示方向的成员函数 return *this; } }; //成员函数指针表初始化 Screen::Action Screen::Menu[] = { &Screen::home, &Screen::forword, &Screen::back, &Screen::up, &Screen::down }; int main() { Screen sc; sc.move(Screen::UP ); }
#include <iostream> #include <typeinfo> using std::cout; using std::endl; class Temp{ public: int sum(int a,int b){ return a+b;} }; template<class R,class T,class T1, class T2> void f1( R (T::*pf)(T1 ,T2 ) ){ cout <<typeid(R).name() << endl; cout <<typeid(T).name() << endl; cout <<typeid(T1).name() << endl; cout <<typeid(T2).name() << endl; T t; cout << (t.*pf)(1,2); } int main() { f1(Temp::sum ); return 0; }
#include <iostream> #include <typeinfo> using std::cout; using std::endl; template<class T> class Temp{ public: template<class T1, class T2> T sum(T1 a,T2 b){ return a+b;} }; template<class R,class T,class T1, class T2> void f1( R (T::*pf)(T1 ,T2 ) ){ cout <<typeid(R).name() << endl; cout <<typeid(T).name() << endl; cout <<typeid(T1).name() << endl; cout <<typeid(T2).name() << endl; T t; cout << (t.*pf)(1,2); } int main() { f1(Temp<int>::sum<int,int> ); return 0; }
18.4 嵌套类
嵌套类的名字在其外围类的作用域 中可见,但在 其他类作用域 或 定义外围类的作用域 中不可见,外围类对嵌套类的成员没有特殊访问权,并且嵌套类对其外围类的成员也没有特殊访问权,一般将嵌套类 设为外围类的private成员,外围类及其友元可以使用,通过保留字struct 定义嵌套类,使其成员为public成员,嵌套类的成员不是外围类的成员,嵌套类的成员 在定义外围类的作用域 定义,从左向右是嵌套类的成员函数的名字,嵌套类的作用域符,外围类的作用域符
可以在外围类的外部定义嵌套类,必须在外围类的定义体中 声明嵌套类,嵌套类在外围类的外部定义必须 用外围类的名字限定 嵌套类的名字,嵌套类在实际定义之前是不完全类型,应用所有使用不完全类型的常规限制,
18.5 联合:节省空间的类
联合提供了便利的办法表示一组相互排斥的值,这些值可以是不同类型的,一个union定义以关键字union开始,后接union名字(可选),以及一组以花括号括住的成员声明,每个union对象的大小在编译时是固定的:它至少与union的最大数据成员一样大
union不能作为基类使用,成员函数不能为虚函数,除非另外指定union的成员都是public成员,union不能具有静态数据成员或引用成员,不能具有定义了构造函数、析构函数或赋值操作符的类类型的成员,默认情况下union对象是未初始化的,可以显式初始化,但只能为第一个成员提供初始化,可以使用访问操作符访问union类型对象的成员,给union对象的某个数据成员一个值使得其他数据成员变为未定义的,使用判别式跟踪存储了什么值,
union最经常用作嵌套类型,其中判别式是外围类的一个成员,经常使用switch语句测试判别式(使用enum类型),然后根据union中当前存储的值进行处理
不用定义对象的未命名union称为匿名联合,其成员的名字出现在外围作用域中,匿名union不提供访问其成员的途径,其成员作为定义匿名union的作用域的一部分直接访问,匿名union不能有私有成员或受保护成员,也不能定义成员函数
18.6 局部类
可以在函数体内部定义类,这样的类称为局部类,一个局部类定义了一个类型,该类型只在定义它的局部作用域中可见,局部类的所有成员(包括函数)必须完全定义在类定义体内部,局部类只能访问在外围作用域中定义的类型名、static变量和枚举成员,不能使用定义该类的函数中的变量,通常局部类的所有成员都为public成员
可以将一个类嵌套在局部类内部,嵌套类的名字必须用外围类的名字进行限定,并且嵌套类的声明必须出现在局部类的定义中,嵌套类的所有成员必须在嵌套类本身定义体内部定义
18.7 固有的不可移植的特征
C++从C继承来的不可移植特征:算术类型的大小随机器不同而变化; 位域; volatile限定符; 链接指示
18.7.1 位域
保存特定位数的数据成员称为位域,位域在内存中的布局是机器相关的,位域必须是整形数据类型,可以是signed或unsigned,通过在成员名后面接一个冒号以及指定位数的常量表达式,指出成员是一个位域,通常最好将位域设为unsigned类型,存储在signed类型中的位域的行为由实现定义,定义了位域成员的类,通常也定义一组内联成员函数来测试和设置位域的值,地址操作符不能应用于位域,所以不可能有引用类位域的指针,位域也不能是类的静态成员
18.7.2 volatile限定符
18.7.3 链接指示:extern "C"