天天看點

C語言位元組對齊問題一、什麼是位元組對齊三、位元組對齊的分類和準則參考

一、什麼是位元組對齊

在計算機中,記憶體空間是按照位元組(1B = 8 bit)劃分的,每一個位元組都有一個編号,這就是位元組的位址。理論上可以從任意起始位址通路任意資料類型的變量,但在實際使用中,通路特定資料類型變量時需要在特定的記憶體起始位址進行通路,這就需要各種資料類型按照一定的規則在空間上進行排列,而不是順序地一個接一個地存放,這就是位元組對齊。

如果一個變量的記憶體起始位址正好是其資料類型長度的整數倍,就被稱作自然對齊。比如,在32系統下,假設一個int型變量的起始位址為0x00000004,那它就是自然對齊的。

1.1 C語言基本資料類型占用的位元組大小

C語言基本資料類型有:

整數型:char, short, int, long, long long。

浮點型:float, double

指針類型:任意資料類型的指針變量占用的存儲空間都是相同的。

我們以64位系統(x64)為例,使用 sizeof() 可以輸出各個基本資料類型占用的位元組長度,代碼如下:

#include <stdio.h>

int main()
{
    printf("sizeof(char)=%d\n", sizeof(char));
    printf("sizeof(short)=%d\n", sizeof(short));
    printf("sizeof(int)=%d\n", sizeof(int));
    printf("sizeof(long)=%d\n", sizeof(long));
    printf("sizeof(long long)=%d\n", sizeof(long long));
    printf("sizeof(float)=%d\n", sizeof(float));
    printf("sizeof(double)=%d\n", sizeof(double));
    printf("sizeof(char*)=%d, sizeof(int*)=%d, sizeof(float*)=%d\n", sizeof(char*), sizeof(int*), sizeof(float*));
    return 0;
}
           

運作結果:

sizeof(char)=1

sizeof(short)=2

sizeof(int)=4

sizeof(long)=4

sizeof(long long)=8

sizeof(float)=4

sizeof(double)=8

sizeof(char*)=8, sizeof(int*)=8, sizeof(float*)=8

<說明> 在32位系統(x86)上,指針類型變量是4位元組;在64位系統(x64)上,指針類型變量是8位元組。這跟計算機的字長有關。32位系統中,CPU一次可以存取4位元組的資料;64位系統中,CPU一次可以存取8位元組的資料。

二、位元組對齊的原因和作用

需要位元組對齊的根本原因在于CPU通路記憶體資料的效率問題。

(1)不同硬體平台對記憶體空間的存取處理方式存在不同。某些硬體平台對特定資料類型的存取隻能從特定位址開始,而不允許其在記憶體中随意存放。

一些硬體系統對位元組對齊要求非常嚴格,比如 SPARC系列處理器,如果取未對齊的資料會發生錯誤,例如:

char ch[8];
char *p=&ch[1];
int  i = *(int *)p;
           

運作時會報 segment error,因為在第3行代碼中,試圖從一個奇數起始位址處讀取一個int型的資料,而在Intel的x86處理器上就不會出現錯誤,隻是效率下降。

(2)如果不按照硬體平台的要求對資料存放進行對齊處理,會影響CPU通路記憶體的效率。比如,對于32位系統的計算機,CPU通過資料總線通路(包括讀和寫)記憶體資料,每個總線周期從偶位址開始通路32位記憶體資料,記憶體資料是以位元組為機關存放的,如果一個32位資料沒有存放在4位元組整除的起始位址處,那麼CPU就需要2個總線周期的時間對其進行通路,顯然通路效率下降了。是以,通過合理的記憶體位元組對齊可以提高CPU訪存效率。為使CPU能夠對記憶體資料進行快速通路,資料的起始位址應具有“對齊”特性。比如,4位元組資料的起始位址應位于4位元組邊界上,8位元組資料位于8位元組邊界上。

(3)合理利用位元組對齊還可以有效地節省存儲空間。但要注意,在32位機器中使用1位元組或2位元組對齊,反而會降低記憶體通路速度。除了需要考慮處理器類型,還應考慮編譯器的類型。在VC/C++和GNU GCC中都是預設以4位元組對齊。

三、位元組對齊的分類和準則

主要基于Intel 的 x86硬體架構介紹結構體對齊、棧對齊和位域對齊,位域對齊本質上為結構體對齊。

對于Intel x86平台,每次配置設定記憶體都是從4的整數倍的起始位址開始配置設定,無論是對結構體變量還是基本資料類型的變量。

3.1 結構體位元組對齊

在C語言中,結構體類型是一種複合資料類型,其構成成員既可以是基本資料類型(如 char, int, float等)的變量,也可以是複合資料類型(如數組、結構體、共用體等)的變量。編譯器在編譯階段,會為結構體的每個成員變量按照其自然邊界配置設定存儲空間。各成員按照他們被聲明的順序在記憶體中順序存儲,第一個成員的位址和整個結構體的起始位址相同。

位元組對齊的問題主要就是針對結構體。

3.1.1 簡單示例

先看一個結構體對齊的簡單示例(32位系統,x86處理器,GCC編譯器)

#include <stdio.h

struct A{
    char  a;
    short b;
    int   c;
};

struct B{
    char  a;
    int   c;
    short b;
};

int main()
{
    printf("sizeof(struct A)=%d\n", sizeof(struct A));
    printf("sizeof(struct B)=%d\n", sizeof(struct B));
    return 0;
}
           

運作結果:>a.exe

sizeof(struct A)=8

sizeof(struct B)=12

分析:可以看到,結構體A 和 B的成員是一樣的,隻是聲明順序不同,但是最終這兩個結構體占用的記憶體空間大小卻是不同的。之是以出現上述結果,就是因為編譯器在編譯程式時要對結構體的成員在存儲空間上進行位元組對齊的緣故。

3.1.2 對齊規則

先說明一下四個重要的基本概念:

(1)基本資料類型自身對齊值:基本資料類型自身占用的存儲空間大小,上面已經給出了各個基本資料類型占用的位元組數大小。

(2)結構體類型自身的對齊值:是結構體成員變量中自身對齊值最大的那個。比如上面的 結構體類型 struct A,其成員變量中最大的對齊值是int類型的對齊值(4位元組),那麼該結構體本身的對齊值也就是4位元組。

(3)指定對齊值:#pragma pack (value)時的指定對齊值value。這個我們在下面再讨論。

(4)結構體成員、結構體的有效對齊值:自身對齊值和指定對齊值中較小者,即有效對齊值=min{自身對齊值,目前指定的pack值}。

其中,有效對齊值 N 是最終用來決定資料存放的對齊值方式。有效對齊N表示“對齊在N上”,即存放資料的起始位址 % N == 0。

結構體中的成員變量都是按定義的先後順序存放的。第一個成員變量的起始位址就是結構體變量本身的起始位址。結構體成員變量要對齊存放,同時結構體本身也要根據自身的有效對齊值進行對齊處理(即結構體占用存儲空間的總長度為結構體有效對齊值的整數倍)。

綜上要求,我們給出結構體位元組對齊的規則如下:

(1)結構體各個成員變量的首位址必須是其自身對齊值的整數倍。

(2)結構體各個成員相對于結構體起始位址的偏移量(offset)是該成員資料類型大小的整數倍,如有需要編譯器會在成員之間加上填充位元組。

(3)結構體配置設定的總空間大小必須是其最寬基本資料類型成員的整數倍,如有需要編譯器會在最末一個成員之後加上填充位元組。

對于上述規則的說明如下:

第1條:編譯器在給結構體開辟存儲空間時,首先找到結構體成員中最寬的基本資料類型,然後尋找記憶體位址能被該基本資料類型所整除的位址,作為結構體的首位址。将這個最寬的基本資料類型的大小作為該結構體自身的對齊值。

第2條:為結構體的一個成員變量開辟空間之前,編譯器首先檢查預開辟空間的首位址相對于結構體首位址的偏移量是否是該成員資料類型大小的整數倍,若是,則存放該成員;若不是,則在該成員和上一個成員之間填充一定數量的多餘位元組,已達到整數倍的要求,也就是将預開辟空間的首位址後移若幹位元組。

第3條:結構體實際占用空間大小是包括了填充位元組的,最後一個成員除了滿足前面兩條之外,還必須滿足第(3)條。

執行個體1:

struct A{
    char  a;  //1
    short b;  //2
    int   c;  //4
};
           

sizeof(struct A) = ?  是 1+2+4=7嗎?答案是:8。分析如下:

(1)首先确定該結構體自身的對齊值是多少?因為最寬的基本資料類型為int型,占4個位元組,是以結構體自身的對齊值為4。

(2)成員a自身對齊值為1,成員b自身對齊值為2,成員c自身對齊值為4。首先成員a占1個位元組沒有問題,如果成員b存放在成員a的下一個位元組的位置的話,那它的偏移量就是1了,不滿足規則2的要求,是以成員b的起始位址需要再後移一個位元組,即:1(a)+1(填充位元組)+2(b)。

(3)成員b之後的下一個位元組的起始位址到結構體的首位址的偏移量剛好是4,而成員c的自身對齊值為4,滿足條件2,是以成員c可以存放在緊随成員b之後的位置上,此時,整個結構體占用的空間大小=2+2+4=8,剛好是4的倍數,滿足規則3。

綜上所述,sizeof(struct A) = 8。

執行個體2:

struct B{
    char  a;  // 1
    int   c;  // 4
    short b;  //2
};
           

sizeof(struct B) = ?  可以看到,struct B 與 struct A 結構體的成員變量是一樣的,隻是成員的順序有變化。那麼,sizeof(struct B) 是否也是等于8呢?

不是!正确答案是:sizeof(struct B) = 12。分析如下:

(1)struct B 的自身對齊值和 struct A 是一樣的,都是4,這是沒問題的。

(2)成員a占用一個位元組,這也是沒問題的,但是成員c的對齊值是4,那麼其存放的起始位址與結構體的首位址的偏移量必須是4的倍數才行,是以成員a和成員c之間須先填充3個位元組,然後下一個位元組才是成員c的起始位址,即:1(a) + 3(填充位元組) + 4(c)。

(3)c成員之後的下一個位元組與結構體首位址的偏移量為8,而成員b的對齊值為2,滿足規則2,此時,結構體的空間大小:8+2(b) = 10。可以發現,雖然結構體的各個成員都已符合對齊規則,但是不滿足規則3,即結構體的空間大小不是其自身對齊值的整數倍。是以,需要在成員b的尾部加上2個填充位元組,即:10 + 2(填充位元組) = 12,這才滿足規則3。

綜上所述,sizeof(struct B) = 12。

可以發現,通過調整結構體各成員的定義順序,合理利用位元組對齊的規則,可以有效地節省結構體的占用空間。

3.1.3 指定對齊方式

主要是更改編譯器的預設位元組對齊方式。在預設情況下,C編譯器(如GCC)為每一個變量按其自然位元組對齊規則配置設定存儲空間。一般可以通過下面的方法來改變預設的位元組對齊的條件:

#pragma pack(n)  //編譯器按照n個位元組的條件對齊

#pragma pack()   //取消自定義位元組對齊方式
           

<說明> #pragma 是一個預處理指令,它的作用是設定編譯器的狀态或者是訓示編譯器完成一些特定的動作。#pragma pack 的主要作用就是改變編譯器的記憶體對齊方式。

當我們在程式代碼中主動設定了自定義的位元組對齊方式後,3.1.2節中所講的位元組對齊規則會有一些變化:

(1)結構體各個成員變量的首位址必須是 min{自身對齊值,指定對齊值} 的整數倍。

(2)結構體各個成員相對于結構體起始位址的偏移量(offset)是 min{該成員資料類型大小,指定對齊值} 的整數倍,如有需要編譯器會在成員之間加上填充位元組。

(3)結構體配置設定的總空間大小必須是 min{其最寬基本資料類型成員,指定對齊值} 的整數倍,如有需要編譯器會在最末一個成員之後加上填充位元組。

執行個體3:

struct Test1{
    char   a;
    int    b;
    short  c;
};

#pragma pack(2)
struct Test2{
    char   a;
    int    b;
    short  c;
};
#pragma pack()
           

sizeof(struct Test1) = 12, sizeof(struct Test2) = 8。分析如下:

這裡,結構體Test1就不贅述了,主要分析結構體Test2的情況:

(1)結構體自身對齊大小為int型長度4位元組,而程式中指定的對齊長度為2,根據規則1,是以結構體自身有效對齊值為2。

(2)成員a的自身對齊值為1,指定對齊值為2,取兩者中較小的為有效對齊值,即為1,是以,成員a占用1個位元組的存儲空間。

(3)成員b的自身對齊值為2,指定對齊值為2,取兩者中較小的為有效對齊值,即為2,而成員b的起始位址偏移量必須是2的整數倍,是以,需要在成員a和成員b之間添加一個填充位元組。此時,占用的記憶體空間=1(a) + 1(填充位元組) + 4(b)

(4)成員c的自身對齊值為2,指定對齊值為2,取兩者中較小的為有效對齊值,即為2,而成員b之後下一個位元組的位址的偏移量為6,滿足規則2,是以,成員c緊跟在成員b之後,此時,占用的記憶體空間=6 + 2(c),即8位元組長度,恰好是結構體自身有效對齊值的倍數。

綜上所述,sizeof(struct Test2) = 8。

執行個體4:

#pragma pack(8)
struct Test3{
    char   a;
    short  b;
    char   c;
};
#pragma pack()
           

sizeof(struct Test3) = ?  答案是:6。分析如下:

(1)結構體自身對齊大小為short型長度2位元組,而程式中指定的對齊長度為8,是以結構體自身有效對齊值為2。根據規則1,此時結構體自身的有效對齊值為2,而不是程式中指定的對齊長度8。

(2)成員a占一個位元組,而成員b的有效對齊值為 min(2, 8)=2,是以,成員a和成員b之間需要加一個填充位元組,1(a) + 1(填充位元組) + 2(b)。

(3)成員c占一個位元組,4+1=5,不滿足規則3,是以需要在成員c之後添加一個填充位元組,即:4 + 1(c) + 1(填充位元組)=6位元組。

綜上所述,sizeof(struct Test3) = 6。

執行個體5:

#pragma pack(1)
struct Test4{
    char   a;
    int    b;
    short  c;
};
#pragma pack()
           

sizeof(struct Test4) = ? 答案是:7。

結構體 Test4 是以1個位元組作為指定對齊值,是以結構體本身以及結構體的所有成員都是按1位元組長度作為有效對齊值進行位元組對齊的。

是以,結構體占用的存儲空間大小為:1+4+2=7。

另外,GNU GCC編譯器中按1位元組長度進行位元組對齊可以寫成如下的形式,改寫結構體 Test4 的定義如下:

#define GNUC_PACKED __attribute__((packed))
struct Test4{
    char   a;
    int    b;
    short  c;
}GNUC_PACKED;
           

執行個體6:微軟面試題解析。

#pragma pack(8)
struct s1{
    short a;
    long  b;
};
struct s2{
    char       c;
    struct s1  d;
    int        e;
};
#pragma pack()
           

問:1、sizeof(struct s2) = ?   2、s2的成員s1中的a後面空了幾個位元組接着才是b?

答案:1、sizeof(struct s2) = 16      2、空了2個位元組。分析如下:

(1)首先分析結構體 s1的記憶體配置設定情況,結構體自身對齊值為long型的長度4,指定對齊值為8,是以結構體s1的有效對齊值為4。成員a的有效對齊值為2,成員b的有效對齊值為4,其偏移量必須是4的倍數,是以,在成員a和成員b之間添加2個填充位元組,此時,占用的空間大小為:2 + 2(填充位元組) + 4 = 8,滿足規則3,故,結構體s1占用的空間大小=8位元組。

(2)再來分析結構體s2,結構體s2的自身對齊值為其成員中最寬基本資料類型的長度,即為成員e的int型,長度為4位元組,指定對齊值為8,故結構體s2的有效對齊值為4位元組。s2中的成員c按1位元組對齊,而成員d是一個8位元組的結構體,而結構體s1的有效對齊值為4,是以成員d按4位元組對齊,需要在成員c和d之間添加3個填充位元組,此時,占用的空間大小為:1 + 3(填充位元組) + 8 = 12位元組。

(3)成員e的有效對齊值,可知是4,其偏移量為12,剛好是其對齊值的倍數,此時,占用的空間大小為:12 + 4 = 16位元組,也恰好是結構體s2的有效對齊值的倍數,是以,結構體s2最終占用的空間大小為16位元組,即:1 + 3(填充位元組) + 8 + 4 = 16位元組。

結構體s2的各個成員變量在記憶體中的布局如下表所示:

c * * *
a a * *
b b b b
e e e e

參考

C語言位元組對齊問題詳解

繼續閱讀