當模板函數遇上數組參數


  在伯樂在線上看到一篇關於數組和指針的文章(文章鏈接:http://blog.jobbole.com/44863/),突然想到自己最近也遇到一個類似的有趣的案例,於是決定寫下來和大家分享。

1. 我的初衷

  我的初衷是想寫一個簡單通用的函數PrintIntArray用於打印一個int數組的各個元素。因為我想數組的長度是數組的屬性,我不想每次調用此函數的時候手動傳入數組長度,於是我將函數聲明為PrintIntArray(int arr[]),然后寫一個簡單的內聯函數(為了通用,聲明為模板函數)用於動態獲取數組長度(如下):

template <class T>
inline int GetArrayLen(T& array)
{
  //數組占用內存數除以單個元素占用內存數得到數組長度
    return sizeof(array)/sizeof(array[0]);
}

  這樣,我的PrintIntArray函數就可以這樣寫:

void PrintIntArray(int arr[])
{
    int len = GetArrayLen(arr);
    for (int i = 0; i < len; i++)
    {
        printf("%d ", arr[i]);
    }
    printf("\r\n");
}

2. 初衷很美好,問題跑不掉

  為了測試打印函數,寫一個main函數進行測試:

void main(int argc, char* argv[])
{
    int arr[] = {1, 2, 3, 4, 5};
    printf("array length: %d\r\n", GetArrayLen(arr));
    printf("elements of array:\r\n");
    PrintIntArray(arr);
    getchar();
}

  當運行測試程序的時候,運行結果卻出乎我的意料:

  可以看到,GetArrayLen函數可以正確地計算數組長度,但是PrintIntArray卻只打印出了數組的第一個元素。

  於是調試進到PrintIntArray函數的 int len = GetArrayLen(arr); 這句話,發現返回的值是1,怪不得只打印了第一個元素:

  這就奇怪了,GetArrayLen在PrintIntArray函數外面(main函數里面)的時候明明可以正確返回數組長度,為什么進到函數里面的時候行為就變得異常了?大家都知道,數組作為函數參數的時候傳遞的是指針,如果這是造成異常的原因,那么在main函數里面GetArrayLen(arr)這句話也是傳遞的指針,應該同樣返回1才對,為什么它就可以正確地返回數組的長度?

3. 問題分析

  后來經過更多的測試分析,發現問題出在GetArrayLen這個模板函數的聲明、以及c++對模板的解析機制上。在PrintIntArray函數內部, int len = GetArrayLen(arr); 這句話返回1的原因稍微分析一下其實是容易理解的,因為當你將數組變量arr傳遞給PrintIntArray函數時,它其實已經退化成了指針,你再將指針傳遞給GetArrayLen函數,sizeof(arr)求得的是指針占用的內存數,結果是4;而sizeof(arr[0])返回的是arr第一個元素占用的內存字節數,因為是int數組,所以結果也是4。這就是GetArrayLen(arr)函數最后返回1的原因。

  而在main函數里面調用GetArrayLen(arr)函數的時候,在arr退化成指針之前,它要先被GetArrayLen模板函數解析,解析的結果就是模板函數形參中的T被解析為int[5](這里似乎很奇怪,后面還有更詳細的分析),形參array被當做實參arr的別名。實際上arr還是被當成數組看待的,即一塊連續的內存,並沒有退化成指針,因此此時sizeof(arr)的結果為5個int的長度,即20字節;而sizeof(arr[0])的結果依然是arr第一個元素占用的內存,即4字節,因此此時會返回正確的數組長度。

  為了驗證這一猜想,我另外寫了個測試程序,但此時我用的是double型數組,按照上面的分析,如果將數組名直接傳遞給GetArrayLen函數,它將依然被當成數組看待,因此sizeof(arr)的結果應該是40(5個double數據的長度),而sizeof(arr[0])的結果應該是8(double型數據長度),最終GetArrayLen函數返回正確的數組長度——5;但是如果將此數組的指針傳遞給GetArrayLen函數,那么sizeof(arr)的結果應該是4(指針占用內存數),而sizeof(arr[0])的結果依然是8,最終GetArrayLen函數返回0。

  為了調試方便,我將GetArrayLen函數重寫為:

template <class T>
int GetArrayLen(T& array)
{
    int len1 = sizeof(array);
    int len2 = sizeof(array[0]);
    return len1 / len2;
}

  測試用main函數如下:

1 void main(int argc, char* argv[])
2 {
3     double arrDouble[] = {1, 2, 3, 4, 5};
4     double* ptrDouble = arrDouble;
5     printf("pass array to func GetArrayLen :%d\r\n", GetArrayLen(arrDouble));
6     printf("pass pointer to func GetArrayLen :%d\r\n", GetArrayLen(ptrDouble));
7     getchar();
8 }

  當傳遞數組arrDouble進去的時候(第5行),單步調試到GetArrayLen函數內部,結果如下:

  當傳遞指針ptrDouble進去的時候(第6行),單步調試到GetArrayLen函數內部,結果如下:

  可見,上面的分析是正確的。程序最終的運行結果如下:

4. 尋求改進

  經過這么多的分析最后發現,自己一開始寫的打印函數PrintIntArray其實根本無法工作,因為他限制傳入的數組不能為引用,這與數組傳引用的機制相矛盾。其實如果清楚c++模板的解析機制,就不用繞這么多彎了,不僅可以寫出數組打印函數,而且是對所有基礎數據類型數組都有效的打印函數。

  我們繼續分析。

  前面的分析寫到,(GetArrayLen)“模板函數形參中的T被解析為int[5]”,不僅如此,如果你傳遞的數組長度為8,T就被解析為int[8],長度為10,T就被解析為int[10]……我們發現模板解析機制可以自動得到輸入數組的長度,這給了我們巨大的驚喜和啟發,是不是可以利用此機制自動獲取傳入的數組長度呢?答案是肯定的,我們還是慢慢來看。

  一開始,針對通用數組打印函數問題,我也是百度了一下,得到的一個版本如下:如果你想打印長度為10的數組,那么可以這樣寫:

template <class T>
void PrintArray(T (&arr)[10])
{
    for (int i = 0; i < 10; i++)
    {
        printf("%d ", arr[i]);
    }
    printf("\r\n");
}

void main(int argc, char* argv[])
{
    int arr[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    PrintArray(arr);
    getchar();
}

  但是這樣還是不夠完美,因為只能限制打印的數組長度為10,如果改變一下數組,例如arr[] = {0, 1, 2, 3, 4},因為傳入數組長度和模板函數聲明的長度不一致,編譯都不會通過,會報如下錯誤:

error C2784: “void PrintArray(T (&)[10])”: 未能從“int [5]”為“T (&)[10]”推導 模板 參數

  雖然不夠完美,但是也正是這個不完美的版本以及這句編譯提示,讓我想到了c++背后的模板解析機制,以及做出“模板函數形參中的T被解析為int[5]”這句結論的原因。其實這個版本已經非常接近最終版本了,既然數組長度是動態解析的,那么我們只需要將模板函數聲明中的常量10改為變量是不是就可以了呢?

  答案就是這樣的,只需多加一個模板參數聲明,最終完美版便誕生了:

template <class T, int size>
void PrintArray(T (&arr)[size])
{
    for (int i = 0; i < size; i++)
    {
        cout<<arr[i]<<" ";
    }
    cout<<endl;
}

  為了通用性,改用cout輸出,為此,需要添加如下兩句預編譯指令:

#include <iostream>

using namespace std;

  這樣,你就可以用PrintArray打印任意(基礎數據)類型、任意長度的數組了。

  這里再回過頭來看一下模板解析過程。以array[] = {0, 1, 2, 3, 4}為例,當調用PrintArray(array)函數,遇到void PrintArray(T (&arr)[size])這樣的模板函數聲明時,編譯器將形參arr作為實參array的別名,同時T被解析為int,size被解析為5(數組長度,可變),這樣就可以正確打印出數組內容了。

5. 結論

  • 數組傳遞給函數時會退化成指針
  • 模板是C++中一種靈活又復雜的機制,弄清楚這種機制能幫助你更簡單高效地解決實際問題
  • 老生常談:學習編程語言沒有捷徑,平時多動手敲代碼,遇到問題善於分析,針對你想到的所有可能設計測試用例,證明或證偽它!

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM