C 語言的編譯器有一個內建的巨集 sizeof() 可以用來取得配置給變數的記憶體大小. 例如:

    uint32_t varX = 1234;
    int      size;

    size = sizeof(varX);

這樣變數 size 所存儲的數值就會是變數 varX 到底在電腦裡佔用了多大的記憶體. uint32_t 型態的變數 (沒有意外的話) 應該都會佔用 4 bytes.

不過對於基本資料型態, sizeof() 基本上並沒有多大用途. 因為在一個系統上 (不論是大型主機, PC, 手機, 平板或是 embedded) 大部份的基本資料型態會佔用多大空間應該是程式師在該系統上撰寫 C 程式時最基本要知道的東西.

基本資料型態中唯一會有變化的是 int 的大小到底是 2 bytes 還是 4 bytes 或者...? 不過這個問題只會在移植舊程式時才會出現. 舊的 CPU 用的 C 編譯器和現下要用的 CPU 的 C 編譯器預設值不同, 才會出現這樣的困擾. 有經驗的工程師 (尤其是有過系統移植經驗的 embedded 工程師), 現在應該都知道要用 int16_t, uint16_t 或者是 int32_t, uint32_t 這類明確大小的資料型態, 來避免這一類問題再出現.

其實 sizeof() 真正好用的地方在於計算下列幾種資料的大小

  • 結構 struct
  • 陣列 array
  • 固定的字串 string literal

結構 struct


對於計算結構 (struct) 所佔的空間大小, 一般會有問題的點是: 不同大小的資料型態的結構成員之間會不會有補空 (padding) 出現? C 編譯器一般都是依據 CPU 的位元數作為預設的 padding 大小. 像 16 位元 CPU 用的 C 編譯器, 預設會在每一個結構成員之間加 padding 成為 2 bytes 的倍數; 32 位元 CPU 用的 C 編譯器, 則預設會 padding 成 4 bytes 的倍數; 當然如果是現下流行的 64位元 x86 CPU 用的 C 編譯器, 則預設會 padding 成 8 bytes 的倍數.

大部份狀況下, 到底有沒有 padding, 或者 padding 成多少的倍數基本上並沒有多大關係, 頂多就浪費一些記憶 (不過 embedded system 記憶體有限, 還是節制一點的好). 但是如果這個結構 (struct) 是用來對應傳輸程式用的資料結構 (data structure), 那問題就大了. 因為如果對方使用了不同的 CPU, 或是用了不同的 C 編譯器, 對我們的結構 (struct) 作了不一致的 padding, 那...結構裡的資料就會全都位移了. 因此傳輸程式用的結構 (struct) 我們一般會使用下列的技巧來避免不一致的 padding:

  • 定義結構 (struct) 時強制修改為程式原本所需要的 padding (一般是不要 padding).
    一般 C 編譯器都會支援 #pragma pack(push, 1)#pragma pack(pop) 或是類似的語法讓使用者改變 C 編譯器預設的 padding.
  • 調整結構 (struct) 的定義.
    例如: 將長度是 4 bytes (或是 4 bytes 倍數) 的成員往結構的前面排, 再接著排放長度是 2 bytes (或是 2 bytes 倍數) 的成員, 最後才是長度是 1 byte (或是奇數) 的成員. 這樣可以確保 4 bytes 的成員變數在記憶體中都攞放在 4 bytes 的邊界上, 而 2 bytes 的成員變數在記憶體中都攞放在 2 bytes 的邊界上, 同時所需要的 padding 也最小.

註一: 一般 16 位元的 CPU 會以 2 bytes (16 bits) 為單位來存取記憶體. 32 位元的 CPU 則是以 4 bytes (32 bits) 為單位來存取記憶體, 64 位元的 CPU 則是以 8 bytes (64 bits) 為單位來存取記憶體.

當配置地址不在邊界上時, CPU 得分二次存取才能拼湊出完整的資料, 並且需要額外的電路來完成這個動作. 因此, 有些 CPU 是可以存取不在邊界上的資料, 但是速度會慢下來 (像是 x86 CPU); 有些 CPU 則是無法存取, 像 ARM7, ARM9 是直接產生 hard fault.

這是為什麼會不同 CPU 的 C 編譯器, 其預設的 padding 值不同的原因. 也是為什麼變數需要依照它的大小配置在正確的記憶體地址邊界上.

例如: uint16_t 的大小是 2 bytes 必需配置在 2 bytes 的邊界上; uint32_t 的大小是 4 bytes 必需配置在 4 bytes 的邊界上. 以 32 位元的 CPU 來說 uint32_t 的變數必需安置在地址未碼 0x0, 0x4, 0x8, 0xc 的地址上; 而不可以是 0x1, 0x2, 0x3, 0x5... 等等.

註二: 曾經見過一篇有關 struct 大小的文章, C/C+语言struct深层探索 裡面第2大段對於 struct 的成員對齊的說明及 microsoft, intel 的考題解說有部份關念, 到了現今都錯了.

首先所謂自然對界的部份, 現今的 C/C++ compiler 並不會這麼複雜, 只有一個組預設的 alignment 設定.

而考題的答案: 如果你用現下的 Ms Visual Studio 來實作的話就全錯了. (那個年代 microsoft x86 CPU 的 C/C++ compiler 預設的 padding 大小還是 4 bytes, 而且 long 的大小還是 4 bytes).

所以請小心, 在網路上看的東西要注意年代同時也要自己加以驗證.

陣列 array


sizeof() 用在陣列變數上, 我們可以取得整個陣列所佔用的記憶體大小. 例如:

    int arrayX[7] = { 1, 2, 3, 4, 5, 6, 7 };
    int size;

    size = sizeof(arrayX);

在 32 位元的 CPU 上, 我們應該是取得數值 28. 這裡要注意的是: 因為我們在前面指定要 7 個元素, 所以 C 編譯器為變數 arrayX 配置了 7 個 int 所需要的空間. 因此不論後面大括號中設定元素數值的個數有沒有填滿 7 個, sizeof(arrayX) 取得的大小都會是 28.

sizeof() 其實真正好用的地方在於我們沒有明確指定的元素個數的陣列上.

    int arrayY[] = { 1, 2, 3, 4, 5, 6 };
    int size;

    size = sizeof(arrayY);

在 32 位元的 CPU 上, 我們應該是取得數值 24. 這裡要注意的是: 因為我們在前面沒有指定變數 arrayY 到底要放多少個元素, 所以 C 編譯器依據後面大括號中設定元素數值的個數 (6 個) 來為變數 arrayY 配置 6 個 int 所需要的空間 24 byte. 這樣子, 變數 arrayY 的元素個數需要改變時, 我們就只需要補充或者刪除後面大括號中的設定數值就可以了.

但是, 大部份狀況下, 我們需要的是陣列的元素個數, 而不是陣列到底佔用多少記憶體. 要取得陣列的元素個數我們只需要把整個陣列佔用的記憶體數除以一個元素所佔用的記憶體數即可. 例如:

    int arrayY[] = { 1, 2, 3, 4, 5, 6 };
    int memSize, elemCnt;

    memSize = sizeof(arrayY);
    elemCnt = sizeof(arrayY)/sizeof(int);

上面的例子中, 變數 elemCnt 的數值會是 6. 這個例子主要是告訴大家 sizeof() 括號裡面除了可以放變數, 也可以放資料型態 (包含指標, 結構 struct 以及我們使用 typedef 定義出來的各種衍生資料型態).

不過, 利用上面例子中的寫法來計算陣列元素個數, 有時會有點小問題: 萬一變數 arrayY 的資料型態變更了 (例如: 原本是 int, 現在因為擴充內容, 必需改成用 struct), 我們就必需要記得修改後面的 sizeof(int)int 改成正確的資料型態. 萬一忘了修改或者是打錯了, C 編譯器可不會對我們提出警告. 這一點對於一些大型專案的應用實在不利. 所以我們需要改一下寫法:

    int arrayY[] = { 1, 2, 3, 4, 5, 6 };
    int memSize, elemCnt;

    memSize = sizeof(arrayY);
    elemCnt = sizeof(arrayY)/sizeof(arrayY[0]);

這樣子, 不論變數 arrayY 的資料型態是什麼 (或者改成什麼) 都不會出錯了. 因此我們可以把它定義成一個巨集, 需要時直接使用:

#define SIZEOF_ARRAY(x)    (sizeof(x)/sizeof(x[0]))

固定的字串 string literal


當然, sizeof() 也可以取得程式中的固定字串所占用的記憶大小, 例如:

    char strX[] = "0123456789";
    int sizeX;

    sizeX = sizeof(strX);

上面的例子, 變數 sizeX 的數值是 11.

啊! 怎麼會這樣? 不對吧! 數字字元 0~9 不是只有 10 個字元嗎? 變數 sizeX 怎麼是 11 呢?

這是因為 sizeof() 計算的是存放變數所需要的空間, 而 C 語言的字串後面必需補一個 null 字元來表示字串結束. 因此存放字串的空間會比字串的字數再多加 1.

指標 pointer


接下來我們要探討一下指標變數和 sizeof() 的關係. 先來看一下有點小機車的例子:

    char strX[] = "0123456789";
    char * strY = "0123456789";
    char * strZ;
    int sizeX, sizeY, slenY, sizeZ;

    sizeX = sizeof(strX);
    sizeY = sizeof(strY);
    slenY = strlen(strY);
    sizeZ = sizeof(strZ);

在 32 位元的 CPU 上, 變數 sizeX 的數值是 11; 而變數 sizeYsizeZ 的數值都是 4; 變數 slenY 則是 10.

前面已經解釋過變數 sizeX 的問題了, 不再多說. 但是有人或許會有疑問: 變數 strY 不就只是變數 strX 換一個寫法而已嗎?

答案是: 真的不是換一個寫法而已. 變數 strX 是一個陣列. 變數 strY 則是一個指向字元的指標. 在 32 位元的 CPU 上指標通常是佔用 4 bytes 記憶體. 所以變數 sizeYsizeZ 的數值都是 4.

如果我們另外定義一個字元變數 ch, 然後用 ch = strX[5]; 這一行程式取出字串中的第 6 個字元, C 編譯器輸出的機器碼程式應該是 "由一個固定地址取出字元". 相對 ch = strY[5]; 這一行程式輸出的機器碼程式則應該是 "由變數 strY 取出字串的地址值加 5 之後, 再以答案當作地址來取出字元". 所以二種寫法編譯出來的程式大小不同, 執行速度也是不同.

補充說明


有一點要注意的是 sizeof() 是一個巨集, C 編譯器會換算成正確的數值填充在程式中, 而不是程式執行的時候才去計算結果.

另外, strlen() 雖然是 C 的標準函數, 但是現代的 C 編譯器也會把固定字串的 strlen() 直接換算成正確的長度數值填充在程式中, 而不是程式執行的時候才去計算結果.

文章標籤
創作者介紹
創作者 MagicJackTing 的頭像
MagicJackTing

傑克! 真是太神奇了!

MagicJackTing 發表在 痞客邦 留言(0) 人氣()