在伯樂在線上看到一篇關於數組和指針的文章(文章鏈接: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++中一種靈活又復雜的機制,弄清楚這種機制能幫助你更簡單高效地解決實際問題
- 老生常談:學習編程語言沒有捷徑,平時多動手敲代碼,遇到問題善於分析,針對你想到的所有可能設計測試用例,證明或證偽它!