C語言之指針

指針

指針概念

計算機中所有的數據都必須放在內存中,不同類型的數據佔用的字節數不一樣,例如 int 佔用4個字節,char 佔用1個字節。爲了正確地訪問這些數據,必須爲每個字節都編上號碼,就像門牌號、身份證號一樣,每個字節的編號是唯一的,根據編號可以準確地找到某個字節。

下圖是 4G 內存中每個字節的編號(以十六進制表示):
1-150521230921W5

我們將內存中字節的編號稱爲地址(Address)或指針(Pointer)。地址從 0 開始依次增加,對於 32 位環境,程序能夠使用的內存爲 4GB,最小的地址爲 0,最大的地址爲 0XFFFFFFFF。

下面的代碼演示瞭如何輸出一個地址:

#include <stdio.h>
int main(){
    int a = 100;
    char str[20] = "c.biancheng.net";
    printf("%#X, %#X\n", &a, str);
    return 0;
}

運行結果:
0X28FF3C, 0X28FF10

%#X表示以十六進制形式輸出,並附帶前綴0X。a 是一個變量,用來存放整數,需要在前面加&來獲得它的地址;str 本身就表示字符串的首地址,不需要加&。

一切都是地址

C語言用變量來存儲數據,用函數來定義一段可以重複使用的代碼,它們最終都要放到內存中才能供 CPU 使用。

數據和代碼都以二進制的形式存儲在內存中,計算機無法從格式上區分某塊內存到底存儲的是數據還是代碼。當程序被加載到內存後,操作系統會給不同的內存塊指定不同的權限,擁有讀取和執行權限的內存塊就是代碼,而擁有讀取和寫入權限(也可能只有讀取權限)的內存塊就是數據。

CPU 只能通過地址來取得內存中的代碼和數據,程序在執行過程中會告知 CPU 要執行的代碼以及要讀寫的數據的地址。如果程序不小心出錯,或者開發者有意爲之,在 CPU 要寫入數據時給它一個代碼區域的地址,就會發生內存訪問錯誤。這種內存訪問錯誤會被硬件和操作系統攔截,強制程序崩潰,程序員沒有挽救的機會。

CPU 訪問內存時需要的是地址,而不是變量名和函數名!變量名和函數名只是地址的一種助記符,當源文件被編譯和鏈接成可執行程序後,它們都會被替換成地址。編譯和鏈接過程的一項重要任務就是找到這些名稱所對應的地址。

一個加法的內存演示

假設變量 a、b、c 在內存中的地址分別是 0X1000、0X2000、0X3000,那麼加法運算c = a + b;將會被轉換成類似下面的形式:
0X3000 = (0X1000) + (0X2000);
( )表示取值操作,整個表達式的意思是,取出地址 0X1000 和 0X2000 上的值,將它們相加,把相加的結果賦值給地址爲 0X3000 的內存

變量名和函數名爲我們提供了方便,讓我們在編寫代碼的過程中可以使用易於閱讀和理解的英文字符串,不用直接面對二進制地址,那場景簡直讓人崩潰。

需要注意的是,雖然變量名、函數名、字符串名和數組名在本質上是一樣的,它們都是地址的助記符,但在編寫代碼的過程中,我們認爲變量名錶示的是數據本身,而函數名、字符串名和數組名錶示的是代碼塊或數據塊的首地址。

指針變量

數據在內存中的地址也稱爲指針,如果一個變量存儲了一份數據的指針,我們就稱它爲指針變量。

在C語言中,允許用一個變量來存放指針,這種變量稱爲指針變量。指針變量的值就是某份數據的地址,這樣的一份數據可以是數組、字符串、函數,也可以是另外的一個普通變量或指針變量。

現在假設有一個 char 類型的變量 c,它存儲了字符 ‘K’(ASCII碼爲十進制數 75),並佔用了地址爲 0X11A 的內存(地址通常用十六進制表示)。另外有一個指針變量 p,它的值爲 0X11A,正好等於變量 c 的地址,這種情況我們就稱 p 指向了 c,或者說 p 是指向變量 c 的指針。
1-150630093413635

定義指針變量

定義指針變量與定義普通變量非常類似,不過要在變量名前面加星號*,格式爲:

datatype *name;
或者
datatype *name = value;

*表示這是一個指針變量,datatype表示該指針變量所指向的數據的類型 。例如:

int *p1;

p1 是一個指向 int 類型數據的指針變量,至於 p1 究竟指向哪一份數據,應該由賦予它的值決定。再如:

int a = 100; int *p_a = &a;

在定義指針變量 p_a 的同時對它進行初始化,並將變量 a 的地址賦予它,此時 p_a 就指向了 a。值得注意的是,p_a 需要的一個地址,a 前面必須要加取地址符&,否則是不對的。

和普通變量一樣,指針變量也可以被多次寫入,只要你想,隨時都能夠改變指針變量的值,請看下面的代碼:

//定義普通變量
float a = 99.5, b = 10.6;
char c = '@', d = '#';
//定義指針變量
float *p1 = &a;
char *p2 = &c;
//修改指針變量的值
p1 = &b;
p2 = &d;

是一個特殊符號,表明一個變量是指針變量,定義 p1、p2 時必須帶。而給 p1、p2 賦值時,因爲已經知道了它是一個指針變量,就沒必要多此一舉再帶上*,後邊可以像使用普通變量一樣來使用指針變量。也就是說,定義指針變量時必須帶*,給指針變量賦值時不能帶*。

假設變量 a、b、c、d 的地址分別爲 0X1000、0X1004、0X2000、0X2004,下面的示意圖很好地反映了 p1、p2 指向的變化:

1-160G1112023444

需要強調的是,p1、p2 的類型分別是float和char,而不是float和char,它們是完全不同的數據類型,讀者要引起注意。

指針變量也可以連續定義,例如:
int *a, *b, *c; //a、b、c 的類型都是 int*
注意每個變量前面都要帶*。如果寫成下面的形式,那麼只有 a 是指針變量,b、c 都是類型爲 int 的普通變量:
int *a, b, c;

通過指針變量取得數據

指針變量存儲了數據的地址,通過指針變量能夠獲得該地址上的數據,格式爲:

*pointer;

這裏的*稱爲指針運算符,用來取得某個地址上的數據,請看下面的例子:

#include <stdio.h>
int main(){
    int a = 15;
    int *p = &a;
    printf("%d, %d\n", a, *p);  //兩種方式都可以輸出a的值
    return 0;
}

運行結果:
15, 15

假設 a 的地址是 0X1000,p 指向 a 後,p 本身的值也會變爲 0X1000,*p 表示獲取地址 0X1000 上的數據,也即變量 a 的值。從運行結果看,*p 和 a 是等價的。

上節我們說過,CPU 讀寫數據必須要知道數據在內存中的地址,普通變量和指針變量都是地址的助記符,雖然通過 *p 和 a 獲取到的數據一樣,但它們的運行過程稍有不同:a 只需要一次運算就能夠取得數據,而 *p 要經過兩次運算,多了一層“間接”。

假設變量 a、p 的地址分別爲 0X1000、0XF0A0,它們的指向關係如下圖所示:
1-160H013032a30

程序被編譯和鏈接後,a、p 被替換成相應的地址。
使用 *p 的話,要先通過地址 0XF0A0 取得變量 p 本身的值,這個值是變量 a 的地址,然後再通過這個值取得變量 a 的數據,前後共有兩次運算;
而使用 a 的話,可以通過地址 0X1000 直接取得它的數據,只需要一步運算。

也就是說,使用指針是間接獲取數據,使用變量名是直接獲取數據,前者比後者的代價要高。

指針除了可以獲取內存上的數據,也可以修改內存上的數據,例如:

#include <stdio.h>
int main(){
    int a = 15, b = 99, c = 222;
    int *p = &a;  //定義指針變量
    *p = b;  //通過指針變量修改內存上的數據
    c = *p;  //通過指針變量獲取內存上的數據
    printf("%d, %d, %d, %d\n", a, b, c, *p);
    return 0;
}

運行結果:

99, 99, 99, 99

*p 代表的是 a 中的數據,它等價於 a,可以將另外的一份數據賦值給它,也可以將它賦值給另外的一個變量。

*在不同的場景下有不同的作用:可以用在指針變量的定義中,表明這是一個指針變量,以和普通變量區分開;使用指針變量時在前面加表示獲取指針指向的數據,或者說表示的是指針指向的數據本身。

也就是說,定義指針變量時的和使用指針變量時的意義完全不同。以下面的語句爲例:

int *p = &a;
*p = 100;

第1行代碼中用來指明 p 是一個指針變量,第2行代碼中用來獲取指針指向的數據。

需要注意的是,給指針變量本身賦值時不能加*。修改上面的語句:

int *p;
p = &a;
*p = 100;

第2行代碼中的 p 前面就不能加*。

指針變量也可以出現在普通變量能出現的任何表達式中,例如:

int x, y, *px = &x, *py = &y;
y = *px + 5;  //表示把x的內容加5並賦給y,*px+5相當於(*px)+5
y = ++*px;  //px的內容加上1之後賦給y,++*px相當於++(*px)
y = *px++;  //相當於y=(*px)++
py = px;  //把一個指針的值賦給另一個指針

【示例】通過指針交換兩個變量的值。

#include <stdio.h>
int main(){
    int a = 100, b = 999, temp;
    int *pa = &a, *pb = &b;
    printf("a=%d, b=%d\n", a, b);
    /*****開始交換*****/
    temp = *pa;  //將a的值先保存起來
    *pa = *pb;  //將b的值交給a
    *pb = temp;  //再將保存起來的a的值交給b
    /*****結束交換*****/
    printf("a=%d, b=%d\n", a, b);
    return 0;
}

運行結果:

a=100, b=999
a=999, b=100

從運行結果可以看出,a、b 的值已經發生了交換。需要注意的是臨時變量 temp,它的作用特別重要,因爲執行*pa = pb;語句後 a 的值會被 b 的值覆蓋,如果不先將 a 的值保存起來以後就找不到了。
關於 * 和 & 的謎題
假設有一個 int 類型的變量 a,pa 是指向它的指針,那麼
&a和&*pa分別是什麼意思呢?

&a可以理解爲(&a),&a表示取變量 a 的地址(等價於 pa),*(&a)表示取這個地址上的數據(等價於 pa),繞來繞去,又回到了原點,&a仍然等價於 a。

&*pa可以理解爲&(*pa),*pa表示取得 pa 指向的數據(等價於 a),&(*pa)表示數據的地址(等價於 &a),所以&*pa等價於 pa。

對星號*的總結

在我們目前所學到的語法中,星號*主要有三種用途:

  1. 表示乘法,例如int a = 3, b = 5, c; c = a * b;,這是最容易理解的。
  2. 表示定義一個指針變量,以和普通變量區分開,例如int a = 100; int *p = &a;。
  3. 表示獲取指針指向的數據,是一種間接操作,例如int a, b, *p = &a; *p = 100; b = *p;。

指針變量的運算

指針變量保存的是地址,本質上是一個整數,可以進行部分運算,例如加法、減法、比較等,請看下面的代碼:

#include <stdio.h>
int main(){
    int    a = 10,   *pa = &a, *paa = &a;
    double b = 99.9, *pb = &b;
    char   c = '@',  *pc = &c;
    //最初的值
    printf("&a=%#X, &b=%#X, &c=%#X\n", &a, &b, &c);
    printf("pa=%#X, pb=%#X, pc=%#X\n", pa, pb, pc);
    //加法運算
    pa++; pb++; pc++;
    printf("pa=%#X, pb=%#X, pc=%#X\n", pa, pb, pc);
    //減法運算
    pa -= 2; pb -= 2; pc -= 2;
    printf("pa=%#X, pb=%#X, pc=%#X\n", pa, pb, pc);
    //比較運算
    if(pa == paa){
        printf("%d\n", *paa);
    }else{
        printf("%d\n", *pa);
    }
    return 0;
}

運行結果:

&a=0X28FF44, &b=0X28FF30, &c=0X28FF2B
pa=0X28FF44, pb=0X28FF30, pc=0X28FF2B
pa=0X28FF48, pb=0X28FF38, pc=0X28FF2C
pa=0X28FF40, pb=0X28FF28, pc=0X28FF2A
2686784

從運算結果可以看出:pa、pb、pc 每次加 1,它們的地址分別增加 4、8、1,正好是 int、double、char 類型的長度;減 2 時,地址分別減少 8、16、2,正好是 int、double、char 類型長度的 2 倍。

這很奇怪,指針變量加減運算的結果跟數據類型的長度有關,而不是簡單地加 1 或減 1,這是爲什麼呢?

以 a 和 pa 爲例,a 的類型爲 int,佔用 4 個字節,pa 是指向 a 的指針,如下圖所示:

1-160G1133340S3

剛開始的時候,pa 指向 a 的開頭,通過 *pa 讀取數據時,從 pa 指向的位置向後移動 4 個字節,把這 4 個字節的內容作爲要獲取的數據,這 4 個字節也正好是變量 a 佔用的內存。

如果pa++;使得地址加 1 的話,就會變成如下圖所示的指向關係:

1-160G1134224B7

這個時候 pa 指向整數 a 的中間,*pa 使用的是紅色虛線畫出的 4 個字節,其中前 3 個是變量 a 的,後面 1 個是其它數據的,把它們“攪和”在一起顯然沒有實際的意義,取得的數據也會非常怪異。

如果pa++;使得地址加 4 的話,正好能夠完全跳過整數 a,指向它後面的內存,如下圖所示:

1-160G1134Z3T3

我們知道,數組中的所有元素在內存中是連續排列的,如果一個指針指向了數組中的某個元素,那麼加 1 就表示指向下一個元素,減 1 就表示指向上一個元素,這樣指針的加減運算就具有了現實的意義,我們將在C語言的數組一節中深入探討。

不過C語言並沒有規定變量的存儲方式,如果連續定義多個變量,它們有可能是挨着的,也有可能是分散的,這取決於變量的類型、編譯器的實現以及具體的編譯模式,所以對於指向普通變量的指針,我們往往不進行加減運算,雖然編譯器並不會報錯,但這樣做沒有意義,因爲不知道它後面指向的是什麼數據。

下面的例子是一個反面教材,警告讀者不要嘗試通過指針獲取下一個變量的地址:

#include <stdio.h>
int main(){
    int a = 1, b = 2, c = 3;
    int *p = &c;
    int i;
    for(i=0; i<8; i++){
        printf("%d, ", *(p+i) );
    }
    return 0;
}

在 VS2010 Debug 模式下的運行結果爲:
3, -858993460, -858993460, 2, -858993460, -858993460, 1, -858993460,

可以發現,變量 a、b、c 並不挨着,它們中間還參雜了別的輔助數據。

指針變量除了可以參與加減運算,還可以參與比較運算。當對指針變量進行比較運算時,比較的是指針變量本身的值,也就是數據的地址。如果地址相等,那麼兩個指針就指向同一份數據,否則就指向不同的數據。

上面的代碼(第一個例子)在比較 pa 和 paa 的值時,pa 已經指向了 a 的上一份數據,所以它們不相等。而 a 的上一份數據又不知道是什麼,所以會導致 printf() 輸出一個沒有意義的數,這正好印證了上面的觀點,不要對指向普通變量的指針進行加減運算。

另外需要說明的是,不能對指針變量進行乘法、除法、取餘等其他運算,除了會發生語法錯誤,也沒有實際的含義。

數組指針(指向數組的指針)和指針數組(數組的元素都是指針)

數組指針

數組(Array)是一系列具有相同類型的數據的集合,每一份數據叫做一個數組元素(Element)。數組中的所有元素在內存中是連續排列的,整個數組佔用的是一塊內存。

以int arr[] = { 99, 15, 100, 888, 252 };爲例,該數組在內存中的分佈如下圖所示:

1-160G11I004A1

定義數組時,要給出數組名和數組長度,數組名可以認爲是一個指針,它指向數組的第 0 個元素。在C語言中,我們將第 0 個元素的地址稱爲數組的首地址。以上面的數組爲例,下圖是 arr 的指向:

1-160G11I410445

注意: 數組名的本意是表示整個數組,也就是表示多份數據的集合,但在使用過程中經常會轉換爲指向數組第 0 個元素的指針,所以上面使用了“認爲”一詞,表示數組名和數組首地址並不總是等價。初學者可以暫時忽略這個細節,把數組名當做指向第 0 個元素的指針使用即可.
下面的例子演示瞭如何以指針的方式遍歷數組元素:

#include <stdio.h>
int main(){
    int arr[] = { 99, 15, 100, 888, 252 };
    int len = sizeof(arr) / sizeof(int);  //求數組長度
    int i;
    for(i=0; i<len; i++){
        printf("%d  ", *(arr+i) );  //*(arr+i)等價於arr[i]
    }
    printf("\n");
    return 0;
}

運行結果:
99 15 100 888 252

第 5 行代碼用來求數組的長度,sizeof(arr) 會獲得整個數組所佔用的字節數,sizeof(int) 會獲得一個數組元素所佔用的字節數,它們相除的結果就是數組包含的元素個數,也即數組長度。

第 8 行代碼中我們使用了*(arr+i)這個表達式,arr 是數組名,指向數組的第 0 個元素,表示數組首地址, arr+i 指向數組的第 i 個元素,(arr+i) 表示取第 i 個元素的數據,它等價於 arr[i]。
arr 是int
類型的指針,每次加 1 時它自身的值會增加 sizeof(int),加 i 時自身的值會增加 sizeof(int) * i,這在指針變量的運算中已經進行了詳細講解。
我們也可以定義一個指向數組的指針,例如:

int arr[] = { 99, 15, 100, 888, 252 };
int *p = arr;

arr 本身就是一個指針,可以直接賦值給指針變量 p。arr 是數組第 0 個元素的地址,所以int *p = arr;也可以寫作int *p = &arr[0];。
也就是說,arr、p、&arr[0] 這三種寫法都是等價的,它們都指向數組第 0 個元素,或者說指向數組的開頭。
再強調一遍,“arr 本身就是一個指針”這種表述並不準確,嚴格來說應該是“arr 被轉換成了一個指針”。這裏請大家先忽略這個細節.
如果一個指針指向了數組,我們就稱它爲數組指針(Array Pointer)。

數組指針指向的是數組中的一個具體元素,而不是整個數組,所以數組指針的類型和數組元素的類型有關,上面的例子中,p 指向的數組元素是 int 類型,所以 p 的類型必須也是int *。

反過來想,p 並不知道它指向的是一個數組,p 只知道它指向的是一個整數,究竟如何使用 p 取決於程序員的編碼。

更改上面的代碼,使用數組指針來遍歷數組元素:

#include <stdio.h>
int main(){
    int arr[] = { 99, 15, 100, 888, 252 };
    int i, *p = arr, len = sizeof(arr) / sizeof(int);
    for(i=0; i<len; i++){
        printf("%d  ", *(p+i) );
    }
    printf("\n");
    return 0;
}

數組在內存中只是數組元素的簡單排列,沒有開始和結束標誌,在求數組的長度時不能使用sizeof§ / sizeof(int),因爲 p 只是一個指向 int 類型的指針,編譯器並不知道它指向的到底是一個整數還是一系列整數(數組),所以 sizeof§ 求得的是 p 這個指針變量本身所佔用的字節數,而不是整個數組佔用的字節數。

也就是說,根據數組指針不能逆推出整個數組元素的個數,以及數組從哪裏開始、到哪裏結束等信息。不像字符串,數組本身也沒有特定的結束標誌,如果不知道數組的長度,那麼就無法遍歷整個數組。

上節我們講到,對指針變量進行加法和減法運算時,是根據數據類型的長度來計算的。如果一個指針變量 p 指向了數組的開頭,那麼 p+i 就指向數組的第 i 個元素;如果 p 指向了數組的第 n 個元素,那麼 p+i 就是指向第 n+i 個元素;而不管 p 指向了數組的第幾個元素,p+1 總是指向下一個元素,p-1 也總是指向上一個元素。

更改上面的代碼,讓 p 指向數組中的第二個元素:

#include <stdio.h>
int main(){
    int arr[] = { 99, 15, 100, 888, 252 };
    int *p = &arr[2];  //也可以寫作 int *p = arr + 2;
    printf("%d, %d, %d, %d, %d\n", *(p-2), *(p-1), *p, *(p+1), *(p+2) );
    return 0;
}

運行結果:
99, 15, 100, 888, 252

引入數組指針後,我們就有兩種方案來訪問數組元素了,一種是使用下標,另外一種是使用指針。

  1. 使用下標

也就是採用 arr[i] 的形式訪問數組元素。如果 p 是指向數組 arr 的指針,那麼也可以使用 p[i] 來訪問數組元素,它等價於 arr[i]。
2) 使用指針

也就是使用 *(p+i) 的形式訪問數組元素。另外數組名本身也是指針,也可以使用 *(arr+i) 來訪問數組元素,它等價於 *(p+i)。

不管是數組名還是數組指針,都可以使用上面的兩種方式來訪問數組元素。不同的是,數組名是常量,它的值不能改變,而數組指針是變量(除非特別指明它是常量),它的值可以任意改變。也就是說,數組名只能指向數組的開頭,而數組指針可以先指向數組開頭,再指向其他元素。

更改上面的代碼,藉助自增運算符來遍歷數組元素:

#include <stdio.h>
int main(){
    int arr[] = { 99, 15, 100, 888, 252 };
    int i, *p = arr, len = sizeof(arr) / sizeof(int);
    for(i=0; i<len; i++){
        printf("%d  ", *p++ );
    }
    printf("\n");
    return 0;
}

運行結果:
99 15 100 888 252

第 6 行代碼中,*p++ 應該理解爲 *(p++),每次循環都會改變 p 的值(p++ 使得 p 自身的值增加),以使 p 指向下一個數組元素。*該語句不能寫爲 arr++,因爲 arr 是常量,而 arr++ 會改變它的值,這顯然是錯誤的

關於數組指針的謎題

假設 p 是指向數組 arr 中第 n 個元素的指針,那麼 p++、++p、(*p)++ 分別是什麼意思呢?

*p++ 等價於 *(p++),表示先取得第 n 個元素的值,再將 p 指向下一個元素,上面已經進行了詳細講解。

*++p 等價於 *(++p),會先進行 ++p 運算,使得 p 的值增加,指向下一個元素,整體上相當於 *(p+1),所以會獲得第 n+1 個數組元素的值。

(*p)++ 就非常簡單了,會先取得第 n 個元素的值,再對該元素的值加 1。假設 p 指向第 0 個元素,並且第 0 個元素的值爲 99,執行完該語句後,第 0 個元素的值就會變爲 100。

指針數組(每個元素都是指針)

如果一個數組中的所有元素保存的都是指針,那麼我們就稱它爲指針數組。指針數組的定義形式一般爲:

dataType *arrayName[length];

[ ]的優先級高於*,該定義形式應該理解爲:

dataType *(arrayName[length]);

括號裏面說明arrayName是一個數組,包含了length個元素,括號外面說明每個元素的類型爲dataType *。

除了每個元素的數據類型不同,指針數組和普通數組在其他方面都是一樣的,下面是一個簡單的例子:

#include <stdio.h>
int main(){
    int a = 16, b = 932, c = 100;
    //定義一個指針數組
    int *arr[3] = {&a, &b, &c};//也可以不指定長度,直接寫作 int *parr[]
    //定義一個指向指針數組的指針
    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;
}

運行結果:

16, 932, 100
16, 932, 100

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 個元素指向的數據。

指針數組還可以和字符串數組結合使用,請看下面的例子:

#include <stdio.h>
int main(){
    char *str[3] = {
        "c.biancheng.net",
        "C語言中文網",
        "C Language"
    };
    printf("%s\n%s\n%s\n", str[0], str[1], str[2]);
    return 0;
}

運行結果:

c.biancheng.net
C語言中文網
C Language

需要注意的是,字符數組 str 中存放的是字符串的首地址,不是字符串本身,字符串本身位於其他的內存區域,和字符數組是分開的。

也只有當指針數組中每個元素的類型都是char *時,才能像上面那樣給指針數組賦值,其他類型不行。

爲了便於理解,可以將上面的字符串數組改成下面的形式,它們都是等價的。

#include <stdio.h>
int main(){
    char *str0 = "c.biancheng.net";
    char *str1 = "C語言中文網";
    char *str2 = "C Language";
    char *str[3] = {str0, str1, str2};
    printf("%s\n%s\n%s\n", str[0], str[1], str[2]);
    return 0;
    }

字符串指針(指向字符串的指針)

C語言中沒有特定的字符串類型,我們通常是將字符串放在一個字符數組中,這在字符數組和字符串中已經進行了詳細講解,這裏不妨再來演示一下:

#include <stdio.h>
#include <string.h>
int main(){
    char str[] = "http://c.biancheng.net";
    int len = strlen(str), i;
    //直接輸出字符串
    printf("%s\n", str);
    //每次輸出一個字符
    for(i=0; i<len; i++){
        printf("%c", str[i]);
    }
    printf("\n");
    return 0;
}

運行結果:

http://c.biancheng.net
http://c.biancheng.net

字符數組歸根結底還是一個數組,上節講到的關於指針和數組的規則同樣也適用於字符數組。更改上面的代碼,使用指針的方式來輸出字符串:

#include <stdio.h>
#include <string.h>
int main(){
    char str[] = "http://c.biancheng.net";
    char *pstr = str;
    int len = strlen(str), i;
    //使用*(pstr+i)
    for(i=0; i<len; i++){
        printf("%c", *(pstr+i));
    }
    printf("\n");
    //使用pstr[i]
    for(i=0; i<len; i++){
        printf("%c", pstr[i]);
    }
    printf("\n");
    //使用*(str+i)
    for(i=0; i<len; i++){
        printf("%c", *(str+i));
    }
    printf("\n");
    return 0;
}

運行結果:

http://c.biancheng.net
http://c.biancheng.net
http://c.biancheng.net

除了字符數組,C語言還支持另外一種表示字符串的方法,就是直接使用一個指針指向字符串,例如:

char *str = "http://c.biancheng.net";
或者:
char *str; str = "http://c.biancheng.net";

字符串中的所有字符在內存中是連續排列的,str 指向的是字符串的第 0 個字符;我們通常將第 0 個字符的地址稱爲字符串的首地址。字符串中每個字符的類型都是char,所以 str 的類型也必須是char *。

下面的例子演示瞭如何輸出這種字符串:

#include <stdio.h>
#include <string.h>
int main(){
    char *str = "http://c.biancheng.net";
    int len = strlen(str), i;
   
    //直接輸出字符串
    printf("%s\n", str);
    //使用*(str+i)
    for(i=0; i<len; i++){
        printf("%c", *(str+i));
    }
    printf("\n");
    //使用str[i]
    for(i=0; i<len; i++){
        printf("%c", str[i]);
    }
    printf("\n");
    return 0;
}

運行結果:

http://c.biancheng.net
http://c.biancheng.net
http://c.biancheng.net

這一切看起來和字符數組是多麼地相似,它們都可以使用%s輸出整個字符串,都可以使用*或[ ]獲取單個字符,這兩種表示字符串的方式是不是就沒有區別了呢?

有!它們最根本的區別是在內存中的存儲區域不一樣,字符數組存儲在全局數據區或棧區,第二種形式的字符串存儲在常量區。全局數據區和棧區的字符串(也包括其他數據)有讀取和寫入的權限,而常量區的字符串(也包括其他數據)只有讀取權限,沒有寫入權限。
關於全局數據區、棧區、常量區以及其他的內存分區,將在《C語言和內存》專題中詳細講解,相信你必將有所頓悟,從根本上理解C語言。
內存權限的不同導致的一個明顯結果就是,字符數組在定義後可以讀取和修改每個字符,而對於第二種形式的字符串,一旦被定義後就只能讀取不能修改,任何對它的賦值都是錯誤的。

我們將第二種形式的字符串稱爲字符串常量,意思很明顯,常量只能讀取不能寫入。請看下面的演示:

#include <stdio.h>
int main(){
    char *str = "Hello World!";
    str = "I love C!";  //正確
    str[3] = 'P';  //錯誤
    return 0;
}

這段代碼能夠正常編譯和鏈接,但在運行時會出現段錯誤(Segment Fault)或者寫入位置錯誤。

第4行代碼是正確的,可以更改指針變量本身的指向;第5行代碼是錯誤的,不能修改字符串中的字符。
到底使用字符數組還是字符串常量
在編程過程中如果只涉及到對字符串的讀取,那麼字符數組和字符串常量都能夠滿足要求;如果有寫入(修改)操作,那麼只能使用字符數組,不能使用字符串常量。

獲取用戶輸入的字符串就是一個典型的寫入操作,只能使用字符數組,不能使用字符串常量,請看下面的代碼:

#include <stdio.h>
int main(){
    char str[30];
    gets(str);
    printf("%s\n", str);
    return 0;
}

運行結果:
C C++ Java Python JavaScript
C C++ Java Python JavaScript

最後我們來總結一下,C語言有兩種表示字符串的方法,一種是字符數組,另一種是字符串常量,它們在內存中的存儲位置不同,使得字符數組可以讀取和修改,而字符串常量只能讀取不能修改。

指針變量作爲函數參數

在C語言中,函數的參數不僅可以是整數、小數、字符等具體的數據,還可以是指向它們的指針。用指針變量作函數參數可以將函數外部的地址傳遞到函數內部,使得在函數內部可以操作函數外部的數據,並且這些數據不會隨着函數的結束而被銷燬。

像數組、字符串、動態分配的內存等都是一系列數據的集合,沒有辦法通過一個參數全部傳入函數內部,只能傳遞它們的指針,在函數內部通過指針來影響這些數據集合。

基本類型數據的操作藉助指針

有的時候,對於整數、小數、字符等基本類型數據的操作也必須要藉助指針,一個典型的例子就是交換兩個變量的值。

有些初學者可能會使用下面的方法來交換兩個變量的值:

#include <stdio.h>
void swap(int a, int b){
    int temp;  //臨時變量
    temp = a;
    a = b;
    b = temp;
}
int main(){
    int a = 66, b = 99;
    swap(a, b);
    printf("a = %d, b = %d\n", a, b);
    return 0;
}

運行結果:
a = 66, b = 99

從結果可以看出,a、b 的值並沒有發生改變,交換失敗。這是因爲 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 = 66, b = 99;
    swap(&a, &b);
    printf("a = %d, b = %d\n", a, b);
    return 0;
}

運行結果:
a = 99, b = 66

調用 swap() 函數時,將變量 a、b 的地址分別賦值給 p1、p2,這樣 *p1、*p2 代表的就是變量 a、b 本身,交換 *p1、*p2 的值也就是交換 a、b 的值。函數運行結束後雖然會將 p1、p2 銷燬,但它對外部 a、b 造成的影響是“持久化”的,不會隨着函數的結束而“恢復原樣”。

需要注意的是臨時變量 temp,它的作用特別重要,因爲執行*p1 = *p2;語句後 a 的值會被 b 的值覆蓋,如果不先將 a 的值保存起來以後就找不到了。

用數組作函數參數

數組是一系列數據的集合,無法通過參數將它們一次性傳遞到函數內部,如果希望在函數內部操作數組,必須傳遞數組指針。下面的例子定義了一個函數 max(),用來查找數組中值最大的元素:

#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("Max value is %d!\n", max(nums, len));
    return 0;
}

運行結果:

12 55 30 8 93 27↙
Max value is 93!

參數 intArr 僅僅是一個數組指針,在函數內部無法通過這個指針獲得數組長度,必須將數組長度作爲函數參數傳遞到函數內部。數組 nums 的每個元素都是整數,scanf() 在讀取用戶輸入的整數時,要求給出存儲它的內存的地址,nums+i就是第 i 個數組元素的地址。

用數組做函數參數時,參數也能夠以“真正”的數組形式給出。例如對於上面的 max() 函數,它的參數可以寫成下面的形式:

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

int intArr[6]好像定義了一個擁有 6 個元素的數組,調用 max() 時可以將數組的所有元素“一股腦”傳遞進來。

讀者也可以省略數組長度,把形參簡寫爲下面的形式:

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 intArr[]雖然定義了一個數組,但沒有指定數組長度,好像可以接受任意長度的數組。

實際上這兩種形式的數組定義都是假象,不管是int intArr[6]還是int intArr[]都不會創建一個數組出來,編譯器也不會爲它們分配內存,實際的數組是不存在的,它們最終還是會轉換爲int *intArr這樣的指針。這就意味着,兩種形式都不能將數組的所有元素“一股腦”傳遞進來,大家還得規規矩矩使用數組指針。

int intArr[6]這種形式只能說明函數期望用戶傳遞的數組有 6 個元素,並不意味着數組只能有 6 個元素,真正傳遞的數組可以有少於或多於 6 個的元素。

需要強調的是,不管使用哪種方式傳遞數組,都不能在函數內部求得數組長度,因爲 intArr 僅僅是一個指針,而不是真正的數組,所以必須要額外增加一個參數來傳遞數組長度。

C語言爲什麼不允許直接傳遞數組的所有元素,而必須傳遞數組指針呢?

參數的傳遞本質上是一次賦值的過程,賦值就是對內存進行拷貝。所謂內存拷貝,是指將一塊內存上的數據複製到另一塊內存上。

對於像 int、float、char 等基本類型的數據,它們佔用的內存往往只有幾個字節,對它們進行內存拷貝非常快速。而數組是一系列數據的集合,數據的數量沒有限制,可能很少,也可能成千上萬,對它們進行內存拷貝有可能是一個漫長的過程,會嚴重拖慢程序的效率,爲了防止技藝不佳的程序員寫出低效的代碼,C語言沒有從語法上支持數據集合的直接賦值。

除了C語言,C++、Java、Python 等其它語言也禁止對大塊內存進行拷貝,在底層都使用類似指針的方式來實現。

指針作爲函數返回值

C語言允許函數的返回值是一個指針(地址),我們將這樣的函數稱爲指針函數。下面的例子定義了一個函數 strlong(),用來返回兩個字符串中較長的一個:

#include <stdio.h>
#include <string.h>
char *strlong(char *str1, char *str2){
    if(strlen(str1) >= strlen(str2)){
        return str1;
    }else{
        return str2;
    }
}
int main(){
    char str1[30], str2[30], *str;
    gets(str1);
    gets(str2);
    str = strlong(str1, str2);
    printf("Longer string: %s\n", str);
    return 0;
}

運行結果:

C Language↙
c.biancheng.net↙
Longer string: c.biancheng.net

用指針作爲函數返回值時需要注意的一點是,函數運行結束後會銷燬在它內部定義的所有局部數據,包括局部變量、局部數組和形式參數,函數返回的指針請儘量不要指向這些數據,C語言沒有任何機制來保證這些數據會一直有效,它們在後續使用過程中可能會引發運行時錯誤。請看下面的例子:

#include <stdio.h>
int *func(){
    int n = 100;
    return &n;
}
int main(){
    int *p = func(), n;
    n = *p;
    printf("value = %d\n", n);
    return 0;
}

運行結果:
value = 100

n 是 func() 內部的局部變量,func() 返回了指向 n 的指針,根據上面的觀點,func() 運行結束後 n 將被銷燬,使用 *p 應該獲取不到 n 的值。但是從運行結果來看,我們的推理好像是錯誤的,func() 運行結束後 *p 依然可以獲取局部變量 n 的值,這個上面的觀點不是相悖嗎?

爲了進一步看清問題的本質,不妨將上面的代碼稍作修改,在第9~10行之間增加一個函數調用,看看會有什麼效果:

#include <stdio.h>
int *func(){
    int n = 100;
    return &n;
}
int main(){
    int *p = func(), n;
    printf("c.biancheng.net\n");
    n = *p;
    printf("value = %d\n", n);
    return 0;
}

運行結果:

c.biancheng.net
value = -2

可以看到,現在 p 指向的數據已經不是原來 n 的值了,它變成了一個毫無意義的甚至有些怪異的值。與前面的代碼相比,該段代碼僅僅是在 *p 之前增加了一個函數調用,這一細節的不同卻導致運行結果有天壤之別,究竟是爲什麼呢?

前面我們說函數運行結束後會銷燬所有的局部數據,這個觀點並沒錯,大部分C語言教材也都強調了這一點。但是,這裏所謂的銷燬並不是將局部數據所佔用的內存全部抹掉,而是程序放棄對它的使用權限,棄之不理,後面的代碼可以隨意使用這塊內存。對於上面的兩個例子,func() 運行結束後 n 的內存依然保持原樣,值還是 100,如果使用及時也能夠得到正確的數據,如果有其它函數被調用就會覆蓋這塊內存,得到的數據就失去了意義
關於函數調用的原理以及函數如何佔用內存的更多細節,我們將在《C語言和內存》專題中深入探討,相信你必將有所頓悟,解開心中的謎團。
第一個例子在調用其他函數之前使用 *p 搶先獲得了 n 的值並將它保存起來,第二個例子顯然沒有抓住機會,有其他函數被調用後才使用 *p 獲取數據,這個時候已經晚了,內存已經被後來的函數覆蓋了,而覆蓋它的究竟是一份什麼樣的數據我們無從推斷(一般是一個沒有意義甚至有些怪異的值)。

函數指針(指向函數的指針)

一個函數總是佔用一段連續的內存區域,函數名在表達式中有時也會被轉換爲該函數所在內存區域的首地址,這和數組名非常類似。我們可以把函數的這個首地址(或稱入口地址)賦予一個指針變量,使指針變量指向函數所在的內存區域,然後通過指針變量就可以找到並調用該函數。這種指針就是函數指針。

函數指針的定義形式爲:

returnType (*pointerName)(param list);

returnType 爲函數返回值類型,pointerNmae 爲指針名稱,param list 爲函數參數列表。參數列表中可以同時給出參數的類型和名稱,也可以只給出參數的類型,省略參數的名稱,這一點和函數原型非常類似。

注意( )的優先級高於*,第一個括號不能省略,如果寫作returnType *pointerName(param list);就成了函數原型,它表明函數的返回值類型爲returnType *。

【實例1】用指針來實現對函數的調用。

#include <stdio.h>
//返回兩個數中較大的一個
int max(int a, int b){
    return a>b ? a : b;
}
int main(){
    int x, y, maxval;
    //定義函數指針
    int (*pmax)(int, int) = max;  //也可以寫作int (*pmax)(int a, int b)
    printf("Input two numbers:");
    scanf("%d %d", &x, &y);
    maxval = (*pmax)(x, y);
    printf("Max value: %d\n", maxval);
    return 0;
}

運行結果:

Input two numbers:10 50↙
Max value: 50

【實例2】用指針來實現對函數的複雜調用。

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
//函數加減乘除
double Add( double x, double y ) { return x + y; }
double Sub( double x, double y ) { return x - y; }
double Mul( double x, double y ) { return x * y; }
double Div( double x, double y ) { return x / y; }

//typedef double func_t( double, double ); // The functions' type is now named func_t.
//func_t *funcTable[5] = { Add, Sub, Mul, Div, pow };

// 包含5個指針的指針數組指向一個獲取兩個 double 參數的方法
// 而且返回值是double:
double (*funcTable[5])(double, double)
= { Add, Sub, Mul, Div, pow }; // 初始化 list.注意其中pow是math.h中提供的計算冪次方的函數

// 用來輸出的指針數組
char *msgTable[5] = { "Sum", "Difference", "Product", "Quotient", "Power" };
int main( )
{
    int i;
    double x = 0, y = 0;
    printf( "Enter two operands for some arithmetic:\n" );
    if ( scanf( "%lf %lf", &x, &y ) != 2 )
        printf( "Invalid input.\n" );
    for ( i = 0; i < 5; ++i )
        printf( "%10s: %6.2f\n", msgTable[i], funcTable[i](x, y) );
    return 0;
}

運行結果:

Enter two operands for some arithmetic:
2 4
       Sum:   6.00
Difference:  -2.00
   Product:   8.00
  Quotient:   0.50
     Power:  16.00

表達式funcTablei調用其地址存儲在指針中的函數funcTable[i]。數組名和下標不需要用括號括起來,因爲函數調用操作符()和下標操作符[]都是最高的優先級和從左到右的結合性

第 14 行代碼對函數進行了調用。pmax 是一個函數指針,在前面加 * 就表示對它指向的函數進行調用。注意( )的優先級高於*,第一個括號不能省略。

空指針NULL和void指針(void *)

NULL

定義了預處理宏NULL (在<stdio.h> 和其它幾個頭文件中) 爲空指針常數, 通常是0 或者((void *)0)。希望區別整數0 和空指針0 的人可以在需要空指針的地方使用NULL。使用NULL 只是一種風格習慣; 預處理器把所有的NULL 都還原回0, 而編譯還是依照上文的描述處理指針上下文的0。特別是, 在函數調用的參數裏, NULL之前(正如在0 之前) 的類型轉換還是需要。

NULL表示內存位置0,NULL指針並不指向任何對象。因此除非是用於賦值或比較運算,出於其他任何目的使用NULL指針都是非法的。

標準定義了NULL指針,它作爲特殊的指針變量,表示不指向任何東西。之所以選擇0這個值是因爲一種源代碼約定,就機器而言,NULL指針的實際值可能與此不同,在這種情況下,編譯器將負責0值和內部值之間的翻譯轉換。NULL指針十分有用,因爲他給了程序員一種方法,表示某個特定的指針目前並未指向任何東西

空指針總是不等於指向對象或函數的任何有效指針。因爲這個原因,返回指針類型的函數通常使用空指針來指示故障條件。
一個例子是標準函數fopen(),如果它沒有打開a,它將返回一個空指針,文件在指定模式下:

#include <stdio.h>
/* ... */
FILE *fp = fopen( "demo.txt", "r" );
if ( fp == NULL )
{
// Error: unable to open the file demo.txt for reading.
}

void*

在編程時經常需要一種通用指針,可以轉換爲任意其它類型的指針,任意其它類型的指針也可以轉換爲通用指針。最初C沒有void 通用指針類型,是把char當通用指針,需要轉換時就用轉換運算符()。void 指針與其它類型的指針之間可以隱式轉換,而不必用類型轉換符。void指針不能直接Dereference(訪問),而必須轉換成別的類型的指針才能做Dereference。而不能用void類型來定義變量(類型暫時不確定的變量),因爲編譯器不知道該分配幾個字節給變量。

按照ANSI標準,不能對void指針進行算法(如++, +=)操作。ANSI標準之所以這樣認爲,是因爲它堅持:進行算法操作的指針必須是確定知道其指向數據類型大小的。也就是說必須知道內存目的地址的確切值。如果函數的參數可以是任意類型指針,那麼應聲明其參數爲void *。如內存操作函數的原型,

void *memcpy(void *dest, const void *src, size_tlen); void *memset(void *buffer, intc, size_t num )。

指向void的指針,或簡稱爲void指針,是類型爲void *的指針。因爲沒有對象的類型爲void,類型void *用作通用指針類型。在其他空指針可以表示任何對象的地址,但不能表示其類型。訪問一個在內存中,必須始終將空指針轉換爲適當的對象指針。

要聲明可以使用不同類型的指針參數調用的函數,可以這樣做將適當的參數聲明爲指向void的指針。當您調用這樣一個函數時編譯器隱式地將對象指針參數轉換爲空指針。一個常見的例子,標準函數memset()是在頭文件string.h中聲明的原型:

void *memset( void *s, int c, size_t n );

指針與二維數組

二維數組在概念上是二維的,有行和列,但在內存中所有的數組元素都是連續排列的,它們之間沒有“縫隙”。以下面的二維數組 a 爲例:

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

從概念上理解,a 的分佈像一個矩陣:

a[0]----> 0   1   2   3
a[1]---->4   5   6   7
a[2]---->8   9  10  11

但在內存中,a 的分佈是一維線性的,整個數組佔用一塊連續的內存:

1-160H11I6303X

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]。

假設數組 a 中第 0 個元素的地址爲 1000,那麼每個一維數組的首地址如下圖所示:

qwfd423vsds

爲了更好的理解指針和二維數組的關係,我們先來定義一個指向 a 的指針變量 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會使得指針指向數組的上一行。

數組名 a 在表達式中也會被轉換爲和 p 等價的指針!

下面我們就來探索一下如何使用指針 p 來訪問二維數組中的每個元素。按照上面的定義:

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

  2. *(p+1)表示取地址上的數據,也就是整個第 1 行數據。注意是一行數據,是多個數據,不是第 1 行中的第 0 個元素,下面的運行結果有力地證明了這一點:

#include <stdio.h>
int main(){
    int a[3][4] = { {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11} };
    int (*p)[4] = a;
    printf("%d\n", sizeof(*(p+1)));
    return 0;
}

運行結果:
16

  1. *(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)

【實例】使用指針遍歷二維數組。

#include <stdio.h>
int main(){
    int a[3][4]={0,1,2,3,4,5,6,7,8,9,10,11};
    int(*p)[4];
    int i,j;
    p=a;
    for(i=0; i<3; i++){
        for(j=0; j<4; j++) printf("%2d  ",*(*(p+i)+j));
        printf("\n");
    }
    return 0;
}

運行結果:

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

指針數組和二維數組指針的區別
指針數組和二維數組指針在定義時非常相似,只是括號的位置不同:

int *(p1[5]);  //指針數組,可以去掉括號直接寫作 int *p1[5];
int (*p2)[5];  //二維數組指針,不能去掉括號
printf("p1=%d p2=%d",sizeof(p1),sizeof(p2));

指針數組和二維數組指針有着本質上的區別:指針數組是一個數組,只是每個元素保存的都是指針,以上面的 p1 爲例,在32位環境下它佔用 4×5 = 20 個字節的內存。二維數組指針是一個指針,它指向一個二維數組,以上面的 p2 爲例,它佔用 4 個字節的內存。

p1=20 p2=4

指針的總結

指針(Pointer)就是內存的地址,C語言允許用一個變量來存放指針,這種變量稱爲指針變量。指針變量可以存放基本類型數據的地址,也可以存放數組、函數以及其他指針變量的地址。

程序在運行過程中需要的是數據和指令的地址,變量名、函數名、字符串名和數組名在本質上是一樣的,它們都是地址的助記符:在編寫代碼的過程中,我們認爲變量名錶示的是數據本身,而函數名、字符串名和數組名錶示的是代碼塊或數據塊的首地址;程序被編譯和鏈接後,這些名字都會消失,取而代之的是它們對應的地址。

定 義 含 義
int *p; p 可以指向 int 類型的數據,也可以指向類似 int arr[n] 的數組。
int **p; p 爲二級指針,指向 int * 類型的數據。
int*p[] p 爲指針數組。[ ] 的優先級高於 *,所以應該理解爲 int *(p[n]);
int (*p)[n]; p 爲二維數組指針。
int *p(); p 是一個函數,它的返回值類型爲 int *。
int (*p)(); p 是一個函數指針,指向原型爲 int func() 的函數。
  1. 指針變量可以進行加減運算,例如p++、p+i、p-=i。指針變量的加減運算並不是簡單的加上或減去一個整數,而是跟指針指向的數據類型有關。

  2. 給指針變量賦值時,要將一份數據的地址賦給它,不能直接賦給一個整數,例如int *p = 1000;是沒有意義的,使用過程中一般會導致程序崩潰。

p=&a; (將變量a的地址賦值給p)
p=array; (將數組array首元素地址賦值給p)
p=&array[i]; (將數組array的第i個元素地址賦值給p)
p=(a+i);/p=(p1+i);(前面表示將二維數組a的第i行的首地址賦值給p/後面表示將int(*p1)[5]中的(p1+i)的地址賦值給p)
p=p1[i];(p1是指向n個一維數組的指針,int (*p1)[5],將第 i 個一維數組的首地址賦值給p)
p=max;(max是已經定義的函數,將max入口地址賦值給p)
p1=p2;(將指針變量p2賦值給p1)

  1. 使用指針變量之前一定要初始化,否則就不能確定指針指向哪裏,如果它指向的內存沒有使用權限,程序就崩潰了。對於暫時沒有指向的指針,建議賦值NULL。

  2. 兩個指針變量可以相減。如果兩個指針變量指向同一個數組中的某個元素,那麼相減的結果就是兩個指針之間相差的元素個數

  3. 數組也是有類型的,數組名的本意是表示一組類型相同的數據。在定義數組時,或者和 sizeof、& 運算符一起使用時數組名才表示整個數組,表達式中的數組名會被轉換爲一個指向數組的指針。

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