一、引言:
sizeof是c語言中的一個運算符,用來求某個變量或者類型的長度,CSDN有篇文章介紹sizeof的特點介紹的比較詳細,我寫這篇文章主要是介紹struct的數據成員對齊。C語言的struct成員對齊與操作系統有關,在window與linux上的表現不同,先來看一個例子:
復制代碼
1 #include <stdio.h>
2 typedef struct{
3 int num1;
4 int num2;
5 double num3;
6
7 }A;
8 typedef struct{
9 int num1;
10 double num3;
11 int num2;
12 }B;
13
14 int main(void) {
15 printf("A:%d\n",sizeof(A));
16 printf("B:%d\n",sizeof(B));
17 return 0;
18 }
復制代碼
二、windows的對齊情況
上面這段程序在windows下執行打印的是:
A:16
B:24
為什麼數據成員一樣,只是成員的順序不同,導致結構體所占的空間會不同,這就是數據對齊的原因,為了提高存儲器的訪問效率,避免讀一個成員數據訪問多次存儲器,操作系統對基本數據類型的合法地址做了限制,要求某種類型對象的地址必須是某個值K的整數倍(K=2或4或8)。Windows給出的對齊要求是:任何K(K=2或4或8)字節的基本對象的地址都必須是K的整數倍。在上面的示例中,num1和num2為int占4個字節,num3為double占8了字節,結構體A、B的數據對齊情況分別如下:
上面的是結構體A的對齊情況,下面的是結構體B的對齊情況,圖中的灰色部分為對齊填充部分,不代表有效數據。可以看到A的分布很緊湊,沒有留下空隙,而B中,有兩段空隙,因為數據A不需要填充就能滿足K(這裡K=4、8)字節的對象的起始地址是K的整數倍了。而B中,第一個數據成員是num1,大小為四個字節,接下來的是num3,大小為8個字節,num3不能緊接在num1的後面,因為4不是8的整數倍,因此需要填充4個字節,這樣num3的起始地址就在8上,滿足要求,之後的num2接在num3後,起始地址為16。有人會問,為什麼B占用的是24個字節,而不是20個字節,從上面的圖中,也看出,用20個字節剛好裝下了num1、num2、num3這三個元素,並且這三個元素都滿足了對齊要求,為什麼num2後面還要填充4個字節?事實上,如果只有一個B對象,20字節確實是滿足對齊要求的,但如果我們聲明一個類型為B的數據:B b[3],每個B對象只用20字節,則其數據偏移情況如下:
可以看到,b[1]的num3的起始地址是28,不滿足8的整數倍的要求,這就是B為什麼要24字節的原因,為了所有的數據滿足”任何K(K=2或4或8)字節的基本對象的地址都必須是K的整數倍“的要求,必須是結構體的整體大小必須是最大的K的整數倍。
三、linux的對齊情況
以上是windows的對齊要求,如果在linux上執行前面的示例,輸出A、B所占的字節數都是16。Linux的對齊要求是:2字節類型的數據(如short)的起始地址必須是2的整數倍,而較大(int *,int double ,long)的數據類型的地址必須是4的整數倍。linux的對齊要求比windows寬松,這樣會更加充分的利用存儲空間,但是訪問效率沒有window好。linux下結構體B的對齊情況如下:
這裡是針對32位的系統,對於X86-64,linux與windows一樣,要求K字節的數據類型是K的倍數。
以上是對struct的數據對齊的簡單介紹,我想,這個數據對齊可以出兩個面試題,一個是已知道結構體定義,求成員的起始地址和結構體大小;另一個是,已知結構體定義,如何排列成員變量的順序,使得整個結構體占有的存儲空間最小。
四、計算結構體的大小和各個成員的起始地址
這個題目是比較簡單的,只要把對齊要求理解了,我們只前往後處理每一個變量,只要當前放入的變量滿足對齊要求,然後遞歸求後門的變量,直接上代碼了:
復制代碼
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define MAX 100
char *vars[MAX];//保存變量名
int lens[MAX];//保存變量的字節長度
int start[MAX];//保存變量的起始地址
int ALIGN;
/*********掃描成員變量和長度,每一組輸入由變量名和長度組成**********/
int scanfStruct(){
char var[20],*p;
int len;
int index = 0;
printf(">>");
while(scanf("%s %d",var,&len) && var[0] != '$'){
p = (char *)malloc(strlen(var));
strcpy(p,var);
vars[index] = p;
lens[index] = len;
printf(">>");
// printf("%s:%d\n>>",vars[index],lens[index]);
index ++;
}
return index;
}
/*********計算linux的對齊長度*********/
int getLinuxAlign(int *lens, int n){
int i=0;
int align = 1;
for(i = 0;i< n;i++){
if(align < 2 && *(lens +i) == 2)
align = 2;
if(align <4 && *(lens +i) >= 4)
align = 4;
}
return align;
}
/*********計算windows的對齊長度*********/
int getWindowsAlign(int *lens, int n){
int i=0;
int align = 1;
for(i = 0;i< n;i++){
if(align < 2 && *(lens +i) == 2)
align = 2;
if(align <4 && *(lens +i) == 4)
align = 4;
if(align <8 && *(lens +i) == 8)
align = 8;
}
return align;
}
/******
遞歸計算各個變量的數據偏移以及結構體大小
i表示正在處理第i個變量,curAddr表示當前地址
size表示結構體的大小,n表示變量個個數
******/
void getStart(int i,int *curAddr, int *size, int n){
if(i >= n)
return;
start[i] = *curAddr;//第i個變量的首地址為當前地址
*curAddr += lens[i];
*size += lens[i];
if( *curAddr % ALIGN ==0)//當前地址能被對齊長度整除,直接遞歸處理下一個變量
getStart(i+1,curAddr,size,n);
else{//當前地址不能被對齊長度整除
int blank = ALIGN - (*curAddr % ALIGN);//需要填充的大小
if(i == n-1){//已經是最後一個變量,只需將結構體大小擴充一下
*size += blank;
return;
}else if(lens[i+1] > blank){//下一個變量的大小大於填充大小,填充後,遞歸處理下一個變量
*curAddr += blank;
*size += blank;
getStart(i+1,curAddr,size,n);
}else{
i++;
while(lens[i] <= blank){//只要下一個變量的大小小於填空空白大小
start[i] = *curAddr;
*curAddr += lens[i];
*size += lens[i];
blank = ALIGN -(*curAddr % ALIGN);
if(i == n-1){
*size += blank;
return ;
}
i++;
}
getStart(i,curAddr,size,n);
}
}
}
void printStart(int n){
int i=0;
for(i=0; i<n; i++)
printf("%s(%d):%d\n",vars[i],lens[i],start[i]);
}
int main(){
int n;
int curAddr = 0;
int size = 0;
n = scanfStruct();
//ALIGN = getLinuxAlign(lens,n);
ALIGN = getWindowsAlign(lens,n);
getStart(0,&curAddr,&size,n);
printf("align:%d\n",ALIGN);
printf("\nsize:%d\n",size);
printStart(n);
return 0;
}
復制代碼
五、排列成員順序,使得結構體占有存儲空間最小
這個題目更有意思,我這裡暫時不貼,有想法的歡迎回復。