天天看點

深度探索C++對象模型》讀書筆記(3)

***純虛拟函數***

在設計抽象基類時,需要注意以下幾點:

(1)不要将destructor聲明為pure virtual function;

如果将destructor聲明為pure virtual function,則設計者一定得定義它。因為每一個derived class destructor會被編譯器加以擴充,以靜态調用得方式調用其“每一個virtual base class”以及“上一層base class”的destructor。

(2)不要将那些函數定義内容并不與類型有關的函數設計為virtual function,因為其幾乎不會被後繼的derived class改寫。

(3)對于其derived class可能修改某一個data member的函數,不應被聲明為const。

***“無繼承”情況下的對象構造***

先定義class Point:

深度探索C++對象模型》讀書筆記(3)
深度探索C++對象模型》讀書筆記(3)

class  Point  ... {

深度探索C++對象模型》讀書筆記(3)

public:

深度探索C++對象模型》讀書筆記(3)
深度探索C++對象模型》讀書筆記(3)

Point(float x = 0.0, float y = 0.0) : _x(x),_y(y) ...{}

深度探索C++對象模型》讀書筆記(3)

virtual float z();

深度探索C++對象模型》讀書筆記(3)

protected:

深度探索C++對象模型》讀書筆記(3)

float _x,_y;

深度探索C++對象模型》讀書筆記(3)

} ;

你可不能小看z()這個virtual function給class Point帶來的巨大變化。virtual function的引入促使每一個class Point擁有一個vtpr,這樣一來,編譯器在constructor中添加了對vptr進行初始化的代碼,而copy constructor和copy assignment operator也會對vptr進行設定,而不再是原先簡單的bitwise操作了。

請看以下的代碼:

深度探索C++對象模型》讀書筆記(3)

Point foobar()

深度探索C++對象模型》讀書筆記(3)
深度探索C++對象模型》讀書筆記(3)

... {

深度探索C++對象模型》讀書筆記(3)

Point local;

深度探索C++對象模型》讀書筆記(3)

Point *heap = new Point;

深度探索C++對象模型》讀書筆記(3)

*heap = local;

深度探索C++對象模型》讀書筆記(3)

delete heap;

深度探索C++對象模型》讀書筆記(3)

return local;

深度探索C++對象模型》讀書筆記(3)

}

将被内部轉化為:

深度探索C++對象模型》讀書筆記(3)

Point foobar(Point  & _result)

深度探索C++對象模型》讀書筆記(3)
深度探索C++對象模型》讀書筆記(3)

... {

深度探索C++對象模型》讀書筆記(3)

Point local;

深度探索C++對象模型》讀書筆記(3)

local.Point::Point();

深度探索C++對象模型》讀書筆記(3)

Point *heap = _new(sizeof(Point));

深度探索C++對象模型》讀書筆記(3)

if(heap != 0)

深度探索C++對象模型》讀書筆記(3)

heap->Point::Point();

深度探索C++對象模型》讀書筆記(3)

*heap = local;

深度探索C++對象模型》讀書筆記(3)

_result.Point::Point(local);  // copy constructor的應用

深度探索C++對象模型》讀書筆記(3)

local.Point::~Point();

深度探索C++對象模型》讀書筆記(3)

return;

深度探索C++對象模型》讀書筆記(3)

}

從以上代碼的轉化可以看出:一般而言,如果你的設計之中有很多函數都需要以傳值方式(by value)傳回一個local class object,那麼提供一個copy constructor就比較合理。

***繼承體系下的對象構造***

假設class Point3d虛拟繼承于class Point,但由于class Point僅存在一份實體,因而class Point3d的constructor需要注意一個問題。

請看下面的繼承關系圖:

深度探索C++對象模型》讀書筆記(3)
深度探索C++對象模型》讀書筆記(3)

class  Point3d :  virtual   public  Point  ... { ... } ;

深度探索C++對象模型》讀書筆記(3)
深度探索C++對象模型》讀書筆記(3)

class  Vertex :  virtual   public  Point  ... { ... } ;

深度探索C++對象模型》讀書筆記(3)
深度探索C++對象模型》讀書筆記(3)

class  Vertex3d :  public  Point3d,  public  Vertex  ... { ... } ;

通常來說,class Point3d和class Vertex的constructor均需調用Point的constructor,然而,當Point3d和Vertex同為Vertex3d的subobject時,它們對Point constructor的調用操作一定不可以發生,而是交由Vertex3d來完成。

那麼如何做到這一點呢?其實隻需在constructor中添加一個參數就可以了。例如,class Vertex3d在調用Point3d和Vertex的constructor之前,總是會把參數_most_derived設為false,于是就壓制了兩個constructors中對Point constructor的調用操作。

深度探索C++對象模型》讀書筆記(3)

//  在virtual base class情況下的constructor擴充内容

深度探索C++對象模型》讀書筆記(3)

Vertex3d *  Vertex3d::Vertex3d(Vertex3d  * this ,  bool  _most_derived,  float  x,  float  y,  float  z)

深度探索C++對象模型》讀書筆記(3)
深度探索C++對象模型》讀書筆記(3)

... {

深度探索C++對象模型》讀書筆記(3)

if(_most_derived != false)

深度探索C++對象模型》讀書筆記(3)

this->Point::Point(x,y);

深度探索C++對象模型》讀書筆記(3)
深度探索C++對象模型》讀書筆記(3)

// 在調用上一層base classes的constructor之前設定_most_derived為false

深度探索C++對象模型》讀書筆記(3)

this->Point3d::Point3d(false,x,y,z);

深度探索C++對象模型》讀書筆記(3)

this->Vertex::Vertex(false,x,y,z);

深度探索C++對象模型》讀書筆記(3)
深度探索C++對象模型》讀書筆記(3)

// 設定vptr

深度探索C++對象模型》讀書筆記(3)

// 安插user code

深度探索C++對象模型》讀書筆記(3)
深度探索C++對象模型》讀書筆記(3)

return this;

深度探索C++對象模型》讀書筆記(3)

}

為了控制一個class中有所作用的函數,編譯器隻要簡單地控制住vptr的初始化和設定操作即可。

vptr初始化操作應該如何處理?實際情況是:應該在base class constructors(具體來說,是所有的virtual base classes和上一層的base classes)調用操作之後,但是在程式員供應的碼或是“member initialization list中所列的members初始化操作”之前。為什麼是這樣呢?

如果每一個constructor都一直等待到其base class constructors執行完畢之後才設定其對象的vptr,那麼每次它都能夠調用正确的virtual function實體。

constructor的執行算法通常如下:

(1)在derived class constructor中,所有virtual base classes的constructor會被調用;

(2)在derived class constructor中,上一層base class的constructor會被調用;

(3)上述完成之後,對象的vptr(s)被初始化,指向相關的virtual table(s);

(4)如果class有member class object,而後者擁有constructor,那麼它們會以其聲明順序的相反順序被調用;

(5)使用者所定義的代碼。

下面是vptr必須被設定的兩種情況:

(1)當一個完整的對象被構造起來時,如果我們聲明一個Point對象,Point constructor必須設定其vptr;

(2)當一個subobject constructor調用了一個virtual function(無論是直接調用或間接調用)時。

在明确了哪些情況下vptr必須被設定,我們在聲明一個PVertex時,各個vptr不再需要在每一個base class constructor中被設定。解決之道是把constructor分裂為一個完整的object實體和一個subobject實體。在subobject實體中,vptr的設定可以忽略。

如果我們不對Point供應一個copy assignment operator,而光是依賴預設的memberwise copy,編譯器通常不會産生出一個實體,除非class不表現出bitwise語意。關于哪些情況class不表現出bitwise語意,請參見讀書筆記(2)。

由于在virtual base class的拷貝操作将造成subobject的多重拷貝,并且該問題至今難以解決。是以筆者的建議是:盡量不要允許一個virtual base class的拷貝操作,甚至建議:不要在任何virtual base class中聲明資料。

***解構語意學***

如果class沒有定義destructor,那麼隻有在class内含的member object(或是class自己的base class)擁有destructor的情況下,編譯器才會自動合成出一個來。

其解構順序與建構順序正好相反。

***對象的構造和解構***

一般而言,我們會把object盡可能放置在使用它的那個程式區段附近,這樣做可以節省不必要的對象産生操作和銷毀操作。

***全局對象***

全局對象的靜态初始化政策包括以下幾個步驟:

(1)為每一個需要靜态初始化的對象産生一個_sti_...()函數,内含必要的constructor調用操作或inline expansions;

(2)為每一個需要靜态的記憶體釋放操作的對象産生一個_std_...()函數,内含必要的destructor調用操作或inline expansions;

(3)在main()函數的首尾分别添加一個_main()函數(用以調用可執行檔案中的所有_sti()函數)和一個_exit()函數(用以調用可執行檔案中的所有_std()函數)。

建議根本不要用那些需要靜态初始化的全局對象。 

***局部靜态對象***

假設我們有以下程式片段:

深度探索C++對象模型》讀書筆記(3)
深度探索C++對象模型》讀書筆記(3)

const  Matrix &  identity()  ... {

深度探索C++對象模型》讀書筆記(3)

static Matrix mat_identity;

深度探索C++對象模型》讀書筆記(3)

// ...

深度探索C++對象模型》讀書筆記(3)

return mat_identity;

深度探索C++對象模型》讀書筆記(3)

}

此處的local static class object保證了以下語意:

(a)mat_identity的constructor必須隻能施行一次,雖然上述函數可能會被調用多次;

(b)mat_identity的destructor必須隻能施行一次,雖然上述函數可能會被調用多次。

編譯器的實際做法如下:在第一次調用identity()時把mat_identity構造出來,而在與相應檔案關聯的靜态記憶體釋放函數中将其解構。(局部靜态對象的位址在downstream component中将會被轉換到程式内用來放置global object的data segment中)

***對象數組***

如果你想要在程式中取出一個constructor的位址,這是不可以的。然而經由一個指針來激活constructor,将無法存取default argument values。那麼,如何支援以下的語句:

深度探索C++對象模型》讀書筆記(3)

complex::complex( double = 0.0 ,  double = 0.0 );

當程式員寫出:

深度探索C++對象模型》讀書筆記(3)

complex c_array[ 10 ];

時,編譯器最終需要調用:

   vec_new(&c_array,sizeof(complex),10,&complex::complex,0);

為了解決這個問題,可由編譯器産生一個内部的constructor,沒有參數,在其函數内調用由程式員提供的constructor,并将default參數值明确地指定過去:

深度探索C++對象模型》讀書筆記(3)

complex::complex()

深度探索C++對象模型》讀書筆記(3)
深度探索C++對象模型》讀書筆記(3)

... {

深度探索C++對象模型》讀書筆記(3)

complex(0.0, 0.0);

深度探索C++對象模型》讀書筆記(3)

}

***new和delete運算符***

以constructor來配置一個class object:

深度探索C++對象模型》讀書筆記(3)

Point3d  * origin  =   new  Point3d;

被轉換為:

深度探索C++對象模型》讀書筆記(3)

Point3d  * origin;

深度探索C++對象模型》讀書筆記(3)
深度探索C++對象模型》讀書筆記(3)

if (origin  =  _new( sizeof (Point3d)))  ... {

深度探索C++對象模型》讀書筆記(3)
深度探索C++對象模型》讀書筆記(3)

try ...{

深度探索C++對象模型》讀書筆記(3)

origin = Point3d::Point3d(origin);

深度探索C++對象模型》讀書筆記(3)

}

深度探索C++對象模型》讀書筆記(3)
深度探索C++對象模型》讀書筆記(3)

catch( ... ) ...{

深度探索C++對象模型》讀書筆記(3)

_delete(origin);  // 釋放因new而配置的記憶體

深度探索C++對象模型》讀書筆記(3)

throw;  // 将原來的exception上傳

深度探索C++對象模型》讀書筆記(3)

}

深度探索C++對象模型》讀書筆記(3)

}

如果我們配置一個數組,内帶10個Point3d objects,我們預期Point和Point3d的constructor被調用各10次,每次作用于數組中的一個元素:

深度探索C++對象模型》讀書筆記(3)

//  完全不是好主意

深度探索C++對象模型》讀書筆記(3)

Point  * ptr  =   new  Point3d[ 10 ];

如果我們進行如下的釋放操作:

深度探索C++對象模型》讀書筆記(3)

//  隻有Point::~Point被調用

深度探索C++對象模型》讀書筆記(3)

delete []ptr;

由于其觸發的vec_delete()是通過疊代走過每一個數組元素,而本例中被傳遞過去的是Point class object的大小而不是Point3d class object的大小,整個運作過程将會失敗。

解決之道在于程式層面,而非語言層面:

深度探索C++對象模型》讀書筆記(3)

for ( int  ix  =   0 ; ix  <   10 ; ix ++ )

深度探索C++對象模型》讀書筆記(3)
深度探索C++對象模型》讀書筆記(3)

... {

深度探索C++對象模型》讀書筆記(3)

Point3d *p = &((Point3d*)ptr)[ix];

深度探索C++對象模型》讀書筆記(3)

delete p;

深度探索C++對象模型》讀書筆記(3)

} 當然,最好還是避免以一個base class指針指向一個derived class objects所組成的數組。

***Template的“具現”行為***

template class中的任何member都隻能通過template class的某個實體來存取或操作。

深度探索C++對象模型》讀書筆記(3)

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

深度探索C++對象模型》讀書筆記(3)

Point::Status s;   //  error

如果我們定義一個指針,指向特定的實體,像這樣:

深度探索C++對象模型》讀書筆記(3)

Point < float >   * ptr  =   0 ;

由于這是一個指向class object的指針,本身并不是一個class object,編譯器不需要知道與該class有關的任何members資料。是以将“Point的一個float實體”具現也就沒有必要。

如果不是一個pointer而是reference,假設:

深度探索C++對象模型》讀書筆記(3)

Point < float >   & ref   =   0 ;

這個定義的真正語意會被擴充為:

深度探索C++對象模型》讀書筆記(3)

//  内部擴充

深度探索C++對象模型》讀書筆記(3)

Point < float >  temp( float ( 0 ));

深度探索C++對象模型》讀書筆記(3)

Point < float >   & ref   =  temp;

以上轉化是因為reference并不是無物(no object)的代名詞,0被視作整數,必須被轉換為類型Point<float>的一個對象。

然而,member functions隻有在member functions被使用的時候,C++ Standard才要求它們被“具現”出來。這個規則的由來主要有兩個原因:

(1)空間和效率的考慮。對于未使用的函數進行“具現”将會花費大量的時間和空間;

(2)尚未實作的功能。并不是一個template具現出來的所有類型一定能夠完整支援一組member functions,因而隻需具現真正需要的member functions。

舉個例子:

深度探索C++對象模型》讀書筆記(3)

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

隻有(a)Point template的float執行個體、(b)new 運算符、(c)default constructor需要被“具現”。

***Template的錯誤報告***

所有與類型相關的檢驗,如果涉及到template參數,都必須延遲到真正的具現操作發生。

對于下面的template聲明:

深度探索C++對象模型》讀書筆記(3)

template  < class  T >

深度探索C++對象模型》讀書筆記(3)

class  Mumble

深度探索C++對象模型》讀書筆記(3)
深度探索C++對象模型》讀書筆記(3)

... {

深度探索C++對象模型》讀書筆記(3)

public:

深度探索C++對象模型》讀書筆記(3)

Mumble(T t = 1024) : _t(t)

深度探索C++對象模型》讀書筆記(3)
深度探索C++對象模型》讀書筆記(3)

...{

深度探索C++對象模型》讀書筆記(3)

if(tt != t)

深度探索C++對象模型》讀書筆記(3)

throw ex ex;

深度探索C++對象模型》讀書筆記(3)

}

深度探索C++對象模型》讀書筆記(3)

private:

深度探索C++對象模型》讀書筆記(3)

T tt;

深度探索C++對象模型》讀書筆記(3)

}

其中像“T t = 1024”、“tt != t”這樣的潛在錯誤在template聲明時并不會報告,而會在每個具現操作發生時被檢查出來并記錄之,其結果将因不同的實際類型而不同。

深度探索C++對象模型》讀書筆記(3)

Mumble < int >  mi;   //  上述兩個潛在錯誤都不存在

深度探索C++對象模型》讀書筆記(3)

Mumble < int *>  pmi;   //  由于不能将一個非零的整數常量指定給一個指針,故“T t = 1024”錯誤

***Template中的名稱決議方式***

區分以下兩種意義:一種是“scope of the template definition”,也就是“定義出template”的程式,另一種是“scope of the template instantiation”,也就是“具現出template”的程式。

深度探索C++對象模型》讀書筆記(3)

//  scope of the template definition

深度探索C++對象模型》讀書筆記(3)

extern   double  foo( double );

深度探索C++對象模型》讀書筆記(3)
深度探索C++對象模型》讀書筆記(3)

template  < class  type >

深度探索C++對象模型》讀書筆記(3)

class  ScopeRules

深度探索C++對象模型》讀書筆記(3)
深度探索C++對象模型》讀書筆記(3)

... {

深度探索C++對象模型》讀書筆記(3)

public:

深度探索C++對象模型》讀書筆記(3)
深度探索C++對象模型》讀書筆記(3)

void invariant() ...{ _member = foo(_val); }

深度探索C++對象模型》讀書筆記(3)
深度探索C++對象模型》讀書筆記(3)

type type_dependent() ...{ return foo(_member); }

深度探索C++對象模型》讀書筆記(3)

// ...

深度探索C++對象模型》讀書筆記(3)

private:

深度探索C++對象模型》讀書筆記(3)

int _val;

深度探索C++對象模型》讀書筆記(3)

type _member;

深度探索C++對象模型》讀書筆記(3)

} ;

深度探索C++對象模型》讀書筆記(3)
深度探索C++對象模型》讀書筆記(3)

//  scope of the template instantiation

深度探索C++對象模型》讀書筆記(3)

extern   int  foo( int );

深度探索C++對象模型》讀書筆記(3)
深度探索C++對象模型》讀書筆記(3)

ScopeRules < int >  sr0;

在“scope of the template definition”中,隻有一個foo()函數聲明位于scope之内;然而在“scope of the template instantiation”中,兩個foo()函數聲明都位于scope之内。對于以下函數操作:

深度探索C++對象模型》讀書筆記(3)

//  scope of the template instantiation

深度探索C++對象模型》讀書筆記(3)

sr0.invariant();

那麼,在invariant()中調用的究竟是哪一個foo()函數實體呢?

Template之中,對于一個nonmember name的決議結果是根據這個name的使用是否與“用以具現出該template的參數類型”有關而決定的,如果其使用互不相關,那麼就以“scope of the template definition”來決定name,否則就以“scope of the template instantiation”來決定name。

深度探索C++對象模型》讀書筆記(3)

//  因為_val的類型是int,而函數的決議隻和函數原型有關,與函數傳回值無關

深度探索C++對象模型》讀書筆記(3)

//  被用來具現這個template的真正類型對于_val的類型沒有影響

深度探索C++對象模型》讀書筆記(3)

_member  =  foo(_val);

故此處的調用操作由“scope of the template definition”來決議。

若是如下的函數調用:

深度探索C++對象模型》讀書筆記(3)

sr0.type_dependent();

由于_member的類型與template參數有關,故此處由“scope of the template instantiation”來決議。

***Member Function的具現行為***

以手動方式在個别的object module中完成預先具現操作,是唯一有效率的辦法。

***執行期類型識别***

dynamic_cast運算符可以在執行期決定真正的類型。如果downcast是安全的(也就是說,一個base type pointer指向一個derived class object),這個運算符會傳回被适當轉型過的指針;如果downcast不是安全的,這個運算符會傳回0。

深度探索C++對象模型》讀書筆記(3)

typedef type  * ptype;

深度探索C++對象模型》讀書筆記(3)

typedef fct  * pfct;

深度探索C++對象模型》讀書筆記(3)
深度探索C++對象模型》讀書筆記(3)

simplify_conv_op(ptype pt)

深度探索C++對象模型》讀書筆記(3)
深度探索C++對象模型》讀書筆記(3)

... {

深度探索C++對象模型》讀書筆記(3)
深度探索C++對象模型》讀書筆記(3)

if(pfct pf = dynamic_cast<pfct>(pt)) ...{

深度探索C++對象模型》讀書筆記(3)

...

深度探索C++對象模型》讀書筆記(3)

}

深度探索C++對象模型》讀書筆記(3)
深度探索C++對象模型》讀書筆記(3)

else ...{ ... }

深度探索C++對象模型》讀書筆記(3)

}

什麼是dynamic_cast的真正成本?pfct的一個類型描述器會被編譯器産生出來,由pt指向之class object類型描述器必須在執行期通過vptr取得。下面是可能的轉換:

深度探索C++對象模型》讀書筆記(3)

//  取得pt的類型描述器

深度探索C++對象模型》讀書筆記(3)

((type_info * )(pt -> vptr[ 0 ])) -> _type_description;

其中,type_info是C++ Standard所定義的類型描述器的class名稱,該class中放置着待索求的類型資訊。virtual table的第一個slot内含type_info object的位址,此type_info object與pt所指之class type有關。

dynamic_cast運算符也适用于reference身上,然而對于一個non-type-safe-cast,其結果不會與施行于指針的情況一樣。一個reference不可以像指針那樣“把自己設為0便代表了no object”;若将一個reference設為0,會引起一個臨時性對象(擁有被參考到的類型)被産生出來,該臨時對象的初值為0,這個reference然後被設定為該臨時變量的一個别名。

因而,如果reference并不真正是某一種derived class,那麼可通過丢出一個bad_cast exception進行處理:

深度探索C++對象模型》讀書筆記(3)

simplify_conv_op( const  type  & rt)

深度探索C++對象模型》讀書筆記(3)
深度探索C++對象模型》讀書筆記(3)

... {

深度探索C++對象模型》讀書筆記(3)
深度探索C++對象模型》讀書筆記(3)

try ...{

深度探索C++對象模型》讀書筆記(3)

fct &rf = dynamic_cast<fct&>(rt);

深度探索C++對象模型》讀書筆記(3)

}

深度探索C++對象模型》讀書筆記(3)
深度探索C++對象模型》讀書筆記(3)

catch(bad cast) ...{

深度探索C++對象模型》讀書筆記(3)

// ...

深度探索C++對象模型》讀書筆記(3)

}

深度探索C++對象模型》讀書筆記(3)

}

當然,你也可以使用typeid運算符來達到同樣的目的:

深度探索C++對象模型》讀書筆記(3)

simplify_conv_op( const  type  & rt)

深度探索C++對象模型》讀書筆記(3)
深度探索C++對象模型》讀書筆記(3)

... {

深度探索C++對象模型》讀書筆記(3)

if(typeid(rt) == typeid(fct))

深度探索C++對象模型》讀書筆記(3)
深度探索C++對象模型》讀書筆記(3)

...{

深度探索C++對象模型》讀書筆記(3)

fct &rf = dynamic_cast<fct&>(rt);

深度探索C++對象模型》讀書筆記(3)

}

深度探索C++對象模型》讀書筆記(3)
深度探索C++對象模型》讀書筆記(3)

else ...{ ... }

深度探索C++對象模型》讀書筆記(3)

}