[TOC]
# 字節對齊
在用sizeof計算結構體占用的空間的時候,不是簡單把所有元素相加的,這里涉及到內存字節對齊的問題
從理論上來講,對于任何變量都可以從任何地址訪問,但是事實不是如此,實際上訪問特定類型的變量只能在特定的地址訪問,這就需要各個變量在空間上按照一定的規則排列而不是簡單的順序排列,這就是內存對齊

cpu雖然每次讀取的單位是字節,但是一次是讀取一個塊,所以有些硬件設備不允許放在內存的奇數位上
內存對齊是操作系統為了提高訪問內存的策略.操作系統在訪問內存的時候,每次讀取一定長度(這個長度是操作系統的對齊數,或者默認對齊數的整數倍).如果沒有對齊,為了訪問一個變量可能產生二次訪問
* 提高存取數據的速度.比如有的平臺每次都是從偶地址讀取數據,對于一個int地址的變量.若從偶地址單元處存放,則需一個讀取周期即可讀取該變量,但是若從奇地址單元處存放,則需要2個讀取周期讀取變量
* 某些平臺只能在特定的地址訪問特定類型的數據,否則拋出硬件異常給操作系統
## 如何內存對齊
* 對于標準數據類型,它的地址只要是它的長度的整數倍
* 對于非標準類型,比如結構體,要遵循一下對齊原則
1. 數組成員對齊規則.第一個數組成員應該放在offset為0的地方,以后每個數組成員應該在offset為min(當前成員的大小, #paragama pack(n))整數倍的地方開始(比如int在32位機器為4字節, #paragam pack(2),那么從2的倍數地方開始存儲)
2. 結構體總的大小,也就是sizeof的結果,必須是min(結構體內部最大成員, #paragama pack(n))的整數倍,不足要補齊
3. 結構體作為成員的對齊規則,如果一個結構體B里嵌套了另一個結構體A,還是以最大成員類型的大小對齊,但是結構體A的起點為A內部最大成員的整數倍地方(struct B里有struct A,A里有char,int,double等成員,那A應該從8的整數倍開始存儲),結構體A中的成員對齊規則仍滿足原則1和原則2
## 手動對齊模式
~~~
#pragma pack(show)
顯示當前packing alignment的字節數,以warning message的形式被顯示
#pragma pack(push)
將當前指定的packing alignment數組進行壓棧操作,這里的棧是the internal compiler stack,同時設置當前的packing alignment為n,如果n沒有指定,則將當前的packing alignment數組壓棧
#pragma pack(pop)
從internal compiler stack中刪除最頂端的reaord,如果沒有指定n,則當前棧頂record即為新的packing alignement數值.如果指定了n,則n成為新的packing alignment值
#pragma pack(n)
指定packing的數值,以字節為單位,缺省數值是8,合法數值分別是1,2,4,8,16
~~~
## 為何需要對齊
計算機內存是以字節(Byte)為單位劃分的,理論上CPU可以訪問任意編號的字節,但實際情況并非如此。
CPU 通過地址總線來訪問內存,一次能處理幾個字節的數據,就命令地址總線讀取幾個字節的數據。32 位的 CPU 一次可以處理4個字節的數據,那么每次就從內存讀取4個字節的數據;少了浪費主頻,多了沒有用。64位的處理器也是這個道理,每次讀取8個字節。
以32位的CPU為例,實際尋址的步長為4個字節,也就是只對編號為 4 的倍數的內存尋址,例如 0、4、8、12、1000 等,而不會對編號為 1、3、11、1001 的內存尋址。如下圖所示:

樣做可以以最快的速度尋址:不遺漏一個字節,也不重復對一個字節尋址。
對于程序來說,一個變量最好位于一個尋址步長的范圍內,這樣一次就可以讀取到變量的值;如果跨步長存儲,就需要讀取兩次,然后再拼接數據,效率顯然降低了。
例如一個 int 類型的數據,如果地址為 8,那么很好辦,對編號為 8 的內存尋址一次就可以。如果編號為 10,就比較麻煩,CPU需要先對編號為 8 的內存尋址,讀取4個字節,得到該數據的前半部分,然后再對編號為 12 的內存尋址,讀取4個字節,得到該數據的后半部分,再將這兩部分拼接起來,才能取得數據的值。
將一個數據盡量放在一個步長之內,避免跨步長存儲,這稱為內存對齊。在32位編譯模式下,默認以4字節對齊;在64位編譯模式下,默認以8字節對齊。
為了提高存取效率,編譯器會自動進行內存對齊
~~~
#include <stdio.h>
#include <stdlib.h>
struct{
int a;
char b;
int c;
}t={ 10, 'C', 20 };
int main(){
printf("length: %d\n", sizeof(t));
printf("&a: %X\n&b: %X\n&c: %X\n", &t.a, &t.b, &t.c);
system("pause");
return 0;
}
~~~
在32位編譯模式下的運行結果:
~~~
length: 12
&a: B69030
&b: B69034
&c: B69038
~~~
如果不考慮內存對齊,結構體變量 t 所占內存應該為 4+1+4 = 9 個字節。考慮到內存對齊,雖然成員 b 只占用1個字節,但它所在的尋址步長內還剩下 3 個字節的空間,放不下一個 int 型的變量了,所以要把成員 c 放到下一個尋址步長。剩下的這3個字節,作為內存填充浪費掉了。請看下圖:

編譯器之所以要內存對齊,是為了更加高效的存取成員 c,而代價就是浪費了3個字節的空間。
除了結構體,變量也會進行內存對齊,請看下面的代碼:
~~~
#include <stdio.h>
#include <stdlib.h>
int m;
char c;
int n;
int main(){
printf("&m: %X\n&c: %X\n&n: %X\n", &m, &c, &n);
system("pause");
return 0;
}
~~~
在VS下運行:
~~~
&m: DE3384
&c: DE338C
&n: DE3388
~~~
可見它們的地址都是4的整數倍,并相互挨著。
經過筆者測試,對于全局變量,GCC在 Debug 和 Release 模式下都會進行內存對齊,而VS只有在 Release 模式下才會進行對齊。而對于局部變量,GCC和VS都不會進行對齊,不管是Debug模式還是Release模式。
# 內存大小端對齊
我們看到上面的十六進制的63,二進制是0110 0011.
那么二進制數據是
~~~
0000 0000 0000 0000 0000 0000 0110 0011
~~~
因為int是4個字節,1個字節等于8位.
4位一組.
但是內存是16進制表示每8位對應一個16進制
~~~
00 00 00 63
~~~
內存大小端對齊
~~~
63 00 00 00
~~~
數組在內存是連續的
后面是0a,十六進制對應十進制是10
正好是下個元素
如果你想在內存地址中看到下個元素,就把那地址+4,注意是16進制表示,結果就是下個元素了
大端和小端是指數據在內存中的存儲模式,它由 CPU 決定:
1. 大端模式(Big-endian)是指將數據的低位(比如 1234 中的 34 就是低位)放在內存的高地址上,而數據的高位(比如 1234 中的 12 就是高位)放在內存的低地址上。這種存儲模式有點兒類似于把數據當作字符串順序處理,地址由小到大增加,而數據從高位往低位存放。
2. 小端模式(Little-endian)是指將數據的低位放在內存的低地址上,而數據的高位放在內存的高地址上。這種存儲模式將地址的高低和數據的大小結合起來,高地址存放數值較大的部分,低地址存放數值較小的部分,這和我們的思維習慣是一致,比較容易理解
## 為什么有大小端模式之分
計算機中的數據是以字節(Byte)為單位存儲的,每個字節都有不同的地址。現代 CPU 的位數(可以理解為一次能處理的數據的位數)都超過了 8 位(一個字節),PC機、服務器的 CPU 基本都是 64 位的,嵌入式系統或單片機系統仍然在使用 32 位和 16 位的 CPU。
對于一次能處理多個字節的CPU,必然存在著如何安排多個字節的問題,也就是大端和小端模式。以 int 類型的?0x12345678 為例,它占用 4 個字節,如果是小端模式(Little-endian),那么在內存中的分布情況為(假設從地址 0x 4000 開始存放):

如果是大端模式(Big-endian),那么分布情況正好相反:

我們的 PC 機上使用的是 X86 結構的 CPU,它是小端模式;51 單片機是大端模式;很多 ARM、DSP 也是小端模式(部分 ARM 處理器還可以由硬件來選擇是大端模式還是小端模式)。
借助共用體,我們可以檢測 CPU 是大端模式還是小端模式,請看代碼:
~~~
#include <stdio.h>
int main(){
union{
int n;
char ch;
} data;
data.n = 0x00000001; //也可以直接寫作 data.n = 1;
if(data.ch == 1){
printf("Little-endian\n");
}else{
printf("Big-endian\n");
}
return 0;
}
~~~
在PC機上的運行結果:
Little-endian
共用體的各個成員是共用一段內存的。1 是數據的低位,如果 1 被存儲在 data 的低字節,就是小端模式,這個時候 data.ch 的值也是 1。如果 1 被存儲在 data 的高字節,就是大端模式,這個時候 data.ch 的值就是 0。
- c語言
- 基礎知識
- 變量和常量
- 宏定義和預處理
- 隨機數
- register變量
- errno全局變量
- 靜態變量
- 類型
- 數組
- 類型轉換
- vs中c4996錯誤
- 數據類型和長度
- 二進制數,八進制數和十六進制數
- 位域
- typedef定義類型
- 函數和編譯
- 函數調用慣例
- 函數進棧和出棧
- 函數
- 編譯
- sizeof
- main函數接收參數
- 宏函數
- 目標文件和可執行文件有什么
- 強符號和弱符號
- 什么是鏈接
- 符號
- 強引用和弱引用
- 字符串處理函數
- sscanf
- 查找子字符串
- 字符串指針
- qt
- MFC
- 指針
- 簡介
- 指針詳解
- 案例
- 指針數組
- 偏移量
- 間接賦值
- 易錯點
- 二級指針
- 結構體指針
- 字節對齊
- 函數指針
- 指針例子
- main接收用戶輸入
- 內存布局
- 內存分區
- 空間開辟和釋放
- 堆空間操作字符串
- 內存處理函數
- 內存分頁
- 內存模型
- 棧
- 棧溢出攻擊
- 內存泄露
- 大小端存儲法
- 寄存器
- 結構體
- 共用體
- 枚舉
- 文件操作
- 文件到底是什么
- 文件打開和關閉
- 文件的順序讀寫
- 文件的隨機讀寫
- 文件復制
- FILE和緩沖區
- 文件大小
- 插入,刪除,更改文件內容
- typeid
- 內部鏈接和外部鏈接
- 動態庫
- 調試器
- 調試的概念
- vs調試
- 多文件編程
- extern關鍵字
- 頭文件規范
- 標準庫以及標準頭文件
- 頭文件只包含一次
- static
- 多線程
- 簡介
- 創建線程threads.h
- 創建線程pthread
- gdb
- 簡介
- mac使用gdb
- setjump和longjump
- 零拷貝
- gc
- 調試器原理
- c++
- c++簡介
- c++對c的擴展
- ::作用域運算符
- 名字控制
- cpp對c的增強
- const
- 變量定義數組
- 盡量以const替換#define
- 引用
- 內聯函數
- 函數默認參數
- 函數占位參數
- 函數重載
- extern "C"
- 類和對象
- 類封裝
- 構造和析構
- 深淺拷貝
- explicit關鍵字
- 動態對象創建
- 靜態成員
- 對象模型
- this
- 友元
- 單例
- 繼承
- 多態
- 運算符重載
- 賦值重載
- 指針運算符(*,->)重載
- 前置和后置++
- 左移<<運算符重載
- 函數調用符重載
- 總結
- bool重載
- 模板
- 簡介
- 普通函數和模板函數調用
- 模板的局限性
- 類模板
- 復數的模板類
- 類模板作為參數
- 類模板繼承
- 類模板類內和類外實現
- 類模板和友元函數
- 類模板實現數組
- 類型轉換
- 異常
- 異常基本語法
- 異常的接口聲明
- 異常的棧解旋
- 異常的多態
- 標準異常庫
- 自定義異常
- io
- 流的概念和類庫結構
- 標準io流
- 標準輸入流
- 標準輸出流
- 文件讀寫
- STL
- 簡介
- string容器
- vector容器
- deque容器
- stack容器
- queue容器
- list容器
- set/multiset容器
- map/multimap容器
- pair對組
- 深淺拷貝問題
- 使用時機
- 常用算法
- 函數對象
- 謂詞
- 內建函數對象
- 函數對象適配器
- 空間適配器
- 常用遍歷算法
- 查找算法
- 排序算法
- 拷貝和替換算法
- 算術生成算法
- 集合算法
- gcc
- GDB
- makefile
- visualstudio
- VisualAssistX
- 各種插件
- utf8編碼
- 制作安裝項目
- 編譯模式
- 內存對齊
- 快捷鍵
- 自動補全
- 查看c++類內存布局
- FFmpeg
- ffmpeg架構
- 命令的基本格式
- 分解與復用
- 處理原始數據
- 錄屏和音
- 濾鏡
- 水印
- 音視頻的拼接與裁剪
- 視頻圖片轉換
- 直播
- ffplay
- 常見問題
- 多媒體文件處理
- ffmpeg代碼結構
- 日志系統
- 處理流數據
- linux
- 系統調用
- 常用IO函數
- 文件操作函數
- 文件描述符復制
- 目錄相關操作
- 時間相關函數
- 進程
- valgrind
- 進程通信
- 信號
- 信號產生函數
- 信號集
- 信號捕捉
- SIGCHLD信號
- 不可重入函數和可重入函數
- 進程組
- 會話
- 守護進程
- 線程
- 線程屬性
- 互斥鎖
- 讀寫鎖
- 條件變量
- 信號量
- 網絡
- 分層模型
- 協議格式
- TCP協議
- socket
- socket概念
- 網絡字節序
- ip地址轉換函數
- sockaddr數據結構
- 網絡套接字函數
- socket模型創建流程圖
- socket函數
- bind函數
- listen函數
- accept函數
- connect函數
- C/S模型-TCP
- 出錯處理封裝函數
- 多進程并發服務器
- 多線程并發服務器
- 多路I/O復用服務器
- select
- poll
- epoll
- epoll事件
- epoll例子
- epoll反應堆思想
- udp
- socket IPC(本地套接字domain)
- 其他常用函數
- libevent
- libevent簡介