網站首頁 編程語言 正文
結構體內存對齊
當我們創建一個結構體變量時,內存就會開辟一塊空間,那么在創建結構體變量時內存到底是怎么開辟空間的呢?會開辟多大的空間呢?我們來看一下下面的代碼
struct S { int i; char c; char b; }; struct G { char c; int i; char b; }; int main() { struct S u; struct G g; printf("%d\n", sizeof(u)); printf("%d\n", sizeof(g)); return 0; }
在這個代碼中,我們創建了兩個結構體類型,并用這兩個類型創建了兩個結構體變量,當變量被創建的時候,內存就會為這些變量開辟空間,而在這兩個變量中,我們都創建了兩個char類型的成員和一個int類型的成員,不同的是這三個成員的排列順序不同,然后我們來打印一下這兩個變量所占的字節,來看一下有什么不同
我們發現,這兩個結構體變量雖然里面的元素類型一樣,但是他們的大小并不一樣,而他們之間的區別就是結構體成員的排列順序不同,所以我們可以知道,結構體成員的排列順序是會影響結構體的大小的,那么他到底是怎么影響的呢,這就和結構體的創建規則有關。
結構體在創建時要進行內存對齊,對齊的規則是:
1.第一個成員在與結構體變量偏移量為0的地址處。
2.其他成員變量要對齊到某個數字(對齊數)的整數倍的地址處。
對齊數 = 編譯器默認的一個對齊數 與 該成員大小的較小值。
VS中默認的值為83.結構體總大小為最大對齊數(每個成員變量都有一個對齊數)的整數倍。
4.如果嵌套了結構體的情況,嵌套的結構體對齊到自己的最大對齊數的整數倍處,結構體的整 體大小就是所有最大對齊數(含嵌套結構體的對齊數)的整數倍。
下面我們來根據對齊規則來算一個下上面的兩個結構體的大小:
struct S { int i; char c; char b; };
首先,我們先看struct S,第一個成員是int類型,占4個字節,它的對齊數就是4,第二個是char類型,占一個字節,對齊數是1,一個放在1的整數位上,也就是可以接著往下放,那就是在int的4個字節后面緊跟著來存放,第三個也是char類型,對齊數也是1,那么也可以接著放在后面,三個成員的最大對齊數是4,根據對齊規則,結構體的大小是最大對齊數的整數倍,而我們剛剛算完這三個占的大小已經是6個字節了,所以為了對齊,我們要浪費兩個字節的空間,使這個結構體的大小為8個字節,是最大對齊數的2倍。
struct G { char c; int i; char b; };
我們再來看第二個,這里,我們的第一個元素變成了char類型,占一個字節,但是第二個成員是int類型,對齊數為4,根據對齊規則要對齊到4的整數倍處,那就只能放在結構體開始儲存的位置往后4個字節的地址處,加上它本身占4個字節,這時候我們在內存中就使用了8個字節,然后在存入第三個成員char類型,對齊數是1,可以直接在后面存放,最大對齊數還是4,但是我們已經使用了9個字節,所以這時候的大小應該是12個字節,4的3倍。
以上的結果也是符合剛剛的運行結果的。
下面我們來看一個結構體里嵌套了一個結構體時的例子:
struct G { char c; int i; char b; }; struct S { char a; int i; struct G c; };
我們在結構體struct S中嵌套了一個結構體struct G,根據對齊規則,我們嵌套的結構體一個對齊到自己的最大對齊數的整數倍上,那就是4的整數倍,而第一個成員是char,第二個是int,int對齊到4的整數倍上,那前兩個成員就占了8個字節,而struct G正好對齊到int的后面,大小我們剛剛算出來是12個字節,加起來就是20個字節,下面我們來驗證一下
與運行結果相同,那么我們的計算就沒有問題。
那么我們結構體在儲存時為什么要進行內存對齊呢?主要有以下兩個原因。
1.平臺原因(移植原因):
不是所有的硬件平臺都能訪問任意地址上的任意數據的;某些硬件平臺只能在某些地址處取某些特定類型的數據,否則拋出硬件異常。
2.性能原因:
數據結構(尤其是棧)應該盡可能地在自然邊界上對齊。
原因在于,為了訪問未對齊的內存,處理器需要作兩次內存訪問;而對齊的內存訪問僅需要一次訪問。
跟主要的可能是第二種原因,為了增加處理器的訪問效率,我們選擇了用浪費空間的方式,來使我們的處理器可以一次性的訪問到我們需要的元素,用空間來換時間。
根據我們的對齊規則我們也可以發現,如果我們想要在創建結構體變量時節省空間,我們應該盡量讓小的成員集中在一起,這樣可以減少空間的浪費,節省空間。
在規則中我們看到,計算每個成員的對齊數時要選擇默認對齊數與該成員大小的較小值,在vs編譯器中這個默認對齊數是8,而在有的編譯器中沒有默認對齊數,如gcc編譯器的默認對齊數就是成員自身的大小,當然,這個默認對齊數也是可以該的,而我們如何來修改默認對齊數呢,我們看下面的代碼
struct H { char c1; double d; char c2; }; int main() { struct H h; printf("%d\n", sizeof(h)); return 0; }
根據我們的對齊規則,這個結構體的大小應該是24個字節,我們來運行一下看看結果
如果我們要把它的默認對齊數改為4,那么我們再來重新計算一下,首先第一個char類型占一個字節,然后double類型占8個字節,但是對齊數為4,對齊到4的倍數,就可以在第4個字節的位置開始存儲,這時候前兩個只占12個字節,最后一個char占一個字節,最大對齊數為4,大小為4的倍數,應該為16,我們來驗證一下
根據運行的結果我們可以看到,確實是改變了默認對齊數
修改默認對齊數的方法就是在結構體類型前加上#pragma pack(n),n表示修改后的默認對齊數的值(一般都是2的次方數,當改為1時,表示不存在對齊),在結構體類型的后面加上#pragma pack()表示取消修改。
#pragma pack(4) struct H { char c1; double d; char c2; }; #pragma pack()
如上面的代碼,表示我們只把#pragma pack(4)~#pragma pack()之間的結構體類型的默認對齊數改為了4。
結構體傳參
在學習函數的時候我們曾經學到,函數在調用時有兩種方法,一種是傳值調用,一種是傳址調用C語言–函數,我們來看下面的代碼。
struct S { int data[1000]; int num; }; //結構體傳參 void print1(struct S s) { printf("%d\n", s.num); } //結構體地址傳參 void print2(struct S* ps) { printf("%d\n", ps->num); } int main() { struct S s = { {1,2,3,4}, 1000 }; print1(s); //傳結構體 print2(&s); //傳地址 return 0; }
在這個代碼中,我們的print函數的目的是打印結構體變量中的一個成員,print1傳參時傳的就是結構體變量的值,print2傳的就是結構體變量的地址,他們的不同點在于傳值的時候,我們的形參是實參的一份臨時拷貝,也就是說,當我把結構體變量的以傳值的方式傳參給函數時,當我們調用這個函數,內存就會把這個結構體拷貝一份,當我們的結構體變量比較小時還好,但是當這個結構體變量里的成員非常多,占據的空間非常大時,就會導致系統開銷比較大,性能下降,而如果我們使用傳址的方式,我們一個地址的大小也就4或者8個字節,就沒有上面的問題,所以在結構體傳參時,我們盡量要傳地址,既可以節省時間,也可以節省空間。
結構體實現位段
什么是位段
位段的聲明和結構是類似的,有兩個不同:
1.位段的成員必須是 char、int、unsigned int 或signed int 。
2.位段的成員名后邊有一個冒號和一個數字。
如
struct A { int a : 2; int b : 5; int c : 10; int d : 30; };
這里的A就是位段,每個成員后面的數字代表他們需要的二進制位。
位段在內存中的存儲
1.位段的成員可以是 int unsigned int signed int 或者是 char (屬于整形家族)類型
2.位段的空間上是按照需要以4個字節( int )或者1個字節( char )的方式來開辟的。
3.位段涉及很多不確定因素,位段是不跨平臺的,注重可移植的程序應該避免使用位段。
我們來舉個例子
struct S { char a : 3; char b : 4; char c : 5; char d : 4; }; int main() { struct S s = { 0 }; s.a = 10; s.b = 12; s.c = 3; s.d = 4; return 0; }
我們來研究這個位段在內存中是如何儲存的
首先,我們看成員a,它占3個二進制位,是一個char類型,我們需要創建出一個字節的空間,也就是8個二進制位,a要走了3個,我們假設在存儲是,我們存儲的順序是與小段存儲的方式類似,也是從右向左存儲的,那么a在內存中存儲的位置應該是后面的三個二進制位
低地址->高地址 ******** aaa
假設一個代表內存中的一個二進制位,這八個代表一個字節,這3個a上面對應的*就是a在這一個字節中所占的空間。
b需要4個二進制位,而我們剛剛創建的一個字節中還有5個二進制位,所以我們把b的4個二進制位放在a的后面。
低地址->高地址 ******** bbbbaaa
這就是a與b在內存中的存儲情況,而這是c需要個二進制位,我們只剩下一個二進制位了,而c又是一個char類型,所以我們需要再創建一個字節的空間,而當我們創建好了新的空間,c的個二進制位應該怎么儲存呢,我們是接著第一個字節把剩下的二進制位用完還是在我們新創建的字節里重新儲存呢,假設內存在存儲時選擇直接浪費掉哪個二進制位,在新的空間進行儲存,這時內存中的存儲分布應該是這樣
低地址->高地址 ******** ******** bbbbaaa ccccc
存儲d時,d需要4個二進制位,第二個字節中的二進制位也不夠,所以我們再創建一個字節,存儲d
低地址->高地址 ******** ******** ******** bbbbaaa ccccc dddd
如果我們的假設沒錯的話,這一個就是a,b,c,d這4個成員在內存中的儲存位置,然后我們又對這4個成員進行了賦值
a=10=>1010(二進制數)=>010(3個二進制位)
b=12=>1100(二進制數)=>1100(4個二進制位)
c=3=>11(二進制數)=>00011(3個二進制位)
d=4=>100(二進制數)=>0100(4個二進制位)
當我們用上面的數據對我們剛剛的位段進行賦值,那么這個位段在內存中存儲的內容應該是這樣的
低地址->高地址 0bbbbaaa 000ccccc 0000dddd 01100010 00000011 00000100(二進制) 62 03 04 (十六進制)
根據我們的計算,我們發現,如果位段按照我們剛剛的假設來存儲,那么在內存中存儲的內容應該是62 03 03,那我們現在來調試一下看看
結果與我們推斷的一樣,那么就說明當前的編譯器位段的存儲是按照我們假設的方式來存儲的。
位段的問題
但是位段在C語言中的規定又有許多不確定的地方,
1.int 位段被當成有符號數還是無符號數是不確定的。
2.位段中最大位的數目不能確定。(16位機器最大16,32位機器最大32,寫成27,在16位機 器會出問題。
3.位段中的成員在內存中從左向右分配,還是從右向左分配標準尚未定義。
4.當一個結構包含兩個位段,第二個位段成員比較大,無法容納于第一個位段剩余的位時,是 舍棄剩余的位還是利用,這是不確定的。
不同的平臺可能對上述的問題有不同的規定,所以位段是不能跨平臺的
跟結構相比,位段可以達到同樣的效果,但是可以很好的節省空間,但是有跨平臺的問題存在。
位段的使用環境是在我們傳輸一個數據包時,可以使用位段使數據包在不能壓縮的情況下,所占的空間最小。
總結
原文鏈接:https://blog.csdn.net/qq_45967533/article/details/123032626
相關推薦
- 2022-04-27 python?數據挖掘算法的過程詳解_python
- 2022-12-04 詳解Go?依賴管理?go?mod?tidy_Golang
- 2022-07-12 Spring Boot項目下JPA自定義雪花算法ID生成器詳解
- 2022-09-03 ASP.NET實現Repeater控件的數據綁定_基礎知識
- 2022-12-09 Oracle遞歸查詢簡單示例_oracle
- 2022-04-18 h5,移動端瀏覽器通過css 實現頁面,元素橫向滾動
- 2023-02-23 Rust個人學習小結之Rust的循環_Rust語言
- 2024-07-18 【探索SpringCloud】服務發現-Nacos服務端數據結構和模型
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支