這段時間在看 Linux 內核,深覺 C 語言功底不扎實,很多代碼都看不太懂,深入學習鞏固 C 語言的知識很有必要。先從指針開始。
一、什么是指針
C語言里,變量存放在內存中,而內存其實就是一組有序字節組成的數組,每個字節有唯一的內存地址。CPU 通過內存尋址對存儲在內存中的某個指定數據對象的地址進行定位。這里,數據對象是指存儲在內存中的一個指定數據類型的數值或字符串,它們都有一個自己的地址,而指針便是保存這個地址的變量。也就是說:指針是一種保存變量地址的變量。
前面已經提到內存其實就是一組有序字節組成的數組,數組中,每個字節大大小固定,都是 8bit。對這些連續的字節從 0 開始進行編號,每個字節都有唯一的一個編號,這個編號就是內存地址。示意如下圖:

這是一個 4GB 的內存,可以存放 2^32 個字節的數據。左側的連續的十六進制編號就是內存地址,每個內存地址對應一個字節的內存空間。而指針變量保存的就是這個編號,也即內存地址。
二、為什么要使用指針
在C語言中,指針的使用非常廣泛,因為使用指針往往可以生成更高效、更緊湊的代碼。總的來說,使用指針有如下好處:
1)指針的使用使得不同區域的代碼可以輕易的共享內存數據,這樣可以使程序更為快速高效;
2)C語言中一些復雜的數據結構往往需要使用指針來構建,如鏈表、二叉樹等;
3)C語言是傳值調用,而有些操作傳值調用是無法完成的,如通過被調函數修改調用函數的對象,但是這種操作可以由指針來完成,而且並不違背傳值調用。
三、如何聲明一個指針
3.1 聲明並初始化一個指針
指針其實就是一個變量,指針的聲明方式與一般的變量聲明方式沒太大區別:
int *p; // 聲明一個 int 類型的指針 p char *p // 聲明一個 char 類型的指針 p int *arr[10] // 聲明一個指針數組,該數組有10個元素,其中每個元素都是一個指向 int 類型對象的指針 int (*arr)[10] // 聲明一個數組指針,該指針指向一個 int 類型的一維數組 int **p; // 聲明一個指針 p ,該指針指向一個 int 類型的指針
指針的聲明比普通變量的聲明多了一個一元運算符 “*”。運算符 “*” 是間接尋址或者間接引用運算符。當它作用於指針時,將訪問指針所指向的對象。在上述的聲明中: p 是一個指針,保存着一個地址,該地址指向內存中的一個變量; *p 則會訪問這個地址所指向的變量。
聲明一個指針變量並不會自動分配任何內存。在對指針進行間接訪問之前,指針必須進行初始化:或是使他指向現有的內存,或者給他動態分配內存,否則我們並不知道指針指向哪兒,這將是一個很嚴重的問題,稍后會討論這個問題。初始化操作如下:
/* 方法1:使指針指向現有的內存 */ int x = 1; int *p = &x; // 指針 p 被初始化,指向變量 x ,其中取地址符 & 用於產生操作數內存地址 /* 方法2:動態分配內存給指針 */ int *p; p = (int *)malloc(sizeof(int) * 10); // malloc 函數用於動態分配內存 free(p); // free 函數用於釋放一塊已經分配的內存,常與 malloc 函數一起使用,要使用這兩個函數需要頭文件 stdlib.h
指針的初始化實際上就是給指針一個合法的地址,讓程序能夠清楚地知道指針指向哪兒。
3.2 未初始化和非法的指針
如果一個指針沒有被初始化,那么程序就不知道它指向哪里。它可能指向一個非法地址,這時,程序會報錯,在 Linux 上,錯誤類型是 Segmentation fault(core dumped),提醒我們段違例或內存錯誤。它也可能指向一個合法地址,實際上,這種情況更嚴重,你的程序或許能正常運行,但是這個沒有被初始化的指針所指向的那個位置的值將會被修改,而你並無意去修改它。用一個例子簡單的演示一下:
#include "stdio.h" int main(){ int *p; *p = 1; printf("%d\n",*p); return 0; }
這個程序可以編譯通過,但是運行的話會報錯,報錯信息如下:

要想使這個程序運行起來,需要先對指針 p 進行初始化:
#include "stdio.h" int main(){ int x = 1; int *p = &x; printf("%d\n",*p); *p = 2; printf("%d\n",*p); return 0; }
這段代碼的輸出結果如下:
可以看到,對指針進行初始化后,便可以正常對指針進行賦值了。
3.3 NULL指針
NULL 指針是一個特殊的指針變量,表示不指向任何東西。可以通過給一個指針賦一個零值來生成一個 NULL 指針。
#include "stdio.h" int main(){ int *p = NULL; printf("p的地址為%d\n",p); return 0; } /*************** * 程序輸出: * p的地址為0 ***************/
可以看到指針指向內存地址0。在大多數的操作系統上,程序不允許訪問地址為 0 的內存,因為該內存是為操作系統保留的。但是,內存地址 0 有一個特別重要的意義,它表明改指針不指向一個可訪問的內存位置。
四、指針的運算
C 指針的算術運算只限於兩種形式:
1) 指針 +/- 整數 :
可以對指針變量 p 進行 p++、p--、p + i 等操作,所得結果也是一個指針,只是指針所指向的內存地址相比於 p 所指的內存地址前進或者后退了 i 個操作數。用一張圖來說明一下:

在上圖中,10000000等是內存地址的十六進制表示(數值是假定的),p 是一個 int 類型的指針,指向內存地址 0x10000008 處。則 p++ 將指向與 p 相鄰的下一個內存地址,由於 int 型數據占 4 個字節,因此 p++ 所指的內存地址為 1000000b。其余類推。不過要注意的是,這種運算並不會改變指針變量 p 自身的地址,只是改變了它所指向的地址。舉個例子:
2)指針 - 指針
只有當兩個指針都指向同一個數組中的元素時,才允許從一個指針減去另一個指針。兩個指針相減的結果的類型是 ptrdiff_t,它是一種有符號整數類型。減法運算的值是兩個指針在內存中的距離(以數組元素的長度為單位,而不是以字節為單位),因為減法運算的結果將除以數組元素類型的長度。舉個例子:
#include "stdio.h" int main(){ int a[10] = {1,2,3,4,5,6,7,8,9,0}; int sub; int *p1 = &a[2]; int *p2 = &a[8]; sub = p2-p1; printf("%d\n",sub); // 輸出結果為 6 return 0; }
五、指針與數組
在C語言中,指針與數組之間的關系十分密切。實際上,許多可以用數組完成的工作都可以使用指針來完成。一般來說,用指針編寫的程序比用數組編寫的程序執行速度快,但另一方面,用指針實現的程序理解起來稍微困難一些。
5.1 指針與數組的關系
我們先聲明一個數組:
int a[10]; // 聲明一個int類型的數組,這個數組有10個元素
我們可以用 a[0]、a[1]、...、a[9] 來表示這個數組中的10個元素,這10個元素是存儲在一段連續相鄰的內存區域中的。
接下來,我們再聲明一個指針:
int *p; // 聲明一個int類型的指針變量
p 是一個指針變量,指向內存中的一個區域。如果我們對指針 p 做如下的初始化:
p = &a[0]; // 對指針進行初始化,p將指向數組 a 的第 1 個元素 a[0]
我們知道,對指針進行自增操作會讓指針指向與當前元素相鄰的下一個元素,即 *(p + 1) 將指向 a[1] ;同樣的, *(p + i) 將指向 a[i] 。因此,我們可以使用該指針來遍歷數組 a[10] 的所有元素。可以看到,數組下標與指針運算之間的關系是一一對應的。而根據定義,數組類型的變量或表達式的值是該數組第 1 個元素的地址,且數組名所代表的的就是該數組第 1 個元素的地址,故,上述賦值語句可以直接寫成:
p = a; // a 為數組名,代表該數組最開始的一個元素的地址
很顯然,一個通過數組和下標實現的表達式可以等價地通過指針及其偏移量來實現,這就是數組和指針的互通之處。但有一點要明確的是,數組和指針並不是完全等價,指針是一個變量,而數組名不是變量,它數組中第 1 個元素的地址,數組可以看做是一個用於保存變量的容器。更直接的方法,我們可以直接看二者的地址,並不一樣:
#include "stdio.h" int main(){ int x[10] = {1,2,3,4,5,6,7,8,9,0}; int *p = x; printf("x的地址為:%p\n",x); printf("x[0]的地址為:%p\n",&x[0]); printf("p的地址為:%p\n",&p); // 打印指針 p 的地址,並不是指針所指向的地方的地址 p += 2; printf("*(p+2)的值為:%d\n",*p); // 輸出結果為 3,*(p+2)指向了 x[2] return 0; }
結果如下:

可以看到, x 的值與 x[0] 的地址是一樣的,也就是說數組名即為數組中第 1 個元素的地址。實際上,打印 &x 后發現,x 的地址也是這個值。而 x 的地址與指針變量 p 的地址是不一樣的。故而數組和指針並不能完全等價。
(筆者注:上述輸出結果是在 centos7 64bit 的環境下使用 gcc 編譯器得到的,可以看到地址是一個12位的十六進制數,轉換成二進制是48位,也就是說尋址空間有 256TB,但是筆者的電腦只有 8GB 內存,猜測是不是由於 linux 系統開啟了內存分頁機制,這里尋址的是虛擬地址?另外,在Windows下使用 vs2015 編譯運行的話,則輸出結果是一個 8位的十六進制數,也就是32位二進制,尋址空間為 4GB)
5.2 指針數組
指針是一個變量,而數組是用於存儲變量的容器,因此,指針也可以像其他變量一樣存儲在數組中,也就是指針數組。 指針數組是一個數組,數組中的每一個元素都是指針。聲明一個指針數組的方法如下:
int *p[10]; // 聲明一個指針數組,該數組有10個元素,其中每個元素都是一個指向int類型的指針
在上述聲明中,由於 [] 的優先級比 * 高,故 p 先與 [] 結合,成為一個數組 p[];再由 int * 指明這是一個 int 類型的指針數組,數組中的元素都是 int 類型的指針。數組的第 i 個元素是 *p[i],而 p[i] 是一個指針。由於指針數組中存放着多個指針,操作靈活,在一些需要操作大量數據的程序中使用,可以使程序更靈活快速。
5.3 數組指針
數組指針是一個指針,它指向一個數組。聲明一個數組指針的方法如下:
int (*p)[10]; // 聲明一個數組指針 p ,該指針指向一個數組
由於 () 的優先級最高,所以 p 是一個指針,指向一個 int 類型的一維數組,這個一維數組的長度是 10,這也是指針 p 的步長。也就是說,執行 p+1 時,p 要跨過 n 個 int 型數據的長度。數組指針與二維數組聯系密切,可以用數組指針來指向一個二維數組,如下:
#include "stdio.h" int main(){ int arr[2][3] = {1,2,3,4,5,6}; // 定義一個二維數組並初始化 int (*p)[3]; // 定義一個數組指針,指針指向一個含有3個元素的一維數組 p = arr; // 將二維數組的首地址賦給 p,此時 p 指向 arr[0] 或 &arr[0][0] printf("%d\n",(*p)[0]); // 輸出結果為 1 p++; // 對 p 進行算術運算,此時 p 將指向二維數組的下一行的首地址,即 &arr[1][0] printf("%d\n",(*p)[1]); // 輸出結果為5 return 0; }
六、指針與結構
6.1 簡單介紹一下結構
結構是一個或多個變量的集合,這些變量可能為不同的類型,為了處理的方便而將這些變量組織在一個名字之下。由於結構將一組相關的變量看做一個單元而不是各自獨立的實體,因此結構有助於組織復雜的數據,特別是在大型的程序中。聲明一個結構的方式如下:
struct message{ // 聲明一個結構 message char name[10]; // 成員 int age; int score; }; typedef struct message s_message; // 類型定義符 typedef s_message mess = {"tongye",23,83}; // 聲明一個 struct message 類型的變量 mess,並對其進行初始化
-------------------------------------------------------------------------------------------------------------- /* 另一種更簡便的聲明方法 */ typedef struct{ char name[10]; int age; int score; }message;
可以使用 結構名.成員 的方式來訪問結構中的成員,如下:
#include "stdio.h" int main(){ printf("%s\n",mess.name); // 輸出結果:tongye printf("%d\n",mess.age); // 輸出結果:23 return 0; }
6.2 結構指針
結構指針是指向結構的指針,以上面的結構為例,可以這樣定義一個結構指針:
s_message *p; // 聲明一個結構指針 p ,該指針指向一個 s_message 類型的結構 *p = &mess; // 對結構指針的初始化與普通指針一樣,也是使用取地址符 &
C語言中使用 -> 操作符來訪問結構指針的成員,舉個例子:
#include "stdio.h" typedef struct{ char name[10]; int age; int score; }message; int main(){ message mess = {"tongye",23,83}; message *p = &mess; printf("%s\n",p->name); // 輸出結果為:tongye printf("%d\n",p->score); // 輸出結果為:83 return 0; }
七、指針與函數
C語言的所有參數均是以“傳值調用”的方式進行傳遞的,這意味着函數將獲得參數值的一份拷貝。這樣,函數可以放心修改這個拷貝值,而不必擔心會修改調用程序實際傳遞給它的參數。
7.1 指針作為函數的參數
傳值調用的好處是是被調函數不會改變調用函數傳過來的值,可以放心修改。但是有時候需要被調函數回傳一個值給調用函數,這樣的話,傳值調用就無法做到。為了解決這個問題,可以使用傳指針調用。指針參數使得被調函數能夠訪問和修改主調函數中對象的值。用一個例子來說明:
#include "stdio.h" void swap1(int a,int b) // 參數為普通的 int 變量 { int temp; temp = a; a = b; b = temp; } void swap2(int *a,int *b) // 參數為指針,接受調用函數傳遞過來的變量地址作為參數,對所指地址處的內容進行操作 { int temp; // 最終結果是,地址本身並沒有改變,但是這一地址所對應的內存段中的內容發生了變化,即x,y的值發生了變化 temp = *a; *a = *b; *b = temp; } int main() { int x = 1,y = 2; swap1(x,y); // 將 x,y 的值本身作為參數傳遞給了被調函數 printf("%d %5d\n",x,y); // 輸出結果為:1 2 swap(&x,&y); // 將 x,y 的地址作為參數傳遞給了被調函數,傳遞過去的也是一個值,與傳值調用不沖突 printf("%d %5d\n",x,y); // 輸出結果為:2 1 return 0; }
7.2 指向函數的指針
在C語言中,函數本身不是變量,但是可以定義指向函數的指針,也稱作函數指針,函數指針指向函數的入口地址。這種類型的指針可以被賦值、存放在數組中、傳遞給函數以及作為函數的返回值等等。 聲明一個函數指針的方法如下:
返回值類型 (* 指針變量名)([形參列表]); int (*pointer)(int *,int *); // 聲明一個函數指針
上述代碼聲明了一個函數指針 pointer ,該指針指向一個函數,函數具有兩個 int * 類型的參數,且返回值類型為 int。下面的代碼演示了函數指針的用法:
#include "stdio.h" #include "string.h" int str_comp(const char *m,const char *n); // 聲明一個函數 str_comp,該函數有兩個 const char 類型的指針,函數的返回值為 int 類型 void comp(char *a,char *b,int (*prr)(const char *,const char*)); // 聲明一個函數 comp ,注意該函數的第三個參數,是一個函數指針 int main() { char str1[20]; // 聲明一個字符數組 char str2[20]; int (*p)(const char *,const char *) = str_comp; // 聲明並初始化一個函數指針,該指針所指向的函數有兩個 const char 類型的指針,且返回值為 int 類型 gets(str1); // 使用 gets() 函數從 I/O 讀取一行字符串 gets(str2); comp(str1,str2,p); // 函數指針 p 作為參數傳給 comp 函數 return 0; } int str_comp(const char *m,const char *n) { // 庫函數 strcmp 用於比較兩個字符串,其原型是: int strcmp(const char *s1,const char *s2); if(strcmp(m,n) == 0) return 0; else return 1; } /* 函數 comp 接受一個函數指針作為它的第三個參數 */ void comp(char *a,char *b,int (*prr)(const char *,const char*)) { if((*prr)(a,b) == 0) printf("str1 = str2\n"); else printf("str1 != str2\n"); }
這段代碼的功能是從鍵盤讀取兩行字符串(長度不超過20),判斷二者是否相等。
注意,聲明一個函數指針時,() 不能漏掉,否則:
int *p(void *,void*);
這表明 p 是一個函數,該函數返回一個指向 int 類型的指針。
參考文獻:
1)C程序設計語言(第2版)
2)C和指針
參考:
