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