现在的位置: 首页 > 综合 > 正文

Inside C++ Object Model阅读笔记:Chapter 7

2018年03月31日 ⁄ 综合 ⁄ 共 3471字 ⁄ 字号 评论关闭
第七章 对象模型的顶点

7.1 模板
模板操作时必须给出相应的具体类型,例如double,float等等。不同的类型给出不同的模板实例化结果。
在模板类中,enum对象虽然是不变的,但是访问的时候必须加上类型限制。静态变量也是如此。它们随着模板实例化而生成。
声明一个模板类的指针并不会将模板实例化。然而对一个模板类的引用声明则导致它被实例化,因为引用必须指向具体对象。同样,生成一个对象也需要对模板进行实例化。然而那些没有用到的成员函数不应当被实例化,C++语言标准确定这一点,从两个方面考虑:
1、编译器和生成代码的时空效率
2、有些模板类型对应的一些函数可能无法生成代码(附注:例如void型)
虽然new是默认的静态函数,无法访问非静态成员,但是编译时依然需要类型的具体实例化。这是显而易见的。
实例化一般发生在两种情况下,一种是编译时期,另一种是链接时期。
一个有趣的地方是,在long和int具有同样的长度时,是否对于long和int产生两种不同的代码,还是相同的实例化?一般而言是两种不同的代码。(附注:我也这么认为,因为C++的强类型机制规定long和int并非相同的类型。)
对于大多数实现,模板在编译时,首先被看作是一些词素,直到需要实例化的时候,才会展开产生相应的代码,并且进行类型检查和其他一系列检查。非成员函数和成员函数在未经实例化之前也不会进行类型检查和其他检查。
模板的声明和模板的实例化发生在程序的不同位置。这就导致了一个有趣的事情:

// somewhere
extern bool foo(double);

template <typename T>
class Test{...};

// somewhere
extern bool foo(int)

Test<int> tt;

对对象tt中成员函数引用了foo时,调用的是foo(int)还是foo(double)呢?在与实例化无关的情况下,调用的是double参数类型的函数,而在与实例化有关的情况下,调用的是int参数类型的函数。
成员函数在实例化中,分为编译时实例化和链接时实例化,链接时实例化需要链接器能够调用某些特定的工具来协助。有三个问题需要考虑:
1、在什么地方找到成员函数实例化中用到的包含代码定义的文件?
有的编译器要求头文件和代码文件采用同一个文件名,有的编译器要求include相应的代码文件。
2、如何确定只实例化需要的函数?
有的编译器忽略这一要求,有的编译器则模仿链接器进行检验。
3、如何保证这些实例化的函数只在一个object文件中存在?
有的链接器直接自动合并这些函数,有的编译器模仿链接器进行检验。
EDG的编译器采用两次编译的方法来实现export模板的功能。(附注:大多数编译器都不支持export。很显然这种东西问题多多。之后Lipperman鬼扯了一大堆问题。我只看到了Andrew Koenig这个名字。另外Koenig实际上应该写成König,德文意思是国王,类似英文King这个姓。)

7.2 异常处理
异常处理的主要问题在于对于抛出的异常选择一个合适的catch从句接受。这就要求实现能够跟踪函数的调用栈(附注:参看Expert C Programming相关部分。)。同时需要提供对抛出对象的具体类型识别的机制(RTTI),也要为这种对象的位置进行构造和清理。异常处理会导致程序膨胀,和大量的内存消耗。
C++的异常处理包括一个throw从句,一个或者多个catch从句和一个try块。当有一个异常被抛出时,控制将回溯,直到一个catch从句符合或者返回到main()函数外。
在异常处理时还有一个问题,即是要在适当的位置调用析构函数,之后再弹出函数调用栈。如果有资源申请,则需要用一个catch(...)从句来进行资源的回收,以防止资源泄漏,之后直接用throw继续抛出异常。在这种情况下,对于new的异常,不必放在try块中,因为new异常将导致堆上的内存申请不成功,进而也不会调用构造函数,所以不用在catch(...)中调用delete。
对于资源申请,最好的方法是RAII(Resource Acquisition Is Initialization),将资源封装到类中,构造代表申请,而析构代表释放。这样在异常抛出时,析构函数自动调用而资源自动释放。对于构造函数的异常处理,编译器保证在构造到一半途中产生异常的情况下,调用已经构造完成的对象的析构函数,而不会调用未经构造的对象的析构函数。在构造一个对象数组的中途产生异常时,只有已经构造的对象会被析构。
如果一个异常被抛出,编译器会检查异常抛出的函数,检测这个抛出的位置是否在一个try块中,如果在try块中,则会比较相应的catch从句,若有匹配的从句则将控制交给catch从句,若不在相应的try块中或者没有对应的catch从句,则系统析构所有活跃的本地对象,将函数从调用栈中弹出,然后返回到函数的调用方进行进一步处理。
一个函数中可以包含一些区域:在try块外并且没有活跃本地对象的区域,在try块外但是有一些需要析构的本地对象的区域,在try块内部的区域。编译器为了支持error handling需要了解所有这些区域。一种处理的策略是,将这些区域的位置生成一个表格,在异常抛出时,将查询这个表格,以确定相应对应的处理方式。
对于每一个抛出的异常,需要对异常对象的类型进行登记(附注:RTTI吧),对于派生类对象,需要对所有基类进行登记。特别的,因为异常可能在成员函数内部产生,所以连同私有继承的基类也要进行记录。编译器同样要为所有的catch从句生成一个类型数据。(附注:catch的时候同样需要满足多态性!)
在异常抛出的时候,异常对象被放在异常数据栈上,传输到catch从句的数据是异常对象的地址,类型修饰符以及异常对象的析构函数地址。对于
catch(exPoint p)
{ throw; }
如果收到一个class exVertex:public exPoint对象,则首先,p被异常对象初始化,就像初始化函数参数一样。之后,由于p不是引用或者指针,派生类对象将被slice off,虚表不会被处理,没有多态性。在再次抛出异常时,抛出的是原始的异常对象,而不是本地的p对象,再次抛出时的对象仍然是原始的exVertex,没有被slice。
对于
catch(exPoint& p)
{ throw; }
则多态性起到了作用,再次抛出的是原始对象。由于p是一个引用,所以本地对于原始对象的修改将在再次抛出的对象中生效。
如果抛出一个已经在本地构造的对象,则拷贝构造函数将被调用,本地构造的对象被复制一份,之后复制品被抛出。(附注:这是显然的,在throw之后本地对象已经被unwind的代码销毁了。)

7.3 RTTI
downcast的问题在于,不能保证基类指针指向的对象的确可以转化成派生类对象。这样需要一个运行时能够解析对象本身信息的材料。对象中需要包含指向类型信息的指针,在运行时需要消耗检查运行时类型的时间。为了在效率和功能上获得平衡,C++仅仅对具有多态性的对象,也就是包含继承和虚机制的对象提供RTTI支持。具体的实施中,C++只对含有虚函数的类提供RTTI(附注:这是一句值得好好琢磨的话。因为只有包含虚函数,downcast才有意义并且才有可能出现问题。事实上,对于g++,非多态的类不支持dynamic_cast)这是通过在虚表的第一项中加入一个RTTI信息指针而实现的。
通过dynamic_cast,可以在运行时判定传递对象的真实身份,并且在downcast安全时才改变指针的类型,否则返回NULL。dynamic_cast的惩罚是需要在编译时生成一个typeinfo,而在运行时检测类型修饰符。
在dynamic_cast时,引用不能被当做指针来处理,因为引用必须指向一个实际对象,这时dynamic_cast不通过返回NULL报错,而是抛出bad_cast。
typeid操作符也可以取得类型信息,并且比较它们是否相同。当存在异常处理时,需要更多的typeinfo来保存类型信息,同时非多态性的对象也需要生成类型信息(附注:真是昂贵的代价啊!)

7.4 高效,但不灵活?
传统的C++对象模型较为高效,然而在动态链接库中,“直接代替”的原则和C++语言的对象内存模型相冲突(附注:因为C++标准并不保证按照声明顺序排列成员,特别是虚继承存在的情况瞎)。同时共享内存也由此出现了问题。(附注:盗用Poincare一句话:C++不是完美的,但是却是有用的。)

抱歉!评论已关闭.