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