蘇子偉
指針簡介
指針是C語言的一個最重要的特征,它提供了一種統一的方法,使其能訪問遠程的數據結構。但對C語言初學者而言,在編程過程中熟練的使用指針并不能像使用int型變量一樣地輕松愉快,容易上手,往往是不得其精髓。我們知道,不論什么時候,運行一個程序A,首先都是操作系統自身的加載器把A裝入內存,然后CPU才能執行。所以A程序的所有要素都會駐留在內存的某個位置。
下面我們看一段示例程序。
#include
intcmp(int first, int second)
{
return ( first > second ? first : second );
}
int main(intargc, char **argv)
{
inti = 5;
int j = 9;
returncmp(i, j);
}
首先,編譯器會為變量i和j開辟內存空間,用來存儲i和j的值。同時也會為函數cmp開辟空間來存放其代碼。這樣使得最終的可執行程序就變為了跟內存一一對應的序列。操作系統的加載器把這個可執行程序載入內存后,cpu就可以按一條條的語句順序執行了。
既然內存空間同程序的所有要素是一一對應的,那么怎么區分各要素的存放位置呢?內存使用不同的地址存放不同的要素,如下所示。
由于變量都存放于內存地址空間,并且與地址之間是一一對應的,那么利用地址能做些什么呢?我們可以把地址存放到別的變量中,以便我們可以在以后程序的某個地方使用它。C語言有一個專門用來存儲內存地址的變量,這就是指針變量,通常我們稱之為指針(pointer)。它是一種變量類型,這種變量方便我們把需要操控的內存地址記憶起來。
定義指針
定義指針的運算符同乘法運算符是一樣的,都用“*”表示。定義一個指針變量在語法上是簡單的,同我們定義其他變量的區別是:首先規定它指向的變量類型,然后并不是立即就給出其變量的標識符,而是在變量類型同變量標識符之間插入指針運算符(星號),這樣就告訴編譯器這是一個指針變量。
C語言中指針可以指向任何的數據類型,包括函數。函數指針的定義是:函數返回值+(* + 函數指針變量標識符)+(函數的參數列表)。函數指針能構建出更加清晰的程序結構。編程中經常使用的指針定義就是這兩種,當然有些定義可能只是語法上面有意義,但是語義上面不一定有具體的意義。例如,int *(*(*(*f)())[])()聲明f是一個函數指針,該函數返回一個指針,該指針指向數組,該數組元素是指針,那些指針指向返回值類型為整型指針的函數。這樣的聲明可能永遠也不能應用到實際的代碼中。
指針和數組
數組是內存中一段連續相同類型的內存數據,這組數據的首地址以數組名字來標識。所有數組對其數據的操控都可以使用指針來實現,同理,指針指向一段內存數據時,也可以使用數組下標的方式來實現操作。
數組與指針在使用上的某些地方是非常相似的,但是數組與指針又有一些細小的區別。數組名表現為一個靜態指針,也可以直接把它賦值給指針變量,但它的大小與指針通常是不同的。數組名的內涵在于其指代的實體是一種數據結構,這種數據結構就是數組。數組名可以作為參數傳入一個接受參數為指針的函數內部,但是此時數組完全丟失了數組的本義,變成了完全的指針類型,其常量特性(可以作自增、自減等操作)可以被修改。并且,數組名不能再重新賦值為其他的數組名字,而指針變量是可以被重新賦值并指向一段新的內存地址的。
指針的運算
指針的運算指的是指針的--、++、-和+運算,一個指針可以加上或者減去一個整數。兩個指針相減得到的是指針之間相隔的元素個數。不同的指針變量之間進行相加運算盡管在語法上是合理的,但是從語義上來講是沒有意義的。除了void型指針和函數指針以外,所有其他類型的指針都可以進行指針運算。通過指針變量的增加或減少,指針變量會指向新的內存地址。
一般來說,指針變量自身的大小在理論上是指機器的字長,但是指針變量的運算并不是按照指針變量自身的大小進行內存偏移的,而是按照指針變量指向的變量類型大小進行內存偏移的。比如,聲明一個整形的指針p,假定p的地址是0x4323672,那么++p后p的值變為0x43236726。偏移的內存大小等于整形變量的內存大小4(sizeof(int))。同理,double型指針進行++運算后偏移值就是8(sizeof(double))。
指針強轉
如同整形變量可以強轉為浮點型變量一樣,指針類型也可以通過強轉變成新的指針類型,比如我們可以把整形指針強轉為字符型指針。指針強轉最誘人的地方就在于對內存數據進行操控就夠了。指針強轉使得指針對數據的操控更具有針對性,而且通過指針的默認強轉可以使得函數的參數更簡單,且傳遞的信息量是不變的。比如,void*作為參數時可以把任意的指針變量傳遞到函數內部進行相關的操作。
下面我們來看一個具體的例子。數據的內存布局如下圖所示,首先是一個字符型數據,緊接著的是兩個整形數據,最后面是三個結構體A型數據。我們需要做的就是把這些數據讀出來。
我們先聲明一個字符型的指針p,使其指向第一個數據的內存地址。取完第一個字符型數據后,通過p++,然后強轉指針為整形指針,就可以很方便地取出整形數據,同理可取出三個結構體數據。
指針作為參數
先看一個例子,我們有兩個整形變量,x的值為777,y的值為888,現在想構建一個函數用來交換兩個整形變量的值,使得x的值為888,y的值為777。首先我們以傳值的方式構建
voidswap_value(int Param1,int Param2)
{
int Temp = Param1;
Param1 = Param2;
Param2 = Temp;
}
我們調用函數swap_value(x,y)后,發現x、y的值并沒有被交換。造成這種結果的原因是由于函數調用時,首先對傳入的實參進行變量的拷貝,交換的值是形參的值,并不是實參的值。而原來的實參與拷貝后的形參變量所處的內存也不同,所以并沒有交換成功。
要想實現函數內部對這兩個值的交換,必須使得實參與拷貝后的形參變量所處的內存是相同的。我們知道了原理后,修正函數參數列表,以指針的方式重新構建函數如下:
voidswap_value(int*Param1,int*Param2)
{
int Temp=*Param1;
*Param1=*Param2;
*Param2=Temp;
}
這時候我們發現x、y的值被交換了。通過上面的例子可以看出,使用指針作為參數可以修改原來的變量值,使得函數實現的機能更加模塊化,方便了程序的設計。
野指針
前面我們已經討論過指針變量同內存的關系,了解了指針變量里面存放的是某個變量的內存地址,該地址可以在程序的某個位置使用,以方便我們更改或取得該變量的值。指針使得我們擁有了操控內存的利器,但同時指針也是一把雙刃劍。我們必須時刻確保指針變量的值是我們意圖操控的內存地址。如果指針變量的值被不受控的更改或者初始化不正確,那么我們就使用了錯誤的地址,從而導致程序錯誤,通常我們稱這個導致程序錯誤的指針變量為野指針。由于使用了野指針而產生的程序錯誤大多時候是隱蔽的,難于跟蹤的。野指針的產生主要是由于以下幾種情況。
(1)聲明了指針變量,但是沒有正確的初始化就使用了該指針變量。
(2)使用指針變量之前沒有對其進行安全檢查。
(3)指針指向的內存變為了無效值,但沒有及時對指針清零,導致程序某處引用了該指針。
(4)多個指針同時指向同一內存區域,程序某處通過某個指針釋放了該內存,但是沒有及時對其他的指針清零,導致程序某處進行了錯誤的引用。
(5)多線程時,對全局的指針變量沒有進行鎖處理。
多級指針
定義一級指針我們使用一個‘*,在定義多級指針時,是幾級指針我們就使用幾個‘*。例如,聲明一個整型的二級指針(int ** ppVar;)。下面以這個二級指針為例說明一下二級指針的意義。
二級指針變量同樣是保存了一個地址,這個地址就是某個一級指針變量的地址,而一級指針變量里面保存了最終需要操作的變量的地址,如下所示。
0x4323640 0x4323668
二級指針變量的值為0x4323640,就是一級指針變量pVar的地址,變量pVar的值為0x4323668,就是變量Var的地址。如果需要修改變量Var的值,我們可以直接修正**ppVar的值就可以了。
三級指針或者更多級指針的原理與二級指針的原理是相同的,只是需要索引的內存空間的深度增加了。在程序設計中,引入多級指針更多的時候并不僅僅是為了關注最后一級指針所能取得的變量,而更多的是為了使用和操控其中間的級數的內存值。比如利用二級指針作為函數的參數在某個函數內部對其分配內存,我們更想利用的是一級指針變量自身。當然,在進行程序設計時,有時我們要在可讀性與語法有效性之間做出選擇,在實現代碼的過程中能用低級指針實現的盡量不要使用多級指針實現,這樣的代碼更利于維護。
小結
在C語言中指針的使用非常的廣泛,有時指針是實現某個計算的唯一方法。同樣的機能使用指針通常也可以獲得更加高效、緊湊的代碼。指針使得函數構建的機能更加的模塊化,使得函數參數棧更加的短小。同時在操縱字符串的運算中,指針更加簡單直觀。
在大項目構建時,把函數指針同數據封裝在一起能夠使得代碼編程面向對象的結構,使得后期代碼的維護成本大大降低,代碼的表現也更加具有現實意義。
當然,使指針具有這些優點的前提是能夠熟練地使用它。粗心大意地使用指針變量,更容易引入程序錯誤。因此,合理正確地使用指針也就成為了C語言愛好者和使用者的一門必修課。endprint