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