[Mac-10.7.1 Lion Intel-based x64 gcc4.2.1]
Q: 結構體的本質是什嗎?
A: 結構體就像一種粘合劑,將事物之間的關係很好地組合在了一起。
Q: 結構體對象中各個變數的記憶體儲存位置和內建基本類型變數的儲存有什麼區別?
A: 簡單地說,它們沒區別;複雜地說,它們有區別。簡單在於它們終究會儲存在記憶體中,複雜地說它們的位置可能有一些不同。如下:
#include <stdio.h>#include <string.h>#define PRINT_D(intValue) printf(#intValue" is %d\n", (intValue));typedef struct{ char sex; int age;}student;// print every byte of the objvoid print_struct(void *obj, int size){ int i; unsigned char *temp = (unsigned char *)obj; for (i = 0; i < size; ++i) { printf("%#x ", temp[i]); } printf("\n");}int main(){ student s; s.sex = 'm'; s.age = 25; print_struct(&s, sizeof(student)); return 0;}
運行結果:
可以看到s對象的sex成員值為'm',對應於0x6d, s的age成員值對應於0x19.因為筆者的機器為小端法,所以它們都在低地址顯示,高地址補零。這裡也可以看到student結構中的sex成員雖然是char類型,卻依然佔用了4位元組,這是被補齊了。
Q: 如何擷取結構體中一個成員的位移位置呢?
A:
#include <stdio.h>#include <string.h>#define PRINT_D(intValue) printf(#intValue" is %d\n", (intValue));typedef struct{ char sex; int age;}student;int main(){ student s; PRINT_D((char *)&s.sex - (char *)&s) PRINT_D((char *)&s.age - (char *)&s) return 0;}
如上代碼,定義一個student類型的對象s,輸出s的sex成員的地址和s地址的差即為位移,同理age的位移也可以獲得。
Q: 有更一般的方法麼,不用建立對象,直接獲得一個結構體某個成員的位移位置?
A: 有的。更一般就意味著更抽象。將上面的過程抽象,取一個特定結構體對象的某個成員的位移。
#include <stdio.h>#include <string.h>#define PRINT_D(intValue) printf(#intValue" is %d\n", (intValue));typedef struct{ char sex; int age;}student;int main(){ PRINT_D(&((student *)0)->sex - (char *)0) PRINT_D((char *)&((student *)0)->age - (char *)0) return 0;}
上面的代碼,在地址0抽象出了一個student類型的指標,然後獲得各個成員的位移;
運行結果:
Q: 上面的格式看起來有點複雜,有更簡單的封裝嗎?
A: 我們可以採用宏定義:
#define OFFSET(struct, member) ((char *)&((struct *)0)->member - (char *)0)
如下代碼:
#include <stdio.h>#include <string.h>#define PRINT_D(intValue) printf(#intValue" is %d\n", (intValue));#define OFFSET(struct, member) ((char *)&((struct *)0)->member - (char *)0)typedef struct{ char sex; int age;}student;int main(){ PRINT_D(OFFSET(student, sex)) PRINT_D(OFFSET(student, age)) return 0;}
輸出結果:
當然,也可以使用系統標頭檔中定義的宏offsetof, 範例程式碼如下:
#include <stdio.h>#include <string.h>#include <stddef.h>#define PRINT_LUD(intValue) printf(#intValue" is %lu\n", (intValue));#define OFFSET(struct, member) ((char *)&((struct *)0)->member - (char *)0)typedef struct{ char sex; int age;}student;int main(){ PRINT_LUD(offsetof(student, sex)) PRINT_LUD(offsetof(student, age)) return 0;}
Q: 上面代碼中的OFFSET宏,用地址0處的指標訪問成員,難道不會出現訪問違例?
A: 先寫如下代碼:
#include <stdio.h>#include <string.h>#define PRINT_D(intValue) printf(#intValue" is %d\n", (intValue));#define OFFSET(struct, member) ((char *)&((struct *)0)->member - (char *)0)typedef struct{ char sex; int age;}student;int main(){ student *s = (student *)0; PRINT_D(s->age) return 0;}
代碼試圖訪問地址0處的資料,運行:
可以看到出現訪問違例。那麼OFFSET宏是如何正確執行的呢?我們查看下使用OFFSET的代碼的彙編:
#include <stdio.h>#include <string.h>#define PRINT_D(intValue) printf(#intValue" is %d\n", (intValue));#define OFFSET(struct, member) ((char *)&((struct *)0)->member - (char *)0)typedef struct{ char sex; int age;}student;int main(){ int ret = OFFSET(student, age); return 0;}
main函數的彙編代碼:
0x00001f70 <main+0>:push %ebp0x00001f71 <main+1>:mov %esp,%ebp0x00001f73 <main+3>:sub $0x8,%esp0x00001f76 <main+6>:mov $0x0,%eax0x00001f7b <main+11>:mov %eax,%ecx0x00001f7d <main+13>:add $0x4,%ecx0x00001f80 <main+16>:movl $0x0,-0x4(%ebp)0x00001f87 <main+23>:mov %ecx,-0x8(%ebp)0x00001f8a <main+26>:add $0x8,%esp0x00001f8d <main+29>:pop %ebp0x00001f8e <main+30>:ret
可以看到,第五行%ecx為0, add $0x4,%ecx將%ecx加4,得到4,然後將這個數值放到ret變數的位置.可見,編譯器已經進行了最佳化。換個更簡單的代碼:
#include <stdio.h>#include <string.h>#define PRINT_D(intValue) printf(#intValue" is %d\n", (intValue));#define OFFSET(struct, member) ((char *)&((struct *)0)->member - (char *)0)typedef struct{ char sex; int age;}student;int main(){ void *p = &((student *)0)->age; return 0;}
上面的代碼僅僅是擷取地址為0的student指標的age成員的地址,彙編如下:
0x00001f70 <main+0>:push %ebp0x00001f71 <main+1>:mov %esp,%ebp0x00001f73 <main+3>:sub $0x8,%esp0x00001f76 <main+6>:mov $0x0,%eax0x00001f7b <main+11>:mov %eax,%ecx0x00001f7d <main+13>:add $0x4,%ecx0x00001f80 <main+16>:movl $0x0,-0x4(%ebp)0x00001f87 <main+23>:mov %ecx,-0x8(%ebp)0x00001f8a <main+26>:add $0x8,%esp0x00001f8d <main+29>:pop %ebp0x00001f8e <main+30>:ret
可以看到,編譯器和上面一樣,也進行了最佳化,直接地址4放入變數p中。當然,這兩個樣本告訴我們,使用地址的形式很容易讓編譯器進行對應的可能最佳化。這也為程式員使用欺騙編譯器來得到需要得到的資料埋下了伏筆。我們繼續看如下代碼,不取地址0處的地址,直接取地址0處的成員資料:
#include <stdio.h>#include <string.h>#define PRINT_D(intValue) printf(#intValue" is %d\n", (intValue));#define OFFSET(struct, member) ((char *)&((struct *)0)->member - (char *)0)typedef struct{ char sex; int age;}student;int main(){ int age = ((student *)0)->age; return 0;}
運行:
可以看出,出現了訪問違例。因為如果代碼非要訪問地址0處的資料,編譯器也不能再做什麼最佳化,只能乖乖地去取資料,結果就違例了。
Q: 關於結構體的對齊,到底遵循什麼原則?
A: 首先先不討論結構體按多少位元組對齊,先看看只以1位元組對齊的情況:
#include <stdio.h>#include <string.h>#define PRINT_D(intValue) printf(#intValue" is %d\n", (intValue));#define OFFSET(struct, member) ((char *)&((struct *)0)->member - (char *)0)#pragma pack(1)typedef struct{ char sex; short score; int age;}student;int main(){ PRINT_D(sizeof(student)) PRINT_D(OFFSET(student, sex)) PRINT_D(OFFSET(student, score)) PRINT_D(OFFSET(student, age)) return 0;}
輸出:
可以看到,如果按1位元組對齊,那麼結構體內部的成員緊密排列,sizeof(char) == 1, sizeof(short) == 2, sizeof(int) == 4.
修改上面的代碼, 去掉#pragma pack語句,代碼如下:
#include <stdio.h>#include <string.h>#define PRINT_D(intValue) printf(#intValue" is %d\n", (intValue));#define OFFSET(struct, member) ((char *)&((struct *)0)->member - (char *)0)typedef struct{ char sex; short score; int age;}student;int main(){ PRINT_D(sizeof(student)) PRINT_D(OFFSET(student, sex)) PRINT_D(OFFSET(student, score)) PRINT_D(OFFSET(student, age)) return 0;}
運行結果:
此時,各個成員之間就不像之前那樣緊密排列了,而是有一些縫隙。這裡需要介紹下對齊原則:
此原則是在沒有#pragma pack語句作用時的原則(不同同台可能會有不同):
原則A: 結構體或者union結構的成員,第一個成員在位移0的位置,之後的每個成員的起始位置必須是當前成員大小的整數倍;
原則B: 如果結構體A含有結構體成員B,那麼B的起始位置必須是B中最大元素大小整數倍地址;
原則C: 結構體的總大小,必須是內部最大成員的整數倍;
依據上面3個原則,我們來具體分析下:
sex在位移0處,佔1位元組;score是short類型,佔2位元組,score必須以2的整數倍為起始位置,所以它的起始位置為2; age為int類型,大小為4位元組,它必須以4的整數倍為起始位置,因為前面有sex佔1位元組,填充的1位元組和score佔2位元組,地址4已經是4的整數倍,所以age的位置為4.最後,總大小為4的倍數,不用繼續填充。
繼續修改上面的代碼,增加#pragma pack語句:
#include <stdio.h>#include <string.h>#define PRINT_D(intValue) printf(#intValue" is %d\n", (intValue));#define OFFSET(struct, member) ((char *)&((struct *)0)->member - (char *)0)#pragma pack(4)typedef struct{ char sex; short score; int age;}student;int main(){ PRINT_D(sizeof(student)) PRINT_D(OFFSET(student, sex)) PRINT_D(OFFSET(student, score)) PRINT_D(OFFSET(student, age)) return 0;}
運行結果:
具體分析下:
有了#pragma pack(4)語句後,之前說的原則A和C就不適用了。實際對齊原則是自身對齊值(成員sizeof大小)和指定對齊值(#pragma pack指定的對齊大小)的較小者。依次原則,sex依然位移為0, 自身對齊值為1,指定對齊值為4,所以實際對齊為1; score成員自身對齊值為2,指定對齊值為4,實際對齊為2;所以前面的sex後面將填充一個1位元組,然後是score的位置,它的位移為2;age自身對齊值為4,指定對齊為4,所以實際對齊值為4;前面的sex和score正好佔用4位元組,所以age接著存放;它的位移為4.
我們繼續修改下代碼:
#include <stdio.h>#include <string.h>#define PRINT_D(intValue) printf(#intValue" is %d\n", (intValue));#define OFFSET(struct, member) ((char *)&((struct *)0)->member - (char *)0)#pragma pack(4)typedef struct{ char sex; int age; short score;}student;int main(){ PRINT_D(sizeof(student)) PRINT_D(OFFSET(student, sex)) PRINT_D(OFFSET(student, age)) PRINT_D(OFFSET(student, score)) return 0;}
運行結果:
這個和上面的不同在於age成員被移到第二個位置;sex的位移依然為0,age自身對齊為4,指定對齊為4,所以實際對齊為4,所以age將從位移為4的位置儲存。
Q: 關於位域的問題,空域到底表示什嗎?
A: 它表示之後的位域從新空間開始。
#include <stdio.h>#include <string.h>#define PRINT_D(intValue) printf(#intValue" is %d\n", (intValue));#define OFFSET(struct, member) ((char *)&((struct *)0)->member - (char *)0)typedef struct { int a : 1; int b : 3; int : 0; int d : 2;}bit_info;int main(){ PRINT_D(sizeof(bit_info)) return 0;}
運行結果:
bit_info中的a, b佔用4個位元組的前4位,到int : 0; 時表示此時將填充餘下所有沒有填充的位,即剛剛的4個位元組的餘下28位;int d : 2; 將從第四個位元組開始填充,又會佔用4個位元組,所以總大小為8.
關於結構體如何和物件導向關聯,c++很好地詮釋了,這裡不做介紹。
xichen
2012-5-18 11:06:47