C語言精要總結-指針系列(一)

文章系本人原創,同時發佈在博客園中 http://www.cnblogs.com/lvyahui/p/6793285.html

考慮到指針內容繁多,這裏將指針作爲一個系列,從簡入繁,帶着沒有研究過指針的朋友,一點一點深挖並掌握這C語言的精華。初步計劃如下

此文爲指針系列第一篇:

C語言精要總結-指針系列(一)

內存與地址

我們可以把內存看做一排連續的房間,每個房間(字節空間)都有一個房間號,房間號就是這個房間的地址,而且每個房間裏都有八個位。

爲了存儲不同大小的值,多數時候我們要用連續幾個房間來存儲一個值,這時我們會用其中一個房間號來表示這一片連續的房間,至於這個房間號是第一個房間的房間號,還是最後一個房間的房間號,不同的機器有不同的規定。文中我們假設這個房間號是左起第一個房間的房間號,這樣房間號(指針)其實就有了類型之分。

 

通過地址,計算機就可以操縱內存單元的內容,雖然在代碼中,類似於*0x0048f93d = 'a';這樣的表達式是合法的,但爲了讓代碼看起來更友好,顯然不能在代碼裏全部寫數字地址。因此高級語言的編譯器爲我們實現了直接通過變量名來訪問內存位置,當然硬件單元依然通過尋址來訪問內存位置

指針變量

指針變量本身也是變量,也需要在內存中佔用一定的存儲單元,只是其存儲的值是其他變量的內存地址,分配的位置也可以是跟基本類型變量是連續的。(下圖中一個長方形並不代表一個字節),如圖:

用代碼來描述即是

1 short shortVar = 10;
2 int intVar = 80;
3 short * p1 = & shortVar;
4 int * p2 = & intVar;

上面的語句給出了指針定義(type *)和初始化(= &var)的方法。這裏定義了一個名叫p1指向short類型的指針變量,並用shortVar的地址來初始化;定義了一個名叫p2指向int類型的指針變量,並用intVar的地址來初始化。當然,像下面這樣直接用一個地址值來初始化指針也是合法的

1 int *p = (int *)0x0048f93d;

雖然這在我們看來是個地址值,但在編譯器眼裏,這是一個int類型的值,所以需要強制轉換。這種寫法,除非很明確這個地址時用來做什麼的,否則不要這麼做。

如果在定義一個指針變量時,還不確定用什麼地址來初始化,則一定要初始化爲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. 解引用p2p得到一個一級指針tmp
  2. 將一級指針tmp強制轉換爲一個指向int 類型的指針
  3. 取p2指針的地址(一個二級指針)賦值給一級指針tmp(注意是賦值給一級指針本身,而不是一級指針指向的變量)

顯然這是不能執行成功的。但是它卻告訴我們,指針是可以強制轉換的。

但對指針類型強制轉換,和普通數據類型會有些不一樣:對指針類型強制轉換,不會改變指針變量本身空間的大小及空間內存儲的地址值,而只會修改符號表中的指針類型及其指向類型佔用空間的大小值(爲指針運算做準備)

一起來圖解一下下面這段代碼

複製代碼
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 );。

 

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