小引
在指針的學習階段,有不少同學都十分畏懼這個物什,甚至“談指針色變”。其實對指針的不理解,其實本質上是對內存的不理解,本篇博客就從零開始、系統的來講解指針這個話題。
首先我們應該明確以下的一些基礎常識:
- 指針是一個變量,用來存放地址,地址唯一標識一塊內存地址。
- 指針的大小是固定的
4 或 8
個字節。(32
或64
位平臺) - 指針是有類型的,指針的類型決定了指針加減整數運算的步長,以及指針解引用時的權限。
字符指針
顧名思義,就是指向字符變量的指針,指針內存放的內容是一個字符。它的一般寫法如下:
int main(){
char ch = 'w';
char *ptr = &ch;
*ptr = 'w';
return 0;
}
如果此時對
ptr
進行strlen
,就會內存訪問越界,屬於未定義行爲,結果不可預期。因爲指針指向的是一個字符而不是字符串。
指向單個字符的使用方式簡單明瞭,但是相對於下面的這種常見的指向字符串的使用方式,就相形見絀了。
int main(){
char *ptr_1 = "this is a C string";
printf("%s\n",ptr_1);
return 0;
}
這樣也可以間接的表現爲指針指向了字符串
。
代碼中char *ptr_1 = "this is a C string";
語句看似是將字符串放入了指針內,但其本質是將這個字符串的首地址存放到了指針內。
如果此時對
ptr_1
進行strlen
,就會正確的打印出字符串的個數,因爲此時指針的指向不再是單個字符,而是字符串。
其實編譯器無法識別
指針的指向是字符數組 還是字符串,所以它的指向需要使用者來定義保證。
請看代碼:
int main(){
char str1[] = "This is a C string.";
char str2[] = "This is a C string.";
char *str3 = "This is a C string.";
char *str4 = "This is a C string.";
if(str1 == str2)
printf("str1 and str2 are same\n");
else
printf("str1 and str2 are not same\n");
if(str3 == str4)
printf("str3 and str4 are same\n");
else
printf("str3 and str4 are not same\n");
return 0;
}
運行結果爲:
- 這裏
str3
和str4
指針指向的是同一個常量字符串。C/C++
語法上會把常量字符串存儲到單獨的一個內存區域,當幾個不同的指針同時指向同一個字符串時,他們實際會指向同一塊內存(這個字符串的首地址)。所以這幾個指針其實是同一個指針。 - 但是用相同的常量字符串對不同的數組進行初始化時就會在內存中開闢出不同的區域。指向其各自首地址的指針也就是不同的指針了。
所以str1和str2不同,str3和str4不同。
數組指針
它的本質是指針,指針的指向是一個 數組
。它的定義如下:
int (*arr)[20];
因爲
[]
的優先級高於*
,而()
的優先級高於[]
。所以加上圓括號()
改變優先級順序。(不加圓括號的情況下面會提及)
先進行括號( )
內的操作,這裏變量名arr
與*
結合就明確了他是一個指針變量
,括號操作符完成再與其他元素進行結合,它指向了一個大小爲10個整型元素的數組。
【二維數組也是一個數組指針】
- 對於
arr
和&arr
:
arr
是數組名,數組名錶示數組首元素的地址
&arr
就是數組指針,指針的指向是整個數組。
請看如下代碼:
int main(){
int arr[10] = {0};
printf("%p\n", arr);
printf("%p\n", &arr);
return 0;
}
運行結果爲:
二者輸出內容相同,這就說明數組名
和& 數組名
是等價的嗎?其實不然,請看如下代碼:
int main(){
int arr[10] = { 0 };
printf("arr = %p\n", arr);
printf("&arr = %p\n", &arr);
printf("arr+1 = %p\n", arr+1);
printf("&arr+1 = %p\n", &arr+1);
return 0;
}
運行結果爲:
這裏對數組名 + 1
和& 數組名 + 1
操作發現二者出現了區別,說明它們有着本質上的差別。
arr
指向數組首元素,&arr
是數組指針,指向數組,二者指向的地址是相同的但類型不同。
arr + 1
是指向數組首元素之後一個元素的指針。
而&arr + 1
是指向下一個數組,跨過了整個數組,步距爲數組元素的大小之和。
與數組指針相混淆的有另一個名詞:指針數組
。
指針數組
int *p1[10];
它的本質是數組,數組中的所有元素都是 指針
。
【二級指針也是一個指針數組】
int *arr1[10]; //整型指針的數組
char *arr2[4]; //一級字符指針的數組
char **arr3[5]; //二級字符指針的數組
具體舉例:
const char *arr[] = {
"hehe",
"haha",
"xixi"
};
arr
數組中的三個字符串也就是三個字符數組,數組可以隱式轉換爲指針,所以就可以使得arr
的類型設爲char *
的字符指針類型,所以arr
中三個元素的類型都是char *
。- 加上
const
關鍵字修飾本數組是爲了確保數組內內容不發生改變。同時因爲是三個字符串常量,其本身也不會發生改變。
這樣就定義好了一個指針數組。
一維數組傳參
1. 整型數組
對於數組:
int arr[10] = { 0 };
使用以下函數進行傳參:
// 情況 1
void test(int arr[])
正確
編譯器會把函數的參數名arr
識別爲*arr
的指針,與其數組大小無關,所以數組大小的參數省略書寫也是完全可以的。
// 情況 2
void test(int arr[10])
正確
這是最直觀的傳參方式,實參和形參變量的類型、大小都相同,很容易理解和書寫,是非常常見的一種寫法。
// 情況 3
void test(int *arr)
正確
和情況1同義,編譯器會把傳入函數的數組參數隱式轉化爲指針*arr
,數組元素也會轉換成數組的移動,所以這種寫法也是可以的。
所以以上三種方法都是正確的,內涵也是相同的,讀者使用時可以根據具體情況選擇不同的表現形式,提高代碼可讀性。
2. 指針數組
對於數組:
int *arr2[20] = { 0 };
使用以下函數進行傳參:
// 情況一
void test2(int *arr[20])
正確
傳參方式直觀,實參與形參參數類型、大小均相同,便於理解。
// 情況二
void test2(int **arr)
正確
在這段代碼中,可以把int*
看作一個整體,將它typedef
成爲一個類型t
那麼實參中的數組定義就可以看成t arr2[20];
了,調用函數傳參就可以看成t *arr
,就與上面所講的一維整型數組傳參如出一轍了。這樣傳參也是正確的。
所以指針數組可以當做二級指針來傳參。
二維數組傳參
對於數組:
int arr[3][5] = {0};
使用以下函數進行傳參:
// 情況 1
void test(int arr[3][5])
正確
同樣是最直觀的寫法,類型大小均相同,不多贅述。
// 情況 2
void test(int arr[][])
- [×]
不正確
因爲對於一個二維數組,兩個[]
中第一個數是行數
,第二個數列數
,行數可以目前不知道,但是每一行有多少元素,即列數,必須要知道,否則給定一串元素就不知道如何劃分行列數,這樣纔會方便運算。
// 情況 3
void test(int arr[][5])
正確
二維數組傳參第一個數字可以省略,如若給定一部分數據,就可以根據列數計算出行數,這些工作都是自動完成的,所以C語言規定二維數組行數可以省略,但列數不可省。
【小結】:
二維數組傳參,函數形參只能省略第一個[]
的數字,第二個[ ]中的數字不可省略。
對一個二維數組,可以不知道有多少行,但是必須知道一行多少元素。
// 情況一
void test(int (*arr)[5])
正確
這樣的寫法是一個數組指針,可以正確傳參。
而除此之外,以下的其他寫法都是錯誤的:
// 情況二
void test(int* arr[5])
- [×]
不正確
這樣的寫法是一個指針數組,而二維數組的傳參應該是一個數組指針,錯誤。
// 情況三
void test(int *arr)
- [×]
不正確
作爲一級指針傳入數組會造成實參與形參類型不符,錯誤。
// 情況四
void test(int **arr)
- [×]
不正確
和第二種情況是等效的,正是因爲指針數組可以當做二級指針來傳參,兩者可以互相轉化,錯誤。
【小結】:二維數組可以作爲 數組指針 傳參。
一級指針傳參
常見的傳參方式如下:
void print(int *p, int sz){
int i = 0;
for(i = 0;i < sz;i++){
printf("%d\n",*(p + i));
}
}
int main(){
int arr[10] = {1,2,3,4,5,6,7,8,9};
int *p = arr;
int sz = sizeof(arr) / sizeof(arr[0]);
print(p,sz); //一級指針p,傳給函數print
return 0;
}
Q:那麼當一個函數的參數部分爲一級指針的時候(例如int *
),函數能接收什麼參數?
int*
類型的一級整型指針- 數組元素類型爲
int
的一維數組
二級指針傳參
常見的傳參方式如下:
void test(int** ptr){
printf("num = %d\n", **ptr);
}
int main(){
int n = 10;
int *p = &n;
int **pp = &p;
test(pp); //二級指針pp傳給函數test
test(&p);
return 0;
}
Q:當函數的參數是二級指針時(例如int **
),可以接收什麼參數?
- 二級指針
- 指針數組