|
相信大家對(duì)于結(jié)構(gòu)體都不陌生。在此,分享出本人對(duì)C語(yǔ)言結(jié)構(gòu)體的學(xué)習(xí)心得。如果你發(fā)現(xiàn)這個(gè)總結(jié)中有你以前所未掌握的,那本文也算是有點(diǎn)價(jià)值了。當(dāng)然,水平有限,若發(fā)現(xiàn)不足之處懇請(qǐng)指出。代碼文件test.c我放在下面。
在此,我會(huì)圍繞以下2個(gè)問(wèn)題來(lái)分析和應(yīng)用C語(yǔ)言結(jié)構(gòu)體:
1. C語(yǔ)言中的結(jié)構(gòu)體有何作用
2. 結(jié)構(gòu)體成員變量?jī)?nèi)存對(duì)齊有何講究(重點(diǎn))
對(duì)于一些概念的說(shuō)明,我就不把C語(yǔ)言教材上的定義搬上來(lái)。我們坐下來(lái)慢慢聊吧。
=============================================================================================================
1. 結(jié)構(gòu)體有何作用
三個(gè)月前,教研室里一個(gè)學(xué)長(zhǎng)在華為南京研究院的面試中就遇到這個(gè)問(wèn)題。當(dāng)然,這只是面試中最基礎(chǔ)的問(wèn)題。如果問(wèn)你你怎么回答?
我的理解是這樣的,C語(yǔ)言中結(jié)構(gòu)體至少有以下三個(gè)作用:
(1)有機(jī)地組織了對(duì)象的屬性。
比如,在STM32的RTC開(kāi)發(fā)中,我們需要數(shù)據(jù)來(lái)表示日期和時(shí)間,這些數(shù)據(jù)通常是年、月、日、時(shí)、分、秒。如果我們不用結(jié)構(gòu)體,那么就需要定義6個(gè)變量來(lái)表示。這樣的話程序的數(shù)據(jù)結(jié)構(gòu)是松散的,我們的數(shù)據(jù)結(jié)構(gòu)最好是“高內(nèi)聚,低耦合”的。所以,用一個(gè)結(jié)構(gòu)體來(lái)表示更好,無(wú)論是從程序的可讀性還是可移植性還是可維護(hù)性皆是:
1.png (3.43 KB, 下載次數(shù): 61)
下載附件
2018-11-15 16:20 上傳
(2)以修改結(jié)構(gòu)體成員變量的方法代替了函數(shù)(入口參數(shù))的重新定義。
如果說(shuō)結(jié)構(gòu)體有機(jī)地組織了對(duì)象的屬性表示結(jié)構(gòu)體“中看”,那么以修改結(jié)構(gòu)體成員變量的方法代替函數(shù)(入口參數(shù))的重新定義就表示了結(jié)構(gòu)體“中用”。繼續(xù)以上面的結(jié)構(gòu)體為例子,我們來(lái)分析。假如現(xiàn)在我有如下函數(shù)來(lái)顯示日期和時(shí)間:
2.png (833 Bytes, 下載次數(shù): 48)
下載附件
2018-11-15 16:21 上傳
那么我們只要將一個(gè)_calendar_obj這個(gè)結(jié)構(gòu)體類型的變量作為實(shí)參調(diào)用DsipDateTime()即可,DsipDateTime()通過(guò)DateTimeVal的成變量來(lái)實(shí)現(xiàn)內(nèi)容的顯示。如果不用結(jié)構(gòu)體,我們很可能需要寫這樣的一個(gè)函數(shù):
3.png (2.25 KB, 下載次數(shù): 56)
下載附件
2018-11-15 16:21 上傳
顯然這樣的形參很不可觀,數(shù)據(jù)結(jié)構(gòu)管理起來(lái)也很繁瑣。如果某個(gè)函數(shù)的返回值得是一個(gè)表示日期和時(shí)間的數(shù)據(jù),那就更復(fù)雜了。這只是一方面。
另一方面,如果用戶需要表示日期和時(shí)間的數(shù)據(jù)中還要包含星期(周),這個(gè)時(shí)候,如果之前沒(méi)有用機(jī)構(gòu)體,那么應(yīng)該在DsipDateTime()函數(shù)中在增加一個(gè)形參vu8 week:
4.png (2.42 KB, 下載次數(shù): 52)
下載附件
2018-11-15 16:22 上傳
可見(jiàn)這種方法來(lái)傳遞參數(shù)非常繁瑣。所以以結(jié)構(gòu)體作為函數(shù)的入口參數(shù)的好處之一就是
函數(shù)的聲明void DsipDateTime( _calendar_obj DateTimeVal)不需要改變,只需要增加結(jié)構(gòu)體的成員變量,然后在函數(shù)的內(nèi)部實(shí)現(xiàn)上對(duì)calendar.week作相應(yīng)的處理即可。這樣,在程序的修改、維護(hù)方面作用顯著。
5.png (3.69 KB, 下載次數(shù): 56)
下載附件
2018-11-15 16:23 上傳
(3)結(jié)構(gòu)體的內(nèi)存對(duì)齊原則可以提高CPU對(duì)內(nèi)存的訪問(wèn)速度(以空間換取時(shí)間)。
并且,結(jié)構(gòu)體成員變量的地址可以根據(jù)基地址(以偏移量offset)計(jì)算。我們先來(lái)看看下面的一段簡(jiǎn)單的程序,對(duì)于此程序的分析會(huì)在第2部分結(jié)構(gòu)體成員變量?jī)?nèi)存對(duì)齊中詳細(xì)說(shuō)明。
6.png (34.5 KB, 下載次數(shù): 48)
下載附件
2018-11-15 16:23 上傳
程序的運(yùn)行結(jié)果如下(注意:括號(hào)內(nèi)的數(shù)據(jù)是成員變量的地址的十進(jìn)制形式):
7.jpg (65.23 KB, 下載次數(shù): 64)
下載附件
2018-11-15 16:24 上傳
2. 結(jié)構(gòu)體成員變量?jī)?nèi)存對(duì)齊
首先,我們來(lái)分析一下上面程序的運(yùn)行結(jié)果。前三行說(shuō)明在我的程序中,char型占1個(gè)字節(jié),short型占2個(gè)字節(jié),long型占4個(gè)字節(jié)。char_short_long、long_short_char和char_long_short是三個(gè)結(jié)構(gòu)體成員相同但是成員變量的排列順序不同。并且從程序的運(yùn)行結(jié)果來(lái)看,
8.png (3.84 KB, 下載次數(shù): 55)
下載附件
2018-11-15 16:25 上傳
并且,還要注意到,1 byte (char)+ 2 byte (short)+ 4 byte (long) = 7 byte,而不是8 byte。
所以,結(jié)構(gòu)體成員變量的放置順序影響著結(jié)構(gòu)體所占的內(nèi)存空間的大小。一個(gè)結(jié)構(gòu)體變量所占內(nèi)存的大小不一定等于其成員變量所占空間之和。如果一個(gè)用戶程序或者操作系統(tǒng)(比如uC/OS-II)中存在大量結(jié)構(gòu)體變量時(shí),這種內(nèi)存占用必須要進(jìn)行優(yōu)化,也就是說(shuō),結(jié)構(gòu)體內(nèi)部成員變量的排列次序是有講究的。
結(jié)構(gòu)體成員變量到底是如何存放的呢?
在這里,我就不賣關(guān)子了,直接給出如下結(jié)論,在沒(méi)有#pragma pack宏的情況下:
原則1 結(jié)構(gòu)(struct或聯(lián)合union)的數(shù)據(jù)成員,第一個(gè)數(shù)據(jù)成員放在offset為0的地方,以后每個(gè)數(shù)據(jù)成員存儲(chǔ)的起始位置要從該成員大小的整數(shù)倍開(kāi)始(比如int在32位機(jī)為4字節(jié),則要從4的整數(shù)倍地址開(kāi)始存儲(chǔ))。
原則2 結(jié)構(gòu)體的總大小,也就是sizeof的結(jié)果,必須是其內(nèi)部最大成員的整數(shù)倍,不足的要補(bǔ)齊。
*原則3 結(jié)構(gòu)體作為成員時(shí),結(jié)構(gòu)體成員要從其內(nèi)部最大元素大小的整數(shù)倍地址開(kāi)始存儲(chǔ)。(struct a里存有struct b,b里有char,int,double等元素時(shí),那么b應(yīng)該從8的整數(shù)倍地址處開(kāi)始存儲(chǔ),因?yàn)閟izeof(double) = 8 bytes)
這里,我們結(jié)合上面的程序來(lái)分析(暫時(shí)不討論原則3)。
先看看char_short_long和long_short_char這兩個(gè)結(jié)構(gòu)體,從它們的成員變量的地址可以看出來(lái),這兩個(gè)結(jié)構(gòu)體符合原則1和原則2。注意,在 char_short_long的成員變量的地址中, char_short_long.s的地址是1244994,也就是說(shuō),1244993是“空的”,只是被“占位”了!
再看看char_long_short這個(gè)結(jié)構(gòu)體,char_long_short的地址分布情況如下表:
9.png (6.41 KB, 下載次數(shù): 50)
下載附件
2018-11-15 16:26 上傳
首先,1244972能被1整除,所以char_long_short.c放在1244972處沒(méi)有問(wèn)題(其實(shí),就char型成員變量自身來(lái)說(shuō),其放在任何地址單元處都沒(méi)有問(wèn)題),根據(jù)原則1,在之后的1244973~1244975中都沒(méi)有能被4(因?yàn)閟izeof(long)=4bytes)整除的,1244976能被4整除,所以char_long_short.l應(yīng)該放在1244976處,那么同理,最后一個(gè).s(sizeof(short)=2 bytes)是應(yīng)該放在1244980處。
是不是這樣就結(jié)束了?不是,還有原則2。根據(jù)原則2的要求,char_long_short這個(gè)結(jié)構(gòu)體所占的空間大小應(yīng)該是其占內(nèi)存空間最大的成員變量的大小的整數(shù)倍。如果我們到此就結(jié)束了,那么char_long_short所占的內(nèi)存空間是1244972~1244981共計(jì)10bytes,不符合原則2,所以,必須在最后補(bǔ)齊2個(gè) bytes(1244982~1244983)。
至此,一個(gè)結(jié)構(gòu)體的內(nèi)存布局完成了。
下面我們按照上述原則,來(lái)驗(yàn)證這樣的分析是不是正確。按上面的分析,地址單元1244973、1244974、1244975以及1244982、1244983都是空的(至少char_long_short未用到,只是“占位”了)。如果我們的分析是正確的,那么,定義這樣一個(gè)結(jié)構(gòu)體,其所占內(nèi)存也應(yīng)該是12 bytes:
10.png (5.92 KB, 下載次數(shù): 66)
下載附件
2018-11-15 16:27 上傳
運(yùn)行結(jié)果如下:
11.jpg (11.2 KB, 下載次數(shù): 51)
下載附件
2018-11-15 16:27 上傳
可見(jiàn),我們的分析是正確的。至于原則3,大家可以自己編程驗(yàn)證,這里就不再討論了。
所以,無(wú)論你是在VC6.0還是Keil C51,還是Keil MDK中,當(dāng)你需要定義一個(gè)結(jié)構(gòu)體時(shí),只要你稍微留心結(jié)構(gòu)體成員變量?jī)?nèi)存對(duì)齊這一現(xiàn)象,就可以在很大程度上節(jié)約MCU的RAM。這一點(diǎn)不僅僅應(yīng)用于實(shí)際編程,在很多大型公司,比如IBM、微軟、百度、華為的筆試和面試中,也是常見(jiàn)的。
|
|