C語言中文網 目錄
首頁 > 編程筆記 > C語言筆記 > 指針 閱讀:4,910

指針與數組的關系,C語言指針和數組的關系詳解

指針和數組的關系是比較高級的內容。本節,我們主要討論指針和一維數組的關系。二維數組本身用得就很少,指針和二維數組的關系用得就更少了。指針和二維數組的關系我們后面也會講。

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

用指針引用數組元素

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

數組包含若干個元素,元素就是變量,變量都有地址。所以每一個數組元素在內存中都占有存儲單元,都有相應的地址。指針變量既然可以指向變量,當然也就可以指向數組元素。同樣,數組的類型和指針變量的基類型一定要相同。下面給大家寫一個程序:
# include <stdio.h>
int main(void)
{
    int a[] = {1, 2, 3, 4, 5};
    int *p = &a[0];
    int *q = a;
    printf("*p = %d, *q = %d\n", *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 = %d\n", p, p+1);    //用十進制格式輸出
    printf("p = %#X, p + 1 = %#X\n", p, p+1);  //也可以用十六進制格式輸出
    printf("p = %p, p + 1 = %p\n", 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 = %d\n", *p);
    q = a;  //第二種寫法
    q = q + 3;
    printf("*q = %d\n", *q);
    r = a;  //第三種寫法
    printf("*(r+3) = %d\n", *(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] = %d\n", p[0]);
    printf("p[1] = %d\n", p[1]);
    printf("p[2] = %d\n", p[2]);
    printf("p[3] = %d\n", p[3]);
    printf("p[4] = %d\n", 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) = %d\n", *(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 = %d\n", *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("%d\n", 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("%d\n", *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("最大的偶數為:%d\n", 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 %d\n", 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 字節。

精美而實用的網站,提供C語言C++STLLinuxShellJavaGo語言等教程,以及socketGCCviSwing設計模式JSP等專題。

Copyright ?2011-2018 biancheng.net, 陜ICP備15000209號

底部Logo