本次實驗環境
環境1:Win10, QT 5.12
一. 背景
當普通的型別無法滿足我們的需求的時候,就需要用到結構體了。結構體可衍生出結構體陣列,結構體還可以巢狀結構體,這下子資料型別就豐富多彩了,我們可以根據需要定義自己的資料型別。有時需要求結構體的大小,這就涉及到記憶體對齊的知識。概念、理論之類,我沒有深入研究,這裡主要是驗證一下計算結構體大小的方法,證明學習到的方法確實有效。關於記憶體對齊,最開始是看了《深入理解計算機系統》中關於“資料對齊”一節,上面輕描淡寫的寫了下求結構體的大小,我沒看明白。看《零基礎入門C語言》中關於計算結構體大小的規則,算是看明白了。
二. 前奏
先說點我覺得有意思的地方。陣列之間是不可以直接賦值的,但是用結構體包裝一下,就達到了這個效果,前者無法做到的事情卻通過結構體做到了。通過程式碼來驗證一下。
定義了兩個陣列arr和arr2,第14行程式碼,將arr賦值給arr2,編譯時會報錯。提示:14: error: array type 'int [5]' is not assignable。
(陣列名有二義性,一是表示陣列名,相當於陣列的定海神針,二是表示首元素的地址。第14行程式碼把一個陣列的首元素的地址賦值給另一個陣列首元素,顯然這是不允許的)
將第14行程式碼註釋後。定義了一個結構體,裡面定義了一個整型陣列。然後定義了兩個結構體變數tt1和tt2,將tt2賦值給了tt1,然後列印變數tt2中陣列裡面的每個元素。
1 #include <iostream> 2 3 using namespace std; 4 5 struct numArr 6 { 7 int m_arr[5]; 8 }; 9 10 int main() 11 { 12 int arr[5] = {1, 2, 3, 4, 5}; 13 int arr2[5] = {0}; 14 // arr2 = arr; 15 16 struct numArr tt1 = {{1, 2, 3, 4, 5}}; 17 struct numArr tt2 = {{0}}; 18 tt2 = tt1; 19 20 for(int i = 0; i < 5; ++i) 21 { 22 cout<<tt2.m_arr[i]<<endl; 23 } 24 25 26 27 return 0; 28 }
執行結果如下
從結果可以看到,列印結構與tt1中的陣列中的元素一致。也就是說,將結構體變數tt1賦值給tt2後,tt2中的陣列與tt1中的陣列也一樣了。
通過結構體這麼一包裝,就產生了陣列"可以"賦值的現象了,挺有意思的。
三. 結構體大小的計算
記憶體對齊,關於這一點,《深入理解計算機系統》這本書用的篇幅蠻少的,兩頁不到。書上是這麼介紹的。
許多計算機系統對基本資料型別的合法地址做出了一些限制,要求某種型別物件的地址必須是某個值K(通常是2、4或8)的倍數。這種對齊限制簡化了形成處理器和記憶體系統之間的介面的硬體設計。
前半句能理解,後半句,涉及硬體的東西,懵逼了。
《零基礎入門C語言》這本書,從現象的角度闡述,它先講記憶體不對齊的情況。
一個成員變數需要多個機器週期去讀的現象,稱為記憶體不對齊。為什麼要對齊呢?本質是犧牲空間,換取時間的方法。
一般來說,大多數系統,即使不對齊,也沒什麼大的問題,只是原本需要一次進行記憶體操作讀或寫,現在需要兩次或多次了。不過這個與硬體有關係,比如有些處理器對於某些指令有特定的要求,否則可能就真的出現異常了。有興趣的話,建議自行去深入學習。
對齊規則/計算方法
x86(Linux預設#pragma pack(4), Window預設#pragma pack(8))。Linux最大支援4位元組對齊。
方法:
1) 取pack(n)的值 (n = 1, 2, 4,8......),取結構體中型別最大值為m。兩者取小即為外對齊大小 Y = (m < n ? m: n);
2) 將每一個結構體的成員大小與Y比較取小者為X,作為內對齊的大小;
3) 所謂按X對齊,即為地址(設起始地址為0)能被X整除的地方開始存放資料;
4) 外部對齊原則是依據Y的值(Y的最小整數倍),進行補空操作;
以上就是通常計算結構體大小的方法了。接下來,我們通過一些簡單實驗來驗證一下。
首先,定義一個結構體,裡面包含了char、short、int型別的變數。
1) 結構體先按照char、short、int的順序定義。然後定義一個結構體變數s1,求結構體的大小,一個是結構體型別的大小(模子),一個是是結構體變數的大小。我們也可以把結構體成員的地址也列印出來,檢視它們的偏移量,分析起來會更清晰一些。
1 #include <stdio.h> 2 3 typedef struct stu 4 { 5 char a; 6 short b; 7 int c; 8 } Stu; 9 10 int main() 11 { 12 Stu s1 = {'m', 1, 20}; 13 printf("sizeof(Stu) = %d\n", sizeof(Stu)); 14 printf("sizeof(s1) = %d\n", sizeof(s1)); 15 16 printf("-----------\n"); 17 printf("&s1 = %p\n", &s1); 18 printf("&s1.a = %p\n", &s1.a); 19 printf("&s1.b = %p\n", &s1.b); 20 printf("&s1.c = %p\n", &s1.c); 21 return 0; 22 }
a) Windows平臺,pack預設為8。先求外對齊大小Y,結構體中型別最大的為int型別,大小為4位元組,4比8小,所以Y值為4.
b) 然後將結構體中的每一個成員與Y進行比較,依次求內對齊的大小。結構體中成員分別為char 1位元組,short 2位元組,int 4位元組,與4(外對齊大小Y)比較,得到內對齊大小分別為 1, 2, 4。
c) 假設起始地址為0x00,0可以被1整除,可以存放a了。a為char型別,大小為1個位元組。接著地址為0x01,但是0x01不能被2整除,然後下一個地址為0x02,0x022可以被2整除,因此b的起始地址為0x02(此時,a與b之間填充了一個位元組)。b為short型別,大小為2個位元組。接著地址到了0x04,它可以被4整除,於是可以存放c了,c為int型別,大小為4個位元組。
d) 接著地址到了0x08,(0x08-0x00)它可以被4(外對齊大小Y)整除,滿足外對齊要求。
經分析,結構體大小為1 + 1 + 2 + 4 = 8個位元組。
圖如下圖所示
程式碼執行結果如下
從列印結果來看,結構體大小為8,與上面的分析結果一致,符合預期。
2) 調整結構體中成員的順序。結構體先按照short、char、int的順序定義。列印成員地址的時候也需要調整下a與b的列印順序。程式碼其它部分保持不變。
1 typedef struct stu 2 { 3 short b; 4 char a; 5 int c; 6 } Stu;
a) Windows平臺,pack預設為8。先求外對齊大小Y,結構體中型別最大的為int型別,大小為4位元組,4比8小,所以Y值為4。
b) 然後將結構體中的每一個成員與Y進行比較,依次求內對齊的大小。結構體中成員分別為short 2位元組,char 1位元組,int 4位元組,與4(外對齊大小Y)比較,得到內對齊大小分別為 2,1, 4。
c) 假設起始地址為0x00,0可以被2整除,可以存放b了。b為short型別,大小為2個位元組。接著地址為0x02,2可以被1整除,a為char型別,大小為一個位元組。然後下一個地址為0x03,0x03不可以被4整除,接著地址為0x04(此時,b與c之間填充了一個位元組)。0x04可以被4整除,於是可以存放c了,c為int型別,大小為4個位元組。
d) 接著地址到了0x08,(0x08-0x00)它可以被4(外對齊大小Y)整除,滿足外對齊要求。
經分析,結構體大小為2 + 1 + 1 + 4 = 8個位元組。
圖如下圖所示
程式碼執行結果如下.
結構體大小為8,符合預期。
3)調整結構體中成員的順序。結構體先按照int、short、char的順序定義。列印成員地址的時候也需要調整為c、b、a的列印順序。程式碼的其它部分保持不變。
1 typedef struct stu 2 { 3 int c; 4 short b; 5 char a; 6 7 } Stu;
a) Windows平臺,pack預設為8。先求外對齊大小Y,結構體中型別最大的為int型別,大小為4位元組,4比8小,所以Y值為4。
b) 然後將結構體中的每一個成員與Y進行比較,依次求內對齊的大小。結構體中成員分別為int 4位元組,short 2位元組,char 1位元組,與4(外對齊大小Y)比較,得到內對齊大小分別為4, 2,1。
c) 假設起始地址為0x00,0x00可以被4整除,可以存放c了。c為int型別,大小為4個位元組。接著地址為0x04,0x04可以被2整除,b為short型別,大小為兩個位元組。然後下一個地址為0x06,0x06可以被1整除,可以存放a,a大小為1個位元組。
d) 接著地址到了0x07,(0x07-0x00)不能被4(外對齊大小Y)整除,為滿足外對齊要求,後面需要填充1個位元組。
經分析,結構體大小為:4 + 2 + 1 + 1 = 8個位元組。
圖示如下
執行結果如下
結構體大小為8,符合預期。
實驗調整
將pack修改為1,在前面三個實驗的基礎上,再驗證一下,看下使用這個規則是否與實際情況一致,在程式碼前面新增
1 #pragma pack(1)
4) 結構體與1)中一致,按照char、short、int的順序定義。列印成員地址也是按照a、b、c這個次序列印。
1 #pragma pack(1) 2 3 typedef struct stu 4 { 5 char a; 6 short b; 7 int c; 8 } Stu;
a) 現在pack為1。先求外對齊大小Y,結構體中型別最大的為int型別,大小為4位元組,1比4小,所以Y值為1。
b) 然後將結構體中的每一個成員與Y進行比較,依次求內對齊的大小。結構體中成員分別為char 1位元組,short 2位元組,int 4位元組,與1(外對齊大小Y)比較,得到內對齊大小分別為 1, 1, 1。
c) 假設起始地址為0x00,0x00可以被1整除,可以存放a了。a為char型別,大小為1個位元組。接著地址為0x01,1可以被1整除,可以存放b了,b的型別為short型別,大小為2個位元組。然後下一個地址為0x03,0x03可以被1整除,可以存放c了。c為int型別,佔4個位元組。
d) 接著下一個地址為0x07,(0x07-0x00)可以被1(外對齊大小Y)整除,滿足外對齊的要求。
經分析,結構體大小為1 + 2 + 4 = 7個位元組。
圖如下圖所示
執行結果如下
結構體大小為7,符合預期。
5) 調整結構體中成員的順序。結構體按照short、char、int的順序定義。列印成員地址的時候也需要調整下a與b的列印順序。程式碼的其它部分不變。
1 #pragma pack(1) 2 3 typedef struct stu 4 { 5 short b; 6 char a; 7 int c; 8 } Stu;
a) 現在pack為1。先求外對齊大小Y,結構體中型別最大的為int型別,大小為4位元組,1比4小,所以Y值為1。
b) 然後將結構體中的每一個成員與Y進行比較,依次求內對齊的大小X。結構體中成員分別為short 2位元組,char 1位元組,int 4位元組,分別與1(外對齊大小Y)比較,1較小,得到內對齊大小分別為 1,1,1。
c) 假設起始地址為0x00,0x00可以被1整除,可以存放b了。b為short型別,大小為2個位元組。接著地址為0x02,0x02可以被1整除,可以存入a了。a為char型別,大小為1個位元組。然後下一個地址為0x03,0x03可以被1整除,c的型別為int,c的大小為4個位元組。
d) 在c起始地址的基礎往後4個位元組,現在地址到了0x07,(0x07-0x00)可以被1整除,滿足外對齊要求(不需要填充位元組了)。
經分析,結構體大小為2 + 1 + 4 =7個位元組。
圖如下圖所示
執行結果如下
結構體大小為7,符合預期。
6) 調整結構體中成員的順序。結構體按照int、short、char的順序定義。列印成員地址的時候也需要調整為c、b、a的列印順序。程式碼的其它部分不變。
1 #pragma pack(1) 2 3 typedef struct stu 4 { 5 int c; 6 short b; 7 char a; 8 } Stu;
a) pack現在為1。先求外對齊大小Y,結構體中型別最大的為int型別,大小為4位元組,1比4小,所以Y值為1。
b) 然後將結構體中的每一個成員與Y進行比較,依次求內對齊的大小。結構體中成員分別為int 4位元組,short 2位元組,char 1位元組,分別與1(外對齊大小Y)比較,得到內對齊大小分別為1, 1,1。
c) 假設起始地址為0x00,0x00可以被4整除,可以存放c了。c為int型別,大小為4個位元組。接著地址為0x04,0x04可以被1整除,b為short型別,大小為兩個位元組。然後下一個地址為0x06,0x06可以被1整除,可以存放a了,a為char型別,a大小為1個位元組。
d) 接著地址到了0x07,(0x07-0x00)可以被1(外對齊大小Y)整除,滿足外對齊要求(不需要填充位元組)。
經分析,結構體大小為:4 + 2 + 1 = 7個位元組。
圖示如下
執行結果如下
結構體大小為7,符合預期。
四.例題演示
有人可能會說了,你舉的這幾個例子太簡單了,有沒有複雜的例子可以看下呢?比如結構體中包含陣列的情況,這個計算方法是否適用呢?好的,我們就拿《深入理解計算機系統》這本書的習題來驗證一下。
書中練習題 3.44 有5道題目,均是求結構體大小與成員的偏移量,我們將其作為案例,按照上述方法驗證一下。
對下面的每個結構體宣告,確定每個欄位的偏移量、結構體總的大小、以及在x86-64下它的對齊要求。
(注:因為書中的案例中K值為8,所以下面的程式碼示例中將pack均顯式設定為了8)
1)
1 struct P1{int i; char c; int j; char d;}
a) K現在為8。先求外對齊大小Y,結構體中型別最大的為int型別,大小為4位元組,4比8小,所以Y值為4。
b) 然後將結構體中的每一個成員與Y進行比較,依次求內對齊的大小,取較小者。結構體中成員分別為int 4位元組,char 1位元組,int 4位元組,char 1位元組,分別與4(外對齊大小Y)比較,得到內對齊大小分別為4,1,4, 1。
c) 假設起始地址為0x00,0x00可以被4整除,可以存放i了。i為int型別,大小為4個位元組。接著地址為0x04,0x04可以被1整除,可以儲存c,c為char型別,大小為1個位元組。然後下一個地址為0x05,0x05不可以被4整除,最近的能被4整除的地址就是0x08了,需要填充3個位元組。於是從0x08開始存放j,j為int型別,j大小為4個位元組。下一個地址為0x0c。
d) 0x0c可以被1整除, d為char型別,大小為1個位元組,接著地址到了0x0d,(0x0d-0x00)不可以被4(外對齊大小Y)整除,為滿足外對齊要求,需要填充3個位元組,直到0x10,(0x10-0x00)是Y的整數倍,滿足外對齊。
經分析,結構體大小為:4 + 1 + 3 + 4 + 1 + 3 = 16個位元組。
i 偏移量 0
c 偏移量 4
j 偏移量 8
d 偏移量 12
圖示如下
程式碼如下,為了避免編譯器告警,與前面相比,作了調整,進行了強制型別轉換。
1 #pragma pack(8) 2 3 #include <stdio.h> 4 5 typedef struct P1{int i; char c; int j; char d;} PP1; 6 7 int main() 8 { 9 PP1 p; 10 printf("sizeof(P1) = %d\n", (int)sizeof(PP1)); 11 printf("sizeof(p) = %d\n", (int)sizeof(p)); 12 13 printf("&p = %p\n", (void *)&p); 14 printf("&p.i = %p\n", (void *)&p.i); 15 printf("&p.c = %p\n", (void *)&p.c); 16 printf("&p.j = %p\n", (void *)&p.j); 17 printf("&p.d = %p\n", (void *)&p.d); 18 19 return 0;
執行結果如下
i,c, j,d的偏移量分別為0, 4, 8, 12,符合預期。
2)
1 struct P2 {int i; char c; char d; long j;};
a) K現在為8。先求外對齊大小Y,結構體中型別最大的為long型別,我這臺電腦上long大小為4位元組,4比8小,所以Y值為4。
b) 然後將結構體中的每一個成員與Y進行比較,依次求內對齊的大小。結構體中成員分別為int 4位元組,char 1位元組,char 1位元組,long 4位元組,分別與4(外對齊大小Y)比較,取較小者,得到內對齊大小分別為4,1,1, 4。
c) 假設起始地址為0x00,0x00可以被4整除,可以存放i了。i為int型別,大小為4個位元組。接著地址為0x04,0x04可以被1整除,可以儲存c。c為char型別,大小為1個位元組。然後下一個地址為0x05,0x05可以被1整除,可以儲存d。d的型別為char,大小為1個位元組。接下來地址為0x06,0x06不能被4整除,最近能被4整除的地址為0x08,需要填充2個位元組,才能到0x08。j為long型別,j大小為4個位元組。
d) 接著地址到了0x0c,(0x0c-0x00)可以被4(外對齊大小Y)整除,滿足外對齊。
經分析,結構體大小為:4 + 1 + 1 + 2 + 4 = 12個位元組。
i 偏移量 0
c 偏移量 4
d 偏移量 5
j 偏移量 8
圖示如下:
程式碼如下
1 #include <stdio.h> 2 3 #pragma pack(8) 4 5 typedef struct P2 6 { 7 int i; 8 char c; 9 char d; 10 long j; 11 } PP2; 12 13 int main() 14 15 { 16 printf("sizeof(long) = %d\n", (int)sizeof(long)); 17 18 PP2 p; 19 printf("sizeof(P1) = %d\n", (int)sizeof(PP2)); 20 printf("sizeof(p) = %d\n", (int)sizeof(p)); 21 22 printf("&p = %p\n", (void *)&p); 23 printf("&p.w = %p\n", (void *)&p.i); 24 printf("&p.c = %p\n", (void *)&p.c); 25 printf("&p.c = %p\n", (void *)&p.d); 26 printf("&p.c = %p\n", (void *)&p.j); 27 28 return 0; 29 }
執行結果如下
i,c,d,j的偏移量分別為0,4,5,8,符合預期。
3)若結構體中有陣列,如何整?
1 struct P3{short w[3]; char c[3]};
a) K現在為8。先求外對齊大小Y。若有陣列,取陣列中元素的型別。結構體中型別最大的是short型別,大小為2位元組,2比8小,所以Y值為2。
b) 然後將結構體中的每一個成員與Y進行比較,依次求內對齊的大小。若結構體成員為陣列,取成員整體的大小。結構體中成員分別為w 6位元組,c 3位元組,分別與2(外對齊大小Y)比較,得到內對齊大小分別為2,2。
c) 假設起始地址為0x00,0x00可以被2整除,可以存放w了。w為short陣列型別,大小為6個位元組。接著地址為0x06,可以被3整除,可以儲存c,c為char陣列型別,大小為3個位元組。
d) 接著地址到了0x09,(0x09-0x00)不可以被2(外對齊大小Y)整除,為滿足外對齊要求,需要填充1個位元組,直到0x0a,(0x0a-0x00)是Y的整數倍,滿足外對齊。
經分析,結構體大小為:6 + 3 + 1 = 10個位元組。
w偏移量 0
c偏移量 6
通過程式碼驗證一下,程式碼如下
1 #include <stdio.h> 2 3 #pragma pack(8) 4 5 typedef struct P3{short w[3]; char c[3];} PP3; 6 7 int main() 8 9 { 10 PP3 p; 11 printf("sizeof(P1) = %d\n", (int)sizeof(PP3)); 12 printf("sizeof(p) = %d\n", (int)sizeof(p)); 13 14 printf("&p = %p\n", (void *)&p); 15 printf("&p.i = %p\n", (void *)&p.w); 16 printf("&p.c = %p\n", (void *)&p.c); 17 18 return 0; 19 }
執行結果如下
w,c的偏移量分別為0,6,符合預期。
4) 如果來個指標陣列呢?
1 struct P4{short w[5]; char *c[3];};
a) K現在為8。先求外對齊大小Y。若有陣列,取陣列中元素的型別。結構體中型別最大的是指標型別,大小為8位元組,8與8相等,所以Y值為8。
b) 然後將結構體中的每一個成員與Y進行比較,依次求內對齊的大小。若結構體成員為陣列,取成員整體的大小。結構體中成員分別為w 10位元組,c 24位元組,分別與8(外對齊大小Y)比較,得到內對齊大小分別為8,8。
c) 假設起始地址為0x00,0x00可以被8整除,可以存放w了。w為short陣列型別,大小為10個位元組。接著地址為0x0a,不可以被8整除,需要填充6個位元組,地址到了0x10,這才可以儲存c。c為指標陣列型別,大小為24個位元組。
d) 接著地址到了0x28,(0x28-0x00)可以被8(外對齊大小Y)整除,滿足外對齊。
w 偏移量 0
c 偏移量 16
經分析,結構體大小為:10 + 6 + 24 = 40個位元組。這個圖有些大,就不放圖片了。
通過程式碼驗證一下,程式碼如下
1 #include <stdio.h> 2 3 #pragma pack(8) 4 5 typedef struct P4 6 { 7 short w[5]; 8 char *c[3]; 9 } PP4; 10 11 int main() 12 13 { 14 PP4 p; 15 printf("sizeof(P1) = %d\n", (int)sizeof(PP4)); 16 printf("sizeof(p) = %d\n", (int)sizeof(p)); 17 18 printf("&p = %p\n", (void *)&p); 19 printf("&p.w = %p\n", (void *)&p.w); 20 printf("&p.c = %p\n", (void *)&p.c); 21 22 return 0; 23 }
執行結果如下
w,c的偏移量分別為0, 16,符合預期。
5)假如結構體中巢狀結構體,這個方法還適用嗎?那再來驗證一波。
1 typedef struct P5 2 { 3 struct P3 a[2]; 4 struct P2 t; 5 } PP5;
看起來有點複雜,不過還是用同樣的方法。
a) K現在為8。先求外對齊大小Y。若有陣列或結構體,取陣列或結構體中成員的型別。結構體中型別最大的是long型別,大小為4位元組,4比8比小,所以Y值為4。
b) 然後將結構體PP5中的每一個成員與Y進行比較,依次求內對齊的大小。若結構體成員為陣列或結構體,取成員整體的大小。前面已經求出,結構體中成員分別為a 20位元組,t 12位元組,分別與4(外對齊大小Y)比較,得到內對齊大小分別為4,4。
c) 假設起始地址為0x00,0x00可以被4整除,可以存放a了。a為結構體陣列,大小為20個位元組。接著地址為0x14,0x14可以被4整除,可以儲存t。t為結構體型別,大小為12個位元組。
d) 接著地址到了0x20,(0x20-0x00)可以被4(外對齊大小Y)整除,滿足外對齊。
a 偏移量 0
t 偏移量 20
經分析,結構體大小為:20 + 12 = 32個位元組。
通過程式碼驗證一下,程式碼如下
1 #include <stdio.h> 2 3 #pragma pack(8) 4 5 struct P2 6 { 7 int i; 8 char c; 9 char d; 10 long j; 11 }; 12 13 struct P3 14 { 15 short w[3]; 16 char c[3]; 17 }; 18 19 typedef struct P5 20 { 21 struct P3 a[2]; 22 struct P2 t; 23 } PP5; 24 25 int main() 26 27 { 28 printf("sizeof(long) = %d\n", (int)sizeof(long)); 29 30 PP5 p; 31 printf("sizeof(PP5) = %d\n", (int)sizeof(PP5)); 32 printf("sizeof(p) = %d\n", (int)sizeof(p)); 33 34 printf("&p = %p\n", (void *)&p); 35 printf("&p.a = %p\n", (void *)&p.a); 36 printf("&p.t = %p\n", (void *)&p.t); 37 38 return 0; 39 }
執行結果如下
a,t的偏移量分別為0, 20,符合預期。
五.結語
目前,已經驗證了結構體中包含基本資料型別,結構體中包含陣列,結構體中包含結構體(結構體巢狀)的情形,可能出現的情況都驗證完了,也證實了書中的方法的確有效。感謝前輩們總結的經驗。
注:如果你在做《深入理解計算機系統》書上練習題時,會發現書中提供的3.44 中B和E兩題練習題答案與我上面的結果不一致。不要慌,我對比了下,原因很可能是作者的機器中long型別是8位元組,而我的機器中long型別是4位元組。依據之一請參見書中P27頁"字資料大小"一節,依據二,根據兩種不同大小的long型別,分別進行計算與驗證,這裡就不贅述了。
參考材料
1.《深入理解計算機系統》布萊恩特,奧哈拉倫
2.《零基礎入門C語言》 王桂林