|
C語(yǔ)言結(jié)構(gòu)體對(duì)齊問題,是面試必備問題。我參與招聘技術(shù)面試的時(shí)候,也喜歡問這個(gè)技術(shù)點(diǎn)。這不是在面試時(shí)要裝B,也不是要故意難為一下面試者,而是這個(gè)知識(shí)點(diǎn)比較基礎(chǔ),但很重要。網(wǎng)上搜出來(lái)的嵌入式或C語(yǔ)言筆試題,很多都有這種題目,連《程序員面試寶典》也有講解這種題目。
uoiyc3ho0aa640332236.png (228.48 KB, 下載次數(shù): 0)
下載附件
保存到相冊(cè)
uoiyc3ho0aa640332236.png
2024-9-14 17:30 上傳
結(jié)構(gòu)體對(duì)齊知識(shí)點(diǎn)考察,儼然成為編程技術(shù)崗面試筆試的一種標(biāo)配。我以前找工作被問這種題的時(shí)候就經(jīng)常想,結(jié)構(gòu)體對(duì)齊這個(gè)東西平常很少用,考這東西干嘛?為什么結(jié)構(gòu)體對(duì)齊那么重要?纯催@個(gè)例子: typedef struct { int e_int; char e_char1; char e_char2; }S2;
typedef struct { char e_char1; int e_int; char e_char2; }S3; S2 s2; S3 s3;你覺得這倆結(jié)構(gòu)體所占內(nèi)存是一樣大嗎?其實(shí)不是!
好像也沒什么啊,一不一樣大對(duì)于C語(yǔ)言程序員有什么所謂!
也許你還還感覺不到,上段代碼:
S2 s2[1024] = {0}; S3 s3[1024] = {0};對(duì)于32位系統(tǒng),s2的大小為8K,而s3的大小為12K,一放大,就有很明顯的區(qū)別了。
再舉個(gè)例子:
unsigned char bytes[10]={0};int* p = (int*)&bytes[3];*p = 0x345678;你覺得執(zhí)行上面的代碼會(huì)發(fā)生什么情況?Warining?只是Warning么?!以前我也沒覺得懂得這個(gè)結(jié)構(gòu)體對(duì)齊或者內(nèi)存對(duì)齊有多重要,直到已經(jīng)從事了嵌入式開發(fā)經(jīng)驗(yàn)不斷積累,才慢慢體會(huì)到,這是一種很基礎(chǔ)的知識(shí),就因?yàn)檫@個(gè)東西不常用,而出現(xiàn)相關(guān)的問題是非常致命的,排查起來(lái)成本非常高。有個(gè)小伙伴,因?yàn)橐粋(gè)內(nèi)存對(duì)齊(結(jié)構(gòu)體對(duì)齊相關(guān)知識(shí)點(diǎn))問題導(dǎo)致的偶發(fā)性Exception問題,折騰了一個(gè)多星期。由于項(xiàng)目接近尾聲,出現(xiàn)這種問題,項(xiàng)目經(jīng)理、老板都操心得不得了。天天不是奶茶水果,就是宵夜,把小伙伴當(dāng)寶貝來(lái)哄,為的就是快速定位這個(gè)問題。然而,他們?nèi)找岳^夜的排查了一個(gè)多星期,依然一臉懵逼。直到讓我參與進(jìn)來(lái)支援,我通過仿真方式碰巧捕捉到了這種異常情況。問題的根本原因就是強(qiáng)制類型轉(zhuǎn)換導(dǎo)致的內(nèi)存對(duì)齊問題。篇幅有限,這個(gè)故事,以后慢慢細(xì)講。接下來(lái)先看看,結(jié)構(gòu)體對(duì)齊的知識(shí)點(diǎn)。結(jié)構(gòu)體對(duì)齊,說(shuō)不難吧,我研究了很多次,都沒完全記。徽f(shuō)難吧,理解其原因本質(zhì),就易如破竹。結(jié)構(gòu)體對(duì)齊,其實(shí)其本質(zhì)就是內(nèi)存對(duì)齊。什么以最大元素變量為單位,什么最小公倍數(shù)等等法則,通通都是讓你死記硬背的,沒兩天就忘了。為什么要結(jié)構(gòu)體對(duì)齊,原因就是內(nèi)存要對(duì)齊,原因是芯片內(nèi)存的制造限制,是制造成本約束,是內(nèi)存讀取效率要求。如果你上學(xué)的時(shí)候認(rèn)真學(xué)習(xí)過微機(jī)原理,應(yīng)該還記得,芯片的地址總線和數(shù)據(jù)總線這個(gè)概念吧。沒學(xué)過微機(jī)原理也沒關(guān)系,8位單片機(jī)、16位單片機(jī)和32位單片機(jī)等等,這些總得聽說(shuō)過吧。
4ktgna2uub1640332336.png (21.85 KB, 下載次數(shù): 0)
下載附件
保存到相冊(cè)
4ktgna2uub1640332336.png
2024-9-14 17:30 上傳
這個(gè)8位、16位和32位等,指的是單片機(jī)一次處理數(shù)據(jù)的寬度,也就和數(shù)據(jù)總線相關(guān)了。
細(xì)心的小伙伴會(huì)知道,16位單片機(jī)的通用寄存器例如R0的長(zhǎng)度是2個(gè)字節(jié)的,而32位的是4字節(jié)的。也就是說(shuō)16位單片機(jī),單指令一次訪問數(shù)據(jù)是2個(gè)字節(jié),而32位單片機(jī)可以訪問4字節(jié)。為了提高M(jìn)CU的運(yùn)行效率,內(nèi)存設(shè)計(jì)上,進(jìn)來(lái)適應(yīng)這個(gè)CPU的總線訪問。以32位MCU為例,其內(nèi)存一般都是每4字節(jié)(32位)為一個(gè)小單元,有時(shí)候也叫1個(gè)字(Word)。
dogx4duolvz640332436.png (4.49 KB, 下載次數(shù): 0)
下載附件
保存到相冊(cè)
dogx4duolvz640332436.png
2024-9-14 17:30 上傳
注意:字節(jié),這個(gè)概念長(zhǎng)度是固定的,就是8bit;而字,卻不是固定的,跟CPU或系統(tǒng)位數(shù)有關(guān),有時(shí)候還會(huì)出現(xiàn)字、雙字這些概念,舉例說(shuō)明下:32位計(jì)算機(jī):1字=32位=4字節(jié),64位計(jì)算機(jī):1字=64位=8字節(jié)所以,對(duì)于C語(yǔ)言的變量的存放和訪問,都會(huì)按著這單位來(lái),例如32位系統(tǒng)中,char是一個(gè)字節(jié)的,就按Byte來(lái),int是4字節(jié)的,那么按Word來(lái)。
為什么要這樣呢?
如果,一塊內(nèi)存在地址上隨便放的,CPU有可能就會(huì)用到多條指令來(lái)訪問,這就會(huì)降低效率。對(duì)于32位系統(tǒng),如下圖的A可能需要2條指令訪問,而B只需1條指令。
bouj1s44qg2640332536.png (16.64 KB, 下載次數(shù): 0)
下載附件
保存到相冊(cè)
bouj1s44qg2640332536.png
2024-9-14 17:30 上傳
x3nwfzafraz640332636.png (16.2 KB, 下載次數(shù): 0)
下載附件
保存到相冊(cè)
x3nwfzafraz640332636.png
2024-9-14 17:30 上傳
不僅單片機(jī)這樣,我們常用的計(jì)算機(jī)也是這樣,你看內(nèi)存條,長(zhǎng)這樣的:
rderld2ec22640332737.png (341.41 KB, 下載次數(shù): 1)
下載附件
保存到相冊(cè)
rderld2ec22640332737.png
2024-9-14 17:30 上傳
你以為,通過總線的方式可以隨便訪問一個(gè)地址嗎
bla2sbogfr1640332837.png (112.08 KB, 下載次數(shù): 0)
下載附件
保存到相冊(cè)
bla2sbogfr1640332837.png
2024-9-14 17:30 上傳
但是,為了提高訪問速度,其設(shè)計(jì)是這樣的:
ukkwsfmwo3h640332937.png (267.32 KB, 下載次數(shù): 1)
下載附件
保存到相冊(cè)
ukkwsfmwo3h640332937.png
2024-9-14 17:30 上傳
s1k2mfkg01t640333037.png (140 KB, 下載次數(shù): 0)
下載附件
保存到相冊(cè)
s1k2mfkg01t640333037.png
2024-9-14 17:30 上傳
這樣,這個(gè)地址就必須是8的倍數(shù)。
如果你要從不對(duì)齊的內(nèi)存讀取數(shù)據(jù),雖然在C語(yǔ)言編程上感覺不到這樣的操作有什么區(qū)別,但CPU是分開多次讀出來(lái)的。這就是內(nèi)存對(duì)齊了。int8(即char)是以1字節(jié)對(duì)齊,int16是以2字節(jié)對(duì)齊,而int32是以4字節(jié)對(duì)齊的,等等。(以上案例看不懂?推薦去B站看這個(gè)視頻:【Golang】這個(gè)內(nèi)存對(duì)齊呀?_嗶哩嗶哩_bilibili,我上面的圖也是參考這個(gè)視頻的。)世界上CPU平臺(tái)、系統(tǒng)那么多,我們?cè)趺粗滥膫(gè)類型到底有多長(zhǎng),是以哪種長(zhǎng)度對(duì)齊的?
不要瞎猜,直接上代碼。每個(gè)平臺(tái)都不一樣,請(qǐng)讀者自行測(cè)試,以下我是基于Windows上MinGW的GCC測(cè)的。
#define BASE_TYPE_SIZE(t) printf("%12s : %2d Byte%s
", #t, sizeof(t), (sizeof(t))>1?"s":"")void base_type_size(void){ BASE_TYPE_SIZE(void); BASE_TYPE_SIZE(char); BASE_TYPE_SIZE(short); BASE_TYPE_SIZE(int); BASE_TYPE_SIZE(long); BASE_TYPE_SIZE(long long); BASE_TYPE_SIZE(float); BASE_TYPE_SIZE(double); BASE_TYPE_SIZE(long double); BASE_TYPE_SIZE(void*); BASE_TYPE_SIZE(char*); BASE_TYPE_SIZE(int*); typedef struct { }StructNull; BASE_TYPE_SIZE(StructNull); BASE_TYPE_SIZE(StructNull*);}結(jié)果是:
void : 1 Byte char : 1 Byte short : 2 Bytes int : 4 Bytes long : 4 Bytes long long : 8 Bytes float : 4 Bytes double : 8 Bytes long double : 12 Bytes void* : 4 Bytes char* : 4 Bytes int* : 4 Bytes StructNull : 0 Byte StructNull* : 4 Bytes這些內(nèi)容不用記住,不同平臺(tái)是不一樣的,使用之前,一定要親自測(cè)試驗(yàn)證下。
這里先解釋下“模數(shù)”的概念:
每個(gè)特定平臺(tái)上的編譯器都有自己的默認(rèn)“對(duì)齊系數(shù)”(也叫對(duì)齊模數(shù))。
接著看網(wǎng)上流傳一個(gè)表:平臺(tái)
| 長(zhǎng)度/模數(shù)
| char
| short
| int
| long
| float
| double
| long long
| long double
| Win-32
| 長(zhǎng)度
| 1
| 2
| 4
| 4
| 4
| 8
| 8
| 8
| 模數(shù)
| 1
| 2
| 4
| 4
| 4
| 8
| 8
| 8
| Linux-32
| 長(zhǎng)度
| 1
| 2
| 4
| 4
| 4
| 8
| 8
| 12
| 模數(shù)
| 1
| 2
| 4
| 4
| 4
| 4
| 4
| 4
| Linux-64
| 長(zhǎng)度
| 1
| 2
| 4
| 8
| 4
| 8
| 8
| 16
| 模數(shù)
| 1
| 2
| 4
| 8
| 4
| 8
| 8
| 16
| 本文的的例子我用的是MinGW32的GCC來(lái)測(cè)試,你猜符合上表的哪一項(xiàng)?
別急,再看一個(gè)例子:
typedef struct { int e_int; double e_double; }S11; S11 s11; STRUCT_E_ADDR_OFFSET(s11, e_int); STRUCT_E_ADDR_OFFSET(s11, e_double);結(jié)果是:
s11 size = 16 s11.e_int addr: 0028FF18, offset: 0 s11 size = 16 s11.e_double addr: 0028FF20, offset: 8很明顯,上表沒有一項(xiàng)完全對(duì)應(yīng)得上的。簡(jiǎn)單匯總以下我測(cè)試的結(jié)果:
長(zhǎng)度/模數(shù)
| char
| short
| int
| long
| float
| double
| long long
| long double
| 長(zhǎng)度
| 1
| 2
| 4
| 4
| 4
| 8
| 8
| 12
| 模數(shù)
| 1
| 2
| 4
| 4
| 4
| 8
| 8
| 8
| 所以,再?gòu)?qiáng)調(diào)一下:因?yàn)榄h(huán)境的差異,在你參考使用之前,請(qǐng)自行測(cè)試一下。
其實(shí),這個(gè)模數(shù)是可以改變的,可以用預(yù)編譯命令#pragma pack(n),n=1,2,4,8,16來(lái)改變這一系數(shù),其中的n就是你要指定的“對(duì)齊系數(shù)”。例如
#pragma pack(1)typedef struct { char e_char; long double e_ld;}S14;#pragma pack()想知道結(jié)構(gòu)圖元素內(nèi)存如何對(duì)齊,其實(shí)非常簡(jiǎn)單。
其實(shí),你只需知道當(dāng)前你使用的這個(gè)系統(tǒng)的基本類型的sizeof是多少,然后根據(jù)這個(gè)大小做對(duì)齊排布。例如,本文一開始的例子: typedef struct { int e_int; char e_char1; char e_char2; }S2;
typedef struct { char e_char1; int e_int; char e_char2; }S3; S2 s2; S3 s3;32位系統(tǒng)中,它們內(nèi)存是這么對(duì)齊的:
sqtkb3alj5l640333137.png (6.48 KB, 下載次數(shù): 0)
下載附件
保存到相冊(cè)
sqtkb3alj5l640333137.png
2024-9-14 17:30 上傳
簡(jiǎn)單解釋下:
S2中的元素e_int是按4字節(jié)對(duì)齊的,其地址位4整數(shù)倍,而e_char1和e_char2就按1字節(jié)對(duì)齊,緊跟其后面就可以了;
而S3中的元素e_char1是按1字節(jié)對(duì)齊的,放在最前面,而e_int是按4字節(jié)對(duì)齊的,其地址位4整數(shù)倍,所以,只能找到個(gè)+4的位置,緊接著e_char2就按1字節(jié)對(duì)齊,跟其后面就可以了。
那么sizeof(s2)和sizeof(s3)各是多少怎么算?
也很簡(jiǎn)單,例如這個(gè)32位系統(tǒng),為了提高執(zhí)行效率,編譯器會(huì)讓數(shù)據(jù)訪問以4字節(jié)為單位的,所以S2里有2個(gè)字節(jié)留空,即sizeof(s2)=8,而sizeof(s3)=12。
是不是很簡(jiǎn)單呢!
接著,來(lái)個(gè)復(fù)雜一點(diǎn)的:
typedef struct { char e_char1; short e_short; char e_char2; int e_int; char e_char3; }S4; S4 s4;其內(nèi)存分布如下:
gsg1vjbqwfb640333237.png (5.66 KB, 下載次數(shù): 0)
下載附件
保存到相冊(cè)
gsg1vjbqwfb640333237.png
2024-9-14 17:30 上傳
按上面的方法,也不難理解。e_int是不能從+5位置開始的,因?yàn)?5不是int的對(duì)齊位置,用int去訪問+5位置是效率很低或者有問題的,所以它只能從+8位置開始。
再?gòu)?fù)雜一點(diǎn)的呢?來(lái)看看union和struct結(jié)合的例子:
typedef struct { int e_int1; union { char ue_chars[9]; int ue_int; }u; double e_double; int e_int2; }SU2; SU2 su2; 得到:
qpoujdwihpa640333338.png (52.05 KB, 下載次數(shù): 0)
下載附件
保存到相冊(cè)
qpoujdwihpa640333338.png
2024-9-14 17:30 上傳
為什么這樣呢?
你這樣想,要時(shí)刻想著CPU訪問數(shù)據(jù)的效率,如果union里的元素類型不一樣,那就以最大長(zhǎng)度的那個(gè)類型對(duì)齊了。另外,還有結(jié)構(gòu)體套著結(jié)構(gòu)體的情況了:
typedef struct { int e_int; char e_char; }S1; typedef struct { S1 e_s; char e_char; }SS1;
typedef struct { short e_short; char e_char; }S6;
typedef struct { S6 e_s; char e_char; }SS2;
得出結(jié)果:
43azjsvlmjw640333438.png (7.55 KB, 下載次數(shù): 0)
下載附件
保存到相冊(cè)
43azjsvlmjw640333438.png
2024-9-14 17:30 上傳
得出結(jié)論:結(jié)構(gòu)體內(nèi)的結(jié)構(gòu)體,結(jié)構(gòu)體內(nèi)的元素并不會(huì)和結(jié)構(gòu)體外的元素合并占一個(gè)對(duì)齊單元。
只要技術(shù)上面的對(duì)齊方法,這些都不難理解。
如果你非要一些規(guī)則的話,我總結(jié)成這樣:首先,不推薦記憶這些條條框框的文字,以下內(nèi)容僅供參考:結(jié)構(gòu)體的內(nèi)存大小,并非其內(nèi)部元素大小之和;
結(jié)構(gòu)體變量的起始地址,可以被最大元素基本類型大小或者模數(shù)整除;結(jié)構(gòu)體的內(nèi)存對(duì)齊,按照其內(nèi)部最大元素基本類型或者模數(shù)大小對(duì)齊;模數(shù)在不同平臺(tái)值不一樣,也可通過#pragma pack(n)方式去改變;如果空間地址允許,結(jié)構(gòu)體內(nèi)部元素會(huì)拼湊一起放在同一個(gè)對(duì)齊空間;結(jié)構(gòu)體內(nèi)有結(jié)構(gòu)體變量元素,其結(jié)構(gòu)體并非展開后再對(duì)齊;union和bitfield變量也遵循結(jié)構(gòu)體內(nèi)存對(duì)齊原則。
[/ol]其實(shí),這些都沒必要去記,多思考多理解就OK了。唯一需要記得是某系統(tǒng)平臺(tái)下的基本類型的sizeof大小,然后按照對(duì)齊原則來(lái)就可以了,就是時(shí)刻想著CPU要提升數(shù)據(jù)訪問效率的。
更多的案例,很早寫在《圖文并茂,一文講透C語(yǔ)言結(jié)構(gòu)體內(nèi)存對(duì)齊》這個(gè)文章里面了,感興趣的小伙伴可以研究下。
里面涉及到很多測(cè)試源碼,如果想要獲取的話,可以關(guān)注公眾號(hào),回復(fù)"struct"即可獲得下載鏈接。 |
|