• 鄭州
      您的位置: 法律保 > 綜合 > 詳情

      全球熱門:你管這破玩意叫指針?

      來源: 程序員客棧 2023-01-19 11:42:02

      本系列分三篇,用破玩意的方式徹底理解好指針的本質:


      (相關資料圖)

      你管這破玩意叫指針 -- 基礎篇

      你管這破玩意叫指針 -- 進階篇

      你管這破玩意叫指針 -- 變態篇

      話不多說,開始!

      內存,通常被嚴謹地畫成下面這個樣子,一個下方是低地址上方是高地址的格子樓。

      但我今天換種畫法,畫成下面這個樣子。

      每個格子代表內存中的 1 個字節(8 位),格子上的數字就代表內存地址,我也直接用 10 進制來表示了,免得 16 進制又算不明白了。

      目前內存是完全空的,格子里沒有任何內容。

      試想一下,如果你忘掉所有的語法規則和編程規范,你會如何描述對這些內存格子的操作呢?

      一、類型系統

      很簡單,往格子 3 處放個數字 29,往格子 6 處放個數字 38,就這么簡單直接地描述即可。

      但是這樣說話太麻煩了,什么往格子 3 處放個數字 29 的,廢話太多,也不方便不講感情的計算機去理解。

      那我們就定個指令,使用 mov $x, (y) 表示把數字 x 放入格子 y,如下:

      mov$29,(3)mov$38,(6)

      這就表示剛剛說的:

      把數字29放入內存格子3把數字38放入內存格子6

      是不是太簡單了?別急,好戲馬上開始!

      如果要把數字 999 放入內存格子 8,該怎么辦呢?

      由于 1 個格子表示 1 個字節,只有 8 位,因此只能表示 256 個數字,要么是有符號的 -128 ~ 127,要么是無符號的 0 ~255,顯然數字 999 無法放在 1 個格子內,只能占用 2 個格子了。

      那也好辦,就這么說,把數字 999 放入格子 8,連續占用兩個格子。

      但這樣,我們剛剛的 mov 指令就得改改了,不但要表示"存放"這個含義,還得表示占用了多少個格子。

      我們用 movb 表示只占 1 個字節,用 movw 表示占用 2 個字節。那么,剛剛的三個數字,就分別可以這樣用指令來表示了:

      movb$29,(3)movb$38,(6)movw$999,(8)

      含義就是:

      把數字29放入內存格子3,占1個字節把數字38放入內存格子6,占1個字節把數字999放入內存格子8,占2個字節

      OK,既然有了 1 字節和 2 字節的的指令,不妨再設計下,用 movl 表示 4 字節,movq 表示 8 字節 ...

      movb占用1字節movw占用2字節movl占用4字節movq占用8字節

      不知不覺,類型系統就被你悄悄設計出來了!當然,雖然這只是個半成品。

      二、變量

      你不斷地往不同格子里放數據。

      比如我把我的年齡放在 11 號格子(占 1 字節),把我的月薪放在 14 號格子(占 4 字節)。

      現在我們的內存已經非常混亂了,你根本記不住原來的 3 號格子放的數據表示什么,11 號格子又表示什么,只能通過看數字知道 14 號格子里放的確實是我的月薪。這該怎么辦呢?

      增加一層抽象嘛!我們給這些放了我們數據的格子,都貼上個標簽,就可以不用再記那些無意義的格子編號了。

      這樣以來,其實我們也不再關心,這些標簽到底在哪個格子里,只要給我找到格子把我的數據放進去就可以了。

      movb$29,amovb$38,bmovw$999,cmovb$18,agemovl$2147483647,salary

      當然,我還需要再通過這個標簽,把我剛剛放進去的數據找出來。

      這很簡單,但存在一個問題,放進去的時候,我們可以通過 movb,movw,movl 等知道占用多少個格子。而取出來的時候,標簽上可沒有寫這個數據占用了多少個格子,這是有問題的。

      因此,在定義這個標簽時,不能光取個名字,還需要有個信息就是,這個標簽對應的數據,占了多少個格子。

      我們就效仿剛剛的存放操作,也規定一系列單詞,來修飾這些標簽,表示占用了多少個格子。

      char 表示 1 個字節,short 表示 2 個字節,int 表示 4 個字節,long 表示 8 個字節 ...

      于是乎剛剛的 5 個數據,就可以表示為如下指令:

      chara=29;charb=38;shortc=999;charage=18;intsalary=2147483647;

      行了,我也別藏著掖著了,相信大家也知道,這里就是 C 語言的寫法,而剛剛那堆 mov 是匯編語言的寫法。

      這些 char a,char b,int salary 等,就是變量!記住,變量不但要有名字,還得有類型!

      三、變量定義與賦值

      其實,剛剛的寫法,是把變量的定義與賦值操作寫在一行了。

      比如有如下語句:

      inta=1;

      實際上是分成兩步的:

      //變量的定義inta;//變量的賦值(此處也可以叫變量的初始化)a=1;

      其中變量的定義是為了方便程序員后面去用它,這部分不是給 CPU 看的。

      而變量的賦值才是真正在內存中把數據放進去,這部分才真正涉及 CPU 具體指令的執行。

      也就是說,如果你僅僅定義了一個變量 int a; 但是沒有給它初始化的賦值操作,那么最終在 CPU 執行指令的時候,這個定義根本就沒有任何體現。

      四、指針

      現在,讓我們把內存清空,回到一開始的那一片凈土上。

      我們來搞點花樣。我將我的密碼(1234)存儲在一個 short a 中,假設這個變量 a 被放在了 6 號格子處。

      同時,我將這個變量 a 的地址,也就是 6 這個數字,存儲在另一個變量 int p 中,假設這個變量 p 被放在了 1 號格子處。

      這樣,我尋找我密碼的方式,就是先通過 p 所在的內存地址找到里面存的值,也就是 a 的內存地址 6,再通過 a 的內存地址找到里面存的值,也就是我要找的密碼 1234。

      我們可以用下面的代碼來表示剛剛的存放邏輯。

      shorta=1234;//假設a被放在了6號格子處intp=6;

      這里的 p 和 a 都是變量,只不過,p 這個變量有點特殊,它里面存放的值是一個內存地址,我們把 p 這個變量形象地成為指針變量,簡稱指針。

      不過,這樣有幾個問題,我一個個來說。

      1. 取地址

      首先,我們在編碼階段,無法知道也無需知道變量 a 會存放在哪里,不然就失去了標簽的含義,又回到了需要關心具體的內存地址(也就是格子編號)的時代了。

      所以,我們應該有個方法,來在編碼階段表示變量 a 的地址的含義,姑且就叫做 &a 吧。

      那么我們的代碼,就可以優化為:

      shorta=1234;//假設a的地址是6//那么下面的p就等于6intp=&a;

      用圖來表示就是:

      2. 指針變量本身的大小

      視角放到這個變量 p 身上,雖然本質上這個變量 p 里面存放的就是一個數值,假設是 6,但是它卻表示了一個內存地址的值。

      如果讓程序員隨便規定這個變量 p 的數據類型(也就是占多少個字節),那顯然容易出問題。

      比如內存地址是 999,那么我用一個 char 類型的變量 p 來存放它,就會有問題。

      我們在編碼階段是無法確定一個變量的內存地址是多少的,所以用什么類型的變量來存放它,也是無法判斷的。

      所以,最穩妥的辦法就是,用一個完全能容納所有內存地址范圍的變量類型來存放指針變量。

      我們姑且認為我們是在一個 32 位的系統上,那么用一個 4 字節大小的變量來存放,就可以了。(當然,實際上這取決于你的編譯器的位數)

      現在,我們的指針變量所占用的內存大小,就是固定的 4 個字節,也就是 4 個格子。

      程序員無需也無法修改這個大小,那么我們就可以把 p 前面的數據類型去掉了。

      shorta=1234;p=&a;

      3. 指針變量的類型

      剛剛我們解決了指針變量本身所占用的內存大小,但是還有一個問題沒有解決,就是指針變量里存放的內存地址處的變量的大小。

      也就是說,上面的指針變量 p 里雖然存放了變量 a 的內存地址 6,但是指針變量 p 卻沒有任何信息,來說明內存地址 6 處的變量,它的大小是多少。

      假如,我們認為內存地址 6 處的變量是個 char 類型,也就是只占用了一個字節,那么顯然,會取出一個不符合預期的值。

      當然,如果認為 6 處的變量是個 int 類型,占 4 個字節,雖然數值上可能沒有問題,但從某種程度上講也是不太符合預期的(假如 8 號和 9 號格子里有其他內容,那就更不符合預期了)。

      所以,必須得完全按照變量本身的類型,也就是 short 類型來讀取此內存地址處的值,才是正確的。

      那我們應該如何表示這個信息呢?即如何表示,變量 p 是一個指針,且這個指針里面存放的內存地址處的變量的類型是 short。

      很好辦,直接說答案吧。

      shorta=1234;short*p=&a;

      p 前面的 * 表示變量 p 是一個指針類型,再前面的 short 表示該指針指向的內存地址處的變量,是個 short 類型的變量。

      當然,更準確的說法是,指針 p 將會按照 short 類型的變量來讀取它指向的內存,至于那里到底是什么,無所謂。

      注意哦,這個 short 并不是表示指針變量本身的大小占 2 個字節,指針變量本身我們前面說過了,就是固定的 4 字節大小。

      不過總是這樣說太繞口了,今后我們就說,變量 p 是個 short * 類型的指針,就可以了。

      用上面的圖形象地說就是,右邊變量 a 藍色的填充,表示 a 是個 short 類型,而外面的虛線框框,表示指針 p 按照 short 類型的變量來"解讀"內存地址 6 處的數值。

      兩者相匹配了,就是"正確"的編程代碼了。

      當然,這里的"正確",是說給程序員聽的,CPU 才不關心。

      4. 指針所指向的值

      上面我們已經可以獲得某個變量的地址,比如獲取 a 的地址就是:

      &a

      同時我們也可以定義一個指針變量,比如定義一個 short * 類型的指針變量 p:

      short*p;

      并且,我們通過直接賦值操作,可以給指針變量進行初始化:

      p=&a;

      當然,上面的代碼也可以連起來寫,即指針變量 p 的定義與初始化寫在同一行:

      short*p=&a;

      不過,我們還沒有一個方法,來表示指針變量 p 所指向的那塊內存。

      那我們就發明一個,比如想把 p 所指向的那塊內存的值改為 999,可以這樣寫。

      *p=999;

      這里的 * 就表示"指向"的含義,即 *p 不是說 p 這個變量的內存地址,而是把 p 這個變量里存的內容當做內存地址來看,指向這個內存地址。

      用圖表示就是:

      所以連起來一個完整的程序就是:

      shorta=1234;//指針的定義short*p;//指針的初始化,也即指針變量本身的值p=&a;//指針變量所指向的內存地址的值*p=999;

      執行過后,a 的值會變成 999,或者說 6 號格子與 7 號格子里的值會變成 999。

      5. 指針的加減

      如果對一個普通變量 +1,比如說:

      inta=1;intb=a+1;

      那顯然,b 的值應該是 2,毫無疑問。

      但是如果對一個指針變量 +1,會怎么樣呢?

      inta=1;int*p=&a;int*p2=p+1;

      我們假設變量 a 放在了格子 1 處。

      變量 a 的值是什么,以及變量 p 被放在了哪里,我們都不關心,就只盯著 p 的值看,顯然,一開始的時候是 1。

      (為方便演示,下面的圖直接表示 p 所指向的內存地址,而不是 p 本身所在的內存地址)

      我們先不考慮,p + 1 應該是幾,如果讓你來設計這個語言,你覺得 p + 1 是幾比較好呢?

      我認為,只有兩種較為合理的設計。

      第一種,p + 1 就等于 2,就簡簡單單當做數值進行加法運算而已。

      第二種,p + 1 等于 5,即跨過一個 p 所指向的內存單元的數據類型的大小,也就是 4 字節的 int。

      你覺得那種比較合理呢?

      那顯然是第二種嘛!不然和普通變量有啥區別了,你既然設計出了指針變量這個玩意,就需要讓它發揮點方便程序員的作用,這才是你設計它的真正目的。

      當然你不服,你就想讓這個 int * 類型的指針變量,就真真正正在數值上只 +1,也就是讓 p 等于 2,該怎么辦呢?

      很簡單,分成三步就好了:

      第一步,把 int * 類型的 p 強轉為 char * 類型的 p。

      第二步,p + 1。

      第三步,再把 char * 類型的 p 強轉為 int * 類型。

      完事!用代碼表示就是:

      p=(int*)((char*)p+1);

      你會看到,C 語言項目中經常使用這樣的玩法。

      當然,你這一頓花里胡哨的操作,在 CPU 眼里,就是對一個內存地址處的值簡簡單單地 +1 而已。

      五、指針的本質

      我們看上面的一張圖:

      其實,別看上面又 short * p 又 short a 的,這是給程序員和編譯器看的。

      在 CPU 眼里,根本沒有這些眼花繚亂的標簽,以及五花八門的解讀,就是 0 ~ 4 號格子里存了個數字 6,然后 6 ~ 7 號格子里存了個數字 1234,僅此而已。

      更進一步講,其實就只是 1 號格子里存儲了數字 6(234 號格子是空的),6 號格子里存儲了數字 12,7 號格子里存儲了數字 34。

      (當然實際得轉換成二進制,再結合大端序還是小端序來看哈,我這里就是簡單直觀告訴大家 CPU 才不管那么多,就一個格子一個格子的放數字就完事了)

      所以,我們經常聽書上講,讓大家一定要記住,指針變量中只能存放地址,不要將一個整數或任何其他非地址類型的數據賦給一個指針變量了。

      這種說法就非常別扭,很多書上,即想講清楚指針的本質,又想講清楚指針的注意事項,混雜在一起,讓讀者即沒有搞清楚指針的本質,又不知道指針的注意事項。

      真糾結!

      說實話,就光看書而沒有經過大量 C 語言的實踐,誰能記得住或者理解透徹那些注意事項。而經過大量 C 語言實踐的人,指針早就融入進血液中了,誰還來看你講指針的本質?所以說,這塊我覺得非常之矛盾。

      實際上,指針變量的本質和普通變量是一樣的:

      普通變量,寫個 short a,是在告訴編譯器,當我 a = 1 時,你給我找到一塊 2 字節的內存,把 1 填充進去。

      指針變量,寫個 short * p,是在告訴編譯器兩件事情:

      當我 p = xxx 時,你給我找到一塊 4 字節的內存(我們假設指針本身的大小固定 4 字節),把 xxx 填充進去,這就和普通變量完全一樣;

      當我 *p = yyy 時,你給我找到 xxx 內存地址,并且按照 short 類型也就是 2 字節大小,把 yyy 填充到這里。

      所以,誰說不能把一個整型變量賦給指針了,我這不就把一個整型變量 xxx 賦給指針 p 了么,我賦值的時候就說它是整型變量了,怎么的吧?

      但是我用它的時候,我 *p 又把 xxx 看做是一個內存地址了,就去找內存 xxx 的地方,又怎么的吧?

      用代碼來表示就是:

      我強行把一個整型數值 6 賦值給指針變量 p,然后 *p 去訪問內存地址 6 并修改那個地方的值:

      int*p=6;*p=999;

      我還可以把一個地址值,強行賦值給一個普通變量:

      inta=1;intb=&a;

      這時普通變量 b 里面存儲著 a 的地址,我 *b 也同樣可以訪問到 a 并修改它的值:

      *b=999;

      當然如果你真這么寫編譯器會報錯,但沒關系,我們可以先把普通變量 b 強轉為指針變量,然后再 * 它:

      *(int*)b=999;

      你還可以玩些更花哨的,先 & 取地址,再 * 取值,雖然沒啥用:

      *((int*)*(&p))=999;

      假如 a 的地址是 6 的話,其實你這些花里胡哨的操作,最后到人家 CPU 眼里,就是一條簡單的指令:

      movl$999,(6)

      就是想把 999 放在 6 號格子嘛!

      所以,不要把指針想得多么復雜和神圣,它就是方便了程序員編程,同時告訴編譯器應該怎么編譯成最終的指令。

      你寫了個 *p,就是把 p 的值當做內存地址去訪問,在匯編語言層面就是加了個括號:

      (p)

      你寫了個 &a,就是取出變量 a 的內存地址,在匯編語言層面就是 lea 指令:

      leaa,xxx

      你如果寫了個 ***p 那就是,相當于加了三次括號:

      (((p)))

      當然啦,以上都是方便理解的偽指令,具體落實到真正的匯編語言,我會在后續的章節中講述,直接從匯編語言理解指針,你就會發現指針就是個工具人而已。

      六、寫在最后

      至此,我們的《你管這破玩意叫指針 -- 基礎篇》就講完了。

      我們從最開始的內存格子出發,逐漸推導出類型系統和變量的作用,進而再引出本質上和普通變量沒有任何區別的指針變量,最后再推導出指針變量相關的操作,帶你看清了指針的本質。

      你不要去記本文的知識點,重在整個推導的過程,要去理解指針想解決的問題是什么,它的合理性在哪,哪一部分信息是給程序員和編譯器看的,哪一部分操作最終又是真正落實到 CPU 指令的,這些才是關鍵。

      當然,我還是給你簡單總結下知識點相關的部分,其實簡單說,就這么幾件事。

      定義一個指針:

      int*p;

      賦值或初始化一個指針:

      p=&a;

      修改指針的內容:

      *p=999;

      指針的加減(其實到后面講的數組才有價值):

      p=p+1;

      完事,就這些!

      最后,給大家推薦兩個網站。

      一個是可以將 C 語言代碼實時編譯成匯編代碼,你可以用它來自己玩指針做實驗,看它最終到 CPU 指令層面是什么樣。

      https://godbolt.org

      一個是 GNU C 手冊,里面對各種語法和作用講述的非常清楚,不要再用百度搜博客了。

      https://www.gnu.org/software/gnu-c-manual/gnu-c-manual.html

      比如講類型系統里的整型類型:

      再比如講指針的定義和初始化:

      我相信本文看下來,一定有人想問,short * p 是不是應該寫成:

      short*p

      或者

      short*p

      自己去上面的文檔里找答案即可。

      OK,本文到這里就終于要結束了,在接下來的進階篇里,我會講述二級指針、數組、函數指針、字符串、結構體、結構體數組與指針等內容。

      雖然說是進階篇,但我認為,指針的本質反而是進階,而指針的進階反而是基礎。

      因為假如理解了上述的一切,下面的所謂指針進階玩法,都可以通過指針的本質以及語言設計的合理性,推導出來,再往后無非是需要花時間熟練使用和掌握罷了。

      所以,理解好今天的內容,非常重要!

      敬請期待:

      你管這破玩意叫指針 -- 進階篇

      你管這破玩意叫指針 -- 變態篇

      標簽: 指針變量 匯編語言 類型系統
      溫馨提示:

      在實際法律問題情景中,個案情況都有所差異,為了高效解決您的問題,保障合法權益,建議您直接向專業律師說明情況,解決您的實際問題。 立即在線咨詢 >

      相關知識推薦
      操作
      分享
      15037178970

      公眾服務

      法制網公眾號

      快速找律師 / 免費咨詢

      查法律知識 / 查看解答 / 隨時追問

      律師服務(工作日8:30-18:00 ,非工作日請QQ留言)

      律師加盟

      律師營銷服務

      在線客服:

      加盟熱線:

      律師營銷診斷

      營銷分析 / 回復咨詢

      案件接洽 / 合作加盟

      法律保,中國知名的 法律咨詢網站,能夠為廣大用戶提供在線 免費法律咨詢服務。
      CopyRight@2003-2023 falvbao.net.cn ALL Rights Reservrd 版權所有
      皖ICP備2022009963號-45
      違法和不良信息聯系郵箱:39 60 29 14 2 @qq.com

      亚洲一区二区三区写真| 久久影视综合亚洲| 亚洲精品伦理熟女国产一区二区| 中文字幕不卡亚洲| 亚洲午夜成激人情在线影院 | 亚洲另类图片另类电影| 亚洲国产成人久久精品99| 亚洲中文字幕无码av在线| 精品亚洲一区二区三区在线播放| 日本亚洲欧美色视频在线播放| 亚洲综合欧美色五月俺也去| 亚洲成a人片7777| 亚洲系列国产精品制服丝袜第| 久久精品国产亚洲AV无码娇色| 亚洲欧洲免费视频| 亚洲人成网址在线观看| 亚洲激情中文字幕| 亚洲视频免费在线看| 亚洲沟沟美女亚洲沟沟| 亚洲国产综合精品| 久久精品国产99国产精品亚洲| 亚洲中文字幕AV在天堂| 中文字幕乱码亚洲无线三区 | 亚洲一区二区三区91| 色偷偷女男人的天堂亚洲网| 久久综合久久综合亚洲| 亚洲私人无码综合久久网| 亚洲AV无码成人网站在线观看| 国产精品亚洲AV三区| 亚洲国产精品无码久久久久久曰 | 国产成人精品久久亚洲高清不卡| 国产一区二区三区亚洲综合| 亚洲成A人片在线观看无码3D | ww亚洲ww在线观看国产| 国产亚洲福利在线视频| 亚洲av日韩aⅴ无码色老头| avtt亚洲天堂| 亚洲无人区午夜福利码高清完整版| 国产亚洲精品国产| 亚洲高清日韩精品第一区| 亚洲人成综合在线播放|