導(dǎo)讀:指針是 C 語言的靈魂,該如何真正理解并運(yùn)用呢?這篇文章告訴你答案!
終于到了 C 語言中最為重要的指針環(huán)節(jié)了。之前一直以積累為主,不敢寫,或者說不愿意寫,因?yàn)闆]有足夠的高度寫出來的東西很多都是片面的,當(dāng)然現(xiàn)在我也不敢說我目前寫出來的就一定是全面的,只是對于普通的程序員來說,也算比較全面了吧!當(dāng)然因?yàn)闁|西太多,有可能會有忘記的地方,到時(shí)候再更新補(bǔ)充吧。 這里將數(shù)組和指針放在一塊介紹,是因?yàn)樗麄兒芟瘢芟竦臇|西放在一塊比較能找出它們的差異性了,以后使用的時(shí)候也就不容易犯迷糊了。并且這里將穿插測試代碼和從匯編的角度去理解 C 語言,這樣可能更容易掌握指針。 注意,所有的測試工作都是在 KEIL 、STM32開發(fā)平臺下的,其他平臺不敢保證這是對的。 先概括一下本文大綱(如果道友看到的不全,請關(guān)注公眾號查看): 1、解析指針的過程與意義(重點(diǎn)) 2、指針大小、強(qiáng)制轉(zhuǎn)換、二級指針、結(jié)構(gòu)體指針、函數(shù)指針 3、指針和數(shù)組 4、指針與寄存器 5、指針與外設(shè) 6、溢出與使用效率問題
解析指針的過程與意義(重點(diǎn))
什么是指針?魚鷹不想用教科書式的語句去介紹,這樣就失去了本篇筆記的意義了。 現(xiàn)在我們想象這樣一個(gè)場景,廣場里有一個(gè)個(gè)由學(xué)生組成的方陣,每個(gè)方陣有大有小,這些學(xué)生由教官統(tǒng)一指揮(軍訓(xùn)場景)。
現(xiàn)在教官想要通知第一方陣的2號同學(xué)唱首軍歌該怎么做?正常情況下應(yīng)該是教官眼睛先看向第一方陣的位置,然后看向2號同學(xué)(發(fā)現(xiàn)是黃四郎),“抬起頭來,唱首《中國軍魂》”,實(shí)際上他想走個(gè)“走個(gè)虎虎生風(fēng),走個(gè)一日千里,走個(gè)恍如隔世”(教官尋找2號同學(xué)的過程其實(shí)就是結(jié)構(gòu)體內(nèi)數(shù)據(jù)尋址的過程)。
一個(gè)、兩個(gè)人的通知,對教官來說不算啥,但是如果有很多人需要分別通知到呢? 怎么辦?分級管理。
既然力不從心,那么我就找?guī)讉€(gè)助手唄(也是學(xué)生,但不屬于各個(gè)方陣內(nèi)的學(xué)生,上圖藍(lán)色部分學(xué)生,學(xué)生助手比較特殊,占用位置空間大小是方陣內(nèi)學(xué)生的4倍,畢竟是小官了)。 剛開始這些學(xué)生助手滿腦子都是在想著怎么偷懶,不想軍訓(xùn),怎么辦,只能先來個(gè)思想教育(指針清零),然后讓每個(gè)助手記憶各自方陣的成員(指針賦值),還有可能時(shí)間緊,思想教育都省了,直接記憶。 一段時(shí)間后,每個(gè)助手對方陣內(nèi)的各個(gè)成員都熟悉起來了。 熟悉到什么程度呢?你隨便問一個(gè)位置他都知道那里站的是誰,比如10號位置就是張三,12號位置是李四。 但是你要問他其它方陣內(nèi)的同學(xué),他就支支吾吾了。 現(xiàn)在假設(shè)他們管理的方陣如下:
現(xiàn)在管理很明確了,咱再來通知一遍! 為了通知黃四郎唱軍歌給教官聽,教官就得先告訴助手學(xué)生A,再讓A同學(xué)通知2號唱歌。 其他方陣內(nèi)的通知也是同樣道理。 這樣一來,想通知哪個(gè)方陣的哪位同學(xué),只要通知管理該方陣的助手即可,該助手自會通知到具體的同學(xué)的。 如此這般,教官只要管理六個(gè)人,就能把六個(gè)方陣的所有學(xué)生都管理起來了,大大減少了教官的管理負(fù)擔(dān),但是好處僅是如此嗎?非也! 現(xiàn)在假設(shè)第一方陣的學(xué)生都有一個(gè)替身(可理解為影分身,特點(diǎn)就是基因相同,也長得一樣,比如1號同學(xué)很聰明,那它的替身也聰明,3號同學(xué)比較笨,它的替身也比較笨,但是它們的記憶可能不一樣):
現(xiàn)在助手人數(shù)只有那么多,不夠該咋辦? 從道理上講,應(yīng)該找管理第一方陣的助手A代理替身第一方陣,為啥,因?yàn)樗钍煜さ谝环疥嚨娜藛T配置?。ū热缢?1號同學(xué)有事請假了,所以替身第一方陣也沒有它的替身,這樣教官要找21號時(shí)助手A就可以馬上他說不在了),但是學(xué)生助手A不愿意啊,這怎么行,一個(gè)人管兩個(gè)方陣,會累死人的,可教官不管,畢竟上哪找一個(gè)能很快熟悉第一方陣的人啊。 但是既想讓助手A管理分身方陣,又要照顧助手A的心情,不能有讓他抗拒心理,咋辦? 很快,教官想出了辦法:催眠(換句話說,就是修改記憶)。 教官是個(gè)催眠高手,很快助手A就被催眠了,雖然第一方陣和替身方陣長得一模一樣,但是它們站的位置是不一樣的,所以他認(rèn)定了離教官遠(yuǎn)的那個(gè)方陣是他管理的方陣,而旁邊那個(gè)才是分身方陣。
現(xiàn)在繼續(xù)通知2號黃四郎(“咋又是我啊”),不過這一次通知的是他的替身,這下該怎么做呢。 因?yàn)槭孪戎諥已經(jīng)被催眠了,所以當(dāng)教官告訴助手A要找黃四郎時(shí),助手A自然而然的通知了替身方陣內(nèi)的黃四郎,而且他很高興的執(zhí)行了,因?yàn)椴挥枚喙芤粋€(gè)方陣了。 當(dāng)教官要通知真正的黃四郎時(shí),又得催眠一遍,讓助手A認(rèn)定離教官近的是自己管理的方隊(duì),沒辦法,缺人嘛,只能教官辛苦一下了。 現(xiàn)在咱們再假設(shè)一種情況,第四方陣的替身組成了一個(gè)新方陣,有幾個(gè)推遲上學(xué)的趕回來軍訓(xùn)了,教官將他們安排在第四方陣后面:
同樣的,還是缺人,這個(gè)時(shí)候應(yīng)該催眠誰呢? 當(dāng)然是學(xué)生助手D了,為啥,因?yàn)榈谒姆疥囁钍煜ぐ?,第四方陣(部分替身)和第四方陣只有兩點(diǎn)不同: 第一:站的位置不一樣。 第二:多了后面部分遲到同學(xué)。 催眠他去管理這部分學(xué)生是不錯(cuò)的選擇,但是因?yàn)閷W(xué)生D只對原生的第四方陣熟悉,對后面趕來的同學(xué)不熟悉,無法管理,也不知道原來30號同學(xué)后面還有一堆人(嗯,近視了),所以教官問催眠后的助手D方陣共有幾人時(shí),只會說共有30人,4人未到。
所以教官被學(xué)生D誤導(dǎo)了,他很相信學(xué)生D報(bào)告的結(jié)果,但是如果他去替身方陣后面查看的話,會發(fā)現(xiàn)那里還站著不少人呢! 上述場景描述應(yīng)該很容易就理解了,那么如何和我們的C語言指針聯(lián)系起來呢? 教官是CPU,一個(gè)個(gè)學(xué)生所站的位置就是內(nèi)存空間,而學(xué)生是這塊空間的屬性(聰明還是笨,或者其他屬性),而學(xué)生的記憶就是內(nèi)存空間的內(nèi)容。 那么方陣是什么?由程序定義的數(shù)據(jù)結(jié)構(gòu)。這個(gè)數(shù)據(jù)結(jié)構(gòu)需要占用空間,有大有小,也可能內(nèi)部空缺(內(nèi)存對齊需要)。 那么那些助手又是什么?本篇的主角:指針。雖然它擔(dān)任著管理任務(wù),但是它的本質(zhì)還是學(xué)生,只是賦予了管理職責(zé)(它也可以只管理一個(gè)學(xué)生(字節(jié)),不一定是方陣(結(jié)構(gòu)體等))。 那么那兩個(gè)假設(shè)在C語言中又代表了什么? 重新賦值和強(qiáng)制轉(zhuǎn)換賦值(強(qiáng)行把一個(gè)大方陣交給一個(gè)只能管理小方陣的助手,但是他自己本身是不知道自己管理了超出自己能力的方陣的)。 現(xiàn)在我們再往細(xì)了說,和實(shí)際C語言代碼聯(lián)系起來:
上圖定義了三個(gè)方陣,每個(gè)方陣的結(jié)構(gòu)是不太一樣的(關(guān)于typedef和struct關(guān)鍵字可看魚鷹相關(guān)筆記),為了更好的理解接下來的知識,以方陣 1 為例介紹上述代碼的含義。 注釋“方陣藍(lán)圖”部分,就如注釋一般,就是一個(gè)藍(lán)圖,它只是告訴編譯器,這個(gè)方陣1應(yīng)該怎么安排,但實(shí)際上根本還不存在這個(gè)東西,這就是一張藍(lán)圖,只用于參考之用(可理解為建筑工人拿著一張建筑圖,準(zhǔn)備按建筑圖的模樣建造一棟房子,但還沒開始建)。 那怎么利用這個(gè)藍(lán)圖搭建一個(gè)方陣出來呢(按建筑圖搭建出一個(gè)實(shí)實(shí)在在的房子)?在C語言里面只需要一句話即可完成,就是上圖中另一個(gè)方框內(nèi)的代碼。 那么第一個(gè)假設(shè)在C語言里是怎樣的呢? 首先需要一個(gè)替身方陣,這個(gè)方陣和方陣1應(yīng)該是一樣的,因?yàn)榉疥?1 是按照藍(lán)圖設(shè)計(jì)的實(shí)物(在內(nèi)存中占有空間,而方陣藍(lán)圖不存在內(nèi)存中),所以咱們可以用藍(lán)圖繼續(xù)建一個(gè)方陣出來:
可以看到這個(gè)替身方陣1(SquareMatrix_1_StandIn)和方陣1的創(chuàng)建過程沒有兩樣,換句話說,兩者是等價(jià)的,只有一點(diǎn)區(qū)別,就是占用的內(nèi)存位置不一樣(可理解為你在北京建了一個(gè)房子,然后又在深圳按照北京的房子樣式又建了一棟一模一樣的房子,區(qū)別就是現(xiàn)實(shí)中你可以直接以北京的房子為藍(lán)圖建造,而在C語言中,你必須先有一個(gè)藍(lán)圖,才能建一棟北京的房子(SquareMatrix_1)和深圳的房子(SquareMatrix_1_StandIn))。 現(xiàn)在我們理解了替身方陣和方陣之間的關(guān)系,再來說說前面所說的催眠問題,在C語言中是如何實(shí)現(xiàn)的呢? 首先需要一個(gè)助手,這樣才有催眠對象嘛。這個(gè)助手有什么特別(屬性)呢:他只能管理以方陣1為藍(lán)圖構(gòu)建的方陣。在C語言怎么達(dá)到這個(gè)要求呢?
這樣符合要求的助手A就誕生了。 現(xiàn)在教練(CPU)讓他管理去管理方陣1:
可以看到,在實(shí)際代碼中根本沒有CPU(教官)的身影,但它卻無處不在,因?yàn)榇a就是靠CPU一步步執(zhí)行的。 現(xiàn)在又想讓他管理替身方陣,就得催眠一下:
可以看到,你理解的催眠在C代碼上和開始教練就讓助手A管理方陣1沒啥區(qū)別,從宏觀上理解,開始就讓助手A管理方陣1也可以認(rèn)為是一次催眠操作,只是在這次催眠之前助手A是沒有任何管理對象的。從這里也可以理解,單獨(dú)的沒有指向一塊內(nèi)存的指針是沒有意義的,就和光桿司令一樣,只有一個(gè)司令,沒有兵,怎么打仗,這樣理解之后,你就不會讓光桿司令(沒有分配士兵)去打仗了,而只要有一個(gè)小兵,那么司令就能指揮了。 那么第二個(gè)假設(shè)在C語言中又是怎么回事呢?
按照C語言要求,必須先有一個(gè)藍(lán)圖(專業(yè)術(shù)語稱為“聲明”),然后才能創(chuàng)建一個(gè)新方陣出來,并且需要一個(gè)能管理方陣2的助手B:
因?yàn)樵谡Q生助手B的時(shí)候,基因上決定了他只能管理方陣1,對使用新藍(lán)圖SquareMatrix_2_PartTypedef創(chuàng)建的SquareMatrix_2_Part是無法管理的,但是因?yàn)镾quareMatrix_2_Part前面部分是利用方陣2的藍(lán)圖構(gòu)建的,所以讓他管理前面部分的倒是沒有問題,這個(gè)和催眠又有點(diǎn)差別:
前面說了C語言,現(xiàn)在又不得不說一說匯編語言與編譯器。那么怎么理解它們之間的關(guān)系呢? C語言和匯編語言之間隔了一個(gè)編譯器。 用通俗的話介紹就是,一個(gè)不懂鳥語的人(C程序員),要和鳥(單片機(jī))對話,就要一個(gè)懂鳥語的人做翻譯官,通過這個(gè)翻譯官將人類的語言(C語言)和轉(zhuǎn)化成鳥語(匯編語言),從而實(shí)現(xiàn)對話。 只不過,他們的交流是一次性完成的,就是說不懂鳥語的人把所有要說的話(C語言代碼)一次性告訴翻譯官,翻譯官翻譯好了之后(匯編語言代碼),再告訴鳥(單片機(jī)),“你應(yīng)該干啥、不該干啥”,小鳥記住了之后,就一遍遍按照要求重復(fù)執(zhí)行了。 從這里可以明白,單片機(jī)存放的是匯編代碼(更準(zhǔn)確是0、1的二進(jìn)制),而不是我們程序員看到的C語言代碼。 另外還可以知道的一點(diǎn)就是,與其說我們程序員是在和單片機(jī)打交道,不如說同時(shí)在和單片機(jī)與編譯器打交道,既要了解單片機(jī)(鳥)能干什么,也要清楚編譯器的規(guī)則,不能讓翻譯官譯錯(cuò)了你的意思,否則你說往東,他還以為你說往西呢! 看過一個(gè)故事,說Unix操作系統(tǒng)的創(chuàng)造者,總是能很快的黑進(jìn)任何一個(gè)Unix系統(tǒng),別人一直以為是Unix代碼中留下了暗門,但查了很久也沒發(fā)現(xiàn),后來才知道是編譯源碼的編譯器留下了暗門。 不管這個(gè)故事是否真實(shí),但編譯器的重要性毋庸置疑。 現(xiàn)在我們再簡單聊聊編譯器的一點(diǎn)好處,讓我們知道為什么需要編譯器。 了解匯編的人都知道,單片機(jī)的內(nèi)存都是程序員分配的,而且是直接和內(nèi)存地址打交道。 比如1號內(nèi)存放一個(gè)同學(xué),2號內(nèi)存放另一個(gè)同學(xué)(所有內(nèi)存空間都進(jìn)行了編號,就是地址),找人的時(shí)候就通過這個(gè)編號找。但是數(shù)字記憶不是人類的強(qiáng)項(xiàng),所以就給這些編號命名了一個(gè)別名,比如1號叫做張三,2號是李四,當(dāng)叫2號唱歌時(shí),只要叫李四即可。
但事實(shí)上很少有人這么干!畢竟如果只是取個(gè)別名也沒方便到哪里去。 更多的時(shí)候,我們都是告訴編譯器,我們需要兩個(gè)空間,一個(gè)空間放張三,一個(gè)空間放李四,但具體放在哪,我不管:
?
編譯后,從 .map 文件中(如何打開該文件可參考魚鷹以前的筆記)可知道,編譯器將這兩個(gè)命名為張三和李四的空間放在了0x20000018 和 0x20000019 (注意這里沒有4字節(jié)對齊)里面(事實(shí)上,每次改變代碼后編譯,這些空間地址可能會發(fā)生變化,不變的是這個(gè)空間名,你總是能通過這個(gè)空間名去訪問一塊內(nèi)存,只是可能兩次編譯后再訪問時(shí),它所在的空間地址不一樣罷了,一般來說,這沒什么大不了的,畢竟每次使用這塊空間前都會進(jìn)行初始化)。 所以在這件事上,編譯器為我們做了兩件事:第一,分配內(nèi)存地址;第二,命名這塊空間名字(事實(shí)上還有第三件事情,規(guī)定這個(gè)空間只能放char類型數(shù)據(jù))。 而張三、李四這個(gè)名字不僅代表了兩塊空間,還有一個(gè)額外的空間屬性要求:只能存放char類型數(shù)據(jù),操作這些空間時(shí)一定要注意這一點(diǎn)(有些錯(cuò)誤操作可能編譯器會發(fā)出提示信息,而有些操作編譯器發(fā)現(xiàn)不了,所以需要額外注意)! 其實(shí)引入編譯器的好處不止于此,這里只是簡單介紹,不深入講述。 現(xiàn)在我們再來從匯編語言的角度去看指針賦值與強(qiáng)制轉(zhuǎn)化過程。
從上面可以看到,所謂的指針賦值和強(qiáng)制轉(zhuǎn)化,在匯編代碼的層面上可以說完全一樣,都只是賦值操作,都是將方陣的地址賦值給寄存器R0,方陣指針的地址給寄存器R1,最后方陣的地址賦值給方陣指針?biāo)诘牡刂罚◤倪@可以了解到,內(nèi)存和內(nèi)存之間不可以直接操作,必須通過寄存器中轉(zhuǎn),這就是為什么明明只有一條C語言代碼,匯編語句卻有多條的原因之一了,C語言封裝了很多操作細(xì)節(jié),雖然我們可以不去深究,但必須了解它的存在)。 以第一條C語句為例,用示意圖表示(只把涉及到的內(nèi)存空間畫出,填充顏色部分為實(shí)際內(nèi)存空間,未填充部分用于說明空間地址或者寄存器,xxxxxxxx表示不必關(guān)心原來的內(nèi)容是什么,紅色表示操作后發(fā)生的變化):
而操作結(jié)構(gòu)體內(nèi)的變量Student_12如下:
?
首先把0賦值給R0,把指針?biāo)诘目臻g地址(0x080010CC處存在一個(gè)地址)賦值給R1,再把R1存在的地址的內(nèi)容賦值給R1(這時(shí)這個(gè)R1存放的就是方陣結(jié)構(gòu)體的首地址),最后把R0賦值給相對R1地址偏移4的Student_12中。 示意圖如下:
而直接操作結(jié)構(gòu)體的方式如下(不采用指針):
首先將0x01賦值給R0,然后將0x080010CC處的內(nèi)容賦值給R1,最后把R1的內(nèi)容當(dāng)做地址,并將R0賦值給這個(gè)地址相對偏移0x04的地址處。 示意圖如下:
從中對比兩者操作可以發(fā)現(xiàn),當(dāng)不使用指針操作0x20000054空間時(shí),只需要三條語句,而使用指針,因?yàn)樯婕暗綄?x20000010內(nèi)存空間的操作,多增加了一條指令,即從0x20000010(指針)處獲得操作基地址0x20000050,再做最終的賦值操作,除此之外的操作指令都是類似的。
????????????????????(通過KEIL查看結(jié)構(gòu)體內(nèi)的變量在內(nèi)存中的地址) 從上面也可了解到,所謂的指針,只是人為的把內(nèi)存里面的內(nèi)容當(dāng)做地址而已,因?yàn)槟惆汛嬖?x20000010處的0x20000050當(dāng)做了地址去操作,才存在如下關(guān)系:
你使用C語言去操作0x20000010時(shí)才會影響到0x20000050處的變量。 但是如果你不把它0x20000050當(dāng)做地址,而只是當(dāng)做普通數(shù)據(jù)處理也是可以的:
(事實(shí)上,當(dāng)增加變量data 編譯之后,SquareMatrix_1_Assistant的內(nèi)容不再是0x20000050,而是變成了0x20000058,這里我們假設(shè)編譯器為之前的變量分配的空間地址不變) 當(dāng)然你現(xiàn)在又希望把這個(gè)數(shù)據(jù)當(dāng)成地址,咋辦?
這樣一來,不僅把數(shù)據(jù)0x20000050當(dāng)成了地址,還改變了可訪問的數(shù)據(jù)大小,即只能訪問int大小數(shù)據(jù),結(jié)構(gòu)體內(nèi)的其他數(shù)據(jù)無法訪問:
我們嘗試對0x20000058這個(gè)地址賦值:
當(dāng)你理解了上述內(nèi)容,你會發(fā)現(xiàn),指針,不過如此! 其實(shí)通過以上內(nèi)容的講述,我們也可以總結(jié)使用一塊內(nèi)存需要注意的三個(gè)要素:地址、基因(屬性)、內(nèi)容。 所謂地址,就是這個(gè)空間所在的地址;屬性,就是規(guī)定這塊內(nèi)存能存放什么東西,char還是int,或者其他自定義類型等等;而內(nèi)容就是內(nèi)存中存放的東西,這是程序員真正需要的東西,前兩者都是為其服務(wù)的。 那這個(gè)和指針有啥關(guān)系,別忘了前面所說,指針也是一塊內(nèi)存,只是這個(gè)內(nèi)存的屬性規(guī)定了兩個(gè),第一,存放指針(這個(gè)也只是普通數(shù)據(jù),只是需要按照C語言要求處理),第二,這個(gè)指針指向的內(nèi)容是char、int……數(shù)據(jù)類型而已(也可能會規(guī)定其他屬性,這里不討論),從內(nèi)存空間的角度上,所謂的指針和普通的數(shù)據(jù)類型沒有本質(zhì)區(qū)別。 那么學(xué)習(xí)指針有什么好辦法嗎?魚鷹認(rèn)為,除了要深刻理解指針外,還有就是要像魚鷹一樣,畫圖去表示它們之間的關(guān)系(不用像前面一樣畫的那么清楚,畫個(gè)示意圖即可),只有這樣,你才不會被那些指向關(guān)系搞得稀里糊涂。也只有這樣,你才能在代碼中靈活運(yùn)行指針去做你任何想做的事情。 ?
這篇筆記修修改改不知道多少次,原以為能比較快就能寫好的,但事實(shí)上花了好幾天才寫完,因?yàn)轸~鷹要盡可能的將故事貼合實(shí)際的 C語言運(yùn)行情況,所以花了不少時(shí)間去思考,但真正難的還在于如何把心中所想畫出相應(yīng)示意圖,這個(gè)是最耗費(fèi)時(shí)間的。
盡管如此,指針這一塊還是沒有完成,道友看了前面大綱也可了解,這只是第一點(diǎn)內(nèi)容,后面還有五點(diǎn)沒寫,以后有時(shí)間再說吧。
編輯:黃飛
評論