第二章為程序設(shè)計技術(shù),本文為2.2.1 內(nèi)存對齊和2.2.2 基本數(shù)據(jù)類型。
我們知道,數(shù)組和指針是相同類型有序數(shù)據(jù)的集合,但很多時候需要將不同類型的數(shù)據(jù)捆綁在一起作為一個整體來對待,使程序設(shè)計更方便。在C語言中,這樣的一組數(shù)據(jù)被稱為結(jié)構(gòu)體。
>>>2.2.1內(nèi)存對齊
雖然所有的變量最后都會保存到特定地址的內(nèi)存中,但相應(yīng)的內(nèi)存空間必須滿足內(nèi)存對齊的要求。主要出于兩個方面的原因:
-
性能原因:如果數(shù)據(jù)存放在未對齊的內(nèi)存空間中,則處理器訪問變量時需要做兩次內(nèi)存訪問,而對齊的內(nèi)存訪問僅需要一次訪問。
在32位微處理器中,處理器訪問內(nèi)存都是按照32位進行的,即一次讀取或?qū)懭攵际?個字節(jié),比如,地址0x0 ~ 0xF這16字節(jié)的內(nèi)存,對于微處理器來說,不是將其看作16個單一字節(jié),而是4個塊,每塊4個字節(jié),詳見圖2.4。
圖2.4 內(nèi)存空間示意圖
顯然,只能從0x0、0x4、0x8、0xC等地址為4的整數(shù)倍的內(nèi)存中一次取出4個字節(jié),并不能從任意地址開始一次讀取4個字節(jié)。假定將一個占用4字節(jié)的int類型數(shù)據(jù)存放到地址0開始的4字節(jié)內(nèi)存中,其示意圖詳見圖2.5。
圖2.5 按內(nèi)存對齊的方式存儲int數(shù)據(jù)
由于int類型數(shù)據(jù)存放在塊0中,因此CPU僅需一次內(nèi)存訪問即可完成對該數(shù)據(jù)的讀取或?qū)懭搿7粗绻麑⒃搃nt類型數(shù)據(jù)存放在地址1開始的4字節(jié)內(nèi)存空間中,其示意圖詳見圖2.6。
圖2.6 按內(nèi)存未對齊的方式存儲int數(shù)據(jù)
此時,數(shù)據(jù)存放在塊0和塊1兩個塊中,若要完成對該數(shù)據(jù)的訪問,必須經(jīng)過兩次內(nèi)存訪問,先通過訪問塊0得到該數(shù)據(jù)的3個字節(jié),再通過訪問塊1得到該數(shù)據(jù)的1個字節(jié),最后通過運算,將這幾個字節(jié)合并為一個完整的int型數(shù)據(jù)。由此可見,若數(shù)據(jù)存儲在未對齊的內(nèi)存空間中,將大大降低CPU的效率。但在某些特定的微處理器中,它根本不愿意干這種事情,這種情況下,就出現(xiàn)系統(tǒng)異常,直接崩潰了。內(nèi)存對齊的具體規(guī)則如下:
(1)結(jié)構(gòu)體各個成員變量的內(nèi)存空間的首地址必須是“對齊系數(shù)”和“變量實際長度”中較小者的整數(shù)倍。假設(shè)要求變量的內(nèi)存空間按照4字節(jié)對齊,則內(nèi)存空間的首地址必須是4的整數(shù)倍,滿足條件的地址有0x0、0x4、0x8、0xC……
(2)對于結(jié)構(gòu)體,在其各個數(shù)據(jù)成員都完成對齊后,結(jié)構(gòu)體本身也需要對齊,即結(jié)構(gòu)體占用的總大小應(yīng)該為“對齊系數(shù)”和“最大數(shù)據(jù)成員長度” 中較小值的整數(shù)倍。
一般來說,對齊系數(shù)與微處理器的字長相同,比如,32位微處理器的對齊系數(shù)是4字節(jié),變量的實際長度與其類型相關(guān),計算類型長度的方法如下:
該程序的輸出為:1、4、4、4、8。假定CPU為32位微處理器,對齊系數(shù)為4,結(jié)構(gòu)體變量data的定義如下:
結(jié)構(gòu)體的各個成員都是從結(jié)構(gòu)體首地址(其由編譯器保證必然滿足內(nèi)存對齊的要求,假定為0)開始計算,按照定義的順序依次存放各個成員,詳見表2.1。
表2.1依次存放各個成員
實際存放位置使用[x,y]表示,x表示起始地址,y表示結(jié)束地址。如果x與y相等,則直接使用[x]表示。以成員b為例,其長度為2,小于對齊系數(shù),因此按照2字節(jié)對齊,就要求其地址必須是2的倍數(shù),地址0已經(jīng)被成員a占用,則只能使用滿足要求的鄰近的內(nèi)存空間[2,3]存放成員b。而空間[1]由于不滿足存放成員b的要求,則只能被棄用。特別地,對于數(shù)組成員c,存放時不能將其看作一個整體,即長度為2的成員,應(yīng)該分別看作兩個成員c[0]和c[1]。由此可見,實際存放位置為[0,24],1、6、7、17、18、19部分內(nèi)存空間被棄用。
當(dāng)所有成員存放完畢后,則結(jié)構(gòu)體本身也需要對齊,即結(jié)構(gòu)體的大小也應(yīng)該為對齊字節(jié)數(shù)的整數(shù)倍,對齊字節(jié)數(shù)取長度最長的成員和“對齊系數(shù)”的較小值。在這里,其長度最長的成員為double類型的成員d,其長度為8,大于對齊系數(shù),因此結(jié)構(gòu)體本身也要按照4字節(jié)對齊,其占用的空間大小必須是4的整數(shù)倍。雖然當(dāng)前存放位置為[0,24],只占用了25個字節(jié)。由于必須滿足4的整數(shù)倍,因此實際上結(jié)構(gòu)體占用的空間是28個字節(jié),即[0,27]。驗證結(jié)構(gòu)體占用空間大小的方法如下:
雖然所有成員的總長度為19個字節(jié),但結(jié)構(gòu)體實際占用了28個字節(jié),多余的9個字節(jié)空間為內(nèi)存對齊棄用的空間,即1、6、7、17、18、19、25、26、27,分為4個段:[1],[6,7],[17,19],[25,27]。查看表2.1可知,這些浪費空間的前面,存放的都是char型數(shù)據(jù),由于char型數(shù)據(jù)只占用一個字節(jié),往往使得其緊接著的空間不能被其它長度更長的數(shù)據(jù)使用。
為了降低內(nèi)存浪費的概率,應(yīng)該在char型數(shù)據(jù)之后,存放長度最小的成員。即在定義結(jié)構(gòu)體時,應(yīng)按照長度遞增的順序依次定義各個成員。優(yōu)化示例結(jié)構(gòu)體的定義如下:
類似地,依次存放各個成員,詳見表2.2。
表2.2依次存放各個成員
所有成員實際存放位置為[0,19],中間的地址為5的內(nèi)存空間被棄用。由于結(jié)構(gòu)體占用的大小為20個字節(jié),已經(jīng)是4的整數(shù)倍,因此無需再做額外的處理。結(jié)構(gòu)體只浪費了1個字節(jié)空間,使用率達到95%。顯然,通過優(yōu)化結(jié)構(gòu)體成員的定義順序,在同樣滿足內(nèi)存對齊的要求下,可以大大地減少內(nèi)存的浪費。
>>>2.2.2基本數(shù)據(jù)類型
1.范圍值校驗
如果有min≤value≤max,則check()范圍值校驗函數(shù)需要3個int型參數(shù)value、min和max。如果value合法,則返回true,否則返回false,詳見程序清單2.10。
程序清單 2.10 rangeCheck()范圍值校驗函數(shù)的實現(xiàn)(1)
-
代碼整潔之道
rangeCheck是一個非常具有描述性的名字,因為它較好地描述了函數(shù)要做的事,所以好名字的價值怎么評價都不過分。如果每個示例都讓你感到深合己意,那就是整潔代碼。函數(shù)越短小,功能越集中,就越容易取一個好名字。名字長一些并不可怕,長而具有描述性的名字,比短而令人費解的名字更好。選擇具有描述性的名字能幫助程序員理清模塊的設(shè)計思路,追索好名字往往會使代碼重構(gòu)得更好。
從代碼整潔之道的角度來看,最理想的函數(shù)參數(shù)個數(shù)是0(零參數(shù)函數(shù)),其次是單參數(shù)函數(shù),再次是雙參數(shù)函數(shù),因盡量避免三參數(shù)函數(shù)。如果需要三個以上的參數(shù),需要有足夠的理由,否則無論如何也不要這樣做,因為參數(shù)帶有太多的概念性。
從測試的角度來看,參數(shù)甚至更叫人感到為難,因為編寫確保參數(shù)的各種組合運行正常的測試用例,且測試覆蓋所有可能值的組合是令人生畏的事情。輸出參數(shù)比輸入?yún)?shù)還要難以理解,因為人們習(xí)慣性地認為,信息通過參數(shù)輸入函數(shù),通過返回值從函數(shù)中輸出,輸出參數(shù)往往讓人苦思之后才會覺得恍然大悟。如果函數(shù)看起來需要兩個、三個或三個以上的參數(shù),說明其中的一些參數(shù)就應(yīng)該封裝為結(jié)構(gòu)體類。比如:
由此可見,減少函數(shù)參數(shù)的最佳方法是一個函數(shù)只做一件事,“函數(shù)要么做什么事,要么回答什么事!”兩者不可兼得。函數(shù)應(yīng)該修改某個對象的狀態(tài),或返回該對象的有關(guān)信息,兩樣都干常常會出現(xiàn)混亂。
2.類型與變量
由于有了結(jié)構(gòu)體,因此可以將rangeCheck()的形參min和max轉(zhuǎn)移到結(jié)構(gòu)體中,不僅減少了一個形參,而且處理起來更方便。比如:
該聲明描述了一個由兩個int類型變量組成的結(jié)構(gòu)體,不僅創(chuàng)建了實際數(shù)據(jù)的對象range,而且描述了該對象是由什么組成的,因為它勾勒出了結(jié)構(gòu)體是如何存儲數(shù)據(jù)的。顯然,range是struct _Range類型的結(jié)構(gòu)體變量,如果在該結(jié)構(gòu)體定義前添加typedef:
此時,range就變成了該結(jié)構(gòu)體的類型,即range等同于struct _Range。習(xí)慣的寫法是將類型名的首字符大寫,將變量名的首字符小寫。有了Range類型,即可同時定義一個Range類型的變量range和一個指向Range *類型的指針變量pRange,當(dāng)然也可以省略類型名_Range。比如:
注意,結(jié)構(gòu)體有兩層含義,一層含義是“結(jié)構(gòu)體布局”,結(jié)構(gòu)體布局告訴編譯器是如何表示數(shù)據(jù)的,但它并未讓編譯器為數(shù)據(jù)分配空間。下一步是創(chuàng)建一個結(jié)構(gòu)體變量,即結(jié)構(gòu)體的另一層含義,其定義如下:
編譯器執(zhí)行這行代碼便創(chuàng)建了一個結(jié)構(gòu)體變量range,編譯器使用Range為該變量分配空間:一個int類型的變量min和一個int類型的變量max,這些存儲空間都與一個名稱range結(jié)合在一起。
3.初始化
假設(shè)value值的有效范圍為0~9,在這里可以使用名為newRangeCheck的宏方便地將結(jié)構(gòu)體初始化。比如:
使用方法如下:
宏展開后如下:
其相當(dāng)于:
從本質(zhì)上來看,.min和.max的作用相當(dāng)于Range結(jié)構(gòu)體的下標。雖然Range是一個結(jié)構(gòu)體,但range.min和range.max都是int類型的變量,因此可以象使用其它int類型變量那樣使用它,比如,&(range.min)。
由此可見,如果初始化一個靜態(tài)存儲期的結(jié)構(gòu)體,初始化列表中的值必須是常量表達式。如果是自動存儲期,初始化列表中的值可以不是常量。
4.接口與實現(xiàn)
(1)傳遞結(jié)構(gòu)體成員
只要結(jié)構(gòu)體成員是一個具有單個值的數(shù)據(jù)類型,比如,int、char、float、double或指針,便可將它作為參數(shù)傳遞給接受該特定類型的函數(shù),rangeCheck()的實現(xiàn)詳見程序清單2.11。
程序清單 2.11 rangeCheck()函數(shù)的實現(xiàn)(2)
其調(diào)用形式如下:
rangeCheck()既不知道也不關(guān)心實參是否是結(jié)構(gòu)體的成員,它只要求傳入的數(shù)據(jù)是int類型。如果需要在被調(diào)函數(shù)中修改主調(diào)函數(shù)中成員的值,就要傳遞成員的地址。
(2)傳遞結(jié)構(gòu)體
雖然傳遞一個結(jié)構(gòu)體比一個單獨的值復(fù)雜,但標準C同樣允許將結(jié)構(gòu)體作為參數(shù)使用,rangeCheck()函數(shù)的實現(xiàn)詳見程序清單2.11。
程序清單 2.12 rangeCheck()函數(shù)的實現(xiàn)(3)
其調(diào)用形式如下:
雖然通過這種方法能夠得到正確的結(jié)果,但它的效率很低,因為C語言的參數(shù)傳址調(diào)用方式要求將參數(shù)的一份拷貝傳遞給函數(shù)。假設(shè)結(jié)構(gòu)體的成員是一個占用128字節(jié)的數(shù)組,甚至更大的數(shù)組。如果要將它作為參數(shù)進行傳遞,則必須將所占用的字節(jié)數(shù)復(fù)制到堆棧中,以后再丟棄。
(3)傳遞結(jié)構(gòu)體的地址
假設(shè)有一組這樣的數(shù)據(jù),存儲在結(jié)構(gòu)體成員數(shù)組中。其數(shù)據(jù)結(jié)構(gòu)如下:
顯然,只要將結(jié)構(gòu)體的地址(int *)&st作為實參傳遞給iMax()的形參,即可求出數(shù)組中元素的最大值,詳見程序清單 2.13。
程序清單 2.13 求數(shù)組中元素的最大值范例程序
下面還是以范圍值校驗器為例,定義一個指向該結(jié)構(gòu)體的指針變量pRange,其初始化、賦值與普通指針變量是一樣的:
和數(shù)組不一樣,結(jié)構(gòu)名并不是結(jié)構(gòu)體的地址,因此要在結(jié)構(gòu)名前加上&運算符,因此這里的pRange為指向Range結(jié)構(gòu)體變量range的指針變量。雖然pRange、&range和&range.min的類型不一樣,但它們的值相等,那么下面的關(guān)系恒成立:
由于.運算符比*運算符的優(yōu)先級高,因此必須使用圓括號。這里著重理解pRange是一個指針,pRange->min表示pRange指向結(jié)構(gòu)體的首成員,所以pRange->min是一個int類型的變量,rangeCheck()函數(shù)的實現(xiàn)詳見程序清單2.14。
程序清單 2.14 rangeCheck()函數(shù)的實現(xiàn)(4)
rangeCheck()使用指向Range的指針pRange作為它的參數(shù),將地址&range傳遞給該函數(shù),使得指針pRange指向range,然后通過->運算符獲取range.min和range.max的值。注意,必須使用&運算符獲取結(jié)構(gòu)體的地址,和數(shù)組名不同,結(jié)構(gòu)體名只是其地址的別名。
其調(diào)用形式如下:
(4)用函數(shù)指針調(diào)用
如果需要增加一個奇偶校驗器對value值進行偶校驗,其數(shù)據(jù)結(jié)構(gòu)如下:
oddEvenCheck()函數(shù)的實現(xiàn)詳見程序清單 2.15。
程序清單 2.15 oddEvenCheck()函數(shù)的實現(xiàn)
當(dāng)系統(tǒng)需要多個校驗器后,在運行時調(diào)用者將根據(jù)實際情況決定調(diào)用哪個函數(shù),根據(jù)依賴倒置原則,最好的方法是用函數(shù)指針隔離變化。無論什么校驗器,其相同的處理部分是value值的合法性判斷,因此將其抽象為模塊。而可變的是value值和校驗參數(shù),由外部傳入的參數(shù)應(yīng)對。由于各種校驗器的類型不一樣,因此必須使用“void *pData”作為形參才能接受任意類型的數(shù)據(jù),即將Range *pRange和OddEven*pOddEven泛化成了void *pData。Validate類型的定義如下:
其中,pData為指向任意校驗器參數(shù)的指針,value為待校驗的值,通用校驗器的接口詳見程序清單 2.16。
程序清單 2.16 通用校驗器接口(validator.h)
以范圍值校驗器為例,其調(diào)用形式如下:
這次傳遞給函數(shù)的是一個指向結(jié)構(gòu)體的指針,指針比整個結(jié)構(gòu)體要小得多,所以將它壓到堆棧上的效率要高很多,validator接口的實現(xiàn)詳見程序清單 2.17。
程序清單 2.17 validator接口的實現(xiàn)(validator.c)
由于pRange、pOddEven與pData的類型不同,因此需要對pData強制類型轉(zhuǎn)換,才能引用相應(yīng)結(jié)構(gòu)體的成員。注意,在這里,作者并沒有提供完整的代碼,請讀者補充完善。
-
C語言編程
+關(guān)注
關(guān)注
6文章
90瀏覽量
21103 -
程序設(shè)計
+關(guān)注
關(guān)注
3文章
261瀏覽量
30391 -
周立功
+關(guān)注
關(guān)注
38文章
130瀏覽量
37616 -
結(jié)構(gòu)體
+關(guān)注
關(guān)注
1文章
130瀏覽量
10840
原文標題:周立功:結(jié)構(gòu)體使你的程序設(shè)計更方便——內(nèi)存對齊和基本數(shù)據(jù)類型
文章出處:【微信號:ZLG_zhiyuan,微信公眾號:ZLG致遠電子】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論