C++对象模型学习——站在对象模型的尖端
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++对象模型学习——站在对象模型的尖端的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Spring-AOP实践 - 统计访问时
- 下一篇: js仿QQ中对联系人向左滑动、滑出删除按