1. 程式人生 > >18. C語言 -- 指標陣列和陣列指標

18. C語言 -- 指標陣列和陣列指標

本部落格主要內容為 “小甲魚” 視訊課程《帶你學C帶你飛》【第一季】 學習筆記,文章的主題內容均來自該課程,在這裡僅作學習交流。在文章中可能出現一些錯誤或者不準確的地方,如發現請積極指出,十分感謝。
也歡迎大家一起討論交流,如果你覺得這篇文章對你有所幫助,記得評論、點贊哦 ~(。・∀・)ノ゙

1. 指標和陣列的區別

  指標是左值,而陣列名只是一個地址常量,它不可以被修改,所以陣列名不是左值。其中的左值在 《12. C語言 – 拾遺》 中的 1.1 部分有講,lvalue 指用於識別或定位一個儲存位置的識別符號,同時還必須是可改變的。

  比如說在下面的這段程式中

#include <stdio.h>

int main()
{
	char str[] = "I love LunZiGongChang!";
	int count = 0;

	while (*str++ != '\0'){
		count++;
	}

	printf("總共有%d個字元!\n", count);

	return 0;
}

執行程式碼會得到如下的錯誤
在這裡插入圖片描述

通過錯誤提醒可以知道,自加運算子 ++ 需要一個左值,雖然陣列名是陣列第一個元素的地址,但是他是不可變的,不滿足左值的要求,即不是一個左值。但是指標是一個左值,所以我們只需要初始化一個指標為陣列第一個元素的地址,就可以解決這個問題,具體程式碼如下

#include <stdio.h>

int main()
{
	char str[] = "I love LunZiGongChang!";
	char *p= str;
	int count = 0;

	while (*p++ != '\0'){
		count++;
	}

	printf("總共有%d個字元!\n", count);

	return 0;
}

其中還有一點需要注意的就是,自加運算子 ++ 與取值運算子 * 相比,++ 的優先順序要更高,所以*p++ 相當於先將指標指向當前位置的下一個位置,然後再取出當前地址的值,實際上就是在逐個的取出字串中的值。執行上面的程式碼可以得到如下的結果

總共有22個字元!

這個地方還可以引申出一個問題:c 中while(*p++);與while(*p){p++;}有什麼區別?

區別在於退出迴圈後, p的值不一樣。

while( *p++ ); //當*p=0時,退出迴圈,此時p++仍然執行了
while( *p ) p++; //當*p=0時,退出迴圈,此時p++不再被執行

例如 char *p="ABCD"; 執行完第一個while迴圈後,p指向的是’\0’後面的一個位元組,p的結果是未知的
而如果是執行第二個迴圈,則p指向的是’\0’,也就是’D’後面的一位元組,即
p=’\0’。如果忘記了可以回顧一下之前的文章《12. C語言 – 拾遺》中第三部分 “自增自減運算子” 中的內容。

  上面這段程式碼是不是和 《17. C語言 – 指標和陣列的關係》 第三部分 指標的運算 很相似。在指標的運算中,我們是使用指標的方式定義了一個數組,因為指標中存放的是陣列中第一個元素的地址,而陣列中第一個元素的地址又是陣列名,所以對於指標定義的陣列,既可以使用陣列的形式訪問,又可以使用指標運算的方式訪問。但是對於直接用陣列形式定義的陣列,由於陣列名雖然和陣列中第一個元素的地址相等,但是並不是一個左值,所以只可以陣列的形式訪問陣列中元素,不可以使用指標的形式訪問,除非向上面那樣新定義一個指標。

2. 指標陣列

  指標陣列,從名字來理解,很容易看出它是一個數組,裡面裝的是指標。比如下面的這段程式碼

int *p1[5];

它就是一個指標陣列,我們可以從運算子的優先順序和結合性進行分析。陣列下標的優先順序要比取值運算子的優先順序高,所以先入為主,p1 被定義為具有 5 個元素的陣列。那麼陣列元素的型別呢?是整型嗎?顯然不是,因為還有一個星號,所以它們應該是指向整型變數的指標。所示上述程式碼所定義的陣列如下所示
在這裡插入圖片描述

即指標陣列是一個數組,陣列中的元素是指標變數。

  比如說下面這段程式碼

#include <stdio.h>

int main()
{
        char *p1[5] = {
                "輪子工廠廠長招親!",
                "身高不限",
                "膚色不限",
                "身材不限",
                "只要你不嫌棄廠長醜帥醜帥的~"
        };
        int i;

        for (i = 0; i < 5; i++){
                printf("%s\n", p1[i]);
        }

        return 0;
}

我們將指標陣列中的每個元素初始化為一個字串,這裡之所以可以這樣寫是因為一個指標可以使用 char *p = "sss" 的方式進行初始化,所以如果想初始化一個指標陣列,就可以通過上面的方式進行。在列印輸出中使用 p1[i] 而不是 *p1[i]*p1[i] 將取出的是字串中的第一個字元,而不能列印整個字串。執行上面的程式碼會得到如下的結果

輪子工廠廠長招親!
身高不限
膚色不限
身材不限
只要你不嫌棄廠長醜帥醜帥的~

3. 指標的步長

  要理解陣列指標,首先要加深對指標的理解。指標的型別決定了指標的視野,指標的視野決定了指標的步長。我們必須清楚一個指標變數將告訴我們兩個資訊:某個資料結構的起始地址,以及該結構的跨度。比如 int p = &a; 說明該指標指向變數 a 的起始地址,以及它的跨度為 sizeof(int)。所以 p + 1 == p + sizeof(int)。

  因此不同的陣列有著不同的指標步長。在這裡我們首先從陣列名的角度進行考慮,對於一個一維陣列 int array[3]={1,2,3}; ,陣列名是陣列的首地址,是一個 int 型的指標,這點很明顯。對於一個二維陣列 int array[2][3]={{1,2,3},{4,5,6}}; 來講,由於記憶體中的二維陣列是以一維陣列的形式存放的,所以二維陣列是巢狀定義的 ,這個二維陣列就是由兩個一維陣列array[0]和array[1]組成的。其中

  • array[0]:是第一個一維陣列的陣列名,這個一維陣列是{1,2,3};
  • array[1]:是第二個一維陣列的陣列名,這個一維陣列是{4,5,6};

這個時候陣列從 array[0] 到 array[1] 雖然只變換了一個位置,但實際上跳過了整個第一行,因此

  • array陣列的元素型別為: int (*)[3]型別 //步長為 3 的指標
  • array[0]陣列的元素型別為: int *型別
  • array[1]陣列的元素型別為: int *型別

  同理對於一個三維陣列

int Sarray[2][2][3] = {
{ { 1, 2, 3 }, { 4, 5, 6 } },
{ { 7, 8, 9 }, { 3, 6, 8 } }
};
  • Sarray: 是指向 int (*)[2][3]型別的指標;//步長為 2行3列的指標。
  • Sarray[0]:是指向int *[3]型別的指標;
  • Sarray[0][0]:是指向int *型別的指標;

因此可以看到,對於陣列名來講,他的確是陣列第一個元素的地址,但是他的步長是根據他的第一個元素的數量確定的,無論是多少維的陣列,把它看成一個一維陣列,裡面包含著怎樣的子陣列,就有怎樣的步長

4. 陣列指標

  陣列指標,顧名思義,是一個指向陣列的指標,比如說下面這個

int (*p2)[5];

從運算子的優先順序和結合性進行分析,因為圓括號和陣列下標位於同一個優先順序佇列,所以我們就要看先來後到的問題了。由於它們的結合性都是從左到右,所以 p2 先被定義為一個指標變數。那麼它指向誰?還能有誰?後邊還緊跟著一個具有 5 個元素的陣列,p2 指向的就是它。由於指標變數的型別事實上就是它所指向的元素的型別,所以這個 int 就是定義陣列元素的型別為整型。即如下圖所示
在這裡插入圖片描述

  所以陣列指標是一個指標,它指向的是一個數組。那麼陣列名,陣列第一個元素的地址,陣列指標這三者之間是什麼關係呢?我們看下面的這段程式,它將打印出這3個指標的值,並且通過指標的方式打印出指標所指向的下一個元素的地址。

#include <stdio.h>

int main()
{
	int temp[5] = {1, 2, 3, 4, 5};
	int (*p)[5] = &temp;
	int *pp = temp;

	printf("%p\n", temp);
	printf("%p\n", &temp[0]);
	printf("%p\n", &temp);
	printf("----------------\n");
	printf("%p\n", pp);
	printf("%p\n", pp+1);
	printf("%p\n", p);
	printf("%p\n", p+1);
	printf("%p\n", *p+1);

	return 0;
}

輸出的結果如下

0x7ffe2758e710
0x7ffe2758e710
0x7ffe2758e710
----------------
0x7ffe2758e710
0x7ffe2758e714
0x7ffe2758e710
0x7ffe2758e724
0x7ffe2758e714

  可以看到陣列名,陣列第一個元素的地址和陣列指標這三個指標指向的都是同一個地址,那麼是否意味著三這是完全等價的呢?在上面的程式碼中可以看到 pp 指標指向陣列的首地址,他的下一個指向的陣列中第二個元素的地址;而作為陣列指標的 p 指標雖然也指向陣列的首地址,但是他的下一個,卻指向陣列外面的位置(與陣列首地址相差了 20,16 進位制的 14 是 20),只用通過 *p+1 才會得到陣列中第二個原始的地址,這就涉及到指標步長這個概念了。

  就像剛剛所講的,實際上這裡 &temp 對陣列取址就是將整個陣列看作是一個元素,那麼指標 int (*p)[5] = &temp; 的跨度就很明顯是 5 啦~這也就解釋了為什麼指向陣列首地址的指標的下一個是陣列中的第二個元素的地址,而陣列指標的下一個會指向陣列的最後(實際上是陣列最後一個元素的後一個位置的地址)。

  根據上面的知識可以知道,下面的程式碼明顯是錯誤的。

#include <stdio.h>

int main()
{

	int (*p2)[5] = {1, 2, 3, 4, 5};
	int i;

	for (i = 0; i < 5; i++)
	{
		printf("%d\n", *(p2 + i));
	}

	return 0;
}

它的本來用意是想用指標法的形式將陣列中的每一個元素打印出來,但是卻得到如下的結果
在這裡插入圖片描述

從 warning 的提示資訊可以看出是第六行指標的定義及初始化的錯誤。

  在上面的程式中,陣列指標是指向陣列的指標,但是 int (*p2)[5] = {1, 2, 3, 4, 5}; 表示指標變數 p2 是指向陣列中的第一個元素的地址,並不是指向陣列的地址。陣列指標和陣列的頭地址雖然在數值上相等,但是兩者的步長卻是不等的,這上面 warningd 的原因,也是結果錯誤的原因。

  因此我們對程式碼進行了如下的修改,將陣列指標初始化為整個陣列的地址(即程式碼中的 5 6 行)。

#include <stdio.h>

int main()
{
	int temp[5] = {1, 2, 3, 4, 5};
	int (*p)[5] = &temp;
	int i;

	for (i = 0; i < 5; i++){
		printf("%d\n", *(p + i));
	}

	return 0;
}

執行之後發現程式依然會報錯,如下所示
在這裡插入圖片描述

很明顯又錯了,從 warning 的資訊來看 printf 使用一個 %d 作為佔位符,但是傳入的確實一個整型指標。


!!!!!!!!!!!!!!!!!!
這個部分不是很明白,為什麼要使用了 printf("%d\n", *(*p2 + i));

  我們看到 int (*p)[5] = &temp; temp 是陣列名,也就是陣列中第一個元素的地址,對這個再取址 (&temp) 相當於獲得的是指標的指標,所以陣列的指標可以理解為陣列首地址的指標,或者陣列首地址的地址。

  另外就是在列印輸出的過程中使用了 printf("%d\n", *(*p2 + i)); ,在 p2 的前面增加了一個 * ,有兩種方式理解這種改動。一種是 temp 前面多了一個取址運算子,p2 前面就要對應著增加一個取值運算子;另一種理解方式是,temp 是陣列名,實際上就是陣列中第一個元素的地址,對地址再進行取值,所以 p2 代表的是陣列自一個元素地址的地址,這個時候 *p2 代表陣列第一個元素的地址,*p2 + i 就是後面第 i 個元素的地址,然後再取值就可以獲得陣列中的元素了。


  所以將程式修改為如下的形式便可以正常執行了

#include <stdio.h>

int main()
{
	int temp[5] = {1, 2, 3, 4, 5};
	int (*p)[5] = &temp;
	int i;

	for (i = 0; i < 5; i++){
		printf("%d\n", *(*p + i));
	}

	return 0;
}

結果就是輸出 1 2 3 4 5 ,這裡就不截圖了。

  我們可以從下面這個例子加深自己對陣列指標的理解

#include <stdio.h>

int main()
{
        int array[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
        int *p = (int *)(&array + 1);

        printf("%d\n", *(p - 6));

        return 0;
}

如果將陣列指標與陣列第一個元素的地址等價起來的話,上面的程式碼很明顯是越界了的,但實際上編譯執行之後輸出了結果 4。實際上是因為雖然 array 和 &array 的值相同,但步長是不一樣的。

  因此,&array + 1 指向的就是整個陣列最後的位置(第二個 array 陣列的起始位置),然後 (int *) 將其強制轉換為一個整型地址(指標),所以指標變數 p 初始化後,指向的地址應該是 array[10](第 11 個元素),所以 *(p - 6) == array[10 - 6] == array[4] == 4。

  重點強調:array 是陣列第一個元素的地址,所以 array + 1 指向陣列第二個元素;&array 是整個陣列的地址,所以 &array + 1 指向整個陣列最後的位置,也就是兩者指標的步長是不一樣的。

  上面的例子是從指標陣列的角度進行考慮的,下面的這個例子將從陣列指標的角度進行考慮,比如下面的這段程式碼


!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
這個也不是很懂,指標數組裡面到底存放的是什麼?

#include <stdio.h>

int main()
{
        int array[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
        int (*p)[10] = &array;

        printf("%d\n", *(*(p+1)-6));

        return 0;
}

參考

[1] “小甲魚” 視訊課程《帶你學C帶你飛》【第一季】P23
[2] 涼涼貓 CSDN部落格 《二維陣列及多維陣列的指標總結》
[3] 百度知道 《c++中while(*p++);與while(*p){p++;}有什麼區別?》
歡迎大家關注我的知乎號(左側)和經常投稿的微信公眾號(右側)

在這裡插入圖片描述