1. 程式人生 > 其它 >C語言的核心--指標補充

C語言的核心--指標補充

指標變數作為函式引數

在C語言體系中,函式的引數不僅可以是整數、小數、字元等具體的資料,還可以是指向它們的指標。我們使用指標變數作函式引數可以將函式外部的地址傳遞到函式內部,使得在函式內部可以操作函式外部的資料,並且這些資料不會隨著函式的結束而被銷燬。從而實現函式之間資料的傳遞。同時類似於陣列、字串、動態分配的記憶體等都是一系列資料的集合,沒有辦法通過一個引數全部傳入函式內部,只能傳遞它們的指標,在函式內部通過指標來影響這些資料集合。

這裡我們舉一個例子來進行論證,也是書中經常使用的一個例子:

#include <stdio.h>

void swap(int a, int b)
{
    int temp;  //臨時變數
    temp = a;
    a = b;
    b = temp;
}


int main()
{
    int a = 1, b = 2;
    swap(a, b);
    printf("a = %d, b = %d\n", a, b);
    return 0;
}

輸出結果:

a = 1,b = 2

我們發現數據並沒有交換:

原因是swap() 函式內部的 a、b 和 main() 函式內部的 a、b 是不同的變數,佔用不同的記憶體,它們除了名字一樣,沒有其他任何關係,swap() 交換的是它內部 a、b 的值,不會影響它外部(main() 內部) a、b 的值。

如果我們使用指標變數進行傳遞引數的操作:

#include <stdio.h>

void swap(int *p1, int *p2)
{
    int temp;  //臨時變數
    temp = *p1;
    *p1 = *p2;
    *p2 = temp;
}

int main()
{
    int a = 1, b = 2;
    swap(&a, &b);
    printf("a = %d, b = %d\n", a, b);
    return 0;
}

輸出結果:

a = 2,b = 1

我們可以發現交換成功,原因在於呼叫 swap() 函式時,將變數 a、b 的地址分別賦值給 p1、p2,這樣 p1、p2 代表的就是變數 a、b 本身,交換 p1、p2 的值也就是交換 a、b 的值。函式執行結束後雖然會將 p1、p2 銷燬,但它對外部 a、b 造成的影響是“持久化”的,不會隨著函式的結束而“恢復原樣”

在這裡我們可以瞭解到用指標傳遞引數的奧妙,那麼我們繼續進一步深入,我們看一下如果我們用指標傳遞陣列會怎?

陣列是一系列資料的集合,無法通過引數將它們一次性傳遞到函式內部,如果希望在函式內部運算元組,必須傳遞陣列指標。

栗子:查詢陣列中值最大的元素

#include <stdio.h>

int max(int *intArr, int len)
{
    int i, maxValue = intArr[0];  //假設第0個元素是最大值
    for(i=1; i<len; i++){
        if(maxValue < intArr[i])
        {
            maxValue = intArr[i];
        }
    }
   
    return maxValue;
}

int main(){
    int nums[6], i;
    int len = sizeof(nums)/sizeof(int);
    //讀取使用者輸入的資料並賦值給陣列元素
    for(i=0; i<len; i++)
    {
        scanf("%d", nums+i);
    }
    printf("%d\n", max(nums, len));
    return 0;
}


執行結果:

輸入:1,2,3,4,5,6
輸出:6

引數 intArr 僅僅是一個數組指標,在函式內部無法通過這個指標獲得陣列長度,必須將陣列長度作為函式引數傳遞到函式內部。陣列 nums 的每個元素都是整數,scanf() 在讀取使用者輸入的整數時,要求給出儲存它的記憶體的地址,nums+i就是第 i 個數組元素的地址。

這裡需要注意的一點:

不管使用哪種方式傳遞陣列,都不能在函式內部求得陣列長度,因為 intArr 僅僅是一個指標,而不是真正的陣列,所以必須要額外增加一個引數來傳遞陣列長度。

這一點非常重要。


我們在來深入瞭解一下為什麼不直接傳遞陣列的所有元素,而必須傳遞陣列指標呢?

正如引數的傳遞本質上是一次賦值的過程,賦值就是對記憶體進行拷貝。所謂記憶體拷貝,是指將一塊記憶體上的資料複製到另一塊記憶體上。對於像 int、float、char 等基本型別的資料,它們佔用的記憶體往往只有幾個位元組,對它們進行記憶體拷貝非常快速。而陣列是一系列資料的集合,資料的數量沒有限制,可能很少,也可能成千上萬,對它們進行記憶體拷貝有可能是一個漫長的過程,會嚴重拖慢程式的效率,為了防止技藝不佳的程式設計師寫出低效的程式碼,C語言沒有從語法上支援資料集合的直接賦值。

當然函式還可以作為返回值,這裡我們不再具體闡述。

指標陣列

如果一個數組中的所有元素儲存的都是指標,那麼我們就稱它為指標陣列。

指標陣列的定義形式一般為:
dataType *arrayName[length];

[ ]的優先順序高於*,該定義形式應該理解為:
dataType *(arrayName[length]);

這裡要注意和陣列指標相區別。
括號裡面說明arrayName是一個數組,包含了length個元素,括號外面說明每個元素的型別為dataType *

除了每個元素的資料型別不同,指標陣列和普通陣列在其他方面都是一樣的。

給出一個具體例子,供大家瞭解:

#include <stdio.h>

int main(){
    int a = 1, b = 2, c = 3;
    //定義一個指標陣列

    int *arr[3] = {&a, &b, &c};//也可以不指定長度,直接寫作 int *arr[]
    //定義一個指向指標陣列的指標

    int **parr = arr;
    printf("%d, %d, %d\n", *arr[0], *arr[1], *arr[2]);
    printf("%d, %d, %d\n", **(parr+0), **(parr+1), **(parr+2));

    return 0;
}

輸出結果:

1,2,3
1,2,3

分析:
arr 是一個指標陣列,它包含了 3 個元素,每個元素都是一個指標,在定義 arr 的同時,我們使用變數 a、b、c 的地址對它進行了初始化,這和普通陣列是多麼地類似。

parr 是指向陣列 arr 的指標,確切地說是指向 arr 第 0 個元素的指標,它的定義形式應該理解為int (parr),括號中的*表示 parr 是一個指標,括號外面的int *表示 parr 指向的資料的型別。arr 第 0 個元素的型別為 int *,所以在定義 parr 時要加兩個 *。

第一個 printf() 語句中,arr[i] 表示獲取第 i 個元素的值,該元素是一個指標,還需要在前面增加一個 * 才能取得它指向的資料,也即 arr[i] 的形式。
第二個 printf() 語句中,parr+i 表示第 i 個元素的地址,
(parr+i) 表示獲取第 i 個元素的值(該元素是一個指標),**(parr+i) 表示獲取第 i 個元素指向的資料。

二維陣列指標

二維陣列在概念上是二維的,有行和列,但在記憶體中所有的陣列元素都是連續排列的,它們之間沒有“縫隙”。

例如:

int a[3][4] = { {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11} };

其實a陣列的分佈更像是一個矩陣:

0   1   2   3
4   5   6   7
8   9  10  11

C語言中的二維陣列是按行排列的,也就是先存放 a[0] 行,再存放 a[1] 行,最後存放 a[2] 行;每行中的 4 個元素也是依次存放。陣列 a 為 int 型別,每個元素佔用 4 個位元組,整個陣列共佔用 4×(3×4) = 48 個位元組。
C語言允許把一個二維陣列分解成多個一維陣列來處理。對於陣列 a,它可以分解成三個一維陣列,即 a[0]、a[1]、a[2]。每一個一維陣列又包含了 4 個元素,例如 a[0] 包含 a[0][0]、a[0][1]、a[0][2]、a[0][3]。

由於陣列在記憶體中為線性排列,這就為指標的使用提供了條件。

例如,我們定義一個指標變數p:

int (*p)[4] = a;

括號中的*表明 p 是一個指標,它指向一個數組,陣列的型別為int [4],是 a 所包含的每個一維陣列的型別。
[ ]的優先順序高於*,( )是必須要加的,如果赤裸裸地寫作int *p[4],那麼應該理解為int *(p[4]),p 就成了一個指標陣列,而不是二維陣列指標

對指標進行加法(減法)運算時,它前進(後退)的步長與它指向的資料型別有關,p 指向的資料型別是int [4],那麼p+1就前進 4×4 = 16 個位元組,p-1就後退 16 個位元組,這正好是陣列 a 所包含的每個一維陣列的長度。也就是說,p+1會使得指標指向二維陣列的下一行,p-1會使得指標指向陣列的上一行。

我們看一下如何用指標 p 來訪問二維陣列中的每個元素:

  1. p指向陣列 a 的開頭,也即第 0 行;p+1前進一行,指向第 1 行。

  2. *(p+1)表示取地址上的資料,也就是整個第 1 行資料。注意是一行資料,是多個數據,不是第 1 行中的第 0 個元素

  3. *(p+1)+1表示第 1 行第 1 個元素的地址。如何理解呢?

*(p+1)單獨使用時表示的是第 1 行資料,放在表示式中會被轉換為第 1 行資料的首地址,也就是第 1 行第 0 個元素的地址,因為使用整行資料沒有實際的含義,編譯器遇到這種情況都會轉換為指向該行第 0 個元素的指標;就像一維陣列的名字,在定義時或者和 sizeof、& 一起使用時才表示整個陣列,出現在表示式中就會被轉換為指向陣列第 0 個元素的指標。

  1. ((p+1)+1)表示第 1 行第 1 個元素的值。很明顯,增加一個 * 表示取地址上的資料。

由此我們得到如下公式,我們稱之為萬能公式:

a+i == p+i
a[i] == p[i] == *(a+i) == *(p+i)
a[i][j] == p[i][j] == *(a[i]+j) == *(p[i]+j) == *(*(a+i)+j) == *(*(p+i)+j)