指標與陣列的關係,C語言指標和陣列的關係詳解

2020-07-16 10:04:24
指標和陣列的關係是比較高階的內容。本節,我們主要討論指標和一維陣列的關係。二維陣列本身用得就很少,指標和二維陣列的關係用得就更少了。指標和二維陣列的關係我們後面也會講。

指標和一維陣列的關係很重要。把這個問題弄清楚了,前面的很多問題就都明白了。比如陣列為什麼是連續的,為什麼需要連續,陣列的下標是什麼意思,到底什麼是一維陣列等。

用指標參照陣列元素

參照陣列元素可以用“下標法”,這個在前面已經講過,除了這種方法之外還可以用指標,即通過指向某個陣列元素的指標變數來參照陣列元素。

陣列包含若干個元素,元素就是變數,變數都有地址。所以每一個陣列元素在記憶體中都佔有儲存單元,都有相應的地址。指標變數既然可以指向變數,當然也就可以指向陣列元素。同樣,陣列的型別和指標變數的基本類型一定要相同。下面給大家寫一個程式:
# include <stdio.h>
int main(void)
{
    int a[] = {1, 2, 3, 4, 5};
    int *p = &a[0];
    int *q = a;
    printf("*p = %d, *q = %dn", *p, *q);
    return 0;
}
輸出結果是:
*p = 1, *q = 1

程式中定義了一個一維陣列 a,它有 5 個元素,即 5 個變數,分別為 a[0]、a[1]、a[2]、a[3]、a[4]。所以 p=&a[0] 就表示將 a[0] 的地址放到指標變數 p 中,即指標變數 p 指向陣列 a 的第一個元素 a[0]。而 C 語言中規定,“陣列名”是一個指標“常數”,表示陣列第一個元素的起始地址。所以 p=&a[0] 和 p=a 是等價的,所以程式輸出的結果 *p 和 *q 是相等的,因為它們都指向 a[0],或者說它們都存放 a[0] 的地址。

那麼 a[0] 的地址到底是哪個地址?“陣列名表示的是陣列第一個元素的起始地址”這句話是什麼意思?“起始地址”表示的到底是哪個地址?

我們知道,系統給每個int型變數都分配了 4 位元組的記憶體單元。陣列 a 是 int 型的,所以陣列 a 中每一個元素都佔 4 位元組的記憶體單元。而每位元組都有一個地址,所以每個元素都有 4 個地址。那麼 p=&a[0] 到底是把哪個地址放到了 p 中?

答案是把這 4 個地址中的第一個地址放到了 p 中。這就是“起始”的含義,即第一個元素的第一位元組的地址。我們將“陣列第一個元素的起始地址”稱為“陣列的首地址”。陣列名表示的就是陣列的首地址,即陣列第一個元素的第一位元組的地址。

注意,陣列名不代表整個陣列,q=a 表示的是“把陣列 a 的第一個元素的起始地址賦給指標變數 q”,而不是“把陣列 a 的各個元素的地址賦給指標變數 q”。

指標的移動

那麼如何使指標變數指向一維陣列中的其他元素呢?比如,如何使指標變數指向 a[3] 呢?

同樣可以寫成 p=&a[3]。但是除了這種方法之外,C 語言規定:如果指標變數 p 已經指向一維陣列的第一個元素,那麼 p+1 就表示指向該陣列的第二個元素。

注意,p+1 不是指向下一個地址,而是指向下一個元素。“下一個地址”和“下一個元素”是不同的。比如 int 型陣列中每個元素都佔 4 位元組,每位元組都有一個地址,所以 int 型陣列中每個元素都有 4 個地址。如果指標變數 p 指向該陣列的首地址,那麼“下一個地址”表示的是第一個元素的第二個地址,即 p 往後移一個地址。而“下一個元素”表示 p 往後移 4 個地址,即第二個元素的首地址。下面寫一個程式驗證一下:
# include <stdio.h>
int main(void)
{
    int a[] = {1, 2, 3, 4, 5};
    int *p = a;
    printf("p = %d, p + 1 = %dn", p, p+1);    //用十進位制格式輸出
    printf("p = %#X, p + 1 = %#Xn", p, p+1);  //也可以用十六進位制格式輸出
    printf("p = %p, p + 1 = %pn", p, p+1);    //%p是專門輸出地址的輸出控制符
    return 0;
}
輸出結果是:
p = 1638196, p + 1 = 1638200
p = 0X18FF34, p + 1 = 0X18FF38
p = 0018FF34, p + 1 = 0018FF38

我們看到,p+1 表示的是地址往後移 4 個。但並不是所有型別的陣列 p+1 都表示往後移 4 個地址。p+1 的本質是移到陣列下一個元素的地址,所以關鍵是看陣列是什麼型別的。比如陣列是 char 型的,每個元素都佔一位元組,那麼此時 p+1 就表示往後移一個地址。所以不同型別的陣列,p+1 移動的地址的個數是不同的,但都是移向下一個元素。

知道元素的地址後參照元素就很簡單了。如果 p 指向的是第一個元素的地址,那麼 *p 表示的就是第一個元素的內容。同樣,p+i 表示的是第 i+1 個元素的地址,那麼 *(p+i) 就表示第 i+1 個元素的內容。即 p+i 就是指向元素 a[i] 的指標,*(p+i) 就等價於 a[i]。

下面寫一個程式驗證一下:
# include <stdio.h>
int main(void)
{
    int a[] = {1, 2, 3, 4, 5};
    int *p, *q, *r;
    p = &a[3];  //第一種寫法
    printf("*p = %dn", *p);
    q = a;  //第二種寫法
    q = q + 3;
    printf("*q = %dn", *q);
    r = a;  //第三種寫法
    printf("*(r+3) = %dn", *(r+3));
    return 0;
}
輸出結果是:
*p = 4
*q = 4
*(r+3) = 4

注意,*(q+i) 兩邊的括號不能省略。因為 *q+i 和 *(q+i)是不等價的。指標運算子“*”的優先順序比加法運算子“+”的優先順序高。所以 *q+i 就相當於 (*q)+i 了。

前面講過,指標和指標只能進行相減運算,不能進行乘、除、加等運算。所以不能把兩個地址加起來,這是沒有意義的。所以指標變數只能加上一個常數,不能加上另一個指標變數。
那麼現在有一個問題:“陣列名 a 表示陣列的首地址,a[i] 表示的是陣列第 i+1 個元素。那麼如果指標變數 p 也指向這個首地址,可以用 p[i] 表示陣列的第 i 個元素嗎?”可以。下面寫一個程式看一下:
# include <stdio.h>
int main(void)
{
    int a[] = {1, 2, 3, 4, 5};
    int *p = a;
    printf("p[0] = %dn", p[0]);
    printf("p[1] = %dn", p[1]);
    printf("p[2] = %dn", p[2]);
    printf("p[3] = %dn", p[3]);
    printf("p[4] = %dn", p[4]);
    return 0;
}
輸出結果是:
p[0] = 1
p[1] = 2
p[2] = 3
p[3] = 4
p[4] = 5

所以 p[i] 和 *(p+i) 是等價的,即“指向陣列的”指標變數也可以寫成“帶下標”的形式。

那麼反過來,因為陣列名 a 表示的也是陣列的首地址,那麼元素 a[i] 的地址也可以用 a+i 表示嗎?回答也是“可以的”。也就是說如果指標變數 p 指向陣列 a 的首地址,那麼 p+i 和 a+i 是等價的,它們可以互換。下面也寫一個程式驗證一下:
# include <stdio.h>
int main(void)
{
    int a[] = {2, 5, 8, 7, 4};
    int *p = a;
    printf("*(p+3) = %d, *(a+3) = %dn", *(p+3), *(a+3));
    return 0;
}
輸出結果是:
*(p+3) = 7, *(a+3) = 7

這時有人說,a 不是指標變數也可以寫成“*”的形式嗎?只要是地址,都可以用“*地址”表示該地址所指向的記憶體單元中的資料。而且也只有地址前面才能加“*”。因為指標變數裡面存放的是地址,所以它前面才能加“*”。

實際上系統在編譯時,陣列元素 a[i] 就是按 *(a+i) 處理的。即首先通過陣列名 a 找到陣列的首地址,然後首地址再加上i就是元素 a[i] 的地址,然後通過該地址找到該單元中的內容。

所以 a[i] 寫成 *(a+i) 的形式,程式的執行效率會更高、速度會更快。因為在執行程式的時候陣列是先計算地址,然後轉化成指標。而直接寫成指標 *(a+i) 的形式就省去了這些重複計算地址的步驟。

指標變數的自增運算

前面說過,p+1 表示指向陣列中的第二個元素。p=p+1 也可以寫成 p++ 或 ++p,即指標變數也可以進行自增運算。當然,也可以進行自減運算。自增就是指標變數向後移,自減就是指標變數向前移。下面給大家寫一個程式:
# include <stdio.h>
int main(void)
{
    int a[] = {2, 5, 8, 7, 4};
    int *p = a;
    printf("*p++ = %d, *++p = %dn", *p++, *++p);
    return 0;
}
輸出結果是:
*p++ = 5, *++p = 5

因為指標運算子“*”和自增運算子“++”的優先順序相同,而它們的結合方向是從右往左,所以 *p++ 就相當於 *(p++),*++p 就相當於 *(++p)。但是為了提高程式的可讀性,最好加上括號。

在程式中如果用迴圈語句有規律地執行 ++p,那麼每個陣列元素就都可以直接用指標指向了,這樣讀取每個陣列元素時執行效率就大大提高了。為什麼?比如:
int a[10] = {0};
int *p = a;

如果執行一次 ++p,那麼此時 p 就直接指向元素 a[1]。而如果用 a[1] 參照該元素,那麼就先要計算陣列名 a 表示的首地址,然後再加 1 才能找到元素 a[1]。同樣再執行一次 ++p,p 就直接指向元素 a[2]。而如果用 a[2] 參照該元素,那麼還要先計算陣列名 a 表示的首地址,然後再加 2 才能找到元素 a[2]……

所以,用陣列的下標形式參照陣列元素時,每次都要重新計算陣列名 a 表示的首地址,然後再加上下標才能找到該元素。而有規律地使用 ++p,則每次 p 都是直接指向那個元素的,不用額外的計算,所以存取速度就大大提高了。陣列長度越長這種差距越明顯!所以當有大批次資料的時候,有規律地使用 ++p 可以大大提高執行效率。

在前面講陣列時寫過這樣一個程式:
# include <stdio.h>
int main(void)
{
    int a[5] = {1, 2, 3, 4, 5};
    int i;
    for (i=0; i<5; ++i)
    {
        printf("%dn", a[i]);
    }
    return 0;
}
輸出結果是:
1
2
3
4
5

用指標的方法實現一下:
# include <stdio.h>
int main(void)
{
    int a[] = {1, 2, 3, 4, 5};
    int *p = NULL;  //先初始化, 好習慣
    for (p=a; p<(a+5); ++p)
    {
        printf("%dn", *p);
    }
    return 0;
}
輸出結果是:
1
2
3
4
5

指標還可以用關係運算子比較大小,使用關係運算子來比較兩個指標的值的前提是兩個指標具有相同的型別。

既然 p+i 和 a+i 等價,那麼能不能寫成 a++?答案是“不能”。在前面講自增和自減的時候強調過,只有變數才能進行自增和自減,常數是不能進行自增和自減的。a 代表的是陣列的首地址,是一個常數,所以不能進行自增,所以不能寫成a++。

下面再來寫一個程式,程式設計要求:實現從鍵盤輸入 10 個整數,然後輸出最大的偶數,要求使用指標存取陣列。
# include <stdio.h>
int main(void)
{
    int a[10] = {0};
    int *p = a;
    int max;
    int i;  //迴圈變數
    int flag = 1;  //標誌位
    printf("請輸入十個整數:");
    for (i=0; i<10; ++i)
    {
        scanf("%d", p+i);  //不要用&p[i], 不要出現陣列的影子
    }
    for (; p<a+10; ++p)
    {
        if (0 == *p%2)  
        {
            if (flag)  //將第一個偶數賦給max;
            {
                max = *p;
                flag = 0;
            }
            else if (*p > max) //尋找最大的偶數
            {
                max = *p;
            }
        }
    }
    if (!flag)  /*這句很經典, 如果flag一直都是1, 那麼說明沒有一個是偶數*/
    {
        printf("最大的偶數為:%dn", max);
    }
    else
    {
        printf("未發現偶數n");
    }
    return 0;
}
輸出結果是:
請輸入十個整數:-33 -26 15 38 74 59 31 -2 27 36
最大的偶數為:74

兩個引數確定一個陣列

前面講過,在函數呼叫時如果要將一個陣列從主調函數傳到被調函數,只需要傳遞兩個引數就能知道整個陣列的資訊。即一維陣列的首地址(陣列名)和陣列元素的個數(陣列長度)。

但是當時還沒有講指標,所以形參定義的是陣列。本節我們再將這個問題複習一下,並將形參改用指標來接收陣列名。因為指標變數中存放的是地址,而陣列名表示的就是一個地址,所以在形參中可以直接定義一個指標變數來接收陣列名。

下面來寫一個程式,把“輸出一維陣列所有元素”的功能寫成函數,然後在主函數中進行呼叫:
# include <stdio.h>
void Output(int *p, int cnt);  //宣告一個輸出陣列的函數
int main(void)
{
    int a[] = {1, 2, 3, 4, 5};
    int b[] = {-5, -9, -8, -7, -4};
    Output(a, 5);
    Output(b, 5);
    printf("n");
    return 0;
}
/*定義一個輸出陣列的函數*/
void Output(int *p, int cnt)  /*p用來接收首地址, cnt用來接收陣列元素的個數*/
{
    int *a = p;
    for (; p<(a+cnt); ++p)  //陣列地址作為迴圈變數
    {
        printf("%d  ", *p);
    }
}
輸出結果是:
1  2  3  4  5  -5  -9  -8  -7  -4

程式中,之所以要傳遞陣列的長度是因為在 C 語言中沒有一個特殊的標記可以作為陣列的結束標記。因為陣列是儲存資料的,任何資料都可以存到陣列中,所以陣列裡面任何一個值都是有效的值。不僅是C語言,任何一門語言都無法用某一個值作為陣列結束的標記。所以“陣列長度”這個引數是必需的。

上面這個程式是以“陣列地址”作為被調函數的迴圈變數,也可以改成以“陣列個數”作為被調函數的迴圈變數:
# include <stdio.h>
void Output(int *p, int cnt);  //宣告一個輸出陣列的函數
int main(void)
{
    int a[] = {1, 2, 3, 4, 5};
    int b[] = {-5, -9, -8, -7, -4};
    Output(a, 5);
    Output(b, 5);
    printf("n");
    return 0;
}
/*定義一個輸出陣列的函數*/
void Output(int *p, int cnt)  /*p用來接收首地址, cnt用來接收陣列元素的個數*/
{
    int i;
    for (i=0; i<cnt; ++i)  //陣列元素個數作為迴圈變數
    {
        printf("%d  ", *(p+i));
    }
}
輸出結果是:
1  2  3  4  5  -5  -9  -8  -7  -4

指標變數占多少位元組

我們講過,指標變數根據“基本類型”的不同有 int* 型、float* 型、double* 型、char* 型等。但是前面在講資料型別的時候講過,int 型變數占 4 位元組,float 型變數占 4 位元組、double 型變數占 8 位元組、char 型變數占 1 位元組。那麼“指標型變數”佔幾位元組?是不是基本類型佔幾位元組,該指標變數就佔幾位元組?同樣,用 sizeof 寫一個程式看一下就知道了:
# include <stdio.h>
int main(void)
{
    int    *a = NULL;
    float  *b = NULL;
    double *c = NULL;
    char   *d = NULL;
    printf("%d %d %d %dn", sizeof(a), sizeof(b), sizeof(c), sizeof(d));
    return 0;
}
輸出結果是:
4 4 4 4

可見,不管是什麼基本類型,系統給指標變數分配的記憶體空間都是 4 位元組。在 C 語言中,只要是指標變數,在記憶體中所佔的位元組數都是 4。指標變數的“基本類型”僅用來指定該指標變數可以指向的變數型別,並沒有其他意思。不管基本類型是什麼型別的指標變數,它仍然是指標變數,所以仍然佔 4 位元組。

我們前面講過,32 位計算機有 32 根地址線,每根地址線要麼是 0 要麼是 1,只有這兩種狀態。記憶體單元中的每個地址都是由這 32 根地址線通過不同的狀態組合而成的。而 4 位元組正好是 32 位,正好能儲存下所有記憶體單元的地址資訊。少一位元組可能就不夠,多一位元組就浪費,所以是 4 位元組。