




版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請進(jìn)行舉報(bào)或認(rèn)領(lǐng)
文檔簡介
第一章硬件基礎(chǔ)
硬件是軟件的運(yùn)行平臺(tái),沒有硬件的支撐軟件也將不復(fù)存在。您能想象沒有顯示器軟件將如何顯示圖形,沒有CPU軟件將如何運(yùn)行嗎?反正我想象不到!但是如果把問題反過來問就問到本質(zhì)了,軟件運(yùn)行需要哪些硬件支持呢?看圖1.1:CPURAMROM輸出設(shè)備輸入設(shè)備存儲(chǔ)設(shè)備圖1.1系統(tǒng)結(jié)構(gòu)框圖
我們拋開硬件的什么電器特性等等,去蕪存菁,就是上面的這個(gè)圖了。如果程序要運(yùn)行沒有CPU是不行的,CPU要快速的交換數(shù)據(jù),沒有RAM也是不行的。因此無論任何系統(tǒng),CPU和RAM都是必不可少的。您一定會(huì)提醒我ROM不也是不變的嗎?這種說法不完全對,因?yàn)樵赑C系統(tǒng)和嵌入式系統(tǒng)之間ROM的作用是不一樣的。在PC系統(tǒng)中ROM就是那個(gè)BIOS芯片,是用來提供系統(tǒng)的啟動(dòng)代碼和基本的輸入輸出功能的;而在嵌入式系統(tǒng)中,ROM存儲(chǔ)了全部的代碼,它已經(jīng)將PC中的BIOS和硬盤的與代碼相關(guān)的功能混合在一起了。設(shè)備PC系統(tǒng)典型硬件設(shè)備嵌入式系統(tǒng)典型硬件設(shè)備CPU任何CPU任何CPURAM任何RAM任何RAMROMBIOS芯片F(xiàn)lash芯片存儲(chǔ)設(shè)備硬盤Flash芯片輸入設(shè)備鍵盤鍵盤輸出設(shè)備顯示卡+顯示器LCD顯示屏
PC的ROM——BIOS芯片可以采用Flash芯片,在這里之所以不寫成Flash芯片是因?yàn)锽IOS的作用和嵌入式系統(tǒng)的Flash作用不大一樣,使用BIOS以示區(qū)分。1.1CPU和RAM從軟件觀點(diǎn)來講,任何CPU和RAM都可以應(yīng)用于各種系統(tǒng)中,不存在明顯的區(qū)別,只要CPU可以執(zhí)行指令控制設(shè)備就可以了。但是考慮到耗電以及體積(嵌入式設(shè)備通常要求耗電低、體積?。┑葐栴},嵌入式系統(tǒng)就發(fā)展出了專用的CPU芯片。當(dāng)前應(yīng)用最廣泛的是ARMCPU。ARMCPU是由英國的ARM公司設(shè)計(jì)的,由于其執(zhí)行效率高,體積小,耗電少等特點(diǎn)被廣泛應(yīng)用于嵌入式系統(tǒng)。由于嵌入式系統(tǒng)要求高集成度,通常不會(huì)存在單獨(dú)的CPU芯片,而是將CPU和很多的外圍電路集成到一起,做成一塊芯片,因此ARM采用授權(quán)的方式提供內(nèi)核芯片設(shè)計(jì),以便于使用者進(jìn)行芯片的集成。CPU按照次執(zhí)行指令的數(shù)據(jù)帶寬可以分為16位、32位、64位等。32位CPU一次只能處理32位,也就是4個(gè)字節(jié)的數(shù)據(jù);而64位CPU一次就能處理64位即8個(gè)字節(jié)的數(shù)據(jù)。如果我們將總長128位的指令分別按照16位、32位、64位為單位進(jìn)行編輯的話:舊的16位CPU(如Intel80286CPU)需要8個(gè)指令,32位的CPU需要4個(gè)指令,而64位CPU則只要兩個(gè)指令。顯然,在工作頻率相同的情況下,64位CPU的處理速度比16位、32位的更快。除了運(yùn)算能力之外,與32位CPU相比,64位CPU的優(yōu)勢還體現(xiàn)在系統(tǒng)對內(nèi)存的控制上。由于地址使用的是特殊的整數(shù),而64位CPU的一個(gè)ALU(算術(shù)邏輯運(yùn)算器)和寄存器可以處理更大的整數(shù),也就是更大的地址。傳統(tǒng)32位CPU的尋址空間最大為4GB,使得很多需要大容量內(nèi)存的大規(guī)模的數(shù)據(jù)處理程序在這時(shí)都會(huì)顯得捉襟見肘,形成了運(yùn)行效率的瓶頸。而64位的處理器在理論上則可以達(dá)到1800萬個(gè)TB(1TB=1024GB),將能夠徹底解決32位計(jì)算系統(tǒng)所遇到的瓶頸現(xiàn)象。當(dāng)然64位尋址空間也有一定的缺點(diǎn):內(nèi)存地址值隨著位數(shù)的增加而變?yōu)樵瓉淼膬杀?,這樣內(nèi)存地址將在緩存中占用更多的空間,其他有用的數(shù)據(jù)就無法載入緩存,從而引起了整體性能一定程度的下降。在進(jìn)行系統(tǒng)設(shè)計(jì)時(shí),會(huì)根據(jù)不同尋址能力的CPU來進(jìn)行尋址空間的分配。由于CPU都是通過設(shè)備的寄存器(這個(gè)寄存器可以理解為設(shè)備本身帶的RAM)來控制設(shè)備的,因此地址空間的劃分就顯得十分重要。例如,一個(gè)具有32位尋址能力的CPU不可能講全部的地址空間都分配給RAM,好比PC系統(tǒng)需要為BIOS分配存儲(chǔ)空間等。也就是說只要是需要CPU直接控制的外部設(shè)備都需要為其分配CPU地址空間。RAM就是可以隨機(jī)訪問,快速讀寫的存儲(chǔ)器。CPU可以直接從RAM中取得數(shù)據(jù)(CPU可以從所有分配了地址空間的設(shè)備寄存器中取得數(shù)據(jù))或代碼指令,因此RAM的訪問速度將直接影響系統(tǒng)的性能。1.2ROM存儲(chǔ)芯片
ROM是每個(gè)計(jì)算機(jī)系統(tǒng)必不可少的,但是其實(shí)現(xiàn)的方式卻不盡相同。在我們熟悉的PC系統(tǒng)中,ROM是一個(gè)稱作BIOS(BaseInput&OutputSystem)的芯片。CPU上電時(shí)會(huì)從ROM中讀取指令,因此沒有ROM的系統(tǒng)是不能夠運(yùn)行的,因?yàn)槿绻麤]有ROM,CPU將無法獲得起始的執(zhí)行指令。在PC系統(tǒng)中BIOS的作用除了提供起始指令以外,還會(huì)掃描硬件設(shè)備并初始化主板(MainBoard)上的硬件接口。由于PC上的接口都遵循著一組通用的協(xié)議,因此BIOS就可以實(shí)現(xiàn)所有硬件接口的驅(qū)動(dòng)(如USB接口、顯示卡、鍵盤和鼠標(biāo)等)和硬件的數(shù)據(jù)輸入輸出功能,這也是BIOS(基本輸入輸出系統(tǒng))名稱的由來了。在BIOS控制的硬件接口中也包含了硬盤的控制接口,在BIOS初始化完成后就會(huì)到硬盤主分區(qū)上查找啟動(dòng)文件(注3),后面的事情就交給PC的操作系統(tǒng)了。這個(gè)過程請看圖1.2。
通常,在硬盤內(nèi)有一個(gè)主引導(dǎo)記錄區(qū),在安裝操作系統(tǒng)的時(shí)候由操作系統(tǒng)寫入Boot程序,BIOS就是取出這段程序然后執(zhí)行。由于Boot程序是由操作系統(tǒng)寫入的,因此從這個(gè)Boot程序開始,系統(tǒng)的運(yùn)行權(quán)限就交由操作系統(tǒng)來控制了。以Windows2000操作系統(tǒng)為例,這段代碼區(qū)域執(zhí)行時(shí)會(huì)搜索名叫“NTLDR”的系統(tǒng)文件。之所以分成Boot程序和NTLDR文件的原因是硬盤的BootSector很?。ㄖ挥?66Bytes),不可能容納全部的啟動(dòng)程序。
圖1.2只是一個(gè)啟動(dòng)的示意圖,在這里我并沒有詳細(xì)的列出每一個(gè)必須的步驟,因?yàn)槲业哪康闹皇亲屛覀兡軌蛄私釨IOS硬件芯片在系統(tǒng)中的作用。
對比PC的BIOS,嵌入式系統(tǒng)由于軟件規(guī)模小,因此將引導(dǎo)代碼和操作系統(tǒng)代碼全部放到了系統(tǒng)的Flash芯片中了。正如我們所知道的,PC機(jī)上大部分的操作系統(tǒng)代碼全部放在硬盤上,然后從硬盤上將程序載入內(nèi)存執(zhí)行。而嵌入式系統(tǒng)中目前大多數(shù)采用直接尋址的方式從NorFlash芯片中讀取代碼并執(zhí)行。因此,實(shí)際上嵌入式系統(tǒng)簡化了PC系統(tǒng)的設(shè)計(jì),將PC系統(tǒng)中的BIOS和硬盤代碼全部集中到了一個(gè)Flash芯片上。因此BIOS雖然也可以使用Flash芯片,但是相對于嵌入式系統(tǒng)來說,他們的含義和作用卻不同。BIOS自檢顯卡BIOS檢測CPU型號(hào)檢測、內(nèi)存測試讀取硬盤啟動(dòng)扇區(qū)獲取操作系統(tǒng)啟動(dòng)文件圖1.2PC開機(jī)流程Flash芯片對于我們來說并不陌生,那些可以更新BIOS程序的BIOS芯片也是使用Flash芯片實(shí)現(xiàn)的,還有MP3用的SD卡等等也是Flash芯片。Flash芯片是一種可以多次擦寫的存儲(chǔ)芯片,廣泛的應(yīng)用于嵌入式系統(tǒng)。Flash的特點(diǎn)是耗電低,容量大(相對于嵌入式系統(tǒng)而言),寫入之前需要先擦除(因?yàn)镕lash芯片的存儲(chǔ)單元只允許從1變到0)。當(dāng)前流行的分為NORFlash和NANDFlash。NAND與NORFlash的區(qū)別主要有:1、NANDFlash的空間比NorFlash大2、NANDFlash的訪問速度比NorFlash快3、NANDFlash只有Page訪問模式,NorFlash可以進(jìn)行Page和直接地址訪問(直接地址訪問也就是CPU可以直接尋址,或者叫做隨機(jī)訪問)4、NANDFlash允許有壞塊,但是NorFlash不能有壞塊5、NANDFlash比NORFlash更加便宜在嵌入式系統(tǒng)中,NOR和NAND都可以做為代碼區(qū)和文件系統(tǒng)區(qū)來使用。通常情況下NOR和NAND做為嵌入式文件系統(tǒng)區(qū)的時(shí)候都使用Page模式。Page模式允許一次讀取多個(gè)字節(jié),就像硬盤的最小讀寫單位是扇區(qū)一樣,只不過Flash的最小讀寫單位叫做Page。Page模式下可以加快Flash的讀寫速度。由于NANDFlash只支持Page讀寫模式,因此使用NANDFlash做為代碼區(qū)的時(shí)候需要外加控制電路。當(dāng)前使用NAND做為代碼區(qū)正在成為一種流行的趨勢(因?yàn)镹ANDFlash成本更低),主要的實(shí)現(xiàn)方式有兩種:一是添加仿真電路使得NANDFlash可以支持隨機(jī)訪問;二是增加一個(gè)類似硬盤的引導(dǎo)區(qū)(通常是第一個(gè)Page),系統(tǒng)啟動(dòng)的時(shí)候使用引導(dǎo)區(qū)的代碼將全部NAND中的代碼復(fù)制到RAM中執(zhí)行。當(dāng)然,可以設(shè)想隨著將來嵌入式操作系統(tǒng)的發(fā)展,動(dòng)態(tài)載入內(nèi)存的形式也許就會(huì)出現(xiàn)了,但是目前嵌入式系統(tǒng)仍然沒有發(fā)展到這個(gè)地步。更進(jìn)一步,當(dāng)前應(yīng)用于嵌入式系統(tǒng)的微型硬盤也已經(jīng)出現(xiàn)了,或許更為復(fù)雜的操作系統(tǒng)也可以應(yīng)用在嵌入式系統(tǒng)上了。在這里我們主要介紹的ROM存儲(chǔ)芯片是Flash,嚴(yán)格意義上說Flash并不能稱作ROM,因?yàn)镽OM是只讀存儲(chǔ)器(ReadOnlyMemory),而Flash是一種可讀可寫的芯片。但是由于ROM“一次成型、終生不變”的特點(diǎn),不便于升級(jí)換代,現(xiàn)在正逐漸的被Flash芯片所取代,但是其功能性的稱謂“ROM”還在為大家所用。在計(jì)算機(jī)系統(tǒng)中主要存在用戶數(shù)據(jù)、程序數(shù)據(jù)和代碼三種二進(jìn)制內(nèi)容。用戶數(shù)據(jù)指用戶文件,程序數(shù)據(jù)是指程序運(yùn)行時(shí)需要修改或使用的非代碼內(nèi)容。下面將就PC系統(tǒng)和嵌入式系統(tǒng)中的這三種二進(jìn)制內(nèi)容做一個(gè)比較,請看下表:二進(jìn)制形態(tài)PC系統(tǒng)嵌入式系統(tǒng)用戶數(shù)據(jù)存儲(chǔ)在文件系統(tǒng)中,典型的設(shè)備是硬盤存儲(chǔ)在文件系統(tǒng)中,典型的設(shè)備是Flash存儲(chǔ)芯片程序數(shù)據(jù)可讀可寫的數(shù)據(jù)存放在RAM中;只讀數(shù)據(jù)存放在硬盤中,運(yùn)行時(shí)與代碼一起讀入RAM可讀可寫的數(shù)據(jù)存放在RAM中;只讀數(shù)據(jù)存放在Flash中,與代碼存儲(chǔ)在同一個(gè)區(qū)域代碼存儲(chǔ)在文件系統(tǒng)中的文件里,運(yùn)行時(shí)讀入RAM由CPU執(zhí)行如果存儲(chǔ)在NORFlash等可隨機(jī)訪問的空間中則CPU直接在芯片中取指令運(yùn)行;如果存儲(chǔ)在NANDFlash等不能隨機(jī)訪問的空間中則需要讀入RAM中運(yùn)行關(guān)于程序數(shù)據(jù)的詳細(xì)情況將在編譯器基礎(chǔ)一節(jié)詳細(xì)介紹。1.3輸出設(shè)備
輸出設(shè)備有很多種,例如顯示器、打印機(jī),在這里我們主要講一下顯示設(shè)備。任何顯示設(shè)備都是點(diǎn)陣式的,至少目前是這樣。還記得初次看見電視里顯示的人物時(shí)內(nèi)心的驚異,這個(gè)世界竟是如此神奇!后來知道了,如果我愿意,我可以買640*480個(gè)燈泡,組成一個(gè)640*480的方陣,然后控制每個(gè)燈泡的亮滅,我也可以顯示一個(gè)人物。終于懂了,原來任何的顯示設(shè)備都是基于這個(gè)原理實(shí)現(xiàn)的。我想關(guān)于這一點(diǎn)我沒有必要再多說什么了,畢竟萬變不離其宗嘛。
當(dāng)前流行的顯示設(shè)備有CRT顯示器(也就是電腦上很大個(gè)頭的那種顯示器),LCD(液晶)等(雖然我在大學(xué)實(shí)驗(yàn)室里用的可以顯示數(shù)字的LED燈也是顯示設(shè)備,但是他太簡單了)。CRT顯示器經(jīng)歷了從球形到純平的物理演變,LCD則經(jīng)歷了從黑白到彩色的變化。按照顯示器每個(gè)點(diǎn)能夠顯示的顏色數(shù)目可以分為黑白兩色、灰度、8位色、16位色、24位真彩色等顯示設(shè)備。這個(gè)顏色數(shù)目就是所謂的色深(ColorDepth),色深越大,每個(gè)點(diǎn)能夠表達(dá)的顏色數(shù)就越多,這個(gè)點(diǎn)就是“象素”。
在嵌入式系統(tǒng)中主要使用LCD的顯示設(shè)備,LCD會(huì)集成一個(gè)顯示的存儲(chǔ)空間,在這個(gè)空間中存儲(chǔ)了對應(yīng)的每個(gè)象素的值。例如16位色的LCD每個(gè)象素需要由2個(gè)字節(jié)來表示,如果顯示屏幕的大小是100*100,那么就需要2*100*100=20000個(gè)字節(jié)的存儲(chǔ)空間。程序正是通過更新這個(gè)存儲(chǔ)空間中的內(nèi)容來控制LCD的顯示。衡量LCD顯示效果的還有象素間距,象素間距越小,畫面就越細(xì)膩,顯示效果越好。舉個(gè)極端的例子,如果一個(gè)象素是整個(gè)LCD那么大,那就只能看見一個(gè)象素點(diǎn)的“燈泡”了,也就沒法顯示圖像了。通過像素個(gè)數(shù)和每個(gè)點(diǎn)的大小就可以換算出顯示屏的大小了,例如常用的xx英寸大小的屏幕等等。1.4輸入設(shè)備
輸入設(shè)備也有很多種,最典型的是鍵盤和觸摸屏。在這一節(jié)里,我將簡單的介紹一下它們的實(shí)現(xiàn)原理。
鍵盤通常是一個(gè)矩陣式的電路,當(dāng)按鍵按下的時(shí)候,接通電路產(chǎn)生信號(hào),如圖1.3:123ABC圖1.3鍵盤原理
圖1.3是一個(gè)簡版的鍵盤原理圖,圖中任何交叉點(diǎn)的橫向和縱向都未連通,并假設(shè)只要交叉點(diǎn)連通,則相應(yīng)的行和列就可以連通并發(fā)生狀態(tài)改變。其中1、2、3和A、B、C連接在控制芯片上,通過掃描行和列,確定行的A、B、C是否連通,再掃描列1、2、3是否連通,這樣就可以唯一確定一個(gè)點(diǎn)是否按下。千萬注意,這只是一個(gè)示意圖,并不是真正的鍵盤原理圖。
觸摸屏是近來應(yīng)用越來越多的輸入器件。典型觸摸屏的工作部分一般由三部分組成:兩層透明的阻性導(dǎo)體層、兩層導(dǎo)體之間的隔離層、電極。阻性導(dǎo)體層選用阻性材料,如銦錫氧化物(ITO)涂在襯底上構(gòu)成,上層襯底用塑料,下層襯底用玻璃。隔離層為粘性絕緣液體材料,如聚脂薄膜。電極選用導(dǎo)電性能極好的材料(如銀粉墨)構(gòu)成。
觸摸屏在工作時(shí),上下導(dǎo)體層相當(dāng)于電阻網(wǎng)絡(luò)。當(dāng)某一層電極加上電壓時(shí),會(huì)在該網(wǎng)絡(luò)上形成電壓梯度。如有外力使得上下兩層在某一點(diǎn)接觸,則在電極未加電壓的另一層可以測得接觸點(diǎn)處的電壓,從而知道接觸點(diǎn)處的坐標(biāo)。比如,在頂層的電極(X+,X-)上加上電壓,則在頂層導(dǎo)體層上形成電壓梯度,當(dāng)有外力使得上下兩層在某一點(diǎn)接觸,在底層就可以測得接觸點(diǎn)處的電壓,再根據(jù)該電壓與電極(X+)之間的距離關(guān)系,知道該處的X坐標(biāo)。然后,將電壓切換到底層電極(Y+,Y-)上,并在頂層測量接觸點(diǎn)處的電壓,從而知道Y坐標(biāo)。通常有專門的控制芯片。很顯然,觸摸屏的控制芯片要完成兩件事情:一是完成電極電壓的切換;二是采集接觸點(diǎn)處的電壓值(即A/D)。雖然還有其他的實(shí)現(xiàn)方法,我就不贅述了,因?yàn)楸緯哪康牟皇侵v解硬件的原理,而僅僅是讓我們能夠基本了解它們。1.5小結(jié)
在這一章里介紹了各種硬件的特性和原理,其中包括了最小系統(tǒng)各種組件的介紹,為的是防止我們見到這些東西的時(shí)候會(huì)一頭霧水,不知來由。只要我們見到這些硬件不再感到神秘,那么這個(gè)基礎(chǔ)就算打好了。思考題
在PC計(jì)算機(jī)系統(tǒng)中硬盤、BIOS、RAM和嵌入式系統(tǒng)中的文件Flash、程序Flash、RAM之間有什么區(qū)別?它們在系統(tǒng)中的作用分別是什么?
第二章軟件基礎(chǔ)
我們正在向我們的軟件王國進(jìn)發(fā),千萬別急,在這條路上“枯燥”是我們最大的敵人,不知有多少人在它的面前臣服,但愿您不是其中之一?;蛟S您覺得應(yīng)該獲得一些鼓勵(lì),寫一些代碼,能夠看見一些諸如“Hello,World!”之類的信息。非常幸運(yùn),從這里開始您將能夠看見它們了,我會(huì)將部分內(nèi)容使用源程序的方式向您講解。在這本書里,我將使用VisualStudio.Net2003的開發(fā)環(huán)境來執(zhí)行這些測試程序。建立測試工程的步驟如下:
1、首先打開VisualStudio.Net2003開發(fā)環(huán)境,選擇新建項(xiàng)目。如圖2.1,選擇項(xiàng)目類型VisualC++項(xiàng)目/Win32控制臺(tái)項(xiàng)目,選擇路徑,填寫測試程序名稱是Test1,點(diǎn)擊確定按鈕。
2、在Win32應(yīng)用程序向?qū)е薪邮苣J(rèn)設(shè)置,點(diǎn)擊完成按鈕。如圖2.2。圖2.1建立測試程序
這樣就完成了一個(gè)測試用的應(yīng)用程序,在以后的測試程序中我將不再重復(fù)這個(gè)步驟。由于我們主要是在C語言的基礎(chǔ)上講解,因此在這里創(chuàng)建的是Win32控制臺(tái)應(yīng)用程序,它使用Windows的命令窗口顯示輸出結(jié)果。不過請注意,雖然它使用Windows的命令窗口,但是它是一個(gè)Win32的應(yīng)用程序,而不是DOS應(yīng)用程序。
在Windows環(huán)境下,應(yīng)用程序分為控制臺(tái)(Console)應(yīng)用程序和窗口應(yīng)用程序??刂婆_(tái)應(yīng)用程序不顯示窗口,其表現(xiàn)形式類似于DOS環(huán)境下的應(yīng)用程序,但是由于它可以調(diào)用Windows的API來實(shí)現(xiàn),所以它是一個(gè)Windows的應(yīng)用程序而不是DOS應(yīng)用程序。同時(shí)控制臺(tái)應(yīng)用程序可以使用標(biāo)準(zhǔn)C/C++的庫,這樣Windows下的控制臺(tái)應(yīng)用程序就和DOS環(huán)境下的C/C++十分的相似了。
在這一部分我們將通過實(shí)例來演示一些C語言中較比令人“眩暈”的話題,期望能夠通過這些實(shí)例讓您弄明白這些問題的本質(zhì)。由于本書是建立在您已經(jīng)有一定的C語言基礎(chǔ)之上的,因此主要是針對C語言中一些較難理解的概念進(jìn)行講解,畢竟我們不是一本專門講解C語言的書籍。在本部分主要講解的內(nèi)容是指針、結(jié)構(gòu)體、預(yù)處理和函數(shù),因?yàn)樵谖覀兘酉聛淼男谐讨斜仨氁浞值睦斫馑鼈儾拍芾^續(xù)前進(jìn)。圖2.2Win32應(yīng)用程序向?qū)?.1重溫C語言的指針
指針是一個(gè)精靈,以至于在我們剛剛接觸它的時(shí)候有點(diǎn)不知所措,甚至有些人懷著敬畏的心情而決心遠(yuǎn)離它!不過,當(dāng)我們掌握了它的時(shí)候,就會(huì)發(fā)現(xiàn)它能讓我們隨心所欲。盡管我們?nèi)匀幻媾R著使用不當(dāng)所帶來的巨大風(fēng)險(xiǎn),但是我還是會(huì)義無反顧的告訴您——一定要使用它。之所以我會(huì)在這里向您發(fā)出這樣的號(hào)召,絕對不是因?yàn)槲覍χ羔樀膫€(gè)人情感,我和指針也是非親非故,只不過是因?yàn)橥高^它我們不但可以編寫出靈活的程序,而且可以窺探到程序的真正世界——二進(jìn)制世界的秘密。這就讓我們開始擁抱它吧!2.1.1指針的本質(zhì)
指針的本質(zhì)是存儲(chǔ)它所指向存儲(chǔ)空間地址的變量,下面將通過一個(gè)測試程序來開始征服它的旅程。打開測試工程Test1,在Test1.cpp中添加如下代碼:#include"stdafx.h"
int_tmain(intargc,_TCHAR*argv[]){
int*pointer;
intnNumber=100;
pointer=&nNumber;
printf("&pointer=0x%xpointer=0x%x*pointer=%d\n",&pointer,pointer,*pointer);return0;}在這段代碼中,我們定義了一個(gè)int型指針pointer和一個(gè)int變量nNumber,然后讓pointer指向nNumber。編譯、運(yùn)行生成可執(zhí)行文件,可以看到輸出的&pointer、pointer和*pointer的值如下:&pointer=0x12fed4pointer=0x12fec8*pointer=100*pointer=100是我們知道的,也就是指針指向的內(nèi)存地址的內(nèi)容,我們在程序中將pointer指針指向nNumber的地址,那么與nNumber的值相等就不足為奇了。pointer=1244872這個(gè)值就是指針指向的內(nèi)存地址,是當(dāng)前函數(shù)運(yùn)行中存儲(chǔ)nNumber的地址。&pointer就是這個(gè)指針變量的地址,由于我們定義了一個(gè)指針變量,因此,在函數(shù)運(yùn)行時(shí)將會(huì)為這個(gè)變量分配一個(gè)存儲(chǔ)空間,因此這個(gè)值是有意義的,而且它與pointer的值十分相近。我們可以這樣理解指針,它是一個(gè)變量(因?yàn)橹羔樢残枰臻g存儲(chǔ)它所指向的地址),這個(gè)變量的值就是一個(gè)內(nèi)存空間的地址。示意圖如下:100??0x12fec80x12fec8??0x12fed4pointernNumber圖2.3指針示意圖在上面的代碼中,&pointer是一個(gè)指向指針的指針,這樣的稱呼實(shí)在是過于繁瑣了,我們就稱它為“二重指針”吧。順延的,如果是指向(指針的指針)的指針我們就叫它“三重指針”。在C語言中,二重指針是一個(gè)非常有用的東西,很多高階的C語言應(yīng)用都會(huì)使用到它。二重指針的主要作用是做為參數(shù)為一個(gè)指針變量賦值,在后面的章節(jié)中我們還會(huì)經(jīng)常使用到它。對上面的圖2.3作一個(gè)說明,pointer和nNumber分別位于兩個(gè)不同的內(nèi)存區(qū)域,nNumber中存儲(chǔ)的值是100,這是程序中指定的;pointer內(nèi)存儲(chǔ)的值是0x12fec8,這個(gè)值正好是nNumber所在的內(nèi)存地址;圖左邊的0x12fed4是存儲(chǔ)pointer值的指針變量的地址,我們可以通過定義一個(gè)二重指針獲得它的內(nèi)容,在本程序中我們通過&pointer來獲得它的內(nèi)容。從本質(zhì)上來說,指針、二重指針、三重指針等等,都是一樣的,從代碼的層次(我們將在“編譯器基礎(chǔ)”部分詳細(xì)講解軟件的層次問題)來講都是一個(gè)“地址”型的變量,只不過使用的時(shí)候會(huì)由編譯器做一下合法性的檢查,目的是為了防止程序出現(xiàn)錯(cuò)誤;在二進(jìn)制層次來說它們就更加沒有分別了,都是一個(gè)存儲(chǔ)內(nèi)容的容器。打個(gè)比方,現(xiàn)在有兩個(gè)盒子,一個(gè)規(guī)定放置籃球,另一個(gè)規(guī)定放置足球。體現(xiàn)在C語言中這個(gè)“規(guī)定”就是定義了兩個(gè)變量,一個(gè)是int型的,另一個(gè)是指針型的;這兩個(gè)盒子就是兩塊存儲(chǔ)空間。在現(xiàn)實(shí)生活中“規(guī)定”是由人來制定的,在程序中就是由C語言定義的。那么我違反規(guī)定放置籃球的盒子我放置足球,放置足球的盒子我放置籃球。這是沒什么問題的,不過會(huì)發(fā)生錯(cuò)誤,因?yàn)闀?huì)讓一場足球比賽變成了踢籃球的比賽,讓一場籃球比賽變成了足球投籃的比賽。同樣的對于指針和變量的關(guān)系也是,它們的內(nèi)容在二進(jìn)制層次是可以互換的。但是我們在這里要考慮兩件事情,一是這個(gè)錯(cuò)誤發(fā)生的前提條件,這個(gè)錯(cuò)誤要發(fā)生必須是在球的管理者不知道錯(cuò)的情況下。如果球的管理者知道哪個(gè)盒子放了籃球哪個(gè)盒子放了足球也不會(huì)出錯(cuò)。體現(xiàn)在程序上,就是增加強(qiáng)制類型轉(zhuǎn)換來告訴編譯器“我知道我要在int變量中放置一個(gè)指針的值”。否則編譯器將檢查到這個(gè)錯(cuò)誤并報(bào)錯(cuò)。二是要考慮“盒子”容量的問題,因?yàn)樽闱虮然@球小,所以互換的時(shí)候不能讓籃球撐壞了足球盒子。在程序中也就是32位的指針變量不能放到char型變量中去存儲(chǔ),因?yàn)檫@樣會(huì)丟掉地址信息。綜上所述,雖然變量的二進(jìn)制本質(zhì)是一樣的,但是在代碼層次要精確控制變量的類型來避免錯(cuò)誤的發(fā)生。指針也是一個(gè)變量,一個(gè)32位的指針變量也可以存儲(chǔ)在一個(gè)4字節(jié)的無符號(hào)整型變量里,前提是我們要知道我們是采用這種方式來存儲(chǔ)它們的。最后使用一個(gè)例子來做個(gè)演示,新建工程Test2,輸入如下代碼:#include"stdafx.h"
int_tmain(intargc,_TCHAR*argv[]){
intnNumber=100;
int*pointer=&nNumber;
unsignedint
dwBox;
unsignedshortwBox;
//將指針的值分別賦給無符號(hào)整型變量
dwBox=(unsignedint)pointer;
wBox
=(unsignedshort)pointer;//地址內(nèi)容將被裁減?。?!
//輸出Box變量的值,注意wBox與dwBox之間的不同
printf("wBox=0x%xdwBox=0x%x\n",wBox,dwBox);
//輸出Box變量指向地址的值,其中*((int*)dwBox)相當(dāng)于*pointer
//此時(shí)不能夠使用*((int*)wBox)輸出值,因?yàn)樗牡刂肥墙?jīng)過裁減的
printf("*dwBox=%d\n",*((int*)dwBox));
return0;}
編譯后運(yùn)行,輸出如下結(jié)果:
wBox=0xfed4dwBox=0x12fed4
*dwBox=100請仔細(xì)體會(huì)上面的代碼,它真正揭示了變量與指針之間微妙的關(guān)系,還有類型轉(zhuǎn)換時(shí)所發(fā)生的數(shù)據(jù)裁減。2.1.2指針的增減關(guān)于指針的增減是一個(gè)比較容易讓人迷惑的問題,指針本身也是一個(gè)變量,它里面存儲(chǔ)的是一個(gè)地址,那么這個(gè)變量的自增是將這個(gè)地址值加1嗎?自增操作和直接加1的操作是一樣的嗎?為了弄清這個(gè)迷惑,這里我們在Test2工程中增加一個(gè)Test2-1的項(xiàng)目(請?jiān)O(shè)置Test2-1為啟動(dòng)項(xiàng)目),然后輸入如下代碼:#include"stdafx.h"
typedefstruct_Test2{
charTest2[100];}Test2;
int_tmain(intargc,_TCHAR*argv[]){
int
nValue=1;
char
cValue='A';
Test2sValue;
int
*intPtr
=&nValue;
char
*charPtr
=&cValue;
Test2*structPtr=&sValue;
printf("Int%dChar%dStruct%d\n",(int)intPtr,(int)charPtr,(int)structPtr);
intPtr++;
charPtr++;
structPtr++;
printf("Int%dChar%dStruct%d\n",(int)intPtr,(int)charPtr,(int)structPtr);
intPtr
+=1;
charPtr
+=1;
structPtr+=1;
printf("Int%dChar%dStruct%d\n",(int)intPtr,(int)charPtr,(int)structPtr);
return0;}
編譯運(yùn)行輸出的結(jié)果如下:
Int1244884Char1244875Struct1244764
Int1244888Char1244876Struct1244864
Int1244892Char1244877Struct1244964
從這個(gè)結(jié)果中,我們可以看出,指針的加減是與指針?biāo)傅淖兞款愋陀嘘P(guān)的,它所增加的步數(shù)是所指變量的大小,而且指針的+1和++操作是一樣的。那么void型的沒有類型的指針怎么處理呢?答案是不處理,編譯器會(huì)通知我們“未知大小”的錯(cuò)誤而終止程序的生成。2.2重溫C語言的結(jié)構(gòu)
結(jié)構(gòu)是C語言中組織不同數(shù)據(jù)類型的一種方式,它將不同的數(shù)據(jù)類型組織到一個(gè)相鄰的地址空間內(nèi)。每個(gè)定義的結(jié)構(gòu)體變量就是一個(gè)多種數(shù)據(jù)的存儲(chǔ)空間。在這里我們主要講述幾個(gè)相對“高階”問題,之所以在這里加上引號(hào),是因?yàn)閷τ谀承﹤髡f中的高手來說,這不過是小菜一碟。2.2.1結(jié)構(gòu)體變量賦值
我們已經(jīng)習(xí)慣了為結(jié)構(gòu)體變量中的每個(gè)成員賦值,那么我們可以在兩個(gè)結(jié)構(gòu)體變更之間直接使用“=”號(hào)賦值嗎?
答案是肯定的,因?yàn)榫幾g器支持。例如定義一個(gè)表示矩形的結(jié)構(gòu)體:
typedefstruct_Rectangle{
intx;
//左上角x坐標(biāo)
inty;
//左上角y坐標(biāo)
intdx;
//矩形寬度
intdy;
//矩形高度
}Rectangle;
定義兩個(gè)矩形結(jié)構(gòu)體變量并賦值:
RectangleRect1,Rect2;
Rect1.x=100;
Rect1.y=100;
Rect1.dx=100;
Rect1.dy=100;
Rect2=Rect1;
上面的賦值在C語言中是支持的,編譯器會(huì)將Rect2=Rect1中的值轉(zhuǎn)化成內(nèi)存拷貝的CPU指令來實(shí)現(xiàn)賦值操作??梢韵胂螅瑢τ诤唵蔚淖兞抠x值,CPU只需要執(zhí)行一個(gè)MOV指令就可以完成了,因此對于包含多個(gè)簡單變量的結(jié)構(gòu)體來說,使用多個(gè)循環(huán)的MOV指令就在情理之中了(在早期16位CPU中,如果對一個(gè)32位的int變量執(zhí)行賦值操作都需要兩條MOV指令)。在使用這種賦值方法的時(shí)候需要注意的是,在這個(gè)結(jié)構(gòu)體變量中最好不要有指針變量,因?yàn)橹羔樧兞靠赡茉谧兞?中指向一個(gè)分配的內(nèi)存區(qū)域,當(dāng)變量2通過賦值操作獲得了這個(gè)指針值的時(shí)候,有可能這個(gè)指針已經(jīng)釋放了,這樣就導(dǎo)致了空指針情況的發(fā)生,后果是使用這個(gè)指針的時(shí)候?qū)?huì)導(dǎo)致程序崩潰。舉例說明如下:
1、定義一個(gè)包含指針類型的結(jié)構(gòu)體:
typedefstruct_TestStruct{
intnMember;
int*Ptr;}TestStruct;
2、定義兩個(gè)這種類型的變量并采用如下使用方法:
TestStructStruct1,Struct2;Struct1.nMember=100;Struct1.Ptr=(int*)malloc(15*sizeof(int));//分配15個(gè)int變量的空間
//結(jié)構(gòu)體賦值Struct2=Struct1;//此時(shí)Struct2.Ptr與Struct1.Ptr的值相等
if(Struct1.Ptr){
free(Struct1.Ptr);
Strict1.Ptr=NULL;}
//這里有很復(fù)雜的處理,其中包含了malloc等操作
Struct2.Ptr[0]=2;//錯(cuò)誤的賦值操作,因?yàn)榇藭r(shí)Struct2.Ptr所指向的內(nèi)容已經(jīng)被釋放了。
對于程序來說,修改一個(gè)已經(jīng)釋放了空間的內(nèi)存地址內(nèi)容是十分危險(xiǎn)的。當(dāng)然,如果程序只有上面那么簡單的話也不會(huì)出現(xiàn)什么嚴(yán)重的問題,頂多只是非法使用了一塊內(nèi)存區(qū)域;但是,如果中間含有復(fù)雜的處理,Struct2.Ptr[0]=2將修改程序其他部分使用的內(nèi)存區(qū)域,那么這樣就可能會(huì)有莫名其妙的死機(jī)之類的事情發(fā)生了。由于其發(fā)生問題的時(shí)間不固定,因此這類問題調(diào)試起來也十分的困難。2.2.2結(jié)構(gòu)體嵌套
在一個(gè)結(jié)構(gòu)體中可以聲明另一個(gè)結(jié)構(gòu)體,形成結(jié)構(gòu)體嵌套,如果將內(nèi)部嵌套的子結(jié)構(gòu)體變量放在父結(jié)構(gòu)體的頂部,那么兩個(gè)結(jié)構(gòu)體之間還可以進(jìn)行類型互換。這個(gè)特性為實(shí)現(xiàn)C語言的數(shù)據(jù)封裝提供了一種方法。例如定義如下結(jié)構(gòu)體:
typedefstruct_Point{
intx;
inty;
}Point;
typedefstruct_Rectangle{
PointLeftTop;
int
dx;
int
dy;}Rectangle;
由于結(jié)構(gòu)體中Rectangle嵌套了結(jié)構(gòu)體Point,因此如果定義變量RectangleRect1則Rect1可以轉(zhuǎn)化成Point使用。例如:RectangleRect1={{100,100},100,100};Rectangle*pRect=&Rect1;Point*pPoint=(Point*)&Rect1;如果需要訪問這個(gè)矩形的左上角的x坐標(biāo)值可以有兩種方法:pRect->LeftTop.x或者pPoint->x。Rectangle結(jié)構(gòu)體的內(nèi)存模式如圖2.4所示:xydxdyRectanglePoint圖2.4結(jié)構(gòu)體內(nèi)存模型從圖2.4可以看出Point是嵌入在Rectangle中的,兩個(gè)結(jié)構(gòu)體的頂端地址是一樣的,因此他們之間的指針可以互換,并且可以正常操作。2.3重溫C語言的預(yù)處理
在編譯器編譯源文件之前,會(huì)首先通過預(yù)處理器來處理源程序中的預(yù)處理選項(xiàng)。大名鼎鼎的宏也是預(yù)處理的一種,預(yù)處理器采用直接替換的方式來處理宏,也就是說將宏定義的內(nèi)容替換到源文件中之后才開始編譯,在每一個(gè)調(diào)用宏的地方就有一個(gè)宏的替換體。例如定義如下宏:
#defineMAX(a,b)(a>b?a:b)
在程序中使用一次MAX(a,b)對應(yīng)的預(yù)處理器就把(a>b?a:b)替換到相應(yīng)的位置,結(jié)果是增加了可執(zhí)行程序的大小(因?yàn)橛衝個(gè)重復(fù)的代碼段)。如果定義的需要多行的宏,則使用“\”做為行與行之間的連接符,請看下面的宏定義:
#defineINTI_RECT_VALUE(a,b)
\{
\
a=0;
\
b=0;
\}注意最后一行就不再使用“\”了。除了宏之外還有代碼的選擇編譯也是預(yù)處理器的主要功能之一。在一個(gè)大型的軟件項(xiàng)目中,有許多功能需要根據(jù)不同的硬件平臺(tái)或者軟件用途來進(jìn)行選擇,例如一個(gè)軟件的中文版和英文版,在發(fā)布的時(shí)候就需要使用定義的中文或英文標(biāo)簽來決定。C語言的預(yù)編譯器使用的關(guān)鍵詞和功能描述如下表:關(guān)鍵詞功能描述#define用來進(jìn)行宏和符號(hào)或常量的定義。#undef取消通過#define定義過的符號(hào)。#if用來判斷預(yù)處理?xiàng)l件,需要#endif做為結(jié)束標(biāo)記。相對應(yīng)的#ifdef和#ifdefined用來判斷符號(hào)是否定義;#ifndef和#if!defined()是判斷符號(hào)是否未定義。#else#ifdef、#ifndef的條件分支語句#endif#ifdef、#ifndef的條件結(jié)束語句,只要有#if就需要有#endif#error無條件的向預(yù)處理器報(bào)錯(cuò)。通常用在#if…#endif之間,用來判斷是否符合編譯條件。例如:#ifndefENABLE_COMPILE#errorDisablecompile#endif#include用來包含文件。通常是用來包含頭文件,但實(shí)際上它什么文件都可以包含。它直接將文件的內(nèi)容引入到當(dāng)前的包含文件中,這些包含都是由預(yù)處理器完成的。#pragma指定編譯器的參數(shù),這個(gè)和具體的編譯器有關(guān)。例如有些編譯器支持startup和exitpragmas,允許用戶指定在程序開始和結(jié)束時(shí)執(zhí)行的函數(shù)。#pragmastartupload_data#pragmaexitclose_files__FILE__預(yù)處理常量,代表當(dāng)前編譯的文件名。例如可以使用如下代碼輸出當(dāng)前的文件名:printf(“Thisfilenameis%s”,__FILE__);__LINE__預(yù)處理常量,代表當(dāng)前編譯的行數(shù)。例如可以使用如下代碼輸出當(dāng)前的行數(shù):printf(“Currentlineis%d”,__LINE__);__DATE__預(yù)處理常量,代表當(dāng)前編譯的日期。例如可以使用如下代碼輸出當(dāng)前的編譯日期:printf(“Currentcompiledateis%s”,__DATE__);__TIME__預(yù)處理常量,代表當(dāng)前編譯的時(shí)間。例如可以使用如下代碼輸出當(dāng)前的編譯時(shí)間:printf(“Currentcompiletimeis%s”,__TIME__);在使用預(yù)處理的時(shí)候需要注意兩件事情:1、在定義宏或常量時(shí)候盡可能的使用括號(hào)。這是因?yàn)轭A(yù)處理器是將宏和常量采用直接替換的方式,如果不是用括號(hào)則有可能產(chǎn)生錯(cuò)誤的程序處理??聪旅娴拇a:#defineDISPLAY1_HEIGHT320#defineDISPLAY2_HEIGHT240#defineDISPLAY_SUMDISPLAY1_HEIGHT+DISPLAY2_HEIGHT…if(DISPLAY_SUM*2>200){
…}…上面的判斷語句的真實(shí)想法是如果顯示高度之和的2倍大于200則進(jìn)行相應(yīng)的處理。而實(shí)際上進(jìn)行預(yù)處理后上面的代碼變成了if(DISPLAY1_HEIGHT+DISPLAY2_HEIGHT*2)也就是if(320+240*2),這與我們期望的值相差太遠(yuǎn)了。因此在定義的時(shí)候最好加上括號(hào),這樣就不會(huì)出問題了。2、在使用宏的時(shí)候必須不能出現(xiàn)參數(shù)變化。請看下面的代碼:#defineSQUARE(a)((a)*(a))
inta=2;intb;b=SQUARE(a++);//結(jié)果:a=4,即執(zhí)行了兩次增1。
正確的用法是:b=SQUARE(a);a++;//結(jié)果:a=3,即只執(zhí)行了一次增1。2.4重溫C語言的函數(shù)
理論上我們可以只使用一個(gè)main函數(shù),因?yàn)椴还芏嗌俪绦蛭覀兌伎梢跃蛯懺谝粋€(gè)main里面。但是您能設(shè)想有一個(gè)10萬行的main函數(shù)嗎?所以我們不得不把程序分成各個(gè)模塊,不但要分成函數(shù),而且還要將不同類型的函數(shù)放在不同的文件中。就我的經(jīng)驗(yàn)而言,通常程序多于200行的時(shí)候應(yīng)該考慮分成函數(shù),而一個(gè)文件中的總共代碼最好不要超過5000行。多了就不利于調(diào)試和閱讀。當(dāng)然,這些內(nèi)容只是為了增強(qiáng)代碼的可讀性和易管理性的問題,但是您也要知道,軟件在達(dá)到一定規(guī)模后就不單單是技術(shù)問題了,還有管理的問題。2.4.1形參和實(shí)參直到現(xiàn)在我還我還記得在我初學(xué)C語言的時(shí)候形參和實(shí)參給我?guī)淼睦Щ?,我期望能夠用非常簡單明了的語言來描述它們,讓我們來試試吧!形參:在函數(shù)定義的時(shí)候使用的參數(shù)叫做形參。實(shí)參:在使用函數(shù)的時(shí)候傳遞的參數(shù)叫做實(shí)參。舉例說明:intadd(inta,intb)//這里是函數(shù)的定義,a,b都屬于形參{
//這里全部都是函數(shù)的實(shí)現(xiàn)
return(a+b);}
上面的函數(shù)展示了一個(gè)函數(shù)的各個(gè)部分——定義和實(shí)現(xiàn),在函數(shù)定義里的a和b都是形參,在函數(shù)的實(shí)現(xiàn)體內(nèi),也就是a+b,中的a和b是實(shí)參的替身。這里是最容易讓人困惑的地方,看一個(gè)例子吧,假設(shè)有一個(gè)函數(shù)calc需要調(diào)用add函數(shù)來實(shí)現(xiàn)加法的運(yùn)算功能:
intcalc(intcalctype,intp1,intp2)//calctype、p1和p2都是形參{
if(calctype==1){
returnadd(p1,p2);
//在這里的p1和p2是形參還是實(shí)參?}return0;}根據(jù)我們對形參和實(shí)參的定義,p1和p2對于calc函數(shù)來說是形參,但是對于add函數(shù)來說是實(shí)參。哦,恍然大悟了沒有?對于判斷到底是實(shí)參還是形參是有參考坐標(biāo)的。同樣的此時(shí)在函數(shù)add內(nèi)部a+b等同于p1+p2,因此說在函數(shù)的實(shí)現(xiàn)體內(nèi)形參(形式上的參數(shù))是調(diào)用時(shí)實(shí)參(實(shí)際上的參數(shù))的替身。相當(dāng)于形參為實(shí)參占了一個(gè)位子,使用函數(shù)的時(shí)候?qū)崊⒕妥诹诉@個(gè)位子上。如果您理解了上面的內(nèi)容,那么在使用函數(shù)的時(shí)候就再也沒有必要糾纏于實(shí)參和形參了,因?yàn)樗鼈兒掀饋硗瓿闪艘粋€(gè)參數(shù)的傳遞過程。調(diào)用函數(shù)的時(shí)候,按照形參的類型傳遞相應(yīng)的變量就可以了。形參和實(shí)參只是一個(gè)概念上的區(qū)分,在實(shí)際的二進(jìn)制層次并沒有這個(gè)概念,因此我們不要過分拘泥于此,理解了就好。2.4.2宏與函數(shù)的比較
對于一個(gè)初級(jí)的程序員來說,什么時(shí)候使用宏,什么時(shí)候使用函數(shù)還是有些暈的,這里我也順帶的提一下吧。從前面可以知道,當(dāng)預(yù)處理器遇到宏的引用時(shí),都是將它用宏語句進(jìn)行替換。因此,在每一個(gè)宏引用的地方都會(huì)增加相應(yīng)的宏代碼,結(jié)果就是使得編譯后的代碼加大。而函數(shù)則是直接調(diào)用函數(shù)的代碼,不會(huì)增加編譯后的代碼大小。不過使用函數(shù)的缺點(diǎn)是在函數(shù)調(diào)用的時(shí)候會(huì)增加一些額外的處理,這使得執(zhí)行函數(shù)的時(shí)間比相應(yīng)的宏代碼的執(zhí)行時(shí)間要長一些。因此如果在需要高效率的時(shí)候就應(yīng)該使用宏,如果需要程序變得更小則使用函數(shù)。關(guān)于函數(shù)的額外處理部分的信息將在下一節(jié)編譯器基礎(chǔ)中進(jìn)行介紹,到那個(gè)時(shí)候我們就會(huì)對函數(shù)是如何執(zhí)行的問題有一個(gè)全面的了解了。2.4.3函數(shù)指針
還記得指針嗎?什么,忘了!對您豎起我的大拇指,恭喜您已經(jīng)修成正果了,因?yàn)樵谀难劾镆呀?jīng)沒有了對指針特別的概念。還記得我們在介紹指針的時(shí)候說的嗎,指針就是一種變量。那么函數(shù)呢?其實(shí)每個(gè)函數(shù)也是一樣,每個(gè)函數(shù)名就是一個(gè)函數(shù)的首地址,因此我們也可以定義一個(gè)指針變量來存儲(chǔ)它,這個(gè)就是函數(shù)指針——指向一個(gè)函數(shù)的指針。這一節(jié)將讓您從另一個(gè)深度上加深對指針的認(rèn)識(shí)。
函數(shù)指針在多任務(wù)的操作系統(tǒng)環(huán)境下是十分有用的。在多任務(wù)操作系統(tǒng)環(huán)境下,由于牽涉到多個(gè)任務(wù)之間的交互,因此通常使用函數(shù)指針的形式將一個(gè)函數(shù)做為參數(shù)傳遞給另一個(gè)任務(wù),以便于當(dāng)另一個(gè)任務(wù)完成操作之后可以調(diào)用當(dāng)前任務(wù)的函數(shù)通知狀態(tài)。這個(gè)由“另一個(gè)任務(wù)”調(diào)用的函數(shù)就是大名鼎鼎的回調(diào)(Callback)函數(shù),這個(gè)回調(diào)函數(shù)的傳遞就是通過函數(shù)指針這個(gè)載體實(shí)現(xiàn)的。回調(diào)函數(shù)還常用在定時(shí)服務(wù)的程序里,在設(shè)置一個(gè)定時(shí)器的時(shí)候我們會(huì)指定一個(gè)回調(diào)函數(shù),用來在到達(dá)定時(shí)時(shí)間的時(shí)候調(diào)用以做出相應(yīng)的處理。
通常情況下使用如下方式定義一個(gè)函數(shù)指針類型:
typedefint(*FunctionType)(intparam1,intparam2);
FunctionTypepFunction;
使用的時(shí)候可以為pFunction賦值,在這里FunctionType的函數(shù)類型與我們上面的add函數(shù)的形式相同,因此我們可以改用下面的代碼來調(diào)用add函數(shù):intnResult;
FunctionTypepAddFunc=add;
nResult=pAddFunc(1,2);
//返回的結(jié)果是3
在這里之所以使用typedef來定義一個(gè)類型是為了保持在C語言環(huán)境下的函數(shù)調(diào)用一致。為了更好的理解函數(shù)指針,我將使用一個(gè)void型的函數(shù)指針來傳遞這個(gè)函數(shù)。打開VisualStudio2003.Net,新建VC控制臺(tái)工程Test3(請參考前面Test1的創(chuàng)建過程),輸入如下代碼:#include"stdafx.h"
typedefint(*FunctionType)(intparam1,intparam2);
intadd(inta,intb){
return(a+b);}
int_tmain(intargc,_TCHAR*argv[]){
intnResult;
void*pVoid=add;//通過Void型的指針傳遞函數(shù)指針
nResult=((FunctionType)pVoid)(1,2);
printf("Resultis%d\n",nResult,0,0);
return0;}編譯鏈接運(yùn)行,可以看見輸出結(jié)果是3。從上面的例子可以看出,一個(gè)函數(shù)名其實(shí)就是一個(gè)地址,可以直接賦值給指針,并且通過指針類型的轉(zhuǎn)換來實(shí)現(xiàn)函數(shù)的調(diào)用。進(jìn)一步,如果FunctionType類型與函數(shù)add之間的參數(shù)和返回值不一樣可以嗎?還是讓事實(shí)說話吧,將上面的程序修改成如下樣式:#include"stdafx.h"
typedefint(*FunctionType)(intparam1);
intadd(inta,intb){
return(a+b);}
int_tmain(intargc,_TCHAR*argv[]){
intnResult;
void*pVoid=add;//通過Void型的指針傳遞函數(shù)指針
nResult=((FunctionType)pVoid)(1);
printf("Resultis%d\n",nResult,0,0);
return0;}編譯鏈接,可以通過。運(yùn)行,輸出結(jié)果是2012749654,好大的數(shù)字!從上面的代碼情況看出,程序是可以運(yùn)行的,只不過輸出的結(jié)果不對。這是因?yàn)閍dd需要兩個(gè)參數(shù),而我們調(diào)用的時(shí)候卻只有一個(gè)參數(shù)。那么另一個(gè)參數(shù)是什么呢?答案是一個(gè)隨機(jī)數(shù)。前面我們說過,函數(shù)中會(huì)通過形參為實(shí)參留個(gè)“位子”,那么現(xiàn)在有兩個(gè)“位子”卻只用了一個(gè),那么可以預(yù)見的,這個(gè)空的位子中就是一個(gè)隨機(jī)數(shù)了。如果到現(xiàn)在為止,您對這個(gè)例子還不是很明白的話,請您一定要再重新體會(huì)一下,因?yàn)檫@部分的內(nèi)容對于您理解程序會(huì)有很大的幫助。2.4.4不要使用結(jié)構(gòu)體或數(shù)組做為函數(shù)的參數(shù)在前面講解函數(shù)參數(shù)的時(shí)候我們曾經(jīng)提到過,函數(shù)會(huì)通過聲明的形參為實(shí)參留“位子”,那么這是一個(gè)多大的“位子”呢?答案是和形參的數(shù)據(jù)類型的大小一樣,而且這個(gè)位子是存放在系統(tǒng)的堆棧中的。結(jié)果是參數(shù)的數(shù)據(jù)類型越大,需要的堆??臻g就越大,這對于內(nèi)存空間很小的系統(tǒng)來說是很不利的。而且在進(jìn)行實(shí)參傳遞的時(shí)候還需要將實(shí)參的內(nèi)容拷貝到對應(yīng)的“位子”中,因此就增加了很多的調(diào)用函數(shù)時(shí)的額外處理。(這里稱呼成堆棧,實(shí)際上應(yīng)該是“?!保?yàn)槲覀兡壳傲?xí)慣于稱呼“?!睘槎褩??!岸选蓖ǔV傅氖且粋€(gè)使用malloc等函數(shù)分配的內(nèi)存空間。)基于上述原因,推薦不直接使用大的結(jié)構(gòu)體或數(shù)組做為函數(shù)的參數(shù),請使用指針進(jìn)行傳遞,這樣可以省掉很多調(diào)用函數(shù)的額外處理,提高函數(shù)的執(zhí)行效率。同時(shí)占用的堆棧空間比較小,減少了堆棧溢出的可能性,因此使用指針傳遞參數(shù)是一個(gè)多贏的方法。千萬不要因?yàn)楹ε轮羔樁皇褂盟?,做為一名C語言的程序員是永遠(yuǎn)也逃脫不了指針的,況且一旦掌握了它的精髓,您就會(huì)對它愛不釋手了。2.4.5使用指針參數(shù)傳遞數(shù)據(jù)指針做為C語言的靈魂具有很強(qiáng)的技巧性和靈活性,而做為參數(shù)傳遞數(shù)據(jù)則是它非常重要的應(yīng)用之一。這里的“傳遞”有兩層含義:傳入和傳出。傳入就是調(diào)用函數(shù)的地方將數(shù)據(jù)傳遞給函數(shù)進(jìn)行處理,例如一個(gè)數(shù)組排序的函數(shù),需要通過指針將數(shù)組傳遞給函數(shù)使用。這里以一個(gè)進(jìn)行字母排序的函數(shù)為例,代碼如下:voidCharSort(char*pString){char*pCurrPtr;chartemp;
//判斷參數(shù)的合法性if(pString==NULL){
return;}
//排序算法for(;*pString!=‘\0’;pString++){
for(pCurrPtr=pString+1;*pCurrPtr!=‘\0’;pCurrPtr++)
{
//將最小的放在當(dāng)前位置
if(*pString>*pCurrPtr)
{
//交換數(shù)據(jù)
temp=*pString;
*pString=*pCurrPtr;
*pCurrPtr=temp;
}
}}}
這個(gè)函數(shù)是一個(gè)簡單的冒泡排序算法,使用這個(gè)函數(shù)需要傳遞一個(gè)char類型的數(shù)組。此時(shí)這個(gè)指針參數(shù)pString就包含了傳遞參數(shù)給函數(shù)使用的功能。正如上一節(jié)所講的一樣,這里面使用指針來傳遞數(shù)據(jù)是十分高效的,只需要在函數(shù)棧空間內(nèi)分配一個(gè)指針空間就好了。
指針參數(shù)的另一個(gè)作用是可以傳出參數(shù),返回函數(shù)的處理結(jié)果。通常情況下我們都是通過函數(shù)的返回值來返回處理結(jié)果的,但是如果遇到需要返回一個(gè)大數(shù)組或大數(shù)據(jù)結(jié)構(gòu)的時(shí)候,使用返回值傳遞參數(shù)就顯得效率低下了。我們?nèi)耘f以上面的CharSort函數(shù)為例,這個(gè)函數(shù)的參數(shù)其實(shí)就具有傳入和傳出數(shù)據(jù)兩種功能,它首先通過pString參數(shù)將排序的字符串傳遞給函數(shù),然后函數(shù)的處理結(jié)果也是直接通過pString來寫入原存儲(chǔ)空間的。這個(gè)過程的示意圖如下:CharSort函數(shù)
直接在pString所指向的Array空間內(nèi)進(jìn)行排序Array[]=“asdfghjkl896”;pString參數(shù)Array[]=“689adfghjkls”;圖2.5指針參數(shù)的傳入傳出數(shù)據(jù)功能
使用指針參數(shù)傳出數(shù)據(jù)在編寫C語言程序時(shí)是十分普遍的,除了上面的傳遞數(shù)組外,還可以傳遞單個(gè)數(shù)據(jù),也可以通過二重指針傳遞指針值??傊羔樧鰹閰?shù)使用是十分靈活多變的,各位讀者可以仔細(xì)體會(huì),關(guān)鍵的是理解指針的本質(zhì)意義——地址類型的變量。2.5C語言中幾個(gè)特殊的關(guān)鍵詞
在這里我只是簡單地介紹一下volatile、__packed和const的作用,省得我們在看到它們的時(shí)候不知所措。2.5.1volatile關(guān)鍵詞
volatile的中文意思是“易揮發(fā)的”,它主要是給編譯器提個(gè)醒,告訴編譯器對于volatile變量不要輕易的進(jìn)行優(yōu)化,因?yàn)樵诔绦蜻\(yùn)行過程中這個(gè)值會(huì)被其他的任務(wù)或硬件改變。在編譯器中對于語句通常會(huì)做一些優(yōu)化,例如有如下程序:
boolbExit==FALSE;
…
for(;;)
{
…
if(bExit)
{
break;
}}
假設(shè)現(xiàn)在有另一個(gè)任務(wù)或線程通過bExit來控制程序的退出。如果此時(shí)變量不使用volatile關(guān)鍵字說明的話,編譯時(shí)就會(huì)對if(bExit)進(jìn)行優(yōu)化,不再在每一次for循環(huán)中判斷bExit了,這樣就會(huì)導(dǎo)致程序運(yùn)行錯(cuò)誤。因此,此時(shí)應(yīng)使用volatile關(guān)鍵字說明bExit變量,這樣編譯器就不會(huì)做這樣的優(yōu)化了。2.5.2__packed關(guān)鍵詞__packed用來聲明結(jié)構(gòu)體采用單字節(jié)偏移。并不是所有的編譯器都支持這個(gè)選項(xiàng)。使用__packed聲明的結(jié)構(gòu)體會(huì)壓縮空間。例如有下面一個(gè)結(jié)構(gòu)體:struct_Test{
inta;
charb;
charc;
intd;}Test;如果不使用__packed聲明,在ARM編譯器中sizeof(Test)等于12(在ARM編譯器中是4字節(jié)偏移,int也是4字節(jié)變量)。加入__packed說明后,sizeof(Test)等于10,編譯器會(huì)壓縮Test結(jié)構(gòu)體中b、c和d變量之間的padding字節(jié)。對比示意圖如下:
inta;(4B)charb;(1B)
charc;(1B)
intd;(4B)
inta;(4B)
charb;(1B)charc;(1B)
intd;(4B)
Padding(2B)
無__packed有__packed
圖2.6結(jié)構(gòu)體內(nèi)存映射
從這個(gè)圖中可以看出,經(jīng)過__packed說明之后的結(jié)構(gòu)體,相對于沒有使用__packed說明的節(jié)省了2字節(jié)的padding存儲(chǔ)空間,實(shí)際上這給我們提供了一種緊湊數(shù)據(jù)的方法。2.5.3const關(guān)鍵詞
使用const的好處在于它允許指定一種語意上的約束——某種數(shù)據(jù)不能被修改——編譯器具體來實(shí)施這種約束。通過const,我們可以告知編譯器和其他程序員某個(gè)值要保持不變。只要是這種情況,我們就要明確地使用const,因?yàn)檫@樣做就可以借助編譯器的幫助確保這種約束不被破壞。對指針來說,可以指定指針本身為const,也可以指定指針?biāo)傅臄?shù)據(jù)為const,或二者同時(shí)指定為const,還有,兩者都不指定為const:char*p
="hello";
//非const指針,非const數(shù)據(jù)constchar*p
="hello";
//非const指針,const數(shù)據(jù)char*constp
="hello";
//const指針,非const數(shù)據(jù)constchar*constp
="hello";
//const指針,const數(shù)據(jù)語法并非看起來那么變化多端。一般來說,我們可以在頭腦里畫一條垂直線穿過指針聲明中的星號(hào)(*)位置,如果const出現(xiàn)在線的左邊,指針指向的數(shù)據(jù)為常量;如果const出現(xiàn)在線的右邊,指針本身為常量;如果const在線的兩邊都出現(xiàn),二者都是常量。標(biāo)示為const的數(shù)據(jù),在編譯器中當(dāng)作RO只讀數(shù)據(jù)處理。
在指針?biāo)笧槌A康那闆r下,有些程序員喜歡把const放在類型名之前,有些則喜歡把const放在類型名之后、星號(hào)之前。所以,下面的函數(shù)取的是同種參數(shù)類型:
voidf1(constint*pw);
//f1取的是指向,widget常量對象的指針
voidf2(intconst*pw);
//同f22.6地址對齊
我們知道,在計(jì)算機(jī)系統(tǒng)中都是使用地址來管理數(shù)據(jù),每一個(gè)數(shù)據(jù)實(shí)體都存儲(chǔ)在一定的地址空間內(nèi),如一個(gè)長度為100的char型數(shù)組存儲(chǔ)在一個(gè)100字節(jié)的連續(xù)空間中。一個(gè)連續(xù)的地址空間中既有奇數(shù)的地址也有偶數(shù)的地址,這中間就存在了一個(gè)數(shù)據(jù)以什么樣的地址做為起始地址的問題。許多實(shí)際的計(jì)算機(jī)系統(tǒng)對基本類型數(shù)據(jù)在內(nèi)存中存放的位置有限制,它們會(huì)要求這些數(shù)據(jù)的首地址的值是某個(gè)數(shù)(通常它為4或8)的倍數(shù),這就是所謂的內(nèi)存對齊。而這個(gè)系數(shù)則被稱為該數(shù)據(jù)類型的對齊模數(shù)(alignmentmodulus)。
這種強(qiáng)制的要求一來簡化了處理器與內(nèi)存之間傳輸系統(tǒng)的設(shè)計(jì),二來可以提升讀取數(shù)據(jù)的速度。比如有這樣的一種處理器,它每次讀寫內(nèi)存的時(shí)候都從某個(gè)8倍數(shù)的地址開始,一次讀出或?qū)懭?個(gè)字節(jié)的數(shù)據(jù),假如軟件能保證double類型的數(shù)據(jù)都從8倍數(shù)地址開始,那么讀或?qū)懸粋€(gè)double類型數(shù)據(jù)就只需要一次內(nèi)存操作。否則,我們就可能需要兩次內(nèi)存操作才能完成這個(gè)動(dòng)作,因?yàn)閿?shù)據(jù)或許恰好橫跨在兩個(gè)符合對齊要求的8字節(jié)內(nèi)存塊上。某些處理器甚至在數(shù)據(jù)不滿足地址對齊要求的情況還會(huì)出錯(cuò),
地址對齊的問題主要表現(xiàn)在兩個(gè)方面:一是通過指針類型轉(zhuǎn)換訪問數(shù)據(jù)的情況;二是在結(jié)構(gòu)體內(nèi)部的數(shù)據(jù)對齊和訪問。接下來我們將就這兩個(gè)問題做一個(gè)闡述。2.6.1指針的數(shù)據(jù)類型轉(zhuǎn)換
C語言的指針是非常靈活的,我可以將任何一種數(shù)據(jù)類型的指針轉(zhuǎn)換成另一種數(shù)據(jù)類型的指針,例如,將一個(gè)(char*)轉(zhuǎn)換成一個(gè)(int*),或者將一個(gè)(int*)轉(zhuǎn)換成(char*)。這樣的指針類型轉(zhuǎn)換在C語言中十分的普遍,更有甚者,我們還可以定義一個(gè)無類型的指針(void*),可以不受限制的接受任何類型的指針,而編譯器也不會(huì)提示任何錯(cuò)誤。這既是C語言靈活的標(biāo)志,但在其中也蘊(yùn)藏著殺機(jī)!
我們可以假設(shè)這樣的一種情況,我們從一個(gè)串口設(shè)備或者網(wǎng)絡(luò)設(shè)備上接收16個(gè)字節(jié)的數(shù)據(jù),這些數(shù)據(jù)都是使用字節(jié)方式傳送的。此時(shí)我們規(guī)定,這些數(shù)據(jù)的第一個(gè)字節(jié)表示數(shù)據(jù)的類型,而其后的數(shù)據(jù)則表示真正的數(shù)據(jù)內(nèi)容。此時(shí)我們會(huì)定義這樣的一個(gè)數(shù)組來接收數(shù)據(jù):
bytersv_buf[16];
現(xiàn)在,我們假設(shè)數(shù)據(jù)已經(jīng)存儲(chǔ)在這個(gè)數(shù)組中了,同時(shí)假設(shè)這個(gè)數(shù)組在內(nèi)存地址中的0x0至0xF的空間內(nèi)。我們通過對這個(gè)數(shù)組第一個(gè)字節(jié)rsv_buf[0]的判斷,知道這里面存儲(chǔ)的是一個(gè)word(半字,雙字節(jié))型的數(shù)據(jù)。于是我們使用下面的方式獲得了數(shù)據(jù):
*((word*)&rsv_buf[1]);
這里面發(fā)生了什么事情呢?我們先對rsv_buf[1]進(jìn)行取址操作,然后轉(zhuǎn)換成word型的指針,然后通過一個(gè)取值運(yùn)算符“*”來獲得這個(gè)word型指針中的數(shù)據(jù)。那么此時(shí)&rsv_buf[1]到底是一個(gè)什么樣的值呢?我們知道rsv_buf的首地址是0,那么當(dāng)然了&rsv_buf[1]的值就是1了。我們現(xiàn)在是要從一個(gè)奇地址中獲得一個(gè)雙字節(jié)的變量。
這個(gè)時(shí)候地址對齊的問題出現(xiàn)了。當(dāng)然對于一個(gè)支持這種訪問方式的CPU來說,不會(huì)出現(xiàn)任何問題,那么對于不支持這種運(yùn)行方式的CPU呢?一個(gè)錯(cuò)誤產(chǎn)生了,或許系統(tǒng)崩潰了,或許獲取了錯(cuò)誤的數(shù)據(jù)。不管怎樣這都將打破系統(tǒng)的正常運(yùn)行,而且CPU不會(huì)為我們提供任何可以更正錯(cuò)誤的機(jī)會(huì)。
由于我們并不能準(zhǔn)確地知道我們所寫的程序?qū)頃?huì)運(yùn)行在什么類型的CPU上面,也不知道它是否支持非地址對齊方式的數(shù)據(jù)訪問,因此我們應(yīng)該盡可能的避免在程序中定義這樣的數(shù)據(jù)結(jié)構(gòu)。即便不可避免的定義了這樣的數(shù)據(jù)結(jié)構(gòu),也要提供一種轉(zhuǎn)換的機(jī)制。比如,在本例中我們可以在定義一個(gè)rsv_data來轉(zhuǎn)存&rsv_buf[1]地址之后的數(shù)據(jù),然后再進(jìn)行類型轉(zhuǎn)換,這樣就不會(huì)出現(xiàn)問題了。當(dāng)然,即便我們清楚的了解我們的程序?qū)⒁\(yùn)行的CPU支持非對齊方式的數(shù)據(jù)訪問,那么也要盡可能的避免這種情況的發(fā)生,因?yàn)樗鼘⒂绊懳覀兊某绦虻膱?zhí)行效率,通常CPU執(zhí)行這些代碼需要更多的指令周期。2.6.2結(jié)構(gòu)體內(nèi)存布局
地址對齊的問題也表現(xiàn)在結(jié)構(gòu)體中。C語言規(guī)定一種結(jié)構(gòu)類型的大小是它所有字段的大小以及字段之間或字段尾部的填充區(qū)大小之和。填充區(qū)?是的,這就是為了使結(jié)構(gòu)體字段滿足地址對齊要求而額外分配給結(jié)構(gòu)體的空間。C語言的標(biāo)準(zhǔn)規(guī)定結(jié)構(gòu)體類型的對齊要求不能比它所有字段中要求最嚴(yán)格的那個(gè)寬松,可以更嚴(yán)格。我們可以看下面的例子:typedefstruct_Example1{
byte
a;
dword
b;}Example1;
我們現(xiàn)在定義了一個(gè)結(jié)構(gòu)體Example1,在其中有兩個(gè)成員變量a和b。假設(shè)這個(gè)結(jié)構(gòu)體按照連續(xù)的方式布局,那么這個(gè)結(jié)構(gòu)體將占用3個(gè)字節(jié)的空間,a成員在結(jié)構(gòu)體中的偏移是0,那么結(jié)構(gòu)體成員b的偏移則是1,而成員b是一個(gè)四字節(jié)的變量。這中間就存在了一個(gè)地址對齊的問題了。如果假設(shè)我們現(xiàn)在是編譯器,我們將如何安排這個(gè)結(jié)構(gòu)體的內(nèi)存結(jié)構(gòu)呢?按照C語言的標(biāo)準(zhǔn),這個(gè)結(jié)構(gòu)體內(nèi)成員變量對齊要求取成員地址對齊要求之大者的原則,這個(gè)結(jié)構(gòu)體中的地址對齊的模數(shù)應(yīng)該是sizeof(word)=4。實(shí)現(xiàn)這種對齊的唯一方式就是在成員a和b之間增加3個(gè)字節(jié)填充區(qū)。增加填充區(qū)后的內(nèi)存模式如下圖2.7所示。apadding…b01234567圖2.7Example1的結(jié)構(gòu)體內(nèi)存模型
這個(gè)方案在a與b之間多分配了3個(gè)填充(padding)字節(jié),這樣當(dāng)整個(gè)結(jié)構(gòu)體首地址滿足4字節(jié)的對齊要求時(shí),b字段也一定能滿足dword型的4字節(jié)對齊規(guī)定。那么sizeof(Example1)顯然就應(yīng)該是8,而b字段相對于結(jié)構(gòu)體首地址的偏移就是4。下面我們再來看一看將這兩個(gè)成員變量調(diào)換位置之后的情況。typedefstruct_Example2{
dword
a;
byte
b;}Example2;
或許您會(huì)認(rèn)為Example2的內(nèi)存布局會(huì)比Example1的簡單,就是一個(gè)4字節(jié)的變量b加上一個(gè)1字節(jié)的變量,總共是5個(gè)字節(jié)長度。因?yàn)镋xample2結(jié)構(gòu)同樣要滿足4字節(jié)對齊規(guī)定,而此時(shí)a的地址與結(jié)構(gòu)體的首地址相等,所以a和b都一定也是4字節(jié)對齊。嗯,分析的有道理,可是不全面。讓我們來考慮一下定義一個(gè)Example2類型的數(shù)組會(huì)出現(xiàn)什么問題。C標(biāo)準(zhǔn)規(guī)定,任何類型(包括自定義結(jié)構(gòu)類型)的數(shù)組所占空間的大小一定等于一個(gè)單獨(dú)的該類型數(shù)據(jù)的大小乘以數(shù)組元素的個(gè)數(shù)。換句話說,數(shù)組各元素之間不會(huì)有空隙。按照上面的方案,一個(gè)Example2數(shù)組的布局就是如圖2.8中所示的一樣。ba01234568ab79…10圖2.8Example2數(shù)組內(nèi)存模型
我們可以看到,此數(shù)組的第一個(gè)成員變量已經(jīng)是四字節(jié)對齊了,可是第二個(gè)成員變量的起始地址卻是5開始的。這就不能滿足C語言的要求,因此在Example2類型的數(shù)組中,依然要增加填充區(qū),如圖2.9所示。bpadding…a01234567圖2.9Example2的結(jié)構(gòu)體內(nèi)存模型
現(xiàn)在無論是定義一個(gè)單獨(dú)的Example2變量還是Example2數(shù)組,均能保證所有元素的所有字段都滿足對齊規(guī)定。那么sizeof(Example2)仍然是8,而a的偏移為0,b的偏移是4。現(xiàn)在我們已經(jīng)掌握了結(jié)構(gòu)體內(nèi)存布局的基本準(zhǔn)則,嘗試分析一個(gè)稍微復(fù)雜點(diǎn)的類型吧。定義一個(gè)Example3的結(jié)構(gòu)體:typedefstruct_Example3{
byte
a;word
b;dword
c;}Example3;
這里面有歧義的地方就是b這個(gè)變量采用什么樣的對齊方式。在這個(gè)結(jié)構(gòu)體中最大的偏移是4字節(jié),那么成員b應(yīng)該是滿足4個(gè)字節(jié)的地址對齊方式嗎?實(shí)際情況是變量b只需要滿足它自身的對齊方式就可以了,也就是sizeof(word)=2。因此現(xiàn)在我們可以得到圖2.10的內(nèi)存布局結(jié)構(gòu)。ac01234567…b圖2.10Example3的結(jié)構(gòu)體內(nèi)存模型
那么現(xiàn)在我們可以知道,在結(jié)構(gòu)體內(nèi)部成員變量地址對齊的要求就是滿足自身的需求,單字節(jié)的變量可以緊接著前面的成員,雙字節(jié)的要求偶數(shù)地址對齊,四字節(jié)的要求4字節(jié)對齊等等,可以依次類推,如果緊接著上一個(gè)成員的地址不符合要求就在中間添加填充字節(jié)。而對于整個(gè)結(jié)構(gòu)體要求的地址對齊方式則取成員變量中要求地址對齊最大的那個(gè)。
在實(shí)際開發(fā)中,我們可以通過指定編譯選項(xiàng)來更改編譯器的對齊規(guī)則(不同的編譯器有不同的設(shè)置方式,請參考相應(yīng)的編譯器文檔)。例如我們可以指定字節(jié)對齊的方式是8,也可以指定是4,甚至還可以是1。在設(shè)置對齊規(guī)則的時(shí)候,采用的是參數(shù)與默認(rèn)取二者之小的方式。例如我們通過編譯器參數(shù)設(shè)置結(jié)構(gòu)體偏移量為2,那么對于Example1中的的內(nèi)存布局就會(huì)變成圖2.11中表示的那樣。a…b012345圖2.11Example1的2字節(jié)偏移內(nèi)存模型
此時(shí)僅僅會(huì)增加一個(gè)字節(jié)的填充區(qū)。雖然結(jié)構(gòu)體中b成員的偏移是4,但是由于我們設(shè)置了編譯器的偏移參數(shù)為2,因此將會(huì)使用2作為此結(jié)構(gòu)體的最大偏移。如果此時(shí)我們將這個(gè)結(jié)構(gòu)體的偏移設(shè)置為1,那么無論這個(gè)結(jié)構(gòu)體的成員排列如何,都不會(huì)有任何的填充字節(jié)。此時(shí)的情況就類似于我們前面所講的編譯器__packed關(guān)鍵詞的意思了。
在這種編譯器設(shè)置的字節(jié)對齊要求比結(jié)構(gòu)體中變量自然要求小的情況下,將會(huì)出現(xiàn)訪問成員變量時(shí)的地址對齊問題。就像圖2.11中所表示的一樣,成員b是一個(gè)4字節(jié)的變量,但是它的首地址卻是2,不能被4整除的一個(gè)地址,我們在使用這樣的結(jié)構(gòu)體成員的時(shí)候,會(huì)同指針的轉(zhuǎn)換的時(shí)候一樣出現(xiàn)地址對齊的問題嗎?
如果我們不做任何事情,肯定會(huì)出現(xiàn)問題。不過請您放心,這些事情已經(jīng)由編譯器為我們做了。這是可以理解的,我們使用編譯器設(shè)置了結(jié)構(gòu)體的字節(jié)對齊要求,不過出現(xiàn)問題當(dāng)然要由編譯器負(fù)責(zé)了。在這種情況下使用結(jié)構(gòu)體成員的時(shí)候,編譯器在編譯相應(yīng)的代碼時(shí),會(huì)額外的插入一些CPU指令來消除地址對齊問題的影響。典型的,在ARMC語言編譯器中,如果訪問一個(gè)使用__packed關(guān)鍵字聲明的結(jié)構(gòu)體成員變量的時(shí)候(如使用pointer->b語句訪問b變量),每一個(gè)訪問成員的C語句都會(huì)變?yōu)?個(gè)CPU指令,同時(shí)使用3個(gè)通用的CPU寄存器。這不但會(huì)影響程序執(zhí)行效率,而且還會(huì)增加代碼的尺寸。所以,不到萬不得已,請不要使用這種方式的結(jié)構(gòu)體。2.7小結(jié)
噢,我要長長的松一口氣了,終于將一些C語言的基礎(chǔ)介紹給完了。在這一章里,主要介紹了C語言的一些特性,其中大部分是讀者在使用中會(huì)遇到的令人迷惑的議題,如果我們能夠?qū)⑦@一章的內(nèi)容透徹的理解了,那么我們就基本上掌握了C語言本身的精髓(雖然還有很多高級(jí)的應(yīng)用,不過那都不是C語言本身的了)。在這里我要提醒那些初級(jí)的程序員朋友們,在掌握一門程序語言技術(shù)的同時(shí),千萬不要忘記養(yǎng)成一個(gè)良好的編碼風(fēng)格。因?yàn)槿绻氤蔀橐幻麅?yōu)秀的程序員不但要能夠?qū)懗鰪?fù)雜的程序,而且要能夠?qū)?fù)雜的問題用容易閱讀的程序來實(shí)現(xiàn)。最好的程序是要大家都能看懂的,而不是只有您一個(gè)人能看懂的程序。記住一個(gè)程序的KISS準(zhǔn)則(此KISS非彼Kiss也):KeepItSimpleandStupid,也就是保持程序簡單性和傻瓜性,別人能容易看懂的程序才是好程序。思考題1、指針和變量的相同點(diǎn)和區(qū)別的地方在哪里?2、函數(shù)的名稱可以賦值給一個(gè)int型的指針變量嗎?3、如果沒有sizeof怎樣能夠獲得一個(gè)結(jié)構(gòu)體的大???第三章編譯器基礎(chǔ)
看了這個(gè)題目,請不要誤會(huì)我要告訴您編譯器是怎么實(shí)現(xiàn)的,我寫這節(jié)的主要目的是告訴您通常編譯器是怎樣對待您所寫的程序的。大家都知道,程序最終都要在CPU上運(yùn)行,那么像C語言這樣的高級(jí)語言來說,編譯器就是聯(lián)結(jié)C和CPU之間的橋梁了。在這一節(jié)里我要告訴您編譯器是如何充當(dāng)這個(gè)“橋梁”角色的。
本節(jié)首先講述一下程序員的層次問題,目的是讓我們自己知道需要成為一名什么樣的程序員;然后分別是編譯器的相關(guān)介紹以及編譯器是如何“對待”程序等內(nèi)容。3.1軟件和程序員的層次
如果要了解程序員的層次,那就要先看看程序的層次了??磮D3.1:程序源代碼編譯器中央處理器(CPU)圖3.1程序的層次上圖的語言描述就是程序源代碼經(jīng)過編譯器生成可以在CPU運(yùn)行的二進(jìn)制代碼。由此衍生出了程序員的兩個(gè)層次——語言層次和二進(jìn)制層次。所謂的語言層次的程序員,就
溫馨提示
- 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
- 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
- 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會(huì)有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
- 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
- 5. 人人文庫網(wǎng)僅提供信息存儲(chǔ)空間,僅對用戶上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對任何下載內(nèi)容負(fù)責(zé)。
- 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請與我們聯(lián)系,我們立即糾正。
- 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時(shí)也不承擔(dān)用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。
最新文檔
- 監(jiān)獄民警餐廳服務(wù)方案(3篇)
- 核酸采樣投標(biāo)方案(3篇)
- 同城快遞管理辦法
- 后勤誠信管理辦法
- 員工借條管理辦法
- 唐山暖氣管理辦法
- 商業(yè)機(jī)密管理辦法
- 商場結(jié)算管理辦法
- 商票貼現(xiàn)管理辦法
- 噴漆環(huán)保管理辦法
- 競爭性談判業(yè)務(wù)培訓(xùn)
- 口腔科年終總結(jié)報(bào)告課件卡通藍(lán)色
- 托管班管理制度范本
- 教育部《中小學(xué)校園食品安全和膳食經(jīng)費(fèi)管理工作指引》專題講座
- 腦機(jī)接口課件
- 除數(shù)是兩位數(shù)的除法練習(xí)題(84道)
- 風(fēng)電、光伏項(xiàng)目前期及建設(shè)手續(xù)辦理流程匯編
- 公安局直屬單位民警食堂食材供應(yīng) 投標(biāo)方案(技術(shù)方案)
- DB11T 220-2014 養(yǎng)老機(jī)構(gòu)醫(yī)務(wù)室服務(wù)規(guī)范
- 標(biāo)準(zhǔn)土方棄土堆放合同協(xié)議
評論
0/150
提交評論