C之指針與數組組合(二十六)

        我們在前面講到數組的本質是一段連續的內存空間,那麼它的大小爲 sizeof(array_type) * array_size,同時數組名可看做指向數組第一個元素的常量指針。那麼問題來了,數組 a + 1 的意義是什麼呢?結果又是怎樣呢?指針運算的意義又是什麼?結果呢?下來我們看個示例代碼,代碼如下

#include <stdio.h>

int main()
{
    int a[5] = {0};
    int* p = NULL;
    
    printf("a = 0x%X\n", (unsigned int)(a));
    printf("a + 1 = 0x%X\n", (unsigned int)(a + 1));
    
    printf("p = 0x%X\n", (unsigned int)(p));
    printf("p + 1 = 0x%X\n", (unsigned int)(p + 1));
    
    return 0;
}

        編譯結果如下

圖片.png

        我們看到數組 a 相當於一個常量指針,而它便指向的首元素的地址,a + 1 便是首元素的地址加 4,也就是數組第二個元素的地址。因爲指針 p int 型,所以  p + 1 相當於加 4。

        指針是一種特殊的變量,它與整數的運算規則爲 p + n <==> (unsigned int)p + n*sizeof(*p);那麼便是當指針指向同一類型的數組的元素時:p + 1 將指向當前元素的下一個元素;p - 1 將指向當前元素的上一個元素。指針之間只支持減法運算,並且參與減法運算的指針類型必須相同。p1 - p2 <==> ((unsigned int)p1 - (unsigned int)p2)/sizeof(type);注意:a> 只有當兩個指針指向同一個數組中的元素時,指針相減纔有意義,其意義爲指針所指元素的下標差;b> 當兩個指針指向的元素不在同一個數組中時,結果爲定義

        指針也可以進行關係運算(<, <=, >, >=),指針關係運算的前提是同時指向同一個數組中的元素;任意兩個指針之間的比較運算(==,!=),參與比較運算的指針類型必須相同。

        下來我們來看個示例代碼,代碼如下

#include <stdio.h>

#define DIM(a) (sizeof(a) / sizeof(*a))

int main()
{
    char s[] = {'H', 'e', 'l', 'l', 'o'};
    char* pBegin = s;
    char* pEnd = s + DIM(s); // Key point
    char* p = NULL;
    
    printf("pBegin = %p\n", pBegin);
    printf("pEnd = %p\n", pEnd);
    
    printf("Size: %d\n", pEnd - pBegin);
    
    for(p=pBegin; p<pEnd; p++)
    {
        printf("%c", *p);
    }
    
    printf("\n");
   
    return 0;
}

        我們在第3行定義的宏是求這個數組元素的個數,在第9行定義的指針 pEnd 爲數組首元素的地址加上數組元素個數,那麼它剛好指向數組最後一個元素的臨界。這是 C 語言中的灰色地帶,在 C 語言中是合法的。我們來看看編譯結果

圖片.png

        我們看到結果是如我們所想的那,因爲是 char 類型的數組,所以 pEnd = pBegin + 5。

        數組名可以當做常量指針使用,那麼指針是否也可以當做數組名來使用呢?我們往後接着說,在數組中的訪問方式有兩種:1、以下標的形式訪問數組中的元素;2、以指針的形式訪問數組中的元素。那麼這兩種方式有何區別呢?當指針以固定增量在數組中移動時,效率要高於下標形式。尤其是指針增量爲 1 且硬件具有硬件增量模型時效率更高。下標形式與指針形式之間還會相互轉換:a[n] <==> *(a + n) <==> *(n + a) <==> n[a]。這種表示法是不是很奇怪?但經過理論推導完全是成立的,下面我們就來看看是否支持這種寫法

#include <stdio.h>

int main()
{
    int a[5] = {0};
    int* p = a;
    int i = 0;
    
    for(i=0; i<5; i++)
    {
        p[i] = i + 1;
    }
    
    for(i=0; i<5; i++)
    {
        printf("a[%d] = %d\n", i, *(a + i));
    }
    
    printf("\n");
    
    for(i=0; i<5; i++)
    {
        i[a] = i + 10;
    }
    
    for(i=0; i<5; i++)
    {
        printf("p[%d] = %d\n", i, p[i]);
    }
    
    return 0;
}

        我們看到在程序第6行定義指針 p 並將它指向數組 a,接下來就是我們之前說的到底指針是否也可以當做數組名來使用呢,如果可以第11行便不會報錯。在第16行我們以指針的形式來打印數組 a 中的值,第23行則驗證我們上面 i[a] 這種寫法是否正確,第28行則通過下標形式來訪問數組。我麼來看看編譯結果

圖片.png

        我們看到程序沒報錯並且完美執行,這就回答了我們上面的問題和疑問。但是得注意:在現代編譯器中,生成代碼優化率已大大提高,在固定增量時,下標形式的效率已經和指針形式相當了;但從代碼的可讀性和維護的角度來看,下標形式更優秀,這就是爲什麼我們平時見到的代碼中的數組都是以下標形式訪問的啦。

        我們下來再做個實驗,看看數組和指針的區別

test.c 代碼

#include <stdio.h>

int main()
{
    extern int a[];
    
    printf("&a = %p\n", &a);
    printf("a = %p\n", a);
    printf("*a = %d\n", *a);

    
    return 0;
}


ext.c 代碼

int a[] = {1, 2, 3, 4, 5};

        我們看到在 ext.c 中定義了一個數組,我們先以數組的方式在 test.c 中訪問,看看打印結果

圖片.png

        我們看到的結果和我們想的是一致的,&a 就代表數組的地址,a 就代表數組首元素的地址,兩個是相同的。*a 的值便是數組中第一個元素的值啦。我們再來將 test.c 中的第5行改成 extern int* a; 這樣呢,我們來看看編譯結果

圖片.png

        我們看到發生段錯誤了,這是什麼情況呢?數組 a 的值爲 1,*a 發生段錯誤了,在內存中,數組的值是這樣存儲的 0001 0002 ... 0005(大端機器)。那麼 a 自然也就爲 1了,計算機中的 1 地址處爲內核態,用戶態的程序想要訪問內核態的地址,計算機當然會報錯。

        那麼 a 和 &a 有何區別呢? a 爲數組首元素的地址,&a 爲整個數組的地址。a 和 &a 的區別在於指針運算。a + 1 ==> (unsigned int)a + sizeof(*a);&a + 1 ==> (unsigned int)(&a) + sizeof(*&a) ==> (unsigned int)(&a) + sizeof(a);

        下來我們來看個經典的指針運算問題,同時也是一道筆試面試題

#include <stdio.h>

int main()
{
    int a[5] = {1, 2, 3, 4, 5};
    int* p1 = (int*)(&a + 1); 
    int* p2 = (int*)((int)a + 1);
    int* p3 = (int*)(a + 1);
    
    printf("%d, %d, %d\n", p1[-1], p2[0], p3[1]);
    
    return 0;
}

        我們看下編譯結果

圖片.png

        我們來分析下,第一個 p1[-1] ==> p1[&a +1 - 1] =>p1[&a],自然它的值也就爲 5 了。p3[1] ==> (a + 1 +2) ==> (a + 2),自然也就爲 3 啦。第二個的數感覺是隨機數,但我們仔細分析下,它是首地址加 1,也就是往後移一位。這個數組在小端系統中,就是 1000 2000 ... 5000這樣分佈的,後移一位就變成了 0002,便是 0x02000000 轉成十進制便是 33554432 啦。

        數組作爲函數參數時,編譯器將其編譯成對應的指針。如:void f(int a[]) <==> void f(int* a);void f(int a[5]) <==> void f(int* a);在一般情況下,當定義的函數中有數組參數時,需要定義另一個參數來標定數組的大小。

        我們來看個示例代碼

#include <stdio.h>

void func1(char a[5])
{
    printf("In func1: sizeof(a) = %d\n", sizeof(a));
    
    *a = 'a';
    
    a = NULL;
}

void func2(char b[])
{
    printf("In func2: sizeof(b) = %d\n", sizeof(b));
    
    *b = 'b';
    
    b = NULL;
}

int main()
{
    char array[10] = {0};
    
    func1(array);
    
    printf("array[0] = %c\n", array[0]);
    
    func2(array);
    
    printf("array[0] = %c\n", array[0]);
    
    return 0;
}

        我們在 func1 中打印它的參數大小,並且以指針方式進行賦值和指向 NULL,如果是數組的話便會報錯。我們來看看編譯結果

圖片.png

        我們發現兩函數的數組參數都被當成指針來處理了。 通過本節對指針和數組的學習,總結如下:1、數組聲明時編譯器自動分配一片連續的內存空間,指針聲明時只分配了用於容納地址值的4字節空間;2、指針和整數可以進行運算,其結果爲指針。指針之間只支持減法運算,其結果爲數組元素下標差;3、指針之間支持比較運算,其類型必須相同;4、數組名和指針僅使用方式相同,數組名的本質不是指針,指針的本質不是數組;5、數組名並不是數組的地址,而是數組首元素的地址;6、函數的數組參數化爲指針。


        歡迎大家一起來學習 C 語言,可以加我QQ:243343083

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章