3 挑戰指針(harib01c)
前面說過“C語言中沒有直接寫入指定內存地址的語句”,實際上這不是C語言的缺陷,因為有替代這種命令的語句。一般大多數程序員主要使用那種替代語句,像這次這樣,做一個函數write_mem8的,也就只有筆者了。如果有替代方案的話,大家肯定想用一下,筆者也想試試看。
write_mem3(i, i & 0x0f);
替代以上語句的是:
*i = i & 0x0f;
兩個語句有點像,但又不盡相同。不管那么多了,先換成后面一種寫法看看吧。好了,改完了,用“make run”命令運行一下。唉?奇怪,怎么會出錯呢?
invalid type argument of `unary *'
類型錯誤?
■■■■■
沒錯,就是類型錯誤。這種寫法,從本質上講沒問題,但這樣就是無法順利運行。我們從編譯器的角度稍微想想就能明白為什么會出錯了。回想一下,如果寫以下匯編語句,會發生什么情況呢?
MOV [ 0x1234], 0x56
是的,會出錯。這是因為指定內存時,不知道到底是BYTE,還是WORD,還是DWORD。只有在另一方也是寄存器的時候才能省略,其他情況都不能省略。
其實C編譯器也面臨著同樣的問題。這次,我們費勁寫了一條C語句,它的編譯結果相當于下面的匯編語句所生成的機器語言,
MOV [i], (i & 0x0f)
但卻不知道[i]到底是BYTE,還是WORD,還是DWORD。剛才就是出現了這種錯誤。
那怎么才能告訴計算機這是BYTE呢?
char *p; /*,變量p是用于內存地址的專用變量*/
聲明一個上面這樣變量p, p里放入與i相同的值,然后執行以下語句。
*p = i & 0x0f;
這樣,C編譯器就會認為“p是地址專用變量,而且是用于存放字符(char)的,所以就是BYTE.”。順便解釋一下類似語句:
char *p; /*用于BYTE類地址*/ short *p; /*用于WORD類地址*/ int *p; /*用于DWORD類地址*/
這次我們是一個字節一個字節地寫入,所以使用了char。
既然說到這里,那我們再介紹點相關知識,“char i; ”是類似AL的1字節變量,“short i; ”是類似AX的2字節變量,“int i; ”是類似EAX的4字節變量。
而不管是“char *p”,還是“short *p”,還是“int *p”,變量p都是4字節。這是因為p是用于記錄地址的變量。在匯編語言中,地址也像ECX一樣,用4字節的寄存器來指定,所以也是4字節。
■■■■■
這樣準備工作就OK了。再用“make run”運行一遍以下內容。
void HariMain(void) { int i; /*變量聲明。變量i是32位整數*/ char *p; /*變量p,用于BYTE型地址*/ for (i = 0xa0000; i <= 0xaffff; i++) { p = i; /*代入地址*/ *p = i & 0x0f; /*這可以替代write_mem8(i, i & 0x0f); */ } for (; ; ) { io_hlt(); } }
哇,居然不使用write_mem8就能顯示出條紋圖案,真是太好了。
嗯?且慢!仔細看看畫面,發現有一行警告。
warning: assignment makes pointer from integer without a cast
這個警告的意思是說,“賦值語句沒有經過類型轉換,由整數生成了指針”。其中有兩個單詞的意思不太明白。類型轉換是什么?指針又是什么?
類型轉換是改變數值類型的命令。一般不必每次都注意類型轉換,但像這次的語句中,如果不明確進行類型轉換,C編譯器就會每次都發出警告:“喂,是不是寫錯了?”順便說一下,cast在英文中的原意是壓入模具,讓材料成為某種特定的形狀。
指針是表示內存地址的數值。C語言中不用“內存地址”這個詞,而是用“指針”。在C語言中,普通數值和表示內存地址的數值被認為是兩種不同的東西,雖然筆者也覺得它們沒什么不同,但也只能接受這種設計思想了。基于這種設計思想,如果將普通整數值賦給內存地址變量,就會有警告。為了避免這種情況的發生,可以這樣寫:
p = (char *)i;
這就對i進行了類型轉換,使之成為表示內存地址的整數。(其實這樣轉換以后,數值一點都沒變,但對于C編譯器來說,類型的不同有著很大的差別。)以后再進行這樣的賦值時,就不會出現這種討厭的警告了。于是我們這樣修改一下。
再運行一次“make run”吧。好了,不再出現那種煩人的警告了。write_mem8已經沒用了,所以可以將它從naskfunc.nas中刪除。
這樣的寫法雖然有點繞圈子了,但我們實現了只用C語言寫入內存的功能。
COLUMN-2 只要使用類型轉換,就可以不用指針之類的方法嗎?
好不容易介紹完了類型轉換,我們來看一個應用實例吧。如果定義:
p = (char *) i;
那么將上式代入下面語句中。
*p = i & 0x0f;
這樣就能得到下式:
*((char *) i) = i & 0x0f;
這個語句執行起來毫無問題。雖然讀起來不是很容易理解,但這樣可以不特意聲明p變量,所以筆者偶爾還是會使用的。
有沒有覺得這種寫法與“BYTE[i] = i & 0x0f; ”有些相像嗎?在特別喜歡匯編語言的筆者看來,會有這種感覺呢。(笑)
COLUMN-3 還是不能理解指針
能有這種想法,說明你很誠實。那好,我們再盡量詳細地講解一下。
如果你曾經使用過C語言,并且聽說過“指針”這個詞,那么剛才的說明肯定讓你覺得混亂,摸不著頭腦。倒是那些從未接觸過C語言的人更能理解一些。
這里,特別重要的一點是,必須想點辦法讓C語言完成以下功能:
MOV BYTE [i], (i & 0x0f)
也就是,向內存的第i號地址寫入i & 0x0f的計算結果。而程序只是偶然地寫成了:
int i; char *p; p = (char *) i; *p = i & 0x0f;
必須要先理解以上程序。這可能與你所知道的指針的使用方法完全不同,不過暫時先不要想這個。總之上面4行,是MOV語句的替代物,這一點是最重要的。
從沒聽說過C語言指針的人,僅僅會想“哦,原來C語言中是這么寫的,沒那么復雜么。”的確如此,沒什么不懂的嘛。
■■■■■
下面再稍微深入說明一下。我們常見的兩個語句是:
p = (char *) i; *p = i & 0x0f;
這兩個語句有什么區別呢?這是不懂匯編的人常有的疑問。將以上語句按匯編的習慣寫一下吧。假設p相當于ECX,那么寫出來就是:
MOV ECX, i MOV BYTE [ECX], (i & 0x0f)
它們的區別很清楚,即一個是給ECX寄存器賦值,一個給ECX號內存地址賦值。這完全是兩回事。存儲它們的半導體也不一樣,一個在CPU里,一個在內存芯片里。在C語言中,雖然p與*p只有一字之差,但意思上的差別卻如此之大。
如果執行順序調過來會怎么樣呢?也就是像這樣:
*p = i & 0x0f; p = (char *) i;
不是很熟悉指針的人可能認為這樣也行。但是,這相當于:
MOV BYTE [ECX], (i & 0x0f) MOV ECX, i
如果這么做,第一個MOV的時候,ECX的值不確定,是個隨機數,這會導致i & 0x0f的結果寫入內存的某個不可知的地址中。這樣的后果很嚴重。
■■■■■
另一個比較常見的疑問,是關于聲明的。在C語言中,如果不聲明變量就不能使用。所謂聲明,就是類似“int i; ”這種語句。有了這句話,變量i就可以使用了(與此不同的是匯編語言中,EAX, DL等,不聲明也可以自由使用)。在C語言中,聲明了10個變量,就可以用10個變量,這是理所當然的事。
既然如此,那為什么只聲明了“char *p; ”卻不僅能使用p,還可以使用*p呢?這讓人搞不懂……確實,這個程序中,給p和*p賦值了。看上去,能夠使用的變量數比實際聲明的變量數要多。
遇到這種情況時,我們先回到匯編語言中看看。
MOV ECX, i MOV BYTE [ECX], (i & 0x0f)
看著這個程序,就不會再有人認為其中有2個變量了。其中只有一個ECX。而且,同樣是“MOV AL, [ECX]”, ECX是123的時候,和ECX是124的時候,放入AL的值也是不同的(只要這兩處地址存放的不是同樣的值)。這是因為地址不同,代表的內存區域不同。就好比不同的住址,住的人也不一樣。
所以,同樣是*p,因為p值的不同,記錄的值也不同。
*p = 3; p = p + 3; i = *p;
也就是說如果執行以上片段,i不一定是3,因為地址已經變了。
費了半天勁,其實筆者想說的就是,*p并不是什么變量。確實,我們可以給*p賦值,也可以引用*p的值,這看起來就像變量一樣。但即便如此,*p也不是一個變量,變量只有p。所謂*p,就相當于匯編中BYTE [p]這種語句的代替。
如果你還執拗地說*p是一個變量,那照這種邏輯,變量可遠不止2個,還有很多很多。因為只要給p賦上不同的值,*p就代表完全不同區域的內存內容。
■■■■■
下一個問題也是關于聲明的:“char *p; ”聲明的是*p,還是p呢?
這也是一個常見的問題。先給出結論吧,聲明的是p。“既然如此,那為什么不寫成char*p;呢?”有這種想法,說明你這方面的直覺很好。筆者也認為這樣寫對于初學者來說更簡單易懂。事實上,在C語言中寫成“char* p; ”也可以,既不出錯,也不出警告,運行也沒問題。
但這種寫法有點小問題。如果寫成“char* p, q; ”,我們看上去會覺得p和q都表示地址的變量,但C編譯器卻不那樣認為,q會被看作是一般的1字節的變量。也就是被解釋成“char*p, q”。為了避免這樣的誤解,一般的程序員不寫成“char* p; ”,所以筆者也按照這個習慣編寫程序。另外,如果想要聲明兩個地址變量,就寫成“char *q, *p; ”。
■■■■■
今天的專欄寫得好長呀,我們來整理總結一下吧。首先,本書中出現的“char *p; ”不必看作指針,這是最重要的決竅。p不是指針,而是地址變量。不要使用“p是指針”這種模棱兩可的說法,“p是地址變量”這種說法比較好。
將地址值賦給地址變量是理所當然的。并且,既然地址代表的是內存的地址,可以讓該地址存放自己想放的任何值。雖然也可以將地址變量說成是指針,但筆者聽到指針這個說法也很茫然,所以除了跟別人討論時以外,筆者也不說指針什么的。
C語言中地址變量的聲明,以及給內存地址賦值的,寫法不是很習慣,但終究這只是寫法的不同,思考問題的方法與匯編語言差不多。在C語言開發人員看來,“C語言的*p比匯編語言BYTE [p],更短小精悍”,確實,簡潔是一個長處,但就是因為簡潔,才讓初學者不好理解。
C語言的很多初學者都在學習指針時受挫,以至于會想“如果沒有指針就好了”。而事實上,沒有指針的語言也確實是存在的。但這種語言很不好用,因為沒有指針就無法往指定內存的地址存入數據,那怎么往VRAM上繪制圖像呢?這種語言只能讓寫操作系統變得更加困難。
筆者也認為,C語言指針的語法很難理解,所以希望能改善。但它像匯編語言一樣,能直接訪問地址,這一點非常好。所以希望大家能這樣想:“不是要廢除指針,而是把指針改善得更直觀易懂。”
- 操作系統實用教程(Linux版)
- 樂學Windows操作系統
- Linux從零開始學(視頻教學版)
- Linux操作系統應用編程
- 嵌入式系統原理及開發
- Joomla! 3 Template Essentials
- ElasticSearch Cookbook
- Django Project Blueprints
- 新編電腦辦公(Windows 10+ Office 2013版)從入門到精通
- Linux系統最佳實踐工具:命令行技術
- Windows 10從新手到高手
- Linux網絡配置與安全管理
- Building Telephony Systems With Asterisk
- 從零開始學安裝與重裝系統
- Learning Continuous Integration with Jenkins(Second Edition)