指針究竟有什么用?

要談到指針有什么用,先要知道指針是什么。直接的解釋是:指針是內(nèi)存的標(biāo)簽。首先把你可以把整個內(nèi)存想象成一個小區(qū),里面都是連戶的小公寓。比如這個樣子:

好吧,是簡陋了一點,但是我一直覺得excel表格真的和內(nèi)存挺像的。內(nèi)存是這樣一組一組的存儲單元組成的(這里不討論太多細(xì)節(jié)),然后他們排列在一起。為了定位每個單元,需要給單元一個唯一的身份識別碼。我們稱為地址。用表格舉例,那么這個格子的地址就是C4

當(dāng)然,實際上內(nèi)存的地址排布是線性的,也就是從0到最大可尋址地址。對于32位尋址的內(nèi)存,最大地址剛好是0xFFFFFFFF,從0到0xFFFFFFFF恰好是 4GBytes個單元。如此一來,就可以通過地址定位一套公寓了。

但是全部按照數(shù)字來定位實在是太難記憶了,不好理解,對人類不友好。這時候我們想給地址起個別名,通過這個別名來找這套房子,只要別名不重復(fù)(后面還有展開)就一樣可以起到定位一套房子的作用對吧。于是:

MyHouse就可以代替C4這個地址幫助我們定位這套房子。
或者我們可以稱:MyHouse指向了C4。
這就是指針。


那么光有指針沒用啊,房子總歸還是要住人的。

我啊,小屌絲,不是在辦公室工作,就是在家宅著,順便一說我是48G的6年老飯了,在家宅著就看看SNH的直播。
如果不是在上面兩種狀態(tài),那就是在兩種狀態(tài)轉(zhuǎn)換的過程中。

對于計算機(jī)中的數(shù)據(jù)也是這樣的,數(shù)據(jù)不是在工作中(在CPU的寄存器內(nèi)),就是在家宅(在Cache或內(nèi)存中)。

我是個新人,剛來公司,大家都不認(rèn)識我,只知道我住在MyHouse,所以老板叫我“住在MyHouse里的那個”。
C語言中的表示就是 *MyHouse
但是這么說別人不禮貌,于是老板給了我個工號zmarsarc,以后公司里面大家就以工號相稱。于是大家就知道了zmarsarc = *MyHouse。
而這個zmarsarc,實際上指的就是我曾某了。

例子舉夠了,實際解釋:

C4:代表了內(nèi)存的實際地址,這是一個絕對地址,任何進(jìn)程都可以使用這個地址找到這個地方。

指針 MyHouse:是代表了一塊內(nèi)存標(biāo)號,這個標(biāo)號可以是絕對的,也可以是相對的,總之擁有這個變量的進(jìn)程(就是你的Boss)可以通過這個指針找到這個地方。

指針解引用 *MyHouse:意思就是指的這塊內(nèi)存存放的實際數(shù)據(jù),在例子中就是曾某,這里的曾某就是這個數(shù)據(jù)的真實含義。

變量名zmarsarc:就是指代了這個變量,某種程度上可以和MyHouse等價,但是MyHouse指的是在C4地址中的數(shù)據(jù),而zmarsarc指的就是數(shù)據(jù)曾某,變量名是變量的標(biāo)簽,而指針是地址的標(biāo)簽。在邏輯上,他們是不同的。

-------------------指針能做什么--------------------
提到指針能做什么還是要額外提一下變量作用域。指針有什么用還是離不開這個概念。其實也很好理解,還是上面的例子。

我們公司里除了我以外還有一個叫馮某的,巧了,他住的地方也叫MyHouse,但是和我不是同一個地址。
那這怎么辦,在公司里如果說“住在MyHouse里的那個”不就分不清了?沒關(guān)系,我和他不在一個部門,我在技術(shù)部,他在銷售部。我們兩個部的部長手上的花名冊里面雖然都有一個叫做 “住在 MyHouse里的”,但是他們互相知道,指的不是同一個人。

兩個部門好比同一個進(jìn)程中的兩個線程,兩個線程各自維護(hù)自己的變量表,他們之間互相不干涉。
如果不用線程舉例子,也可以理解成兩個函數(shù),函數(shù)在自己的內(nèi)部維護(hù)自己的變量,在不同的作用域中可以各自存在一個相同的名稱,但是在同一個作用域中,不能存在相同的名稱。

為什么呢,因為C語言在運行時棧上維護(hù)函數(shù)的局部變量。
我借用別人的一張圖,這是INTEL規(guī)定的x86架構(gòu)調(diào)用過程棧幀(侵刪):

從被保存的EBP(棧基址指針)開始,是當(dāng)前棧的結(jié)構(gòu)??梢钥吹?,所有局部變量和臨時變量都在棧上。同時調(diào)用過程的局部變量在調(diào)用過程自己的棧幀內(nèi)部。假如在調(diào)用過程有一個變量叫foo,而當(dāng)前幀也有一個變量叫foo,它們當(dāng)然不會混淆,因為它們在不同的棧幀內(nèi)部。

不僅如此,當(dāng)前過程根本就不知道它的調(diào)用者還有一個做foo的變量,因為每一個過程只維護(hù)自己的棧幀,而對于棧的歷史不需要了解。
如果,我們希望在當(dāng)前過程當(dāng)中修改其它過程的一個變量應(yīng)該怎么辦?這時候就需要傳遞這個變量的地址了。

*棧是在內(nèi)存上的。

有很多同學(xué)學(xué)了兩年都沒搞清這個事情,所以很重要我要提一下。
我們通過指針就可以訪問內(nèi)存上任意一個變量(當(dāng)然是權(quán)限允許訪問的區(qū)域)。假設(shè)我們只傳遞一個參數(shù),就是這個變量的指針,并且不使用寄存器傳遞參數(shù)。那么調(diào)用過程將把指針的值放在圖中<參數(shù)1>的位置,然后調(diào)用子過程。進(jìn)入子過程后,子過程首先將當(dāng)前的?;分羔槈喝霔?,然后把當(dāng)前的棧頂指針賦給棧基址指針,就是這樣

PUSH    EBP
MOV     EBP, ESP

這樣,子過程就和調(diào)用過程的棧隔離了。棧幀創(chuàng)建完畢之后,子過程通過EBP+4尋址到<參數(shù)1>的位置,取出這個值,然后使用這個值來尋找需要修改的變量。這樣子過程就可以在不了解調(diào)用過程的情況下 ,修改調(diào)用過程局部變量。這個子過程看起來可以像是這樣的:

void foo(int* arg){
 *arg = new_value; 
//return;
}

結(jié)合之前的例子,就可以看明白了。

但是注意到,對于自動變量,只能是子過程修改棧上歷史的變量,而不能是調(diào)用者修改子過程的變量。
因為,當(dāng)子過程退出調(diào)用時,會將當(dāng)前棧幀釋放,函數(shù)的返回值,放在寄存器EXA當(dāng)中。彈出所有被保存的寄存器以恢復(fù)上下文,彈出保存的EBP值恢復(fù)棧基址,然后將返回地址裝入PC中,返回到調(diào)用的位置。
當(dāng)父過程嘗試修改子過程的局部變量時,子過程的局部變量永遠(yuǎn)是不存在的。
不妨考慮這樣兩個過程:

void foo(void){ 
int* num;
num = foo_1(void);
*num++;
//return;
}

int* foo_1(void){ 
int never_try_this = 0; 
return &never_try_this;
} 

首先foo調(diào)用了foo_1,foo_1創(chuàng)建了變量never_try_this,然后將變量的地址放在寄存器EXA中,返回。foo使用一個指針num保存了foo_1返回的never_try_this的地址,然后嘗試對這個地址的值自加。但是在上面的例子中我們已經(jīng)知道,當(dāng)foo得到never_try_this的地址時,foo_1已經(jīng)釋放了自己的棧幀,所以這時候never_try_this變量實際已經(jīng)不存在了,通過它的地址,訪問將造成不可預(yù)知的后果。
這就是很多教材不斷提到的:絕對不要嘗試返回局部變量的指針。

還有一種用途。比如我們的調(diào)用過程希望給子過程傳遞一個數(shù)組,假如這個數(shù)組有5000個數(shù)吧。那么在如果我們將這個數(shù)組作為參數(shù)傳遞會怎樣?
在傳值的方式下,調(diào)用過程會將這個數(shù)組在棧上全部復(fù)制一遍,就像上圖中看到的,從<參數(shù)1>的位置開始,連續(xù)放5000個數(shù)。
棧當(dāng)然不可能是無限大的,總是有一個到頭的地方,如果到頭了還放不下這5000個數(shù),那么,恩,棧就直接爆掉。然后操作系統(tǒng)提示你棧溢出,程序崩潰了。

這時候我們就可以在其它地方開辟一塊連續(xù)的內(nèi)存,然后使用指針指向這段內(nèi)存的起始位置,將指針傳遞給子過程。這樣傳遞過程中,只需要在棧上傳遞一個數(shù),不僅省空間,而且速度快。
可惜,傳遞過程中,C語言的編譯器沒有幫你一起把這段內(nèi)存的長度一起傳了,在子過程中,你要冒著數(shù)組下標(biāo)越界的風(fēng)險來處理數(shù)據(jù)。所以檢查下標(biāo)是否越界的責(zé)任就要交給程序員,你需要在傳遞指針的同時傳遞長度,或者,直接在子過程里寫出長度。數(shù)組下標(biāo)越界造成的后果也是不可知的,唯一可以知道的是,有名的蠕蟲病毒正是利用了數(shù)組下標(biāo)越界。

剩下的還有一個我常用的用法就屬于硬件工程師常用的方式:使用指針直接訪問一個特定的內(nèi)存區(qū)域。比如在哈佛結(jié)構(gòu)的處理器上,外設(shè)的訪問端口也是映射到內(nèi)存上的。
假設(shè)我們在手冊上查到某GPIO的寫入寄存器地址是

0x48000000

那么我就可以直接寫:

#define GPIOx (*(volatile unsigned *)0x48000000)

這樣就可以通過GPIOx來直接訪問端口了。

就是這樣。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容