深度探索C++對象模型之第一章:關於對象之對象的差別

1、三種程序設計範式:程序員

C++程序設計模型支持三種程序設計範式(programming paradiams).算法

  • 程序模型(procedural model)
char boy[] = "ccpang";
char *p_son;

p_son = new char[strlen(boy) +1 ];
strcpy(p_son,boy);

if(!strcmp(p_son,boy))
   take_to_disneyland(boy);
  •  抽象數據模型(abstract data type model)

    此模型的抽象是和一組表達式(public接口)一塊兒提供,那時其運算定義仍然隱而未明的。編程

1 String girl = "Anna"
2 String daughter;
3 
4 //String ::operator();
5 daughter = girl;
6 
7 //String::operator == ();
8 if(girl ==daughter)
9       take_to_disneyland(girl);

 

  • 面向對象模型(object-orlented model)

    此模型中有一些彼此相關的類型,經過一個抽象的基類(用以提供共同接口)被封裝起來。Library_materials class就是一個例子,真正的子類例如Book、Video、等都可以從那裏派生而來。數組

1 void check_in (Library_materials *pmat)
2 {
3    if(pmat ->late())
4        pmat->fine();
5 pmat->check_in();
6 
7     if(Lender *plend = pmat->reserved())
8           pmat->notify(plend);
9 }

 


 

 

  只以一種程序設計範式寫代碼,有助於總體行爲的良好穩固。若是混合了不一樣的範式,就可能會帶來讓人吃驚的後果。常出現的問題以下所示:ide

  以一個基類的具體實例來完成某種多態(polymorphism)狀況是時:函數

 1 Library_materials thing1;
 2 
 3 //class Book : public Library_materials{....};
 4 Book book;
 5 
 6 //thing1不是一個book,book被裁減了(sliced)
 7 thing1 = book;   //調用賦值運算符,只對基類部分進行操做,派生類的其餘部分將被忽略。
 8 
 9 //調用的是Library_materials::check_in()
10 thing1.check_in();
11 
12 //經過基類的指針或引用來完成多態局面:
13 Library_materials &thing2 = book;  //基類的指針或引用能夠指向派生類
14 
15 //如今使用的是Book::check_in()
16 thing2.check_in();

   雖然你能夠直接或間接(賦值或引用)處理繼承體系中的一個基類對象,可是隻有經過指針或引用的間接處理,才支持面向對象程序設計所需的多態性質。thing2的定義和運用符號面向對象編程的良好習慣。而thing1的定義和運用則不是面向對象的習慣,它反映的是一個抽象數據類型範式的良好習慣。thing1的行爲是好是壞,取決於程序員的意圖。佈局

   在面向對象編程範式中,須要處理的一個未知實例,雖然它的類型有所界定,可是卻存在無數種可能,它的類型受限於其繼承體系,然而該體系理論上沒有深度和廣度的限制。原則上,被指定的對象的真實類型在每個特定執行點以前,是沒法解析的。在C++中,只有經過指針和引用操做才能完成。相反,在抽象數據類型範式中程序員處理的是一個擁有固定而單一類型的實例,它在編譯時期就已經徹底定義好了。例以下面:spa

1 //描述對象:不肯定類型
2 
3 //基類的一個指針或引用,可能指向一個基類對象或者基類對象的派生類(子類型)
4 Librar_materials *px = retrieve_some_material(); 
5 Librar_materials &rx = *px;
6 
7 //dx是一個基類對象,它的值是px所指向的值。賦值運算符。
8 Librar_materials dx = *px;

 

   值得說明的是:這樣的行爲雖然或許未如你所預期,倒是良好的行爲。雖然對於對象的多態操做,要求此對象必須能夠由一個指針或引用來存取,可是有指針和引用並非多態。設計

  在C++中,多態只存在於公有類的繼承體系中(public class),px只能指向某個類型的基類對象,或是根據public繼承關係派生而來的一個子類型。非公有的派生行爲以及類型爲void*的指針能夠說是多態的,可是它們並無被語言所明確的支持,也就是它們必須由程序員經過顯式的轉換操做來管理。指針


2、C++支持多態的方法:

  • 隱式轉換操做
1  //例如將一個派生類指針轉換爲一個指向其基類型的指針
2 shape *ps = new circle();
  • 由虛函數機制
1 ps->rotate();
  • 由dynamic_cast和typeid運算符
1 if(circle *pc = dynameic_cast<circle*>() ps)

    多態的主要用途是經由一個共同的接口來影響類型的封裝,這個接口一般被定義在一個抽象的基類中。 

 1 void rotate(X datum,cosnt X *pointer,cosnt X &reference)
 2 {
 3    //在執行期以前,沒法決定到底調用哪個rotate()實例
 4    (*pointer).rotate();
 5    reference.rotate();
 6    
 7    //
 8    //datum.rotate();
 9 
10 }
11 
12 
13 main(){
14   Z z; //Z是X的一個子類型
15   rotate(z,&z,z);
16   return 0;
17 }

 

   在上例中,經pointer 和reference完成的兩個函數調用操做會被動態完成,而通過datum完成的函數調用操做則不通過virtual機制。

 


3、表示一個類對象須要多少內存:

  • 非靜態數據成員的總和大小
  • 加上因爲alignment的需求而填補上去的空間。(alignment就是將數值調整到某數的倍數)
  • 加上爲了支持virtual而由內部產生的任何額外負擔(overhead)

 


 

 4、指針的類型:

  一個指向ZooAnimal的指針和一個指向整數的指針和一個指向模板數組的指針有什麼不一樣呢?

1 ZooAnimal *px;
2 int *pi;
3 Array<String> *pta;

 

 答案是沒有什麼不一樣。指向不一樣類型的指針間的差別,既不在其指針表示法不一樣,也不在其內容(表明一個地址)不一樣,而是在其所尋址出來的對象不一樣。也就是說,指針類型會教導編譯器如何解釋某個特定的地址中的內存內容及其大小。

 舉個例子:如下是一個ZooAnimal的聲明:

  

 1 class ZooAnimal {
 2 public:
 3     ZooAnimal();
 4     virtual ~ZooAnimal();
 5 
 6 //。。。
 7    virtual void rotate():
 8 
 9 protected:
10      int loc;
11      String name;
12 };
13 
14 ZooAnimal za("pig");
15 ZooAnimal *pza = &za;

 ZooAnimal在內存中的佈局:

 

那麼一個指向地址1000而類型爲void*的指針,將涵蓋怎樣的地址空間呢?答案是不知道,這就是爲何一個void*指針只可以持有一個地址而不能經過它操做所指對象。

綜上獲得,轉換隻是一種編譯器指令,大部分狀況下它並不改變一個指針所含的真正地址,它只會影響被指出的內存的大小和其內容。


5、加上多態以後:

  以下所示一個Bear類,它繼承自ZooAnimal:

class Bear :public ZooAnimal{
public;
  Bear();
 ~Bear();
  void rotate():
  virtual void dance();

protected:
  enum Dances{...};
  Dances dances_know;
  int cell_blook:

};

Bear b("panda");
Bear *Pb = &b;
Bear &rb = *pb;

 

   不論是指針(pointer)或引用(reference)都只須要一個word(在32位機器上是4-bytes)的空間。Beard對象須要24個bytes,也就是ZooAnimal的16個bytes再加上Bear所帶來的8bytes.以下圖所示:

若是把一個Bear對象放在地址1000處,那麼一個Bear指針和一個ZooAnimal指針有什麼不一樣? 這就是將一個派生類賦給基類的指針

Bear b;
ZooAnimal *Pz = &b;   //pz是一個指向派生類Bear的基類指針
Bear *pb = &b;    //pb是一個指向Bear的Bear指針

 

 答案是它們都會指向Bear對象的第一個字節,可是pb所涵蓋的地址包含整個Bear對象,而pz所涵蓋的地址只包含Bear對象中的ZooAnimal實例。(將Bear的部分截取掉了)

 除了ZooAnimal實例中出現的成員,你不能用pz來直接處理Bear的任何members。惟一例外是經過virtual機制。

1 pz ->cell_block;//這是不合法的,雖然咱們知道pz指向一個Bear對象,可是cell_block不是ZooAnimal的一個成員;
2 
3 //通過一個顯示轉換操做就能夠了
4 (static_cast<Bear*>(pz))->cell_block;
5 
6 //下面這樣更好
7 if(Bear *pb2 = dynamic_cast<Bear*>(pz))
8    pb2->cell_block;

 

   當咱們使用pz->rotate();時,pz的類型將在編譯時期決定如下兩點:

  • 固定的可用接口,也就是說,pz只能調用ZooAnimal的public接口
  • 該接口的access level。

在每個執行點,pz所指向的對象類型能夠決定rotate()所調用的實例。類型信息的封裝並非維護在pz上,而是維護在link之中,這個link存在於對象的vptr和vptr所指的虛函數表之間。


 

 6、面向對象程序設計並不支持對對象的直接處理:

{
   ZooAnimal za;
   ZooAnimal *pza;
  
    Bear b;
    Panda *pp = new Panda;

    pza = &b;
}

 

 其內存佈局可能以下圖所示:
  

注意:new開闢的是Heap Memory(堆內存),其餘都是在棧上開闢的。

 將za或b的地址或pp所指的內容(也是地址),指定給pza,顯然不是問題。一個指針或引用之因此支持多態,是由於它們並不引起內存中任何「與類型有關的內存委託操做(type-dependent commitment)」,會受到改變的只有它們所指向的內存的「大小和內容解釋方式而已」

   然而若是將一個Bear對象附給一個ZooAnimal,會違法其定義中受契約保護的「資源需求量」。將一個Bear對象指定給za,則會溢出它所分配獲得的內存。(za = Bear)

   而當一個基類對象被初始化爲一個派生類對象時(會引發切割),派生類對象就會被切割以塞入較小的基類內存中,派生類型將沒有留下任何痕跡,多態將再也不呈現。

 


總而言之,多態容許你繼一個抽象的public接口以後,封裝相關的類型。可是要付出的代價就是額外的間接性——不管是在「內存的得到」,仍是類型的決斷上。C++經過類的指針和引用來支持多態,這種程序設計風格就被稱爲面向對象。

C++也支持具體的抽象數據類型風格,現在被稱爲基於對象(object-based(OB)),例如String,一種非多態的數據類型。String class能夠展現封裝的非多態形式:它提供一個public接口和一個private實現品:包括數據和算法,可是不支持類型的擴充。一個OB設計可能比OO設計速度更快且空間更緊湊:速度快是由於全部的函數調用都是編譯時期完成的,對象構建起來並不須要設置virtual機制;空間緊湊是由於每個類對象不須要負擔傳統上爲了支持virtual機制而須要的額外負擔,不過OB沒有彈性。

相關文章
相關標籤/搜索