工作好幾年了,一直用的都是C。自認為指針應該很熟悉了。然而,前段時間我對二維指針和二維數組的一個混用,并且我們項目組的一個大牛(博士畢業,工作10+年)在review我的代碼也沒發現問題,導致代碼上線后出現一個異常。我才覺得我對指針只是學廢了。找了一些指針和數組的博客資料,記錄一下。希望下次不會再犯類似的錯誤。
引子
首先看一段代碼:
?
void test(int *p){ } int main(){ int arr[]= {30, 450,14,5}; test(arr); return 0;}
?
毫無疑問,上面這段代碼是運行OK的。因為C語言標準中有以下規則:在函數參數的聲明中,數組名被編譯器當作指向該數組第一個元素的指針。
第一種錯誤
那下面這段代碼,會正確運行嗎:
?
#include void test(int **p){ }int main(){ int arr[]={30,450,14,5}; test(&arr); return 0;}
?
可能有的同學認為這段代碼是正確的,因為數組作為函數參數時退化成一個指針,那么我對數組進行取址,這樣&arr就是一個二維指針了,所以可以作為函數test的入參。
這個理由貌似有道理,但很遺憾,這段代碼會報編譯錯誤:
?
main.c:3:17: note: expected 'int **' but argument is of type 'int (*)[4]' void test(int **p) ~~~~~~^
?
這個錯誤是說,test函數期望的入參類型是int **。但是實際傳入的參數類型是 int (*)[4],即實際傳入的類型是指向數組的指針。
C語言標準中是定義了:在函數參數的聲明中,數組名被編譯器當作指向該數組第一個元素的指針。但是你不能因為數組在函數參數中當成一個指針,你對數組名取地址&arr就認為它的類型就是指向指針的指針(int **),這樣以為是錯的,因為不具備這樣的傳遞性。C語言規范中只規定了數組名作為函數的入參時會被當做一個指針,但是并沒有規定對數組取地址會被當成指向指針的指針。
根據上面代碼的報錯信息提示,將不能編譯通過的代碼片段作一個小的修改,就會編譯通過了:
?
#include #include void test(int (*p)[4]){ (*p)[2] = 10;} int main(){ int arr[] = {30, 450, 14, 5}; test(&arr); printf("%d ", arr[2]); return 0;}
?
輸出: 10
但其實上面的這種用法非常的怪。至少我在平時的工作中沒遇到過這樣的寫法。這個函數的意圖是改變數組的某個元素的內容。那下面的這種寫法更加清晰和常見:
?
void test(int *p){ p[2] = 10;} int main(){ int arr[] = {30, 450, 14, 5}; test(arr); printf("%d ", arr[2]); return 0;}
?
第二種錯誤
這種錯誤就是我之前在工作中犯的,從而導致出現異常的。
假如有一段代碼:
?
#include #include void foo(int **arr, int m, int n){ int i,j; for (i = 0; i < m; i++) { for (j = 0; j < n; j++) { printf("%d ", arr[i][j]); } printf(" "); }} int **alloc_2d(int m, int n){ int **arr = malloc(m * sizeof(*arr)); int i; for (i = 0; i < m; i++) { arr[i] = malloc(n * sizeof(**arr)); } return arr;} int main(){ int **joe = alloc_2d(2, 3); joe[0][0] = 1; joe[0][1] = 2; joe[0][2] = 3; joe[1][0] = 4; joe[1][1] = 5; joe[1][2] = 6; return 0;}
?
上面的代碼至此還是能正常工作的。
現在,我想用函數foo來打印一個二維的數組,那我能用下面的這段代碼嗎?
?
int moe[2][3];moe[0][0] = 1;moe[0][1] = 2;moe[0][2] = 3;moe[1][0] = 4;moe[1][1] = 5;moe[1][2] = 6; foo(moe, 2, 3);
?
很遺憾,這段代碼同樣會報編譯錯誤:
?
ain.c:42:9: warning: passing argument 1 of 'foo' from incompatible pointer type [-Wincompatible-pointer-types] foo(moe, 2, 3); ^~~main.c:3:16: note: expected 'int **' but argument is of type 'int (*)[3]' void foo(int **arr, int m, int n)
?
將二維數組作為入參傳入foo函數的原因可能是錯誤的認為二維數組就是二維指針。為什么會這么想呢? 我以前的一個思考過程是這樣的:一維數組作為函數參數的時候,編譯器會把數組名當作指向該數組第一個元素的指針。所以我想當然的以為:既然一維數組和一維指針在函數參數中等價,那二維數組應該就等價于二維指針。
但是很遺憾,二維數組作為函數參數并等價于二維指針。因為數組作為函數參數時轉換為指針沒有傳遞性。也就是說你不能認為一維數組和一維指針作為函數參數等價,就認為二維數組和二維指針就等價了。在C語言中沒有這樣的傳遞性。
其實仔細想想,也是很容易明白的。二維數組其實就是一個數組的數組(即它是一個一維數組,只不過它的每個元素又都是一個一維數組)。當二維數組作為函數入參時,比如 int a[3][4]; 它的第一維數組會被轉換成指針,相當于是傳入了一個指向數組的指針。即作為函數參數, int a[3][4]和 int (*p)[4]等價。那 int (*p)[4]和 int **pp等價嗎?肯定不等價, p指針指向類型是 int [4],而pp指向的類型是int *。
那有什么辦法能讓上面的foo函數編譯通過呢?
最直接的方式是將二維數組作為函數的參數傳入:
?
void foo(int arr[2][3], int m, int n){}
?
事實上,上面的第一維的參數可以去掉:
?
void foo(int arr[][3], int m, int n){}
?
上面也分析了,二維數組作為函數入參和 int (*p)[3]等價,所以也可以寫成下面的形式:
?
void foo(int (*p)[3], int m, int n){}
?
總結
上面說了2維數組作為函數參數的情況,那3維數組呢?
和二維一樣:首先將類似三維數組arr[2][3][4]傳入到 int ***類型的函數參數是錯誤的。但是可以將這個三維數組傳入到參數類型為 int arr[][3][4]或者 int (*arr)[3][4]的函數中。
"數組名被改寫成一個指針參數"規則并不是遞歸定義的(沒有傳遞性)。數組的數組會被改寫為"數組的指針",而不是"指針的指針"。
你之所以能在main()函數中看到char **argv這樣的參數,是因為argv是個指針數組(即 char *argv[])。這個表達式被編譯器改寫為指向數組第一個元素的指針,也就是一個指向指針的指針。如果argv參數事實上被聲明為一個數組的數組(也就是char argv[10][5]), 它將被編譯器改寫為 char (*argv)[15],也就是一個字符數組的指針,而不是char **argv。
審核編輯:湯梓紅
?
評論
查看更多