详解结构体、类等内存字节对齐
? ? 先說(shuō)個(gè)題外話:早些年我學(xué)C程序設(shè)計(jì)時(shí),寫(xiě)過(guò)一段解釋硬盤(pán)MBR分區(qū)表的代碼,對(duì)著磁盤(pán)編輯器怎么看,怎么對(duì),可一執(zhí)行,結(jié)果就錯(cuò)了。當(dāng)時(shí)調(diào)試也不太會(huì),又根本沒(méi)聽(tīng)過(guò)結(jié)構(gòu)體對(duì)齊這一說(shuō),所以,問(wèn)題解決不了,好幾天都十分糾結(jié)。后來(lái)萬(wàn)般無(wú)奈請(qǐng)教一個(gè)朋友,才獲悉可能是結(jié)構(gòu)體對(duì)齊的事,一查、一改,果真如此。
? ? 問(wèn)題是解決了,可網(wǎng)上的資料多數(shù)只提到內(nèi)存對(duì)齊是如何做的,卻鮮有提及為什么這樣做(即使提,也相當(dāng)簡(jiǎn)單)。筆者是個(gè)超級(jí)健忘者,很難機(jī)械式的記住這些破規(guī)則,于是仔細(xì)想了想,總算明白了原因,這樣,這些對(duì)齊的規(guī)則也就不會(huì)再輕易忘記了。? ? ?
? ? 不光結(jié)構(gòu)體存在內(nèi)存對(duì)齊一說(shuō),類(lèi)(對(duì)象)也如此,甚至于所有變量在內(nèi)存中的存儲(chǔ)也有對(duì)齊一說(shuō)(只是這些對(duì)程序員是透明的,不需要關(guān)心)。實(shí)際上,這種對(duì)齊是為了在空間與復(fù)雜度上達(dá)到平衡的一種技術(shù)手段,簡(jiǎn)單的講,是為了在可接受的空間浪費(fèi)的前提下,盡可能的提高對(duì)相同運(yùn)算過(guò)程的最少(快)處理。先舉個(gè)例子:
? ? 假設(shè)機(jī)器字長(zhǎng)是32位的(即4字節(jié),下面示例均按此字長(zhǎng)),也就是說(shuō)處理任何內(nèi)存中的數(shù)據(jù),其實(shí)都是按32位的單位進(jìn)行的?,F(xiàn)在有2個(gè)變量:? ??
? ? ?假設(shè)這2個(gè)變量是從內(nèi)存地址0開(kāi)始分配的,如果不考慮對(duì)齊,應(yīng)該是這樣存儲(chǔ)的(見(jiàn)下圖,以intel上的little endian為例,為了形象,每16個(gè)字節(jié)分做一行,后同):
? ? 因?yàn)橛?jì)算機(jī)的字長(zhǎng)是4字節(jié)的,所以在處理變量A與B時(shí)的過(guò)程可能大致為:
? ? A:將0x00-0x03共32位讀入寄存器,再通過(guò)左移24位再右移24位運(yùn)算得到a的值(或與0x000000FF做與運(yùn)算)
? ? B:將0x00-0x03這32位讀入寄存器,通過(guò)位運(yùn)算得到低24位的值;再將0x04-0x07這32位讀入寄存器,通過(guò)位運(yùn)算得到高8位的值;再與最先得到的24位做位運(yùn)算,才可得到整個(gè)32位的值。
? ? 上面敘述可知,對(duì)a的處理是最簡(jiǎn)處理,可對(duì)b的處理,本身是個(gè)32位數(shù),處理的時(shí)候卻得折成2部分,之后再合并,效率上就有些低了。
? ? 想解決這個(gè)問(wèn)題,就需要付出幾個(gè)字節(jié)浪費(fèi)的代價(jià),改為下圖的分配方式:
? ? 按上面的分配方式,A的處理過(guò)程不變;B卻簡(jiǎn)單得多了:只需將0x04-0x07這32位讀入寄存器就OK了。
? ? 我們可以具體談結(jié)構(gòu)體或類(lèi)成員的對(duì)齊了:
? ? 結(jié)構(gòu)體在編譯成機(jī)器代碼后,其實(shí)就沒(méi)有本身的集合概念了,而類(lèi),實(shí)際上是個(gè)加強(qiáng)版的結(jié)構(gòu)體,類(lèi)的對(duì)象在實(shí)例化時(shí),內(nèi)存中申請(qǐng)的就是一些變量的空間集合(類(lèi)似于結(jié)構(gòu)體,同時(shí)也不包含函數(shù)指針)。這些集合中的每個(gè)變量,在使用中,都需要涉及上述的加工原則,自然也就需要在效率與空間之間做出權(quán)衡。
? ? 為了便捷加工連續(xù)多個(gè)相同類(lèi)型原始變量,同時(shí)簡(jiǎn)化原始變量尋址,再匯總上述最少處理原則,通??梢詫⒃甲兞康拈L(zhǎng)度做為針對(duì)此變量的分配單位,比如內(nèi)存可用64個(gè)單元,如果某原始變量長(zhǎng)度為8字節(jié),即使機(jī)器字長(zhǎng)為4字節(jié),分配的時(shí)候也以8字節(jié)對(duì)齊(看似IO次數(shù)是相同的),這樣,尋址、分配時(shí),均可以按每8字節(jié)為單位進(jìn)行,簡(jiǎn)化了操作,也可以更高效。
? ? 系統(tǒng)默認(rèn)的對(duì)齊規(guī)則,追求的至少兩點(diǎn):1、變量的最高效加工 2、達(dá)到目的1的最少空間?
? ? 舉個(gè)例子,一個(gè)結(jié)構(gòu)體如下:
?
? ? 假設(shè)定義了一個(gè)結(jié)構(gòu)體變量C,在內(nèi)存中分配到了0x00的位置,顯然:
? ? 對(duì)于成員C.c ?無(wú)論如何,也是一次寄存器讀入,所以先占一個(gè)字節(jié)。
? ? 對(duì)于成員C.d ?是個(gè)64位的變量,如果緊跟著C.c存儲(chǔ),則讀入寄存器至少需要3次,為了實(shí)現(xiàn)最少的2次讀入,至少需要以4字節(jié)對(duì)齊;同時(shí)對(duì)于8字節(jié)的原始變量,為了在尋址單位上統(tǒng)一,則需要按8字節(jié)對(duì)齊,所以,應(yīng)該分配到0x08-0xF的位置。
? ? 對(duì)于成員C.e ?是個(gè)32位的變量,自然只需滿足分配起始為整數(shù)個(gè)32位即可,所以分配至0x10-0x13。
? ? 對(duì)于成員C.f ?是個(gè)16位的變量,直接分配在0x14-0x16上,這樣,反正只需一次讀入寄存器后加工,邊界也與16位對(duì)齊。
? ? 對(duì)于成員C.g ?是個(gè)8位的變量,本身也得一次讀入寄存器后加工,同時(shí)對(duì)于1個(gè)字節(jié)的變量,存儲(chǔ)在任何字節(jié)開(kāi)始都是對(duì)齊,所以,分配到0x17的位置。
? ? 對(duì)于成員C.h ?是個(gè)16位的變量,為了保證與16位邊界對(duì)齊,所以,分配到0x18-0x1A的位置。
? ? 分配圖如下(還不正確,耐心讀下去):
? ? 結(jié)構(gòu)體C的占用空間到h結(jié)束就可以了嗎?我們找個(gè)示例:如果定義一個(gè)結(jié)構(gòu)體數(shù)組 CA[2],按變量分配的原則,這2個(gè)結(jié)構(gòu)體應(yīng)該是在內(nèi)存中連續(xù)存儲(chǔ)的,分配應(yīng)該如下圖:
?
? ? 分析一下上圖,明顯可知,CA[1]的很多成員都不再對(duì)齊了,究其原因,是結(jié)構(gòu)體的開(kāi)始邊界不對(duì)齊。
? ? 那結(jié)構(gòu)體的開(kāi)始偏移滿足什么條件才可以使其成員全部對(duì)齊呢。想一想就明白了:很簡(jiǎn)單,保證結(jié)構(gòu)體長(zhǎng)度是原始成員最長(zhǎng)分配的整數(shù)倍即可。
? ? 上述結(jié)構(gòu)體應(yīng)該按最長(zhǎng)的.d成員對(duì)齊,即與8字節(jié)對(duì)齊,這樣正確的分配圖如下:
? ? 當(dāng)然結(jié)構(gòu)體T的長(zhǎng)度:sizeof(T)==0x20;
? ? ?再舉個(gè)例子,看看在默認(rèn)對(duì)齊規(guī)則下,各結(jié)構(gòu)體成員的對(duì)齊規(guī)則:
?
? ? 具體的分配圖如下:
?
?上述全部測(cè)試代碼如下:
?
運(yùn)行時(shí)的內(nèi)存情況如下圖:
?
最后,簡(jiǎn)單加工一下轉(zhuǎn)載過(guò)來(lái)的內(nèi)存對(duì)齊正式原則:
?
? 先介紹四個(gè)概念:
1)數(shù)據(jù)類(lèi)型自身的對(duì)齊值:基本數(shù)據(jù)類(lèi)型的自身對(duì)齊值,等于sizeof(基本數(shù)據(jù)類(lèi)型)。
2)指定對(duì)齊值:#pragma pack (value)時(shí)的指定對(duì)齊值value。
3)結(jié)構(gòu)體或者類(lèi)的自身對(duì)齊值:其成員中自身對(duì)齊值最大的那個(gè)值。
4)數(shù)據(jù)成員、結(jié)構(gòu)體和類(lèi)的有效對(duì)齊值:自身對(duì)齊值和指定對(duì)齊值中較小的那個(gè)值。
? 有效對(duì)齊值N是最終用來(lái)決定數(shù)據(jù)存放地址方式的值,最重要。有效對(duì)齊N,就是表示“對(duì)齊在N上”,也就是說(shuō)該數(shù)據(jù)的"存放起始地址%N=0".而數(shù)據(jù)結(jié)構(gòu)中的數(shù)據(jù)變量都是按定義的先后順序來(lái)排放的。第一個(gè)數(shù)據(jù)變量的起始地址就是 數(shù)據(jù)結(jié)構(gòu)的起始地址。結(jié)構(gòu)體的成員變量要對(duì)齊排放,結(jié)構(gòu)體本身也要根據(jù)自身的有效對(duì)齊值圓整(就是結(jié)構(gòu)體成員變量占用總長(zhǎng)度需要是對(duì)結(jié)構(gòu)體有效對(duì)齊值的整 數(shù)倍)
?
#pragma pack (value)來(lái)告訴編譯器,使用我們指定的對(duì)齊值來(lái)取代缺省的。
如#pragma pack (1)??/*指定按2字節(jié)對(duì)齊*/
#pragma?pack () /*取消指定對(duì)齊,恢復(fù)缺省對(duì)齊*/
?
?
總結(jié)
以上是生活随笔為你收集整理的详解结构体、类等内存字节对齐的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 器械空气机组属于医疗器械吗
- 下一篇: asp.net使用httphandler