考慮到指針內容繁多,這裏將指針做爲一個系列,從簡入繁,帶着沒有研究過指針的朋友,一點一點深挖並掌握這C語言的精華。初步計劃以下html
此文爲指針系列第一篇:數組
咱們能夠把內存看作一排連續的房間,每一個房間(字節空間)都有一個房間號,房間號就是這個房間的地址,並且每一個房間裏都有八個位。優化
爲了存儲不一樣大小的值,多數時候咱們要用連續幾個房間來存儲一個值,這時咱們會用其中一個房間號來表示這一片連續的房間,至於這個房間號是第一個房間的房間號,仍是最後一個房間的房間號,不一樣的機器有不一樣的規定。文中咱們假設這個房間號是左起第一個房間的房間號,這樣房間號(指針)其實就有了類型之分。ui
經過地址,計算機就能夠操縱內存單元的內容,雖然在代碼中,相似於*0x0048f93d = 'a';這樣的表達式是合法的,但爲了讓代碼看起來更友好,顯然不能在代碼裏所有寫數字地址。所以高級語言的編譯器爲咱們實現了直接經過變量名來訪問內存位置,固然硬件單元依然經過尋址來訪問內存位置。spa
指針變量自己也是變量,也須要在內存中佔用必定的存儲單元,只是其存儲的值是其餘變量的內存地址,分配的位置也能夠是跟基本類型變量是連續的。(下圖中一個長方形並不表明一個字節),如圖:3d
用代碼來描述便是指針
1 short shortVar = 10; 2 int intVar = 80; 3 short * p1 = & shortVar; 4 int * p2 = & intVar;
上面的語句給出了指針定義(type *)和初始化(= &var)的方法。這裏定義了一個名叫p1指向short類型的指針變量,並用shortVar的地址來初始化;定義了一個名叫p2指向int類型的指針變量,並用intVar的地址來初始化。固然,像下面這樣直接用一個地址值來初始化指針也是合法的code
1 int *p = (int *)0x0048f93d;
雖然這在咱們看來是個地址值,但在編譯器眼裏,這是一個int類型的值,因此須要強制轉換。這種寫法,除非很明確這個地址時用來作什麼的,不然不要這麼作。htm
若是在定義一個指針變量時,還不肯定用什麼地址來初始化,則必定要初始化爲NULL,這是一個空指針值,也是一個值爲0的宏,它表明指針不指向任何位置。若是不給一個局部指針變量作任何初始化,它存儲的將是一個不可預知的值,指向一個不可預知的位置,若是對這樣一個指針變量進行操做,很容易引發異常中斷。而對於全局變量,編譯器會自動初始化爲0。
解引用,又叫間接訪問,即經過一個指針變量訪問它所指向的地址的過程。這個解引用操做符即是單目操做符*。但注意對一個指針進行進行解引用,不必定是取值,也多是寫值,這取決於解引用表達式是做爲左值(賦值符號左邊)仍是右值(賦值符號右邊)。
例如對上述指針p1,p2進行解引用操做,以下代碼
1 printf("%d\n",* p1); // 10 2 printf("%d\n",* p2); // 80 3 *p1 = 1; 4 *p2 = 8; 5 printf("%d\n",* p1); // 1 6 printf("%d\n",* p2); // 8
那麼像下面這個表達式作了什麼呢?
1 *&shortVar = 1;
很顯然,這是將1賦值給變量shortVar,根據右結合性,取地址(&)以後當即解引用(*),這其實畫蛇添足,若是編譯器不對這樣的代碼作優化,那將生成一些無心義的操做代碼。
二級指針,也叫指針的指針,也就是一個指向指針變量的指針。按照指針變量的定義方法(type * pVar),咱們要定義一個指向整型指針變量的指針,應該像下面這樣定義
1 int * * p2p = & p2;
沒錯,這就是定義一個指向整型指針的指針的定義方式。在內存中結構(假設分配的剛好是連續的)就以下圖所示
很顯然,二級指針變量依然也是一個指針變量,哪怕後面還有三級、四級指針變量,都始終是一個指針變量,對它進行解引用或者取地址,原理跟一級指針是同樣的。好比對二級指針p2p進行一次解引用,將獲得p2這個指針變量,再進行一次解引用將獲得intVar這個變量,正如上圖所示。
1 printf("%d\n",**p2p); // 80
二級指針跟二維數組名是有很大區別的,這會在後續的文章中指出。另外,若是對一個二級指針取地址,將獲得一個三級地址,依次類推。
咱們知道指針是用來存儲地址值的,而分配給指針變量的空間,只用來存儲地址值,而不會記錄變量類型等信息,這跟普通變量是同樣,它們被記錄在編譯器的符號表中。
既然指針變量自身的空間只存地址,那麼無論什麼類型的指針,它們佔用的空間大小應該是同樣的,那究竟應該分配多大的空間?這取決於CPU最大尋址地址的大小。爲了保證指針變量能存下最大的尋址地址,應該給指針變量分配足以存儲最大尋址地址的大小。
在32位CPU上,CPU最大尋址空間爲2的32次方(4G),所以要存下最大的32位的地址值,須要爲指針變量分配4個字節的空間,而在64位CPU中,爲了能尋到2的64次方的內存空間,須要爲指針變量分配8個字節的空間。
所以,編譯器也充分考慮了這個問題。它能夠控制分配的指針變量的空間大小。用VS在寫Console Application時,默認編譯的是32位 Console Application,這是爲了保證程序的兼容性,以保證程序必定能夠在32位和64位機器上運行,此時,vs編譯器默爲指針變量分配4個字節的空間。可是本人的筆記本是支持64位尋址的CPU,所以,本人用gcc version 5.1.0 (tdm64-1)編譯出來的程序,指針變量分配了8個字節的空間。
後續文章中,如不明確指出,咱們認爲指針變量佔4個字節的空間。
在看怎麼定義二級指針時,有讀者可能考慮,爲何不能這樣定義
1 (int *) * p2p = & p2;
乍一看可能沒什麼不對,但實際上,這個表達式並非定義一個變量,而是在執行一個非法的賦值操做。假如前面已經定義過p2p這樣的一個二級指針,這個表達式還真會作一些事情:
顯然這是不能執行成功的。可是它卻告訴咱們,指針是能夠強制轉換的。
但對指針類型強制轉換,和普通數據類型會有些不同:對指針類型強制轉換,不會改變指針變量自己空間的大小及空間內存儲的地址值,而只會修改符號表中的指針類型及其指向類型佔用空間的大小值(爲指針運算作準備)。
一塊兒來圖解一下下面這段代碼
1 // int a = 0x12345678; 2 // return *(char*)(&a) == (char) a; 3 int a = 0x12345678; 4 int * pa = &a ; 5 char * pch = (char *) pa; 6 char ch = (char) a; 7 printf("%x\n",*pch); 8 printf("%x\n",ch);
假如程序出現的變量按以下方式分配
對指針強制轉換以後,pch存儲的地址值跟pa存儲的地址值時同樣的,可是他們在編譯器符號表中的類型是不一致的,所以指向的空間大小是不同的,pa指向整個變量a,而pch指向變量a的第一個低字節。ch變量毫無疑問存儲的將是78,由於對一個基本數據強制轉換,只會取數據的低位。
很顯然,若是按照圖中所示,程序的第7行第8行將輸出12和78。但實際上在本人的筆記本上,兩次都是輸出78。
這其實就是很經典的大端存儲和小端存儲的判別。若是按照圖中所示,其實變量a是按大端模式存儲(即低地址存高位)。而若是按照小端模式存儲,則應該低地址存低數據位,以下圖。
而上面那段代碼,就是用來檢測計算機是按大端存儲仍是按小端存儲的,很顯然,本人的筆記本按小端存儲。
用這個程序想說明的是,對一個指針進行調整級別的強制轉換再解引用,可能會引發一些兼容性問題,由於這取決於系統實現。
另外在程序中咱們會常常看到void * 類型的指針,這樣的指針主要是爲了寫通用的代碼,你能夠將任意類型的指針強制轉換爲void* 類型的指針,在以後要解引用的時候,再強制轉換回正確的指針類型進行解引用。例如咱們常見的c語言庫函數qsort中的:int comparator ( const void * elem1, const void * elem2 );。