電子產(chǎn)業(yè)一站式賦能平臺(tái)

PCB聯(lián)盟網(wǎng)

搜索
查看: 59|回復(fù): 0
收起左側(cè)

面試愛問之結(jié)構(gòu)體對(duì)齊

[復(fù)制鏈接]

171

主題

171

帖子

1296

積分

三級(jí)會(huì)員

Rank: 3Rank: 3

積分
1296
跳轉(zhuǎn)到指定樓層
樓主
發(fā)表于 2023-5-25 21:42:00 | 只看該作者 |只看大圖 回帖獎(jiǎng)勵(lì) |倒序?yàn)g覽 |閱讀模式
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ǔ)言筆試題,很多都有這種題目,連《程序員面試寶典》也有講解這種題目。

結(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ō)過吧。

    這個(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)。

    注意:字節(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條指令。



    不僅單片機(jī)這樣,我們常用的計(jì)算機(jī)也是這樣,你看內(nèi)存條,長(zhǎng)這樣的:

    你以為,通過總線的方式可以隨便訪問一個(gè)地址嗎

    但是,為了提高訪問速度,其設(shè)計(jì)是這樣的:




    這樣,這個(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ì)齊的:

    簡(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)存分布如下:

    按上面的方法,也不難理解。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;  得到:

    為什么這樣呢?
    你這樣想,要時(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é)果:

    得出結(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"即可獲得下載鏈接。
  • 發(fā)表回復(fù)

    本版積分規(guī)則

    關(guān)閉

    站長(zhǎng)推薦上一條 /1 下一條


    聯(lián)系客服 關(guān)注微信 下載APP 返回頂部 返回列表