這裏對 C 語言的指針進行比較詳細的整理總結,參考網絡上部分資料整理以下。c++
計算機中全部的數據都必須放在內存中,不一樣類型的數據佔用的字節數不同,例如 int 佔用4個字節,char 佔用1個字節。爲了正確地訪問這些數據,必須爲每一個字節都編上號碼,就像門牌號、身份證號同樣,每一個字節的編號是惟一的,根據編號能夠準確地找到某個字節。git
咱們將內存中字節的編號稱爲地址(Address)或指針(Pointer)。地址從 0 開始依次增長,對於 32 位環境,程序可以使用的內存爲 4GB,最小的地址爲 0,最大的地址爲 0XFFFFFFFF。程序員
輸出一個地址:github
int a = 100; char str[20] = "tanweime.com"; printf("%#X, %p\n", &a, str); --- 運行結果: 0XE42523AC, 0XE4252390
%#X
和 %p
表示以十六進制形式輸出,並附帶前綴0X
。a 是一個變量,用來存放整數,須要在前面加&
來得到它的地址;str 自己就表示字符串的首地址,不須要加&
。segmentfault
C語言中有一個控制符%p
,專門用來以十六進制形式輸出地址,不過 %p 的輸出格式並不統一,有的編譯器帶0x
前綴,有的不帶
C語言用變量來存儲數據,用函數來定義一段能夠重複使用的代碼,它們最終都要放到內存中才能供 CPU 使用。數組
數據和代碼都以二進制的形式存儲在內存中,計算機沒法從格式上區分某塊內存到底存儲的是數據仍是代碼。當程序被加載到內存後,操做系統會給不一樣的內存塊指定不一樣的權限,擁有讀取和執行權限的內存塊就是代碼,而擁有讀取和寫入權限(也可能只有讀取權限)的內存塊就是數據。網絡
CPU 只能經過地址來取得內存中的代碼和數據,程序在執行過程當中會告知 CPU 要執行的代碼以及要讀寫的數據的地址。若是程序不當心出錯,或者開發者有意爲之,在 CPU 要寫入數據時給它一個代碼區域的地址,就會發生內存訪問錯誤。這種內存訪問錯誤會被硬件和操做系統攔截,強制程序崩潰,程序員沒有挽救的機會。函數
CPU 訪問內存時須要的是地址,而不是變量名和函數名!變量名和函數名只是地址的一種助記符,當源文件被編譯和連接成可執行程序後,它們都會被替換成地址。編譯和連接過程的一項重要任務就是找到這些名稱所對應的地址。編碼
數據在內存中的地址也稱爲指針,若是一個變量存儲了一份數據的指針,咱們就稱它爲指針變量。spa
在C語言中,容許用一個變量來存放指針,這種變量稱爲指針變量。指針變量的值就是某份數據的地址,這樣的一份數據能夠是數組、字符串、函數,也能夠是另外的一個普通變量或指針變量。
如今假設有一個 char 類型的變量 c,它存儲了字符 'K'(ASCII碼爲十進制數 75),並佔用了地址爲 0X11A 的內存(地址一般用十六進制表示)。另外有一個指針變量 p,它的值爲 0X11A,正好等於變量 c 的地址,這種狀況咱們就稱 p 指向了 c,或者說 p 是指向變量 c 的指針。
定義指針變量與定義普通變量很是相似,不過要在變量名前面加星號*
,格式爲:
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;
*
是一個特殊符號,代表一個變量是指針變量,定義 p一、p2 時必須帶*
。而給 p一、p2 賦值時,由於已經知道了它是一個指針變量,就不必畫蛇添足再帶上*
,後邊能夠像使用普通變量同樣來使用指針變量。也就是說,定義指針變量時必須帶*
,給指針變量賦值時不能帶*
。
指針變量也能夠連續定義,例如:
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,它們的指向關係以下圖所示:
程序被編譯和連接後,a、p 被替換成相應的地址。使用 *p 的話,要先經過地址 0XF0A0 取得變量 p 自己的值,這個值是變量 a 的地址,而後再經過這個值取得變量 a 的數據,先後共有兩次運算;而使用 a 的話,能夠經過地址 0X1000 直接取得它的數據,只須要一步運算。
也就是說,使用指針是間接獲取數據,使用變量名是直接獲取數據,前者比後者的代價要高。
指針除了能夠獲取內存上的數據,也能夠修改內存上的數據,例如:
int a = 15, b = 99, c = 222; int *p = &a; //定義指針變量 *p = b; //經過指針變量修改內存上的數據 c = *p; //經過指針變量獲取內存上的數據 printf("%d, %d, %d, %d\n", a, b, c, *p);
運行結果:
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; //把一個指針的值賦給另外一個指針
假設有一個 int 類型的變量 a,pa 是指向它的指針,那麼*&a
和&*pa
分別是什麼意思呢?
*&a
能夠理解爲*(&a)
,&a
表示取變量 a 的地址(等價於 pa),*(&a)
表示取這個地址上的數據(等價於 *pa),繞來繞去,又回到了原點,*&a
仍然等價於 a。
&*pa
能夠理解爲&(*pa)
,*pa
表示取得 pa 指向的數據(等價於 a),&(*pa)
表示數據的地址(等價於 &a),因此&*pa
等價於 pa。
*
的總結在咱們目前所學到的語法中,星號*
主要有三種用途:
int a = 3, b = 5, c; c = a * b;
,這是最容易理解的。int a = 100; int *p = &a;
。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,它們的地址分別增長 四、八、1,正好是 int、double、char 類型的長度;減 2 時,地址分別減小 八、1六、2,正好是 int、double、char 類型長度的 2 倍。
咱們知道,數組中的全部元素在內存中是連續排列的,若是一個指針指向了數組中的某個元素,那麼加 1 就表示指向下一個元素,減 1 就表示指向上一個元素,這樣指針的加減運算就具備了現實的意義。
數組(Array)是一系列具備相同類型的數據的集合,每一份數據叫作一個數組元素(Element)。數組中的全部元素在內存中是連續排列的,整個數組佔用的是一塊內存。以int arr[] = { 99, 15, 100, 888, 252 };
爲例,該數組在內存中的分佈以下圖所示:
定義數組時,要給出數組名和數組長度,數組名能夠認爲是一個指針,它指向數組的第 0 個元素。在C語言中,咱們將第 0 個元素的地址稱爲數組的首地址。以上面的數組爲例,下圖是 arr 的指向:
#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 個元素,或者說指向數組的開頭。
若是一個指針指向了數組,咱們就稱它爲數組指針(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; }
引入數組指針後,咱們就有兩種方案來訪問數組元素了,一種是使用下標,另一種是使用指針。
也就是採用 arr[i] 的形式訪問數組元素。若是 p 是指向數組 arr 的指針,那麼也可使用 p[i] 來訪問數組元素,它等價於 arr[i]。
也就是使用 (p+i) 的形式訪問數組元素。另外數組名自己也是指針,也可使用 (arr+i) 來訪問數組元素,它等價於 *(p+i)。
假設 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。
C語言中沒有特定的字符串類型,咱們一般是將字符串放在一個字符數組中:
#include <stdio.h> #include <string.h> int main(){ char str[] = "tanweime"; int len = strlen(str), i; //直接輸出字符串 printf("%s\n", str); //每次輸出一個字符 for(i=0; i<len; i++){ printf("%c", str[i]); } printf("\n"); return 0; }
除了字符數組,C語言還支持另一種表示字符串的方法,就是直接使用一個指針指向字符串,例如:
char *str = "tanweime";
或者:
char *str;str = "tanweime";
字符串中的全部字符在內存中是連續排列的,str 指向的是字符串的第 0 個字符;咱們一般將第 0 個字符的地址稱爲字符串的首地址。字符串中每一個字符的類型都是char
,因此 str 的類型也必須是char *
。
下面的例子演示瞭如何輸出這種字符串:
#include <stdio.h> #include <string.h> int main(){ char *str = "tanweime"; 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; }
這一切看起來和字符數組是多麼地類似,它們均可以使用%s
輸出整個字符串,均可以使用*
或[ ]
獲取單個字符,這兩種表示字符串的方式是否是就沒有區別了呢?
有!它們最根本的區別是在內存中的存儲區域不同,字符數組存儲在全局數據區或棧區,第二種形式的字符串存儲在常量區。全局數據區和棧區的字符串(也包括其餘數據)有讀取和寫入的權限,而常量區的字符串(也包括其餘數據)只有讀取權限,沒有寫入權限。
內存權限的不一樣致使的一個明顯結果就是,字符數組在定義後能夠讀取和修改每一個字符,而對於第二種形式的字符串,一旦被定義後就只能讀取不能修改,任何對它的賦值都是錯誤的。
咱們將第二種形式的字符串稱爲字符串常量,意思很明顯,常量只能讀取不能寫入。
在C語言中,函數的參數不只能夠是整數、小數、字符等具體的數據,還能夠是指向它們的指針。用指針變量做函數參數能夠將函數外部的地址傳遞到函數內部,使得在函數內部能夠操做函數外部的數據,而且這些數據不會隨着函數的結束而被銷燬。
像數組、字符串、動態分配的內存等都是一系列數據的集合,沒有辦法經過一個參數所有傳入函數內部,只能傳遞它們的指針,在函數內部經過指針來影響這些數據集合。
有的時候,對於整數、小數、字符等基本類型數據的操做也必需要藉助指針,一個典型的例子就是交換兩個變量的值。
#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; }
調用 swap() 函數時,將變量 a、b 的地址分別賦值給 p一、p2,這樣 p一、p2 表明的就是變量 a、b 自己,交換 p一、p2 的值也就是交換 a、b 的值。函數運行結束後雖然會將 p一、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[], 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語言沒有任何機制來保證這些數據會一直有效,它們在後續使用過程當中可能會引起運行時錯誤。請看下面的例子:
#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; }
前面咱們說函數運行結束後會銷燬全部的局部數據,這個觀點並沒錯,大部分C語言教材也都強調了這一點。可是,這裏所謂的銷燬並非將局部數據所佔用的內存所有抹掉,而是程序放棄對它的使用權限,棄之不理,後面的代碼能夠隨意使用這塊內存。對於上面的兩個例子,func() 運行結束後 n 的內存依然保持原樣,值仍是 100,若是使用及時也可以獲得正確的數據,若是有其它函數被調用就會覆蓋這塊內存,獲得的數據就失去了意義。
指針能夠指向一份普通類型的數據,例如 int、double、char 等,也能夠指向一份指針類型的數據,例如 int 、double 、char * 等。
若是一個指針指向的是另一個指針,咱們就稱它爲二級指針,或者指向指針的指針。
假設有一個 int 類型的變量 a,p1是指向 a 的指針變量,p2 又是指向 p1 的指針變量,它們的關係以下圖所示:
將這種關係轉換爲C語言代碼:
int a =100; int *p1 = &a; int **p2 = &p1;
指針變量也是一種變量,也會佔用存儲空間,也可使用&
獲取它的地址。C語言不限制指針的級數,每增長一級指針,在定義指針變量時就得增長一個星號*
。p1 是一級指針,指向普通類型的數據,定義時有一個*
;p2 是二級指針,指向一級指針 p1,定義時有兩個*
。
若是咱們但願再定義一個三級指針 p3,讓它指向 p2,那麼能夠這樣寫:
int ***p3 = &p2;
四級指針也是相似的道理:
int ****p4 = &p3;
實際開發中會常用一級指針和二級指針,幾乎用不到高級指針。
想要獲取指針指向的數據時,一級指針加一個*
,二級指針加兩個*
,三級指針加三個*
,以此類推,請看代碼:
#include <stdio.h> int main(){ int a =100; int *p1 = &a; int **p2 = &p1; int ***p3 = &p2; printf("%d, %d, %d, %d\n", a, *p1, **p2, ***p3); printf("&p2 = %#X, p3 = %#X\n", &p2, p3); printf("&p1 = %#X, p2 = %#X, *p3 = %#X\n", &p1, p2, *p3); printf(" &a = %#X, p1 = %#X, *p2 = %#X, **p3 = %#X\n", &a, p1, *p2, **p3); return 0; } ------ 100, 100, 100, 100 &p2 = 0XE19322F8, p3 = 0XE19322F8 &p1 = 0XE1932300, p2 = 0XE1932300, *p3 = 0XE1932300 &twa = 0XE193230C, p1 = 0XE193230C, *p2 = 0XE193230C, **p3 = 0XE193230C
以三級指針 p3 爲例來分析上面的代碼。***p3
等價於*(*(*p3))
。p3 獲得的是 p2 的值,也即 p1 的地址;(p3) 獲得的是 p1 的值,也即 a 的地址;通過三次「取值」操做後,((p3)) 獲得的纔是 a 的值。
假設 a、p一、p二、p3 的地址分別是 0X00A0、0X1000、0X2000、0X3000,它們之間的關係能夠用下圖來描述:
方框裏面是變量自己的值,方框下面是變量的地址。
若是一個數組中的全部元素保存的都是指針,那麼咱們就稱它爲指針數組。指針數組的定義形式通常爲:
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
指針數組還能夠和字符串數組結合使用,請看下面的例子:
#include <stdio.h> int main(){ char *str[3] = { "tanwei", "譚巍", "C Language" }; printf("%s\n%s\n%s\n", str[0], str[1], str[2]); return 0; }
須要注意的是,字符數組 str 中存放的是字符串的首地址,不是字符串自己,字符串自己位於其餘的內存區域,和字符數組是分開的。
也只有當指針數組中每一個元素的類型都是char *
時,才能像上面那樣給指針數組賦值,其餘類型不行。
爲了便於理解,能夠將上面的字符串數組改爲下面的形式,它們都是等價的。
#include <stdio.h> int main(){ char *str0 = "tanwei"; char *str1 = "譚巍"; 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; }
二維數組在概念上是二維的,有行和列,但在內存中全部的數組元素都是連續排列的,它們之間沒有「縫隙」。如下面的二維數組 a 爲例:
int a3 = { {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
但在內存中,a 的分佈是一維線性的,整個數組佔用一塊連續的內存:
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] 包含 a0、a0、a0、a0。
假設數組 a 中第 0 個元素的地址爲 1000,那麼每一個一維數組的首地址以下圖所示:
爲了更好的理解指針和二維數組的關係,咱們先來定義一個指向 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
3) *(p+1)+1
表示第 1 行第 1 個元素的地址。如何理解呢?
*(p+1)
單獨使用時表示的是第 1 行數據,放在表達式中會被轉換爲第 1 行數據的首地址,也就是第 1 行第 0 個元素的地址,由於使用整行數據沒有實際的含義,編譯器遇到這種狀況都會轉換爲指向該行第 0 個元素的指針;就像一維數組的名字,在定義時或者和 sizeof、& 一塊兒使用時才表示整個數組,出如今表達式中就會被轉換爲指向數組第 0 個元素的指針。
4) *(*(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; }
指針數組和二維數組指針在定義時很是類似,只是括號的位置不一樣:
int *(p1[5]); //指針數組,能夠去掉括號直接寫做 int *p1[5];int (*p2)[5]; //二維數組指針,不能去掉括號
指針數組和二維數組指針有着本質上的區別:指針數組是一個數組,只是每一個元素保存的都是指針,以上面的 p1 爲例,在32位環境下它佔用 4×5 = 20 個字節的內存。二維數組指針是一個指針,它指向一個二維數組,以上面的 p2 爲例,它佔用 4 個字節的內存。
一個函數老是佔用一段連續的內存區域,函數名在表達式中有時也會被轉換爲該函數所在內存區域的首地址,這和數組名很是相似。咱們能夠把函數的這個首地址(或稱入口地址)賦予一個指針變量,使指針變量指向函數所在的內存區域,而後經過指針變量就能夠找到並調用該函數。這種指針就是函數指針。
函數指針的定義形式爲:
returnType (*pointerName)(param list);
returnType 爲函數返回值類型,pointerNmae 爲指針名稱,param list 爲函數參數列表。參數列表中能夠同時給出參數的類型和名稱,也能夠只給出參數的類型,省略參數的名稱,這一點和函數原型很是相似。
注意( )
的優先級高於*
,第一個括號不能省略,若是寫做returnType *pointerName(param list);
就成了函數原型,它代表函數的返回值類型爲returnType *
。
#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; }
指針(Pointer)就是內存的地址,C語言容許用一個變量來存放指針,這種變量稱爲指針變量。指針變量能夠存放基本類型數據的地址,也能夠存放數組、函數以及其餘指針變量的地址。
程序在運行過程當中須要的是數據和指令的地址,變量名、函數名、字符串名和數組名在本質上是同樣的,它們都是地址的助記符:在編寫代碼的過程當中,咱們認爲變量名錶示的是數據自己,而函數名、字符串名和數組名錶示的是代碼塊或數據塊的首地址;程序被編譯和連接後,這些名字都會消失,取而代之的是它們對應的地址。
定 義 | 含 義 |
---|---|
int *p; | p 能夠指向 int 類型的數據,也能夠指向相似 int arr[n] 的數組。 |
int **p; | p 爲二級指針,指向 int * 類型的數據。 |
int *p[n]; | 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;
是沒有意義的,使用過程當中通常會致使程序崩潰。
3) 使用指針變量以前必定要初始化,不然就不能肯定指針指向哪裏,若是它指向的內存沒有使用權限,程序就崩潰了。對於暫時沒有指向的指針,建議賦值NULL
。
4) 兩個指針變量能夠相減。若是兩個指針變量指向同一個數組中的某個元素,那麼相減的結果就是兩個指針之間相差的元素個數。
5) 數組也是有類型的,數組名的本意是表示一組類型相同的數據。在定義數組時,或者和 sizeof、& 運算符一塊兒使用時數組名才表示整個數組,表達式中的數組名會被轉換爲一個指向數組的指針。
歡迎訪問個人 博客和 github!