我們之前研究過變量、數組、函數和指針,他們都可以看作是內存中存儲的一段數據,當程序需要用到它們時,會通過它們的地址找到它們並進行調用,只是調用的用途不同而已:變量和數組元素是作為常量來處理,對它們進行賦值、運算、取址等操作,而程序是從首地址開始執行直到返回,指針是用來對地址進行操作,或者對指向的內容進行操作。但是我們要知道,它們在內存中都是以一個字節一個字節的數據形式存儲的,我們可將他們的存儲空間都看作是一個char型數組。
現在定義了一個有200個元素的char型數組a,要我們向a中加入數組元素,使程序可以在屏幕中間打印一個字符“c”。在執行程序main裡只有一句語句:((void (far *)())(long)a)();分析語句:a是數組的首地址,將它強制轉換成long型使它的數據包含段地址和偏移地址的數據,但此時它還不是一個地址而是一個long型變量,那麼我們將它再強制類型轉換成一個void(far *)()型的函數指針,它是一個遠指針,指向一個void型的函數,所以這個函數的入口地址就是數組a的首地址。即程序是要執行以數組a裡的元素構成的語句所組成的函數。那麼我們要向數組a裡填充的是一段內存空間的數據,這些數據連起來能被翻譯成一段語句,這段語句的功能是在屏幕中間打印一個字符“c”。
那麼我們先考慮怎麼在屏幕中間打印一個字符“c”。我們知道輸出函數都是在當前光標的位置輸出,而我們要在屏幕中間打印是在固定位置輸出,即在段地址為b800的數據段的某一個位置存放要輸出的數據,我們知道dos窗口的大小是80*25的,一共占4000個字節,那麼屏幕中間是,即偏移地址為:(13*80+40-1)*2=2158=0x86e,那麼要打印的地址為0xb800086e。我們怎麼把字符c放到內存中地址0xb800086e處呢?要對地址操作首先想到的就是指針,因為要存放段地址加偏移地址,所以要定義一個far指針。我們先寫一個輸出的程序如下:
輸出結果為:
可見結果是正確的。現在我們要找到這個程序在內存中存儲的數據,我們用debug加載程序:
可以看到,程序從01fa開始,到0215結束,我們查看這一段的內存:
可見這一段的內存裡的數據為55 8b ec 83 ec 04 c7 46 fe 00 b8 c7 46 fc 6e 08 c4 5e fc 26 c6 07 63 8b e5 5d c3,我們將這些數據存到數組a中,看能否打印出c,結果發現雖然輸出了c,但是還在屏幕上輸出了兩個字符,還有程序輸出了之後沒有正確返回:
但是查看內存,我們向數組a裡補充的數據完全正確,用u命令查看也發現和我們之前寫的輸出語句的匯編語句一樣,那是為什麼呢?我們之前寫的輸出語句是在main函數裡輸出的,但是這裡數組a是在數據段裡的,不是在一個段裡,需要返回值,所以我們在輸出函數裡把輸出語句寫在一個far型的子函數裡,程序如下:
查看函數f的匯編語句為:
可以發現最後的返回語句變成了retf,再查看內存空間:
只有21e處的c3變成了cb,修改之後發現能夠正常顯示:
所以我們在寫程序要轉換程序和數據時一定要注意這個程序的位置,它的數據能否直接移植到其他程序裡面使用。還有一定要注意,我們將一個數組的首地址強制轉換成函數指針時,一定要先將它強制轉換成long型數據,這樣它才能包含段地址和偏移地址,函數指針才能正確指向到函數。
到現在為止我們已經學過了c語言一些比較重要的也是主要的部分:變量、數組、函數、指針,我們還了解了一些編譯原理和編譯器的命令,現在來總結一下:
變量:
變量是一種存放數據方式,與常量不同,它的內容是可以改變的。它可以分為全局變量和局部變量,它們的本質區別是存儲的位置不同,全局變量是在內存中存儲的,而局部變量是在棧段中存儲的,這個差別導致了它們的一系列區別:全局變量存儲的內存空間是沒有內存對齊的情況的,而局部變量有;全局變量作為參數傳遞是直接用地址調用,而局部變量是入棧的方式;全局變量的生命周期是整個程序,而局部變量的生命周期是當前函數;全局變量的段地址在ds寄存器裡,局部變量的段地址在ss寄存器裡;全局變量定義是自動清零的,而局部變量定義時在棧中的空間還是原來的數據。比較特別的靜態局部變量的存儲位置和生命周期都和全局變量一樣,只是靜態局部變量只能在定義的函數中使用。
比較重要的變量類型有char、int、long、double和結構體,它們分別占的大小為1字節、2字節、4字節、8字節,結構體的大小是結構體中數據項之和。結構體也存在內存對齊的情況,結構體中各數據項存儲位置是相鄰的。結構體作為參數傳遞和返回比一般變量要復雜,一般變量都是直接入棧,而結構體必須創建一個臨時變量,用塊搬移函數將結構體的各數據項復制到臨時變量裡,在子函數裡再將臨時變量的值搬移到棧段裡面,返回的原理也是相同的。
數組:數組是利用一段連續的內存空間存放一系列相同類型的數據。一維數組是存儲的數據按照線性的順序來排列,二位數組是存儲的數據按照網狀的順序來排列,多維數組是存儲的數據以多維的形式存儲,我們可以通過當前哪一組不斷向下查找到某一個元素。數組也可以根據存放的元素類型不同來分類:整型數組的元素是int型數據、指針型數組的元素是指針型數據、結構體數組的元素是結構體數據。數組中的元素是連續存放的。數組名相當於數組的首地址,也是數組第一個元素的地址,它的使用和指針有相似之處,如果p是一個指針,那麼p[n]等同於*(p+n),即跳到下一個元素就相當於在當前地址上加上數組的類型大小。數組還有函數指針數組,存放的元素是函數指針,指向函數指針,函數指針數組可以將要運行的程序以數據的形式寫入並對函數進行調用。
函數:函數是一段語句的集合。函數名相當於一個函數指針,存儲函數的入口地址,程序由這個入口地址跳轉到當前函數。函數的參數是局部變量,在調用該函數的函數中將參數壓入棧中,在子函數裡用bp寄存器找到參數的地址進行調用。函數可以有返回值,void函數沒有返回值,函數的返回值一般是存儲在寄存器中,如果返回值為結構體,則將結構體的內容傳遞到一個臨時變量裡。函數是一段數據,它同樣存儲在內存空間裡,這樣我們可以以數據的形式將一個函數寫到內存中執行。
指針:指針存儲的數據是一個地址,我們可以通過“*”來取得指針存儲的這個地址處的內容,通過“&”來取得一個內存空間的地址賦給指針。指針加減一個數並不是以它的值加減一個數字,而是加減它所指向的存儲空間的數據類型的大小,即如果它指向的是int型數據,那麼加1就是在當前地址上加上2個字節。我們可以將一個地址賦給一個整形變量,但我們不能對一個整形變量使用“*”取得它所存儲的地址處的值,因為它不是一個指針,同時如果一個指針是一級指針,即定義成*p,那麼只能用“*”對它取一次值,如果一個指針是二級指針,可以用“*”對它取兩次值,總之,一個指針是幾級指針,就可以對它取幾次值。對於指針的使用我們一定要注意它和其他的變量的類型匹配問題,近指針占2個字節,存儲偏移地址,遠指針占4個字節,存儲段地址加偏移地址,我們要用%p輸出近指針的值,用%Fp輸出遠指針的值。雖然指針的值的大小是固定的,但是指針指向的值的大小和指針的定義有關。指針在地址和內容之間建立了一條聯系,這種聯系是c語言最重要的基礎,我們可以用它來實現多種數據結構。指針可以指向任意的數據類型,結構體指針指向的是一個結構體,它可以以->符號調用結構體的數據項。函數指針是指向一個函數入口的指針,當定義一個函數指針時要指明函數的類型和參數類型和個數,通過函數指針可以調用指定位置的函數。
c語言十分精簡也十分快捷,它的優點是建立在用戶可以直接對內存進行操作的基礎上,這樣用戶實現一種功能可以有很多種寫法,可以說是能夠充分發揮用戶的想象力和創造性,但是這樣也有缺點,就是錯誤可能很多。我們需要將這些知識融合起來,融合的最好的方式就是多寫程序,發揮自己的想象力和創造性,對於一個問題,思考多種解決辦法,如果碰到了問題,要仔細思考問題在哪裡,這樣才能夠提高。對於有問題的地方,不要隨便問問題,要自己先寫程序來試驗自己的猜想。