天天看點

用彙編的眼光看C++(之預設模闆、特化模闆) 18

【 聲明:版權所有,歡迎轉載,請勿用于商業用途。  】

    預設函數是C++的一個基本特色。預設函數定義比較簡單,也就是說,對于函數的某一個輸入參數或者幾個輸入參數,如果你沒有特定的數值的話,那我們就會 用預設的資料進行代替。如果你在調用的過程中使用了自己的資料,那麼預設資料将被我們自己定義的資料覆寫。下面就是一個預設函數的示例:

[cpp] view plaincopy

  1. int  add(int m, int n = 10)  
  2. {  
  3.     return m + n;  
  4. }  

     如果調用呢,有什麼差別?

  1. 262:      int p = add(2);  
  2. 00401488   push        0Ah  
  3. 0040148A   push        2  
  4. 0040148C   call        @ILT+15(add) (00401014)  
  5. 00401491   add         esp,8  
  6. 00401494   mov         dword ptr [ebp-4],eax  
  7. 263:      p = add(3, 4);  
  8. 00401497   push        4  
  9. 00401499   push        3  
  10. 0040149B   call        @ILT+15(add) (00401014)  
  11. 004014A0   add         esp,8  
  12. 004014A3   mov         dword ptr [ebp-4],eax  

    可以從上面的代碼看到,如果單獨輸入一個資料2,那麼編譯器幫我們預設輸入了10;如果輸了的資料是3、4呢,那麼編譯器将用4代替預設的資料10。所 以說,編譯器幫我們做了中間的替換和判斷工作。那麼回到我們今天讨論的預設模闆類型上面,那會是什麼樣的情形呢?我們可以編寫一個範例:

  1. template <typename type1, typename type2 = int>  
  2. class data  
  3.     type2 value;  
  4. public:  
  5.     data(type2 m): value(m) {}  
  6.     ~data() {}  
  7. };  

    可以看到,我們在第二個參數使用了預設類型int,那麼怎麼證明預設類型可以使用呢?我們設計了下面一個測試用例:

  1. 239:      data<int, int> m(2);  
  2. 004013BD   push        2  
  3. 004013BF   lea         ecx,[ebp-10h]  
  4. 004013C2   call        @ILT+5(data<int,int>::data<int,int>) (0040100a)  
  5. 004013C7   mov         dword ptr [ebp-4],0  
  6. 240:      data<int> n(3);  
  7. 004013CE   push        3  
  8. 004013D0   lea         ecx,[ebp-14h]  
  9. 004013D3   call        @ILT+5(data<int,int>::data<int,int>) (0040100a)  

    上面的代碼定義了兩個臨時變量,其中第一個是m,輸入類型是int;第二個臨時變量是n,輸入類型是int和int。前面我們說過預設類型是int,那 麼第一個臨時變量m和第二個臨時變量n的構造函數位址應該是一樣的。那麼事實上兩者的構造函數是不是一樣的呢?我們可以檢視兩者的函數位址,發現一個是 0x0040100a,另外一個也是0x0040100a。範例證明我們的判斷是正确的。

    明白了上面的預設模闆構造,下面我們談一下特化模闆。特化模闆是什麼意思呢?其實并不複雜。因為模闆類既然是通用模闆,那麼其中的資料類型可以是任意數 據類型,但是難免有一些資料類型(比如說指針),我們需要對其中的一些操作做一些細微的修改,但是這些小的修改在原來的模闆定義上是無法做的。那麼怎麼 辦?我們隻好重新定義一種形式,它和模闆類定義的名稱一緻,但是形式稍有差别。我們可以編寫一個測試看看:

  1. template <typename type>  
  2.     data() {printf("normal!\n");}  
  3.     ~data() {printf("~normal!\n");}  
  4. template <>  
  5. class data<int*>  
  6.     data() {printf("point!\n");}  
  7.     ~data() {printf("point!\n");}  

    上面的代碼定義了兩個類模闆。但是兩者的名稱是一樣的,說明這兩個類定義的内容其實具有很大的相似性。第一種定義就是标準模闆類的定義,第二中稍微複雜 一點,使用預設的int*,因為沒有使用到特定的type類型,是以此時template後面的内容為空。那麼怎麼判斷這兩個類都是可以正常使用的呢?大 家可以看看下面的範例:

  1. 249:      data<int> p;  
  2. 004013BD   lea         ecx,[ebp-10h]  
  3. 004013C0   call        @ILT+45(data<int>::data<int>) (00401032)  
  4. 004013C5   mov         dword ptr [ebp-4],0  
  5. 250:      data<int*> q;  
  6. 004013CC   lea         ecx,[ebp-14h]  
  7. 004013CF   call        @ILT+35(data<int *>::data<int *>) (00401028)  
  8. 251:  }  

    我們發現,第一個函數的call位址是0x00401032,第二個位址為0x00401028。但是這說明不了什麼,因為第二個位址完全也可能是第一個模闆類引申的。我們應該跟到每一個函數裡面(其實這裡的位址在VC下都是跳轉位址)。

    第一個變量的實際進入函數如下所示:

  1. 234:      data() {printf("normal!\n");}  
  2. 00401340   push        ebp  
  3. 00401341   mov         ebp,esp  
  4. 00401343   sub         esp,44h  
  5. 00401346   push        ebx  
  6. 00401347   push        esi  
  7. 00401348   push        edi  
  8. 00401349   push        ecx  
  9. 0040134A   lea         edi,[ebp-44h]  
  10. 0040134D   mov         ecx,11h  
  11. 00401352   mov         eax,0CCCCCCCCh  
  12. 00401357   rep stos    dword ptr [edi]  
  13. 00401359   pop         ecx  
  14. 0040135A   mov         dword ptr [ebp-4],ecx  
  15. 0040135D   push        offset string "normal!\n" (0042607c)  
  16. 00401362   call        printf (00401540)  
  17. 00401367   add         esp,4  
  18. 0040136A   mov         eax,dword ptr [ebp-4]  
  19. 0040136D   pop         edi  
  20. 0040136E   pop         esi  
  21. 0040136F   pop         ebx  
  22. 00401370   add         esp,44h  
  23. 00401373   cmp         ebp,esp  
  24. 00401375   call        __chkesp (004023b0)  
  25. 0040137A   mov         esp,ebp  
  26. 0040137C   pop         ebp  
  27. 0040137D   ret  

    那麼,第二個變量呢,同樣需要跟入函數:

  1. 242:      data() {printf("point!\n");}  
  2. 00401430   push        ebp  
  3. 00401431   mov         ebp,esp  
  4. 00401433   sub         esp,44h  
  5. 00401436   push        ebx  
  6. 00401437   push        esi  
  7. 00401438   push        edi  
  8. 00401439   push        ecx  
  9. 0040143A   lea         edi,[ebp-44h]  
  10. 0040143D   mov         ecx,11h  
  11. 00401442   mov         eax,0CCCCCCCCh  
  12. 00401447   rep stos    dword ptr [edi]  
  13. 00401449   pop         ecx  
  14. 0040144A   mov         dword ptr [ebp-4],ecx  
  15. 0040144D   push        offset string "point!\n" (00426074)  
  16. 00401452   call        printf (00401540)  
  17. 00401457   add         esp,4  
  18. 0040145A   mov         eax,dword ptr [ebp-4]  
  19. 0040145D   pop         edi  
  20. 0040145E   pop         esi  
  21. 0040145F   pop         ebx  
  22. 00401460   add         esp,44h  
  23. 00401463   cmp         ebp,esp  
  24. 00401465   call        __chkesp (004023b0)  
  25. 0040146A   mov         esp,ebp  
  26. 0040146C   pop         ebp  
  27. 0040146D   ret  

    看到上面的函數,大家應該明白了兩者調用的構造函數并不一樣。是以說,特化模闆通常就是為了那些特殊的資料類型準備的。這樣我們使用者在使用模闆的時候 就沒有什麼顧慮了,可以忽略各個資料類型處理上的差别了。當然,特化模闆因為考慮了特殊模型資料,使得我們的代碼更加完畢,更加健壯了,建議在設計模闆的 時候适當多使用。

思考題:

    (1)模闆類第一個type可以預設嗎?為什麼會這麼考慮? 

    (2)下面的代碼在vc 6.0和vc 2005上都能編譯過?為什麼呢?對于我們設計代碼有什麼思考呢?  (建議從相容性上面考慮)

  1. class data <type*>  

【預告: 下面的兩個部落格非常有意思,介紹遞歸模闆和模闆的模闆内容】