1. 程式人生 > >C語言:詳解指標

C語言:詳解指標

指標應該算得上是c語言的精華,但也是難點。很多教程或者部落格都有對其詳細的講解與分析。我這一節的內容,也是講解指標,但我會盡量使用圖解的方式,使大家很容易理解及掌握。

一、基本使用

先來看看下面的程式碼:

int i = 3; 
int *p;    
p = &i;
    
printf("i 存放的內容的值: %d, i 自己所在的地址: %p\n", i, &i);
    
printf("p 存放的地址的值: %p; p 自己所在的地址: %p; p 存放的地址所指所存放內容的值: %d", p, &p, *p);
    
return 0;
變數i是int型別,所以存放的是int資料。

變數p是int *型別,所以存放的是指向int型別的地址。

這樣說,似乎還是沒有表達清楚,我使用下面的一張圖進行說明:

1. int i = 3; 這句話執行完畢之後,變數i中的內容是3,假設 變數i本身的記憶體地址為 "0x8000"。

2. int *p; 只是為指標變數p申請了一塊記憶體地址,假設它的記憶體地址為 "0x7000",此時變數p中存放的內容為nil。

3. p = &i; 表示將變數i的地址賦值給指標p所存放的內容,至此,就呈現出了上圖的情形。

所以程式中的那兩句列印結果就很明顯了。

i 存放的內容的值: 3, i 自己所在的地址: 0x8000

p 存放的地址的值: 0x8000; p 自己所在的地址: 0x7000; p 存放的地址所指所存放內容的值: 3


二、 交換兩個整數的值

示例程式碼如下:

void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main(int argc, const char * argv[]) {
    
    int a = 3, b = 5;
    
    swap(&a ,&b);
    
    printf("a: %d; b: %d", a ,b);
    
    return 0;
}
當程式執行完 int a = 3, b = 5, 之後,假設a,b他們自己在記憶體中的地址分別為0x8000, 0x9000。如下圖:

然後呼叫swap(&a, &b); 注意,這裡傳遞的是變數a, b 本身的地址。而函式swap的接受形參int *a, int *b均是指標變數,用來接受傳遞過來的變數a, b的地址,即傳遞過來的變數a, b 和 函式 swap中的形式引數a, b 完全是兩回事;為了以示區別,我將形參中的a, b 表達為 swap_a, swap_b。所以將變數a, b 的地址賦值給swap_a 和 swap_b 之後,記憶體中的地址分佈大致如下(假設swap_a本身的地址為0x6000, swap_b本身的地址為0x7000)。


int temp = *a,  定義一個臨時變數temp,用來儲存指標swap_a 所指向變數a所儲存的值,所以臨時變數temp此時儲存的值為3,如下圖:


*a = *b, 表示將將指標swap_b所指向變數b的內容賦值給指標swap_a所指向變數a的內容,所以執行完畢之後,記憶體圖大致如下:


最後一句程式碼 *b = temp, 就是將temp所儲存的內容賦值給指標swap_b所指向的變數b儲存的內容,所以執行完這句話之後,記憶體圖大致如下:



當程式執行到swap函式 右邊 “}” 結束後,此時,表示函式swap已經結束,而變數temp是區域性變數,所以此時它也會被立刻銷燬,所以最終的記憶體結構圖大致而下:


通過上面兩個例子的圖解,相信大家對指標的概念有了初步的瞭解,下面的內容,我就直接講解其內容不做畫圖處理了,如果自己感興趣的話,也可以畫圖嘗試嘗試。

三、 字元陣列與字串常量

1. 字元陣列

char str[] = "good";
while (*str != '\0') {
   putchar(*str++);
}
注意:字元陣列名是一個常量指標,不能進行類似於 str = str + 1 或者 str++ 等操作,所以上面的程式碼是錯誤的。而且str存放的是陣列第一個元素的地址。如果想了解更所的關於地址方面的知識,可以參考我前面講解的內容C語言:記憶體地址分析 & sizeof和strlen用法總結

2. 字串常量

char *str2 = "good";
while (*str2 != '\0') {
    putchar(*str2++);
}
注意: "good"本身就是一個常量內容,它存放在只讀儲存區,並且有自己的地址,而變數str2中存放的內容就是常量 "good"所在的地址。所以上述str2++操作完全是正確的。

比如下面的Demo

const char *str = "hello";
printf("address: %p\t%s\t%c\n", &str,str, *str);
str++;
printf("address: %p\t%s\n", &str,str);
列印的結果為:

address: 0x7fff5fbff6b8helloh

address: 0x7fff5fbff6b8ello

所以str的值就是指標所在位置及後面字元的值,而*str取得的就是指標所在位置字元的值。

四、函式指標

示例程式碼:

int add(int num1, int num2) {
    return num1 + num2;
}

int main(int argc, const char * argv[]) {
    printf("result:%d", add(1, 2));
    printf("\n%p", add); 
    int (*p)(int,int) = add; 
    printf("\n%d", p(3, 4));
    return 0;
}
函式指標,顧名思義,就是一個指向函式的指標。函式指標的目的就是為了實現方法的回撥。而回調不是本節講解的重點,我就不做具體說明了。上面的Demo中定義了一個函式add,它是一個有兩個引數,返回值為int的函式。printf("\n%p", add) 打印出來的結果就是函式add入口點的地址。
int (*p)(int,int) = add;
就是自定義一個函式指標p指向具體的函式add。需要注意的一點是:add是常量地址,不能被修改;而指標p是變數地址,可以被修改。

五、 泛型指標

泛型指標就是在定義的時候還不知道指標的具體型別,直到呼叫的時候才確定型別,並且進行相應的強制型別轉換工作,完成任務。泛型指標的形式就是void *。下面用泛型指標實現一個氣泡排序,程式碼如下:

void init_array(int *a, int n) {
    
    srand(time(NULL));
    
    for (int i = 0; i < n; i++) {
        a[i] = rand() % 100;
    }
}

int cmp_array(void *a, void *b) {
    int num1 = *((int *)a);
    int num2 = *((int *)b);
    return num1 > num2;
}

void swap_array(void *a, void *b) {
    int temp = *((int *)a);
    *((int *)a) = *((int *)b);
    *((int *)b) = temp;
}

// 這裡的void * 就是 泛型指標,需要進行指標轉換,達到自己想要的結果
void sort_array(int *a, int n, int (*cmp)(void *, void *), void (*swap)(void *, void *)){
    int i,j;
    // 氣泡排序
    for (i = 0; i < n; i++) {
        for (j = 0; j < n - i - 1; j++) {
            if (cmp(&a[j], &a[j+1])) {
                swap(&a[j], &a[j+1]);
            }
        }
    }
}

int main(int argc, const char * argv[]) {
    
    int a[10];
    init_array(a, 10);
    sort_array(a, 10, cmp_array, swap_array);
    return 0;
}

在c語言中,書寫起來看起來是有點複雜了,其實現在的高階語言沒有必要這麼麻煩了,用泛型就可以解決問題了,但是這些高階語言的泛型底層還是依賴於c語言的泛型指標。

六、更為複雜的指標

注:以下的測試程式碼均為在64位系統處理所得。

1) 指標陣列

char *p[10]; 
printf("sizeof(p):%lu \t sizeof(*p):%lu\n",sizeof(p), sizeof(*p));

1. p存放的是一個指標陣列的首地址,而指標陣列中每一個元素又是指向char *型別元素的地址。

2. sizeof(p)計算的是陣列位元組大小,輸出80,而sizeof(*p)是計算首元素中存放內容的大小,而存放的內容是地址,所以結果為8。

3. p+1 就是 p[1], 每一個地址中存放的就是char *型別元素的地址,即8個位元組,所以p+1的地址是在首元素地址的基礎上面加8。

最終列印結果:

sizeof(p):80 sizeof(*p):8

2) 指標的指標

char **pl; 
printf("sizeof(pl):%lu \t sizeof(*pl):%lu, \t **pl:%lu \t pl:%p \t pl+1:%p\n",sizeof(pl), sizeof(*pl), sizeof(**p), p, p+1);

1. pl是指標的指標,所以pl指向的是一個指標的地址,所以sizeof(pl)結果為8, 打印出來的pl是一個地址,由於pl指向的是一個“指標的地址”,而“指標的地址”存放的是char型別變數的地址,所以佔8個位元組,所以pl+1 比pl大8。

2. *pl指向char型別變數的,所以它存放的是char型別變數的地址,所以sizeof(*pl)結果為8。

3. **pl,獲取char型別變數的值,由於是char型別,所以sizeof(**p)結果為1。

最終列印結果:

sizeof(pl):8 sizeof(*pl):8, **pl:1 pl:0x7fff5fbff6b0 pl+1:0x7fff5fbff6b8

3) 函式指標
char (*pt)(void);

1. pt存放的是函式的首地址,所以sizeof(pt)結果為8

2. *pt 獲取的是函式內部的程式碼塊,所以sizeof(*pt) 和 pt + 1 均沒有實際意義

4) 陣列指標
char (*pk)[10]; // 陣列指標
printf("sizeof(pk):%lu \t sizeof(*pk):%lu, pk:%p \t *pk:%p \t pk + 1:%p \t *pk + 1: %p\n",sizeof(pk), sizeof(*pk),pk,*pk, pk + 1, *pk + 1);

1. char (*pk)[10];相當於二維陣列 char a[][10]

2. pk相當於指向二維陣列的指標,存放的是地址,所以sizeof(pk)結果為8;pk存放的是整個二維陣列的首地址; pk + 1 就是相當於指標移動了一個一維陣列的距離,所以地址在pk的基礎上面加了10.

3. *pk就是取得二維陣列第一行的值,它是一個含有10個char元素的一維陣列,所以sizeof(*pk)結果為10;*pk存放的是二維陣列中第一維陣列的首地址; *pk + 1就是在一維陣列的基礎上,移動了一個char的距離,所以地址在*pk基礎上面加1。

4. 所以 pk和*pk列印的地址結果是一樣的。

最終列印結果:

sizeof(pk):8 sizeof(*pk):10, pk:0x7fff5fc27190 *pk:0x7fff5fc27190 pk + 1:0x7fff5fc2719a *pk + 1: 0x7fff5fc27191