在C語言中,對於三維或三維以上數組的使用並沒有很好的支援,而且使用率也非常的低,後面會對三維數組做一些簡單的分析,這篇文章主要以二維數組來探討一些C語言中數組使用的相關概念和技巧。
1 一個var[i][j]引用形式的可能聲明
當你看見像var[i][j]這樣的二維數組引用形式時,你能知道他是怎麼被聲明的嗎?答案是否定的,指標和數組使用的部分通用性會讓你無法判斷這樣的一種形式的聲明原型,對於一個二維數組而言,它一般的聲明方式是:
int var[10][12]; /* 標準的int類型二維數組 */
它可以通過var[i][j]形式來訪問,那麼這種形式的引用還有三種可能的聲明:
(1) int *var[10]; /* int類型指標數組 */
(2) int **var; /* int類型的指標的指標 */
(3) int (*var)[12]; /* 類型為int數組的指標 */
很顯然,這非常像函數內部無法分配傳遞給它的實參究竟是一個數組函數一個指標一樣(上一篇文章數組與指標之間的較量已經詳細說明),基於同樣的理由:作為左值的數組名被編譯前當做是指標。
2 數組參數如何被編譯前修改
將一個數組作為參數傳遞給函數會被編譯器做修改,在函數中實際上把傳遞過來的數組名當做了指標來使用,就像第一小節中的var[i][j]引用形式可能有四種聲明方式,這四種方式的聲明在作為參數傳遞給函數時,編譯器是怎樣對其進行修改的?是不是像var[i][j]這樣的二維數組被修改成**var?
事實上,數組名被修改成一個指標參數的規則不是遞迴的定義,數組的數組被被改寫為數組的指標而不是指標的指標,下面是四種聲明方式的修改情況:
(1)數組的數組: char var[i][j];作為實參傳遞給函數時,所匹配的形式參數為數組的指標char (*)[j];
(2)指標數組: char *var[j];作為實參傳遞給函數時,所匹配的形式參數為指標的指標char **var;
(3)數組指標: char (*var)[j];作為實參傳遞給函數時,所匹配的形式參數還是數組的指標char (*var)[j];
(4)指標的指標: char **var;作為實參傳遞給函數時,所匹配的形式參數還是指標的指標char **var;
從上面的四種情況,你或許已經領略了這句話:被修改成指標的只是數組名而已,這相當於只修改最左面的維度為指標(不遞迴的精妙所在),如果聲明已經不是數組名形式或者說已經成為純指標(數組名的深刻含義),那麼編譯器不會對其做任何修改。
可以舉個例子來說明:
通常我們在main()函數中可以看到char **argv(基於傳參的入口函數)參數,它實際上保持的是使用者傳遞過來的多字串,這些字串在被作為實參傳遞之前被儲存為字元(串)指標數組char *temp[i](暫且叫做temp),temp被編譯器修改位char **類型,因為temp實際上是一個數組名。
3 如何使用數組參數
(1) 一維數組
一維數組作為函數參數是數組傳參最簡單的形式,形參被改寫為指標,所以需要一個約定來表示數組的長度,一般有兩種方法:
A 增加一個額外的參數,表示元素的個數
B 賦予數組最後一個元素一個特殊的值,提示他是數組的尾部,但要保證這個特殊值不會出現在正常元素值中
(2) 二維數組
和一維數組的方法一樣,對於B方法需要多加一行來填所有的元素為特殊值即可,二維數組傳參的使用後面會詳細說明。
4 使用指標對函數傳遞多維陣列
第三小節描述的方法比較笨拙,可以標記數組範圍這個問題,但是這種方法不能表達“這個數組的邊界在不同的調用中可以變化”這個概念,如果一個二維數組的每個元素(一個一維數組)的大小不同,這將無法表示。
C語言中,我們需要知道每一維度長度,這樣才能為地址運算提供正確的單位長度,然而沒有辦法向函數傳遞一個普通的多維陣列,即使數組名被編譯器改寫為指標(非遞迴改寫,這隻能解決一維數組),事實上,我們需要提供除了最左邊一維以外的所有維度長度。對於下面的函式宣告,我們可以這樣調用:
void my_func(int a[][5][10]);
調用方法:
int b[10][5][10];
int c[5][5][10];
my_func(b);
my_func(c);
但是不可以這樣調用:
int d[10][10][10];
int e[20][10][5];
my_func(d);
my_func(e);
這樣的調用肯定是過不了編譯器這一關的。
那就是說:你可以像函數傳遞預先確定長度的特殊數組,但這個方法不能滿足一般的情況,下面我們來一步一步說明這個問題。
1)方法一
my_func(int array[10][20]);
這樣的方法雖然簡單,但同時也是作用做小的聲明方式,因為它只能處理10行20列的int類型的數組,如果要一個更為普通的多維陣列形參的方法,使函數能操作任意長度數組。
2)方法二
my_func(int (*array)[20]);
這樣可以確保他被編譯器當做一個指向20個元素的int數組的指標,但對於二維數組的參數傳遞,它並不具有通用性,因為還有一個20感覺很糟糕。
3)方法三
本為的第二小節的最後有分析過:
main(int argc, char **argv);
當然也可能是:
main(int argc, char *argv[]);
前面一種是一個指標的指標,後面一種是一個指標數組,那這裡我們就可以這樣聲明一個比較通用的可以傳遞二維數組的函數:
my_func(int **array);或者
my_func(int *array[]);
這樣也可以通過最後一個指標元素設定成NULL來標識該二維數組的結束。實際上,我們真的可以通過一些技術來解決一維和二維數組的通用聲明,但是對於三維和更多維的數組都無法實現的很好,這也是C語言的一個內在限制。
5 函數返回數組
嚴格的說來,無法完成直接從函數返回一個數組,這是C語言的一個限制,但是,可以讓函數返回一個指向任何資料結構的指標,當然包括數組的指標。
對於返回數組的指標的辦法我們必須知道一些事項:
1)在函數中動態分配數組
我們知道,如果這樣聲明一個函數:
int (*my_func())[5]; /* 返回的類型為一個指向保護5個int元素的數組的指標 */
可以在函數中聲明這樣的傳回型別,然後通過動態分配記憶體的方式給它分配記憶體,經過處理後返回。這種動態分配記憶體的一個典型的應用情境是:我們並不知道要定義多大的記憶體,可能很小也可能很大,他的大小在運行期間可能會動態變化。
要注意的是:在函數內部使用動態分配並在外部使用,很可能會忘記釋放這段記憶體,從而造成記憶體泄露,所以一定要記得使用完之後釋放記憶體。
2)千萬不要在函數的內部聲明局部數組,然後作為傳回值從函數返回。
函數的局部變數在函數到期時,將被釋放掉(系統回收),如果你這樣做,幸運的話,可能在短時間內還可以取得你想要的資料(實際上函數的局部變數在進程的堆棧中,在函數到期時堆棧一定會變化),但天知道後面會發生什麼事情。
6 總結
通過這一篇和前面一篇“C語言數組和指標之間的較量”的學習,相信對C語言的數組和指標已經有了比較深刻的瞭解,其中的特性可能需要你在實際編程中領悟和體會,其實個人覺得還是盡量少用C語言的一些比較晦澀的特性,能簡單解決的話又何樂而不為呢?
關於講解數組和指標的資料有很多,比如“明明白白C指標”等對指標和數組的講解尤其獨到之處,但這裡做一些自己的總結也算是給過去學習東西一點交代,以後還可以經常拿過來回顧一下,呵呵,“溫故而知新,可以為師矣...”