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