C語言:數組和指針的聯繫與區別

數組和指針的聯繫

數組名就是該數組首元素的地址

所以,下面的式子是正確的:

int data[] = {1, 2, 3}
data == &data[0]

可以驗證一下:

#include <stdio.h>
int main()
{
    int data[] = { 1, 2, 3, 4, 5 };
    int* p;

    p = data;
    printf("p = %p \n", p);   // p = 008FFE04

    if ( data == &data[0] )   // True
    {
        printf("True \n");   
    }

    printf("data = %p \n", data);           // data = 008FFE04
    printf("&data[0] = %p \n", &data[0]);   // &data[0] = 008FFE04

    return 0;
}

從下面這個例子也能看出,其實數組名就是一個地址,因爲在 scanf 中並沒有用 &str,而是直接使用了 str:

#include <stdio.h>
int main()
{
    char str[128];
    printf("請輸入一串字符:");
    scanf("%s", str);           // ABCDE
    printf("str = %s\n", str);  // ABCDE
    return 0;
}

那麼若要輸出數組中的全部元素,就需要使用循環操作。其中分爲三種方法:1)直接用數組元素下標循環獲取;2)利用數組名循環計算各元素的地址並獲取;3)利用指針的移動獲取數組元素。

#include <stdio.h>
int main()
{
    int data[] = { 1, 2, 3, 4, 5 };
    int* p;

    p = data;  
    
    for (int i = 0; i < 5; i++)  // 利用下標直接獲取數組元素
    {
        printf("i = %d, data = %d \n", i, data[i]);
    }
    for (int i = 0; i < 5; i++)  // 利用數組名計算各數組元素的位置並獲取
    {
        printf("p = %p, data = %d \n", p + i, *(p + i));
    }
    for (int* pt = data; pt < (data + 5); pt++)  // 利用指針間接獲取數組元素
    {
        printf("pt = %p, data = %d \n", pt, *pt);
    }

    return 0;
}

輸出:

i = 0, data = 1
i = 1, data = 2
i = 2, data = 3
i = 3, data = 4
i = 4, data = 5

p = 008FFE04, data = 1
p = 008FFE08, data = 2
p = 008FFE0C, data = 3
p = 008FFE10, data = 4
p = 008FFE14, data = 5

pt = 008FFE04, data = 1
pt = 008FFE08, data = 2
pt = 008FFE0C, data = 3
pt = 008FFE10, data = 4
pt = 008FFE14, data = 5

注意:對指針+1,等價於對指針的值加上它所指向的對象的字節大小。對於數組來說,地址會增加到下一個元素的地址,而不是下一個字節。這就是爲什麼在聲明指針變量時必須聲明它所指向對象的類型。所以下面的式子是成立的:

data + 2 == &data[2];
*(data + 2) == data[2];

數組和指針的區別

數組名非左值,不能對其值進行改變

比如下面這個例子:

int main()
{
    char str[] = "Hello";
    char* target = str;
    int count = 0;
    // while (*str++ != '\0')   // 這樣寫是錯誤的
    while (*target++ != '\0')
    {
        count++;
    }
    printf("共有%d個字符\n", count);
}

不能直接利用數組名(地址)進行自增運算,會報錯“error: lvalue required as increment operand”,這說明數組名非左值(左值:指用於識別或定位一個存儲位置的標識符,同時該標識符是可變的),由於數組名是不可改變的,所以數組名就不是左值。

如果不小心對 str 進行自增,會向你提示:表達式必須是可修改的左值:
在這裏插入圖片描述
但是下面這樣的運算是允許的,因爲並沒有對數組名(地址)進行修改:

int main()
{
    int data[] = { 1, 2, 3, 4, 5 };
    int* p;
    
    printf("data[0] = %d, data[1] = %d, data[2] = %d \n", *data, *(data+1), *(data+2));

    return 0;
}

>>>
data[0] = 1, data[1] = 2, data[2] = 3

指針數組和數組指針

指針數組是數組,每個數組元素存放一個指針變量;
數組指針是指針,它指向一個有n個元素的數組。

關於指針數組:

int main()
{}
    char *p[3] = {"ABC", "DEF", "GHI"};
    int i 
    for (i = 0; i < 3; i++)
    {
        printf("%s ", p[i]);  // ABC DEF GHI
    }
    
    return 0;
}

需要注意的是,字符串常量和數組名一樣,被編譯器當作指針對待,字符串的值就是字符串的基地址。

關於字符串本身就是地址,可以驗證一下:

int main()
{
    char str[] = "We have some apple.";
    printf("%s \n", str);
    printf("%p \n", str);
    printf("%p \n", &str[0]);
    printf("%c \n", *str);
    return 0;
}

>>>
We have some apple.
006FFDB8
006FFDB8
W

關於數組指針:

重點是區分“常規的指向數組的指針”和“數組指針”之間的差別,看一個例子:

int main()
{
    int str[] = { 1, 2, 3 };
    int* p1 = str;               // 這是指向數組第一個元素的指針
    int(*p2)[3] = &str;          // 這是數組指針,是一個整體
    for (int i = 0; i < 3; i++)
    {
        // p1本身就是數組第一個元素的地址
        printf("%d \n", *(p1 + i));
    }
    printf("%p \n", *p2);
    for (int j = 0; j < 3; j++)
    {   
        // 內部p2上的*是取數組第一個元素的地址,外部*是取值
        printf("%p, %d \n", *p2 + j, *(*p2 + j));
    }

    return 0;
}

1
2
3
012FFE9C
012FFE9C, 1
012FFEA0, 2
012FFEA4, 3

可以發現二者的區別,對數組指針初始化,必須是一個“地址”。可以將二者理解爲,p1其實是指向數組第一個元素的指針,它存儲的是數組第一個元素的地址。而p2指向的是“整個數組”。

數組指針是指向整個數組的,這個概念怎麼理解呢?再看一個例子:

int main()
{
    int str[] = { 1, 2, 3 };
    int* p1 = str;
    int(*p2)[3] = &str;          // 數組指針

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

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

    return 0;
}

>>>
p1 + 0 = 00D6FCDC, *(p1 + 0) = 1
p1 + 1 = 00D6FCE0, *(p1 + 1) = 2
p1 + 2 = 00D6FCE4, *(p1 + 2) = 3
p2 + 0 = 00D6FCDC, *(p2 + 0) = 00D6FCDC
p2 + 1 = 00D6FCE8, *(p2 + 1) = 00D6FCE8
p2 + 2 = 00D6FCF4, *(p2 + 2) = 00D6FCF4

第一個for循環很好理解,就是普通的指針不斷+1並取出對應位置上的元素,每個int型變量4個byte,所以p1+0,p1+1,p1+2相差4。
第二個for循環,p2+i 和 *(p2+i) 的輸出結果爲什麼一摸一樣?而且爲什麼每兩個值之間相差8?
因爲數組指針是“指向數組整體”,所以數組指針 p2+1,就相當於指針要跳過“整個數組的大小”,而非“一個int變量的大小”,我們上面定義數組str的大小剛好是12byte(3x4byte),所以每次p2加1,就是跳過12個byte的大小。p2+i是指向數組整體,那麼*(p2+i)就是指向當前數組的第一個元素,所以兩者值是相同的。

並且第二個for循環裏的循環變量i實際上和數組大小並沒有關係,你也可以指定爲更大的數:

for (int i = 0; i < 10; i++)
    {
        printf("p2 + %d = %p, *(p2 + %d) = %p \n", i, p2 + i, i, *(p2 + i));
    }
>>>
p2 + 0 = 001EF748, *(p2 + 0) = 001EF748
p2 + 1 = 001EF754, *(p2 + 1) = 001EF754
p2 + 2 = 001EF760, *(p2 + 2) = 001EF760
p2 + 3 = 001EF76C, *(p2 + 3) = 001EF76C
p2 + 4 = 001EF778, *(p2 + 4) = 001EF778
p2 + 5 = 001EF784, *(p2 + 5) = 001EF784
p2 + 6 = 001EF790, *(p2 + 6) = 001EF790
p2 + 7 = 001EF79C, *(p2 + 7) = 001EF79C
p2 + 8 = 001EF7A8, *(p2 + 8) = 001EF7A8
p2 + 9 = 001EF7B4, *(p2 + 9) = 001EF7B4

現在知道了p2實際上是指向整個數組,就不難想到如何來取值了:先*p2取得數組第一個元素的地址,然後再*(*p2+i)取數組第i個元素的地址,這樣兩種方法得到的結果就是一樣的:

int main()
{
    int str[] = { 1, 2, 3 };
    int* p1 = str;
    int(*p2)[3] = &str;

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

    printf("p2 = %p, *p2 = %p \n", p2, *p2);

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

    return 0;
}

>>>
p1 + 0 = 012FFC78, *(p1 + 0) = 1
p1 + 1 = 012FFC7C, *(p1 + 1) = 2
p1 + 2 = 012FFC80, *(p1 + 2) = 3

p2 = 012FFC78, *p2 = 012FFC78

*p2 + 0 = 012FFC78, *(*p2 + 0) = 1
*p2 + 1 = 012FFC7C, *(*p2 + 1) = 2
*p2 + 2 = 012FFC80, *(*p2 + 2) = 3
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章