2019独角兽企业重金招聘Python工程师标准>>>

这里要讨论三个著名的C++语言扩充性质,它们都会影响C++对象。它们分别是template、

exception handling(EH)和runtime type identification(RTTI)。

一、Template

C++程序设计的风格及习惯,自从1991年的cfront 3.0引入tempaltes之后就深深地改变了。原

本template被视为对container classes如Lists和Arrays的一项支持,但现在它已经成为标准模板库

(也就是Standard Template Library,STL)的基础。它也被用于属性混合(如内存配置策略或互

斥(mutual exclusion)机制的参数技术之中。它甚至被用于一项所谓的template metaprogram技

术:class expression templates将在编译时期而非执行期被评估,因而带来重大的效率提升)。

下面是template的三个主要讨论方向:

1)template的声明。基本来说就是当声明一个template class、template class member

function等待时,会发生什么事情。

2)如何”实例化(instantiates)“class object、inline nonmember以及member template

functions。这些是”每一个编译单位都会拥有一份实例“的东西。

3)如何”实例化(instantiates)“nonmember、member tempalte functions以及static

template class members。这些都是”每一个可执行文件中只需要一份实例“的东西。这就是一般

而言template所带来的问题。

这里使用”实例化“(instantiation)这个字眼来表示”进程(process)将真正的类型和表达式

绑定到template相关形式参数(formal parameters)上头“的操作。举个例子,下面是一个

template function:

template <class Type>
Type
min( const Type &t1, const Type &t2 ) { ... }  

用法如下:

min( 1.0, 2.0 );

于是进程把Type绑定为double并产生min()的一个程序文字实例(并施以”mangling“方法,给

它一个独一无二的名称),其中t1和t2的类型都是double。

1、Template的”实例化“行为(Template instantiation)

考虑下面的template Point class:

template <class Type>
class Point
{public:enum Status { unallocated, normalized };Point( Type x = 0.0, Type y = 0.0, Type z = 0.0 );~Point();void* operator new( size_t );void operator delete( void*, size_t );// ...private:static Point<Type> *freeList;static int chunkSize; Type _x, _y, _z;
};

首先,当编译器看到template class声明时,它会做出什么反应?在实际程序中,什么反应也

没有!也就是说,上述的static data members并不可用。nested enum或其enumerators也一

样。

虽然enum Status的真正类型在所有的Point instantiations中都一样,其enumerators也是,

但它们每一个都只能够通过template Point class的某个实例来存取或操作。因此我们可以这样

写:

// ok:
Point<float>::Status s;

但不能这样写:

// error:
Point::Status s;

即使两种类型抽象地来说是一样的(而且,最理想的情况下,我们希望这个enum只有一个

实例被产生出来。如果不是这样,我们可能会想要把这个enum抽出到一个nontemplate base

class中,以避免多份拷贝)。

同样道理,freeList和chunkSize对程序而言也还不可用。我们不能够写:

// error :
Point::freeList;

我们必须显式地指定类型,才能使用freeList:

// ok:
Point<float>::freeList;

像上面这样使用static member,会使其一份实例与Point class的float instantiation在程序中

产生关联。如果我们写:

// ok : 另一个实例(instance)
Point<double>::freeList;

就会出现第二个freeList实例,与Point class的double instantiation产生关联。

如果我们定义一个指针,指向特定的实例,像这样:

Point<float> *ptr = 0;

再一次,程序中什么也没发生。因为一个指向class object的指针,本身并不是一个class

object,编译器不需要知道与该class有关的任何members的数据或object布局数据。所以将

“Point的一个float实例”实例化也就没有必要了。在C++ Standard完成之前,“声明一个指针指向

某个template class”这件事情并未被强制定义,编译器可以自行决定要或不要将template“实例

化”。cfront就是这么做的!如今C++ Standard已经禁止编译器这么做。

如果不是pointer而是reference,又如何?假设:

const Point<float> &ref = 0;

它真的会实例化一个“Point的float实例”。这个定义的真正语意会被扩展为:

// 内部扩展
Point<float> temporary( float ( 0 ) );
const Point<float> &ref = temporary;

因为reference并不是无物(no object)的代名词。0被视为整数,必须被转换为以下类型的

一个对象:

Point <float>

如果没有转换的可能,这个定义就是错误的,会在编译时被挑出来。

所以,一个class object的定义,不论是由编译器暗中地做(像稍早程序代码中出现过的

temporary),或是由程序员像下面这样显示地做:

const Point <float> origin;

都会导致template class的“实例化”,也就是说,float instantiation的真正对象布局会被产生

出来。回顾先前的template声明,我们看到Point有三个nonstatic members,每一个的类型都是

Type。Type现在被绑定为float,所以origin的配置空间必须足够容纳三个float成员。

然而,member functions(至少对于那些未被使用过的)不应该被“实例化”。只有在

member functions被使用的时候,C++ Standard才要求它们被“实例化”。目前的编译器并不精确

遵循这项要求。之所以由使用者来主导“实例化”(instantiation)规则,有两个主要原因:

1)空间和时间效率的考虑。如果class中有100个member functions,但程序里只针对某个

类型使用其中两个,针对另一个类型使用其中五个,那么将其他193个函数都“实例化”将花费大

量的时间和空间。

2)尚未实现的机能。并不是一个template实例化的所有类型就一定能够完整支持一组

member functions所需要的所有运算符。如果只“实例化”那些真正用到的member

functions,template就能够支持那些原本可能会造成编译时期错误的类型(types)。

举个例子,origin的定义需要调用Point的default constructor和destructor,那么只有这两个

函数需要被“实例化”。类似的道理,当写下:

Point <float> *p = new Point <float>;

时,只有(1)Point template的float实例、(2)new运算符、(3)default constructor需要

被“实例化”。有趣的是,虽然new运算符是这个class的一个implicitly static member,以至于它

不能够处理其中任何一个nonstatic members,但它还是依赖真正的template参数类型,因为它

的第一参数size_t代表class的大小。

这些函数在什么时候“实例化”?目前流行两种策略:

1)在编译的时候,那么函数将“实例化”于origin和p存在的那个文件中。

2)在链接的时候。那么编译器会被一些辅助工具重新激活。template函数实例可能被放在

这一文件中、别的文件中或一个分离的存储位置。

在“int和long一致”(或“double和long double一致”)的架构之中,两个类型实例化操作:

Point <int> pi;
Point <long> pl;

应该产生一个还是两个实例呢?目前所知道的所有编译器都产生两个实例(可能有两组

完整的member functions)。C++ Standard并未对此有什么强制规定。

#include <iostream>template <class Type>
class Point
{public:enum Status { unallocated, normalized };Point( Type x = 0.0, Type y = 0.0, Type z = 0.0 ): _x( x ), _y( y ), _z( z ) { }~Point() { }void* operator new( size_t size ) { return ::operator new( size ); }void operator delete( void* pointee ) { ::operator delete( pointee ); } Type y() { return _y; }// ...public:static Point<Type> *freeList;static int chunkSize; Type _x, _y, _z;
};int main()
{Point<float> *ptr = 0;const Point<float> &ref = 0;Point<float> *p = new Point<float>;Point<int> pi;Point<long> pl;std::cout << "sizeof( *ptr ) = " << sizeof( *ptr ) << std::endl;//std::cout << "ptr->_x = " << ptr->_x << std::endl; std::cout << "sizeof( ref ) = " << sizeof( ref ) << std::endl;std::cout << "ref._x = " << ref._x << std::endl;std::cout << " &pi = " << &pi << std::endl;std::cout << " &pl = " << &pl << std::endl;
}

当输出“ptr->_x”时:

可以看到,指针ptr的确未产生对象。

当输出“ref._x”时:

可以看到引用产生了对象实例。pi和pl是不同的实例。

在汇编生成的代码中有:

_ZN5PointIfEC2Efff    // Point<float>::Point(float, float, float)
_ZN5PointIfED2Ev      // int<float>::~Point()
_ZN5PointIfEnwEj      // Point<float>::operator new(unsigned int)
_ZN5PointIiEC2Eiii    // Point<int>::Point(int, int, int)
_ZN5PointIiED2Ev      // Point<int>::~Point()
_ZN5PointIlEC2Elll    // Point<long>::Point(long, long, long)
_ZN5PointIlED2Ev      // Point<long>::~Point()

可以看到的确没有产生y()函数和operator delete( void* pointee )函数的代码,也产生了int

和long的两组完整的member functions。

2、Template的错误报告(Error Reporting within a Template)

考虑下面的template声明:

(1)   template <class T>
(2)   class Mumble
(3)   {
(4)     public$:
(5)       Mumble( T t = 1024 )
(6)         : _t( t )
(7)       {
(8)         if( tt != t )
(9)           throw ex ex;
(10)      }
(11)    private:
(12)      T tt;
(13)  }

这个Mumble template class的声明内含一些既露骨又潜沉的错误:

1)L4:使用$4字符是不对的。这项错误有两方面。第一,$并不是一个可以合法用于标识

符的字符;第二,class声明中只允许有public、protected、private三个标签(labels),$的出现

使public$不成为public。第一点是语汇(lexical)上的错误,第二点则是造句/解析(syntactic

/parsing)上的错误。

2)L5:t被初始化为整数常量1024,或许可以,也或许不可以,视T的真实类型而定。一般

而言,只有template的各个实例才诊断得出来。

3)L6:_t并不是哪一个member的名称,tt才是。这种错误一般会在“类型检验”这个阶段被

找出来。是的,每一个名称必须绑定于一个定义身上,要不就会产生错误。

4)L8:!=运算符可能已定义好,但也可能还没有,视T的真正类型而定。和第二点一

样,只有template的各个实例才诊断得出来。

5)L9:我们意外地键入ex两次。这个错误会在编译时期的解析(parsing)阶段被发现。

C++语言中一个合法的句子不允许一个标识符紧跟在另一个标识符之后。

6)L13:我们忘记了一个分号作为class声明的结束。这项错误也会在编译时期的语句分析

(parsing)阶段被发现。

在一个nontemplate class声明中,这6个既露骨又潜沉的错误会被编译器挑出来。但

template class却不同。例如,所有与类型有关的检验,如果牵涉到template参数,都必须延迟

到真正实例化操作(instantiation)发生,才得为之。也就是说,L5和L8的潜在错误会在每个实

例操作(instantiation)发生时被检查出来并记录之,其结果将因不同的实际类型而不同。于是

结果:

#include <iostream>template <class T>
class Mumble
{public$:Mumble( T t = 1024 ): _t( t ){if( tt != t )throw ex ex;}private:T tt;
};int main()
{std::cout << "Hello World!" << std::endl;
}

当只修改"public$"这一行时:

  public:

修改"_t(t)"和"ex"两行:

int ex;...: tt( t )...throw ex;

通过编译:

当在main函数里添加:

Mumble<int> mi;

则L5和L8是正确的,编译通过:

而如果:

Mumble<int*> pmi;

那么L8正确L5错误,因为不能够将一个整数常量(除了0)指定给一个指针。

面对这样的声明:

class SmallInt
{public:SmallInt( int _x ) : x( _x ) { }// ...private:int x;
};

由于其!=运算并未定义,所以下面的句子:

Mumble<SmallInt> smi;

会造成L8错误,而L5正确。当然,下面这个例子:

Mumble<SmallInt*> psmi;

又造成L8正确而L5错误。

那么,什么样的错误会在编译器处理template声明时被标示出来?这有一部分和template的

处理策略有关。cfront对template的处理完全解析(parse)但不做类型检验;只有在每一个实例

化操作(instantiation)发生时才做类型检验。所以在一个parsing策略之下,所有语汇

(lexing)错误和解析(parsing)错误都会在处理template声明的过程中被标示出来。

语汇分析器(lexical analyzer)会在L4捕捉到一个不合法的字符,解析器(parser)会这

样标示它:

public$:  // caught

表示这是一个不合法的标签(label)。解析器(parser)不会把“对一个未命名的member

作出参考操作”视为错误:

_t( t )  // not caught

但它会抓出L9“ex出现两次”以及L13“缺少一个分号”这两种错误。

在一个十分普遍的代替策略中,template的声明被收集成一系列的“lexical tokens”,而

parsing操作延迟直到真正有实例化操作(instantiation)发生时才开始。每当看到一个

instantiation发生,这组token就被推往parser,然后调用类型检验,等等。面对先前出现的那个

template声明,“lexical tokenizing”会指出什么错误吗?事实上很少,只有L4所使用的不合法字

符会被指出。其余的template声明都被解析为合法的tokens并被收集起来。

目前的编译器,面对一个template声明,在它被一组实际参数实例化之前,只能施行以有

限的错误检查。template中那些与语法无关的错误,程序员可能认为十分明显,编译器却通过

了,只有在特定实例被定义之后,才会发出抱怨。这是目前实现技术上的一个大问题。

Nonmember和member template functions在实例化行为(instantiation)发生之前也一样

没有做完完全的类型检验。这导致某些十分露骨的template错误声明竟然得以通过编译。例如

下面的template声明:

template<class type>
class Foo
{public:Foo();type val();void val( type v );private:type _val;
};
// bogus_member不是class的一个member function
// dbx不是class的一个data member
template <class type>
double Foo<type>::bogus_member() { return this->dbx; }

在g++4.8.4中,编译结果如下:

可以看到class中的函数被显示出错误。

如果在class中加入成员函数:

template<class type>
class Foo
{public:Foo();type val();void val( type v );double bogus_member();private:type _val;
};

编译通过,并不会报没有dbx的错误。

这些都是编译器设计者自己的决定。Template facility并没有说不允许对template声明的类

型部分有更严格的检验。

3、Template 中的名称决议法(Name Resolution within a Template)

必须能够区分以下两种意义。一种是C++ Standard所谓的“scope of the template

definition”,也就是“定义出template”的程序端。另一种是C++ Standard所谓的“scope of the

template instantiation”,也就是“实例化template”的程序端。第一种情况举例如下:

// scope of the template definition
extern double foo( double );template<class type>
class ScopeRules
{public:void invariant(){_member = foo( _val );}type type_dependent(){return foo( _member );}// ...private:int _val;type _member;
};

第二种情况举例如下:

// scope of the template instantiation
extern int foo( int );
// ...
ScopeRules<int> sr0;

在ScopeRules template中有两个foo()调用操作。在“scope of template definition”中,只有

一个foo()函数声明位于scope之内。然而在“scope of template instantiation”中,两个foo()函数声

明都位于scope之内。如果我们有一个函数调用操作:

// scope of the template instantiation
sr0.invariant();

那么,在invariant()中调用的究竟是哪一个foo()函数实例呢?

// 调用的是哪一个foo()函数实例?
_member = foo( _val );

在调用操作的那一点上,程序中的两个函数实例是:

// scope of the template declaration
extern double foo( double );// scope of the template instantiation
extern int foo( int );

而_val的类型是int。结果被选中的是直觉以外的那一个:

// scope of the template declaration
extern double foo ( double );

Template之中,对于一个nonmember name的决议结果,是根据这个name的使用是否与“用

以实例化该template的参数类型”有关而决定的。如果其使用互不相关,那么就以“scope of the

template declaration”来决定name。如果其使用互有关联,那么就以“scope of the template

instantiation”来决定name。在第一个例子中,foo()与用以实例化ScopeRules的参数类型无关:

// the resolution of foo() is not
// dependent on the template argument
_member = foo( _val );

这是因为_val的类型是int:_val是一个“类型不会变动”的template class member。也就是

说,被用来实例化这个template的真正类型,对于_val的类型并没有影响。此外,函数的决议结

果只和函数的原型(signature)有关,和函数的返回值没有关系。因此_member的类型并不会

影响哪一个foo()实例被选中。foo()的调用与template参数毫无关系!所以调用操作必须根据

“scope of the template declaration”来决议。在此scope中,只有一个foo()候选者(注意,这种

行为不能够以一个简单的宏扩展——像是使用一个#define宏——重现之)。

让我们另外看看“与类型相关”(type-dependent)的用法:

sr0.type_dependent();

这个函数的内容如下:

return foo( _member );

这个例子很清楚地与template参数有关,因为该参数将决定_member的真正类型。所以这一

次foo()必须在“scope of the template instantiation”中决议,本例中这个scope有两个foo()函数声

明。由于_member的类型在本例中为int,所以应该是int版的foo()。如果ScopeRules以double

类型实例化,那么就应该是double版的foo()出现。如果ScopeRules是以double类型实例化,那

么该调用操作就暧昧不明。最后,如果ScopeRules以某一个class类型实例化,而该class没有

针对int或double实现出convertion运算符,那么foo()调用操作会被表示为错误。不管如何演变,

都是由“scope of the template instantiation”来决定,而不是由“scope of the template

declaration”。

这意味着一个编译器bib保持两个scope contexts:

1)“scope of the template declarartion”,用以专注于一般的template class。

2)“scope of the template instantiation”,用以专注于特定的实例。

编译器的决议(resolution)算法必须决定哪一个才是适当的scope,然后在其中搜索适当的

name。

4、Member Function的实例化行为(Member Funciton Instantiation)

对于template的支持,最困难的莫过于template function的实例化(instantiation)。目前编

译器提供了两个策略:一个是编译时期策略,程序代码在program text file中备妥可用:另一个

是链接时期策略,有一些meta-compilation工具可以导引编译器的实例化行为

(instantiation)。

下面是编译器设计者必须回答的三个主要问题:

1)编译器如何找出函数的定义?

答案之一是包含template program text file,就好像它是一个header文件一样。Borland编

译器就遵循这个策略。另一种方法是要求一个文件命名规则,例如,我们可以要求,在Point.h

文件中发现的函数声明,其template program text一定要放置于文件Point.C或Point.cpp中,依

此类推。cfront就遵循这个策略。Edison Design Group编译器对这两种策略都支持。

2)编译器如何能够只实例化程序中用到的member functions?

解决办法之一就是,根本忽略这项要求,把一个已经实例化的class的所有member

functions都产生出来。Borland就是这么做的——虽然它也提供#pragmas可以压制(或实例

化)特定实例。另一种策略就是模拟链接操作,检测看看哪一个函数真正需要,然后只为它

(们)产生实例。cfront就是这么做的。Edison Design Group编译器对这两种策略都支持。

3)编译器如何阻止member definition在多个.o文件中都被实例化呢?

解决办法之一就是产生多个实例,然后从链接器中提供支持,只留下其中一个实例,其余

都忽略。另一个办法就是由使用者来引导“模拟链接阶段”的实例化策略,决定哪些实例

(instance)才是所需求的。

目前,不论是编译时期还是链接时期的实例化(instantiation)策略,均存在以下弱点:

当template实例被产生出来时,有时候会大量增加编译时间。很明显,这将是template

functions第一次实例化时的必要条件。然而当那些函数被非必要地再次实例化,或是当“决定那

些函数是否需要再实例化”所花的代价太大时,编译器的表现令人失望!

C++支持template的原始意图可以想见是一个由使用者导引的自动实例化机制(use-

directed automatic instantiation mechanism),既不需要使用者的介入,也不需要相同文件有

多次的实例化行为。但是这已被证明是非常难以达成的任务,比任何人此刻所能想象的还要

难。ptlink,随着cfront3.0版所附的原始实例化工具,提供了一个由使用者驱动的自动实例化机

制(use-driven automatic instantiation mechanism),但是它实在太复杂了,即使是久经世故

的人也没办法一下子了解。

Edison Design Group开发出一套第二代的directed-instantiation机制,非常接近于

template facility原始含义。它主要运作如下:

1)一个程序的原始码被编译时,最初并不会产生任何“template实例化”。然而,相关信息

已经被产生于object files之中。

2)当object files被链接在一块时,会有一个prelinker程序被执行起来。它会检查object

files,寻找template实例的相互参考以及对应的定义。

3)对于每一个“参考到template实例”而“该实例却没有定义”的情况,prelinker将该文件视为

与另一个实例化(在其中,实例已经实例化)等同。以这种方法,就可以将必要的程序实例化

操作指定给特定的文件。这些都会注册在prelinker所产生的.ii文件中(放在磁盘目录ii_file)。

4)prelinker重新执行编译器,重新编译每一个“.ii文件曾被改变过”的文件。这个过程不断

重复,直到所有必要的实例化操作都已完成。

5)所有的object files被链接成一个可执行文件。

这种direct-instantiation体制的主要成本在于,程序第一次被编译时的.ii文件设定时间。次

要成本则是必须针对每一个“complie afterwards”执行prelinker,以确保所有被参考到的

templates都存在着定义。在最初的设计以及成功地第一次链接之后,重新编译操作包含以下程

序:

1)对于每一个将被重新编译的program text file,编译器检查其对应的.ii文件。

2)如果对应的.ii文件列出一组要被实例化(instantiated)的templates,那些templates(而

且只有那些templates)会在此编译时被实例化。

3)prelinker必须执行起来,确保所有被参考到的template已经被定义妥当。

出现某种形式的automated template机制,是“对程序员友善的C++编译系统”的一个必要组

件。

不幸的是,没有任何一个机制是没有bugs的。Edison Design Group的编译器使用了一个由

cfront2.0引入的算法,针对程序中的每一个class自动产生virtual table的单一实例。例如下面的

class声明:

class PrimitiveObject : public Geometry
{public:virtual ~PrimitiveObject();virtual void draw();...
};

如果它被含入于15个或45个程序源码中,编译器如何能够确保只有一个virtual table实例被

产生出来呢?产生15份或45份实例倒还容易些!

Koenig以下面的方法解决这个问题:每一个virtual function的地址都被放置于active classes

的virtual table中。如果取得函数地址,表示virtual function的定义必定出现在程序的某个地点;

否则程序就无法链接成功。此外,此函数只能有一个实例,否则也是链接不成功。那么,就把

virtual table放在定义了该class之第一个non-inline、nonpure virtual function的文件中。以我们

的例子而言,编译器会将virtual table产生在存储着virtual destructor的文件之中。

不幸的是,在template之中,这种单一定义并不一定为真。在template所支持的“将模块中

的每一样东西都编译”的模型下,不只是多个定义可能被产生,而且链接器也放任让多个定义同

时出现,它只要选择其中一个而将其余都忽略,也就是了。

但Edison Design Group的automatic instantiation机制做什么事呢?考虑下面这个library函

数:

void foo( const Point<float> *ptr )
{ptr->virtual_func();
}

virtual function call被转换为类似这样的东西:

// C++伪码
// ptr->virtual_func();
( *ptr->_vtbl_Point<float>[ 2 ] )( ptr );

于是导致实例化(instantiated)Point class的一个float实例及其virtual_func()。由于每一个

virtual function的地址被放置于table之中,如果virtual table被产生出来,每一个virtual function

也都必须被实例化(instantiated)。这就是为什么C++ Standard有下面的文字说明的缘故:

如果一个vitual function被实例化(instantiated),其实例化点紧跟在其class的实例化点之

后。

然而,如果编译器遵循cfront的virtual table实现体制,那么在”Point的float实例有一个virtual

destructor定义被实例化“之前,这个table不会被产生。除非,在这一点上,并没有显式使用

virtual destructor以担保其实例化行为(instantiation)。

Edison Design Group的automatic template机制并不明白它自己的编译器对第一个non-

inline、nonpure virtual function的隐式使用,所以并没有把它标于.ii文件中。结果,链接器反而

回头抱怨下面这个符号没有出现:

_vtbl_Point<float>

并拒绝产生一个可执行文件。Automatic instantiation在此失效!程序员必须显式地强迫将

destructor实例化。目前的编译系统以#program指令来支持此需求。然而C++ Standard也已经

扩充了对template的支持,允许程序员显式地要求在一个文件中将整个class template实例化:

template class Point3d<float>;

或是针对一个template class的个别member function:

template float Point3d<float>::X() const;

或是针对一个个别template function:

template Point3d<float> operator+
( const Point3d<float>&, const Point3d<float>& );

实际上,template instantitation似乎拒绝了全面的自动化。甚至虽然每一件工作都做对了,

产生出来的object files的重新编译成本仍然可能很高——如果程序十分巨大的话!以手动方式先

在个别的object module中完成预先实例化操作(pre-instantiation),虽然沉闷,却是唯一有效

率的做法。

二、异常处理(Exception Handling)

欲支持exception handling,编译器的主要工作就是找出catch子句,以处理被抛

出来的exception。这多少需要追踪程序堆栈中的每一个函数的目前作用区域(包括

追踪函数中local class objects当时的情况)。同时,编译器必须提供某种查询

exception objects的方法,以知道其实际类型(这直接导致某种形式的执行期类型识

别,也就是RTTI)。最后,还需要某种机制用以管理被抛出的object,包括它的产

生、存储、可能的析构(如果有相关的destructor)、清理(clean up)以及一般存

取。也可能有一个以上的objects同时起作用。一般而言,exception handling机制需

要与编译器所产生的数据结构以及执行期的一个exception library紧密合作。在程序

大小和执行速度之间,编译器必须有所抉择:

1)为了维护执行速度,编译器可以在编译时期建立起用于支持的数据结构。这

会使程序的大小发生膨胀,但编译器可以几乎忽略这些结构,直到exception被抛

出。

2)为了维护程序大小,编译器可以在执行期建立起用于支持的数据结构。这会影响程序的

执行速度,但意味着编译器只有在必要的时候才建立那些数据结构(并且可以抛弃之)。

1、Exception Handling快速检阅

C++的exception handling由三个主要的语汇组件构成:

1)一个throw子句。它在程序某处发出一个exception。被抛出去的exception可以是内建类

型,也可以是使用者自定类型。

2)一个或多个catch子句。每一个catch子句都是一个exception handler。它用来表示说,这

个子句准备处理某种类型的exception,并且在封闭的大括号区段中提供实际的处理程序。

3)一个try区段。它被围绕以一系列的叙述句(statements),这些叙述句可能会引发catch

子句起作用。

当一个exception被抛出去时,控制权会从函数调用中被释放出来,并寻找一个吻合的catch

子句。如果没有吻合者,那么默认的处理例程terminate()会被调用。当控制权被放弃后,堆栈中

的每一个函数调用也就被推离(popped up)。这个程序称为unwinding the stack。在每一个函

数被推离堆栈之前,函数的local class objects的destructor会被调用。

Exception handling 中比较不那么直觉的就是它对于那些似乎没什么事做的函数所带来的冲

击。例如下面这个函数:

(1)   Point*
(2)   mumble()
(3)   {
(4)     Point *ptl, *pt2;
(5)    pt1 = foo();
(6)     if( !pt1 )
(7)       return 0;
(8)
(9)     Point p;
(10)
(11)     pt2 = foo();
(12)     if( !pt2 )
(13)      return pt1;
(14)
(15)     ...
(16)   }

如果有一个exception在第一次调用foo()(L5)时被抛出,那么这个mumble()函数会被推出程

序堆栈。由于调用foo()的操作并不在一个try区段之内,也就不需要尝试和一个catch子句吻合。

这里也没有任何local class objects需要析构。然而如果有一个exception在第二次调用foo()

(L11)时被抛出,exception handling机制就必须在”从程序堆栈中”unwindling“这个函数“之

前,先调用p的destructor。

在exception handling之下,L4~L8和L9~L16被视为两块语意不同的区域,因为当exception

被抛出来时,这两块区域有不同的执行期语意。而且,欲支持exception handling,需要额外的

一些”薄记“操作与数据。编译器的做法有两种:一种是把两块区域以个别的”将被摧毁之local

objects“链表(已在编译时期设妥)联合起来;另一种做法是让两块区域共享同一个链表,该链表

会在执行期扩大或缩小。

在程序员层面,exception handling也改变了函数在资源管理上的语意。例如,下面的函数

中含有对一块共享内存的locking和unlocking操作,虽然看起来和exceptions没有什么关系,但

在exception handling之下并不保证能够正确允许:

void
mumble( void *arena )
{Point *p = new Point;smLock( arena ); // function call// 如果有一个exception在此发生,问题就来了// ...smUnLock( arena ); // function calldelete p;
}

本例之中,exception handling机制把整个函数视为单一区域,不需要操心”将函数从程序堆

栈中“unwinding”的事情。然而从语意上来说,在函数被推出堆栈之前,我们需要unlock共享内

存,并delete p。让函数称为“exception proof”的最明确(但不是最有效率)方法就是安插一个

default catch子句,像这样:

void
mumble( void *arena )
{Point *p p = new Point;try{smLock( arena ); // function call// ...}catch( ... ){smUnLock( arena );delete p;throw; }smUnLock( arena ); delete p;
}

这个函数现在有了两个区域:

1)try block以外的区域,在那里,exception handling机制除了“pop”程序堆栈之外,没有其

他事情要做。

2)try block以内的区域(以及它所联合的default catch子句)。

请注意,new运算符的调用并非在try区段内。如果new运算符或是Point constructor在配置内

存之后发生一个exception,那么内存既不会被unlocking,p也不会被delete(这两个操作都在

catch区段内)。这是正确的语意吗?

是的,它是。如果new运算符抛出一个exception,那么就不需要配置heap中的内存,Point

constructor也不需要被调用。所以也就没有理由调用delete运算符。然而如果是在Point

constructor中发生exception,此时内存已配置完成,那么Point之中任何构建好的合成物或子对

象(subobject,也就是一个member class object或base class object)都将自动被析构掉,然

后heap内存也会被释放掉。不论哪种情况,都不需要delete运算符。

类似的道理,如果一个exception是在new运算符执行过程中被抛出的,arena所指向的内存

就绝不会被locked,因此,也没有必要unlock之。

处理这些资源管理问题,一个建议办法就是,将资源需求封装于一个class object体内,并由

destructor来释放资源(然而如果资源必须被索求、被释放、再被索求、再被释放......许多次的

时候,这种风格会变得优点累赘):

void
mumble( void *arena )
{auto_ptr<Point> ph ( new Point );SMLock sm( arena );// 如果这里抛出一个exception,现在就没有问题了// ...// 不需要显式地unlock和delete// local destructors在这里被调用// sm.SMLock::~SMLock();// ph.auto_ptr<Point>::~auto_ptr<Point>()
}

从exception handling的角度看,这个函数现在有三个区段:

1)第一区是auto_ptr被定义之处。

2)第二区段是SMLock被定义之处。

3)上述两个定义之后的整个函数。

如果exception是在auto_ptr constructor中被抛出的,那么就没有active local objects需要被

EH机制摧毁。然而如果SMLock constructor中抛出一个exception,auto_ptr object必须在

“unwinding”之前先被摧毁。至于在第三个区段中,两个local objects当然都必须被摧毁。

支持EH,会使那些拥有member class subobjects或base class subobjects(并且它们也都

有constructors)的classes的constructor更复杂。一个class如果被部分构造,其destructor必须

只施行于那些已被构造的subobjets和(或)member objects身上。例如,假设class X有

member objects A, B和C,都各有一对constructor和destructor,如果A的constructor抛出一个

exception,不论A、B或C都不需要调用其destructor。如果B的constructor抛出一个

exception,A的destructor必须被调用,但C不用。处理所有这些意外事故,是编译器的责任。

同样的道理,如果程序员写下:

// class Point3d : public Point2d { ... }
Point3d *cvs = new Point3d[ 512 ];

会发生两件事:

1)从heap中配置足以给512个Point3d objects所用的内存。

2)如果成功,先是Point2d constructor,然后是Point3d constructor,会施行于每一个元素

身上。

如果#27元素的Point3d constructor抛出一个exception,会怎样?对于#27元素,只有

Point2d destructor需要调用执行。对于前26个元素,Point3d destructor和Point2d destructor都

需要调用执行。然后内存必须被释放回去。

2、对Exception Handling的支持

当一个exception发生时,编译系统必须完成以下事情:

1)检验发生throw操作的函数。

2)决定throw操作是否发生在try区段中。

3)若是,编译系统必须把exception type拿来和每一个catch子句进行比较。

4)如果比较后吻合,流程控制应该交到catch子句手中。

5)如果throw的发生并不在try区段中,或没有一个catch子句吻合,那么系统必须(a)摧毁

所有active local objects,(b)从堆栈中将目前的函数“unwind”掉,(c)进行到程序堆栈的下

一个函数中去,然后重复上述步骤2~5。

决定throw是否发生在一个try区段中

一个函数可以被想象为好几个区域:

1)try区段以外的区域,而且没有active local objects。

2)try区段以外的区域,但有一个(或以上)的active local objects需要析构。

3)try区段以内的区域。

编译器必须表示出以上各区域,并使它们对执行期的exception handling系统有所作用。一个

很棒的策略就是构造出program counter-range表格。

program counter(EIP寄存器)内含下一个即将执行的程序指令。为了在一个内含try区段的

函数中表示出某个区域,可以把program counter的起始值和结束值(或是起始值和范围)存储

在一个表格中。

当throw操作发生时,目前的program counter值被拿来与对应的“范围表格”进行对比,比决

定目前作用中的区域是否在一个try区域中。如果是,就需要找出相关的catch子句。如果这个

exception无法被处理(或者它被再次抛出),目前的这个函数会从程序中被推出(popped),

而program counter会被设定为调用端地址,然后这样的循环再重新开始。

将exception的类型和每一个catch子句的类型做比较

对于每一个被抛出来的exception,编译器必须产生一个类型描述器,对exception的类型进

行编码。如果那是一个derived type,编码内容必须包括其所有base class的类型信息。只编进

public base class的类型是不够的,因为这个exception可能被一个member function捕捉,而在

一个member function的范围(scope)之中,derived class和nonpublic base class之间可以转

换。

类型描述器(type descriptor)是必要的,因为真正的exception是在执行期被处理的,其

object必须有自己的类型信息。RTTI正是因为支持EH而获得的副产品。

编译器还必须为每一个catch子句产生一个类型描述器。执行期的exception handler会将“被

抛出之object的类型描述器”和“每一个cause子句的类型描述器”进行比较,直到找到吻合的一

个,或是直到堆栈已经被“unwound”而terminate()已被调用。

每一个函数会产生一个exception表格,它描述与函数相关的各区域,任何必要的善后处理

代码(cleanup code,被local class object destructors调用)以及catch子句的位置(如果某个

区域是在try区段之中的话)。

当一个实际对象在程序执行时被抛出,会发生什么事?

当一个exception被抛出时,exception object会被产生出来并通常放置在相同形式的

exception数据堆栈中。从throw端传给catch子句的,是exceotion object的地址、类型描述器

(或是一个函数指针,该函数会传回与该exception type有关的类型描述器对象)以及可能会有

的exception object描述器(如果有人定义它的话)。

考虑一个catch子句如下:

catch ( exPoint p )
{// do somethingthrow;
}

以及一个exception object,类型为exVertex,派生自exPoint。这两种类型都吻合,于是

catch子句会作用起来。那么p会发生什么事?

1)p将以exception object作为初值,就像一个函数参数一样。这意味着如果定义有(或由

编译器合成出)一个copy constructor和一个destructor的话,它们都会实施于local copy身上。

2)由于p是一个object而不是一个reference,当其内容被拷贝的时候,这个exception

object的non-exPoint部分会被切掉(sliced off)。此外,如果为了exception的继承而提供

virtual function,那么p的vptr会被设为exPoint的virtual table;exception object的vptr不会拷贝。

当这个exception被再抛出一次时,会发生什么事情?p现在是繁殖出来的object?还是从

throw端产生的原始exception object?p是一个local object,在catch子句的末端将被摧毁。抛出

p需要产生另一个临时对象,并意味着丧失了原来的exception的exVertex部分。原来的

exception object被再一次抛出:任何对p的修改都会被抛弃。

像下面这样的一个catch子句:

catch( exPoint &rp )
{// do somethingthrow;
}

则是参考到真正的exception object。任何虚拟调用都会被决议(resolved)为instances

active for exVertex,也就是exception object的真正类型。任何对此object的改变都被繁殖到下

一个catch子句。

最后,这里提出一个有趣的谜题。如果我们有下面的throw操作:

exVertex errVer;// ...
mumble()
{// ...if( mumble_cond ){errVer.fileName( "mumble()" );throw errVer;}// ...
}

究竟是真正的exception errVer被繁殖,还是errVer的一个复制品被构造于exception stack之

中并不被繁殖?答案是一个复制品被构造出来,全局性的errVer并没有被繁殖。这意味着在一个

catch子句中对于exception object的任何改变都是局部性的,不会影响errVer。只是在一个catch

子句评估完毕并且知道它不会再抛出excption之后,真正的exception object才会被摧毁。

三、执行期类型识别(Runtime Type Identification,RTTI)

在cfront中,用以表现出一个程序的所谓“内部类型体系”,看起来像这样:

// 程序层次结构的根类(root class)
class node { ... };// root of the 'type' subtree:basic types,
//  'derived' types: pointers, arrays,
//  functions, classes, enums ...
class type : public node { ... };// two representtations for functions
class fct : public type { ... };
class gen : public type { ... };

其中gen是generic的简写,用来表现一个overloaded function。

于是只要你有一个变量,或是类型为type*的成员(并知道它代表一个函数),你就必须决定

其特定的derived type是否为fct或是gen。在2.0之前,除了destructor之外唯一不能够被overload

的函数就是conversion运算符,例如:

class String
{public:operator char*();// ...
};

在2.0导入const member functions之前,conversion运算符不能够被overload,因为它们不

使用参数。直到引进了const member functions,情况才有所变化。现在,像下面这样的声明就

可能了:

class String
{public:// ok with Release 2.0operator char*();operator char*() const;// ...
};

也就是说,在2.0版本之前,以一个explicit cast来存取derived object总是安全(而且比较快

速)的,像下面这样:

typedef type *ptype;
typedef fct *pfct;simlipy_conv_op( ptype pt )
{// ok : conversion operators can only be fctspfct pf = pfct( pt );// ...
}

在const member functions引入之前,这份代码是正确的。但之后就不对了。是因为String

class声明的改变,因为char* conversion运算符现在被内部视为一个gen而不是一个fct。

下面这样的转换形式:

pfct pf = pfct( pt );

被称为downcast(向下转换),因为它有效地把一个base class转换至继承架构的末端,变

成其derived classes中某一个Downcast有潜在性的危险,因为它遏制了类型系统的作用,不正

确的使用可能会带来错误的解释(如果它是一个read操作)或腐蚀掉程序内存(如果它是一个

write操作)。在我们的例子中,一个指向gen object的指针被不正确地转换为一个指向fct object

的指针pf。所有后续对pf的使用都是不正确的(除非只是检查它是否为0,或只是把它拿来和其

他指针进行比较)。

1、Type-Safe Downcast(保证安全的向下转换操作)

C++被吹毛求疵的一点就是,它缺乏一个保证安全的downcast(向下转换操作)。只有在“类

型真的可以被适当转换”的情况下,才能够执行downcast。一个type-safe downcast必须在执行

期有所查询,看看它是否指向它所展现(表达)之object的真正类型。因此,欲支持type-safe

downcast,在object空间和执行时间上都需要一些额外负担:

1)需要额外的空间以存储类型信息(type information),通常是一个指针,指向某个类型信

息节点。

2)需要额外的空间以决定执行期的类型(runtime type),因为,正如其名所示,这需要在

执行期才能决定。

这样的机制面对下面这样平常的C结构,会如何影响其大小、效率以及链接兼容性呢?

char *winnie_tbl[] = { "rumbly in my tummy", "oh, bother" };

它所导致的空间和效率上的不良后果甚为可观。

冲突发生在两组使用者之间:

1)程序员大量使用多态(polymorphism),并因而需要正统而合法的大量downcast操作。

2)程序员使用内建数据类型以及非多态设备,因而不受各种额外负担所带来的不良后果。

理想的解决方案是,为两派使用者提供正统而合法的需要——虽然或许得牺牲一些设计上的

纯度与优雅性。

C++的RTTI机制提供了一个安全的downcast设备,但支队那些展现”多态(也就是使用继承

和动态绑定)“的类型有效。我们如何分辨这些?编译器能否光看class 的定义就决定这个class

用以表现一个独立的ADT还是一个支持多态的可继承子类型(subtype)?当然,策略之一就是

导入一个新的关键词,优点是可以清楚地识别支持新特性的类型,缺点则是必须翻新旧的程

序。

另一个策略是通过声明一个或多个virtual functions来区别class声明。其优点是透明化地将旧

有程序转换过来,只要重新编译就好。缺点则是可能会将一个起始并非必要的virtual function强

迫导入继承体系的base class身上。这正是目前RTTI机制所支持的策略。在C++中,一个具备

多态性质的class(所谓的pilymorphic class),正式内含着继承而来(或直接声明)的virtual

functions。

从编译器的角度来说,这个策略还有其他优点,就是大量降低额外负担。所有polymorphic

classes的objects都维护了一个指针(vptr),指向virtual function table。只要我们把与该class

相关的RTTI object地址放进virtual table(通常是第一个slot),那么额外负担就降低为:每一

个class object只多花费一个指针。这一指针只需要被设定一次,它是被编译器静态设定的,而

非在执行期由class constructor设定(vptr才是这么设定的)。

2、Type-Safe Dynamic Cast(保证安全的动态转换)

dunamic_cast运算符可以在执行期决定真正的类型。如果downcast是安全的(也就是说,

如果base type pointer指向一个derived class object),这个运算符会传回适当转换过的指针。

如果downcast不是安全的,这个运算符会传回0。下面就我们如何重写我们原本的cfront

downcast:

typedef type *ptype;
typedef fct *pfct;simplify_conv_op( ptype pt )
{if( pgct pf = dynamic_cast<pfct>( pt ) ){// ...process of}else { ... }
}

什么是dynamic_cast的真正成本呢?pfct的一个类型描述器会被编译器产生出来。由pt所指向

的class object类型描述器必须在执行期通过vptr取得。下面就是可能的转换:

// 取得pt的类型描述器
( ( type_info* )( pt->vptr[ 0 ] ) )->_type_descriptor;

type_info是C++ Standard所定义的类型描述器的class名称,该class中放置着带索求的类型

信息。virtual table的第一个slot内含type_info object的地址;此type_info object与pt所指的class

type有关。这两个类型描述器被交给一个runtime library函数,比较之后告诉我们是否吻合。很

显然这笔static cast昂贵得多,但却安全得多(如果我们把一个fct类型”downcast”为一个gen类

型的话)。

最初对runtime cast的支持提议中,并未引进任何关键词或额外的语法。下面这样的转换操

作:

// 最初对runtime cast的提议语法
pfct pf = pfct( pt );

究竟是static还是dynamic,必须视pt是否指向一个多态class object而定。

3、References并不是Pointers

程序执行中对一个class指针类型施以dynamic_cast运算符,会获得true或false:

1)如果传回真正的地址,则表示这一object的动态类型被确认了,一些与类型有关的操作现

在可以施行于其上。

2)如果传回0,则表示没有指向任何object,意味着应该以另一种逻辑施行于这个动态类型未

确定的object身上。

dynamic_cast运算符也使用于reference身上。然而对于一个non-type-safe cast,其结果不会

与施行于指针的情况相同。为什么?一个reference设为0,会引起一个临时性对象(拥有被参考

的类型)被产生出来,该临时对象的初值为0,这个reference然后被设定成为该临时对象的一个

别名(alias)。因此当dynamic_cast运算符施行于一个reference时,不能够提供对等于指针情

况下的那一组true/false。取而代之的是,会发生下列事情:

1)如果reference真正参考到适当的derived class(包括下一层或下下一层,或下下下一层

或...),downcast会被执行而程序可以继进行。

2)如果reference并不真正是某一种derived class,那么,由于不能够传回0,因此抛出一个

bad_cast exception。

下面是重新实现后的simplify_conv_op函数,参数改为一个reference:

simplify_conv_op( const type &rt )
{try{fct &rf = dynamic_cast<fct&>( rt );// ... }catch( bad_cast ){// ... mumble ...}
}

其中执行的操作十分理想地表现出某种exception failure,而不知是简单(一如从前)的控制

流程。

4、Typeid运算符

使用typeid运算符,就有可能以一个reference达到相同的执行期代替路线(runtime

"alternative pathway"):

simplify_conv_op( const type &rt )
{if( typeid( rt ) == typeid( fct ) ){fct &rf = static_cast<fct&>( rt );// ...}else { ... }
}

在这里,一个明显的较好实现策略是在gen和fctlasses中都引进一个virtual function。

typeid运算符传回一个const reference,类型为type_info。在先前测试中出现的equlity(等

号)运算符,其实是一个被overloaded的函数:

bool
type_info::
operator==( const type_info& ) const;

如果两个type_info objects相等,这个equality运算符就传回true。

type_info object由什么组成?C++ Standard中对type_info的定义如下:

class type_info
{public:virtual ~type_info();bool operator==( const type_info& ) const;bool operator!=( const type_info& ) const;bool before( const type_info& ) const;const char* name() const;  // 传回class原始名称private:// prevent memberwise init and copytype_info( const type_info& );type_info& operator=( const type_info& );// data members
};

编译器必须提供的最小量信息是class的真实名称和在type_info objects之间的某些排序算符

(这就是before()函数的目的),以及某些形式的描述器,用来表现eplicit class type和这一

class的任何subtypes。

虽然RTTI提供的type_info对于exception handling的支持是必要的,但对于exception

handling的完整支持而言,还不够。如果再加上额外的一些type_info derived classes,就可以

在exception发生时提供关于指针、函数、类等等的更详细信息。例如MetaWare就定义了以下

的额外类:

class Pointer_type_info : public type_info { ... };
class Member_pointer_info : public type_info { ... };
class Modified_type_info : public type_info { ... };
class Array_type_info : public type_info { ... };
class Func_type_info : public type_info { ... };
class Class_type_info : public type_info { ... };

并允许使用者取用它们。RTTI只适用于多态类(ploymorphic classese),事实上type_info

objects也适用于内建类型,以及非多态的使用者自定类型。这对于exception handling的支持是

有必要的。例如:

int ex_errno;
...
throw ex_errno;

其中int类型也有它自己的type_info object。下面就是使用方法:

int *ptr;
...
if( typeid( ptr ) == typeid( int* ) )...

在程序中使用typeid(expression),像这样:

int ival;
...
typeid( ival ) ...;

或是使用typeid( type ),像这样:

typeid( double ) ...;

会传回一个const type_info&。这与先前使用多态类型(polymorphic types)的差异在于,这

时候的type_info object是静态取得,而非执行期取得。一般的实现策略是在需要时才产生

type_info object,而非程序一开头就产生之。

四、效率有了,弹性呢?

传统的C++对象模型提供有效率的执行期支持。这份效率,再加上与C之间的兼容性,造成了

C++的广泛被接受度。然而,在某些领域方面,像是动态共享函数库(dynamically shared

libraries)、共享内存(shared memory)以及分布式对象(distrubuted object)方面,这个对

象模型的弹性还是不够。

转载于:https://my.oschina.net/u/2537915/blog/713503

C++对象模型学习——站在对象模型的尖端相关推荐

  1. 《深度探索C++对象模型》--7 站在对象模型的尖端

     C++语言的扩充性质 1.template (1)memberfunctions只有在被使用时,才会被实例化. (2)注意使用template时,注意针对特定参数时可能有些操作没有定义. tem ...

  2. 《深度探索C++对象模型》阅读笔记 第七章 站在对象模型的尖端

    文章目录 Template的"实例化"行为(Template Instantiation) Template的错误报告(Error Reporting within a Templ ...

  3. Sharepoint学习笔记—ECMAScript对象模型系列-- 7、获取和修改List的Lookup字段

    在前面我们提到了如何使用ECMAscript对象模型来操作普通的List Items,但如果我们操作的List包含有Lookup字段,那么我们又该怎么做呢? 首先参考此文搭建我们本文的测试环境 Sha ...

  4. Sharepoint学习笔记—ECMAScript对象模型系列-- 8、组与用户操作(一)

    这里总结一下关于使用ECMAscript对象模型来操作Goup与User的常用情况,因为内容较多,所以拆分为两个部分,这部分主要内容如下:      1.取得当前Sharepoint网站所有的Grou ...

  5. Sharepoint学习笔记—ECMAScript对象模型系列-- 9、组与用户操作(二)

    接着上面的继续,这里我们描述的关于User与Group的操作如下: 6. 向指定Group中添加指定User      7. 获取指定Group的Owner      8. 把当前登录用户添加到指定G ...

  6. 深入探索C++对象模型学习笔记2

    密码管理请下载: http://a.app.qq.com/o/simple.jsp?pkgname=com.wa505.kf.epassword 1.   关于对象 1.1.  虚表: 1.1.1.  ...

  7. 深入探索C++对象模型学习笔记

    密码管理请下载: http://a.app.qq.com/o/simple.jsp?pkgname=com.wa505.kf.epassword 1.   关于对象 1.1.  虚表: 1.1.1.  ...

  8. 分析模式:可复用的对象模型学习笔记

    1.符号

  9. C++ 对象模型学习记录(3)--- 第1章 关于对象(未完)

    1. C++中有两种数据成员,static 和 非static,以及3种成员函数,static,非static和virtual函数 static数据成员和 非static 成员函数放在class 对象 ...

最新文章

  1. 基于Spring可扩展Schema提供自定义配置支持
  2. 试用JAVA的免费空间JHOST
  3. 几种作图软件使用感言
  4. lisp实战文库_LISP编程举例
  5. mysql登录之后可以写什么_MYSQL登陆完之后如何操作???(新手求助)
  6. 《高质量c++/c编程指南》学习摘要
  7. app抢购脚本如何编写_如何用1个记事本文件征服全世界?——cmd批处理脚本编写...
  8. 【LeetCode】剑指 Offer 11. 旋转数组的最小数字
  9. sql删除主键_产品经理的第一节SQL课——ID到底是干什么的?!
  10. anaconda pycharm_搭建 Python 高效开发环境: Pycharm + Anaconda
  11. SPI子系统分析之一:框架
  12. 计算机毕业设计Java宠物医院后台管理系统设计与实现(源码+系统+mysql数据库+lw文档)
  13. 【转】只有运用你的逻辑才能看懂其中的恐怖
  14. Win7+vmware+xpsp3+vs2010驱动开发环境搭建及调试方法
  15. 了解Java对象(抽象和具体)
  16. 如何同时查询多个京东快递单号的物流状态、签收时间
  17. 云原生 -- contour + envoy部署
  18. 没有android手机确切内核头文件,绕过模块的版本检查,构建一个内核模块
  19. 完整的保存onetab的书签信息
  20. 华为防火墙理论与管理

热门文章

  1. js数组的sort排序详解
  2. Silverlight 参考:三维效果(透视转换) -- MSN
  3. 拼接图像亮度均匀调整_液晶拼接屏如何才能达到更好的显示效果
  4. hdu1978 简单记忆化搜索
  5. hdu4768 非常规的二分
  6. 【Android 逆向】代码调试器开发 ( 使用 NDK 中的 ndk-build + Android.mk 编译 Android 平台的代码调试器可执行应用 )
  7. 【Android 插件化】插件化简介 ( 组件化与插件化 )
  8. 【Flutter】Flutter 开源项目参考
  9. 【错误记录】NDK 报错 java.lang.UnsatisfiedLinkError 的一种处理方案 ( 主应用与依赖库 Module 的 CPU 架构配置不匹配导致 )
  10. linux -- ./configure --prefix 命令