這篇文章續 [HFC] Hidden Features of User Defined Type in C 。

union 說穿了其實沒什麼好 Hidden Feature 的,只是一般在寫 code 時,較高階部份大概沒什麼機會看到這個,寫較低階常和 struct 合用。早期使用 union 其中一項原因為,在只有幾百k記憶體的時代,它是拿來省記憶體的技巧之一。

對於 union 特性不熟,請先找資料補充。另這篇文章筆者沒再翻過哪些已不被標準接納。

由於筆者在書上較少看到 union 範例,書本大多是提一下就帶過,此文敘述方式以範例較多。

由於筆者對語法分析不擅長,故這部份的範例便不再提及。

 

附帶一提,C++ union 也可以拿來宣告 class 。

 

8bits 記憶體完成九九乘法表

 

想一下一般99乘法表怎麼寫。

 

Code Snippet
  1. for(int i=2; i<=9; ++i){
  2.     for(int j=1; j<=9; ++j)
  3.         printf("%d *%d = %2d\n", i, j, i*j);
  4.     puts("");
  5. }

 

上面的 i, j 為 int,若 sizeof(int) 為 4bytes,共吃了 8 bytes (32bits)。但事實上 i, j 都只有跑到 9 ,用 4 bits 就可完成。

這題目大致有兩種方向,一種是用bitwise hacker,另一是用 struct 之 bit field 特性。以 bitwise 方式,是宣告一個 unsigned char 出來,前 4 bits 存 i ,後 4 bits 存 j ,所以要判斷大小、要做加法,都要先以取出前4或後4 bits 之值,這種作法顯然較花時間刻程式碼,不附上。

另一種使用 struct bit field 方式如下。

 

Code Snippet
  1. struct {
  2.     unsigned char i:4;
  3.     unsigned char j:4;
  4. }var;
  5.  
  6. for(var.i=2; var.i<=9; ++var.i){
  7.     for(var.j=1; var.j<=9; ++var.j)
  8.         printf("%d * %d = %2d\n", var.i, var.j, var.i*var.j);
  9.     puts("");
  10. }

 

直接造一個 struct 出來,裡面兩個變數都指定 4 bits,直接對它們做為運算即可。

 

bit pattern

 

若對於一個數,需要常拿查第 n bit 為 1/0 時,可以這麼做

 

Code Snippet
  1. int GetBit(unsigned char x, size_t idx)
  2. {
  3.     return ( x & (1<<idx) )!=0;
  4. }

 

另一種方式是 union 裡再包 struct 

 

Code Snippet
  1. union U8{
  2.     unsigned char val;
  3.     struct{unsignedchar b0:1, b1:1, b2:1, b3:1;,b4:1, b5:1, b6:1, b7:1;};
  4. };
  5. void print_binary(union U8 var)
  6. {
  7.     printf("%d%d%d%d%d%d%d%d\n",
  8.         var.b7,var.b6,var.b5,var.b4,
  9.         var.b3,var.b2,var.b1,var.b0);
  10. }
  11. int main()
  12. {    
  13.     union U8 var;
  14.     var.val = 10;
  15.     print_binary(var); // display 00001010
  16.     return 0;
  17. }

 

< 寫到這裡讓人有點痛恨為什麼 printf 沒有 %b ... >

上面這例舉得很差,原因是若用 GetBit(x, idx) 時, idx 可以放一變數,自然就可以跑回圈,如

 

Code Snippet
  1. for(int idx=0; idx<8; ++idx)
  2.     printf("%d", GetBit(x, idx));

 

甚至在一些情況下,idx 是必須事先指定算好代入的,這點 struct bit field 辦不到,因 bit field 一項重要的限制為,它不能寫成陣列型式,即

 

Code Snippet
  1. union U8{
  2.     unsigned char val;
  3.     struct {unsigned char b[8]:1;};
  4. };

 

上述這段程式碼敘述是錯誤的。沒有 array 可用,只有 8 bits 還可以像上面這樣硬爆,如果 32 bits 呢?若以這種設計模式,的確是寫到 unsigned int b31 : 1 可能性較高,而不會用 4 個 union U8 。使用 4 個 union U8 沒有 1 個 U32 存取來得簡便,除非有必要知道 BYTE0、BYTE1、BYTE2、BYTE3 裡面的第幾個 bits 。

 

 Print Type

 

早期一段較有名教學用的 code  , enum , struct , union 都用上了,它的作用和 C++ cout 頗相似,使用這段 code 不必在意資料型態為何,便直接做輸出。由於筆者認為是罕見讓人驚艷的範例,故放上較完整之程式碼。

首先定義一些資料型態。

 

Code Snippet
  1. typedef enum { INTEGER, POINTER } Type;

 

再做一份 struct 包 union

 

Code Snippet
  1. typedef struct
  2. {
  3.     Type type;
  4.     union {
  5.         int integer;
  6.         void *pointer;
  7.     } ;
  8. } Value;

 

再來是針對各資料型態做相對應的 function。

 

Code Snippet
  1. Value make_val_int(int x) {
  2.     Value v={INTEGER};
  3.     v.integer = x;
  4.     return v;
  5. }
  6. Value make_val_ptr(void * ptr)
  7. {
  8.     Value v={POINTER};
  9.     v.pointer = ptr;
  10.     return v;
  11. }

 

做完 new function 後再做一份 print function。

 

Code Snippet
  1. void PrintVal(Value v)
  2. {
  3.     switch(v.type){
  4.     case INTEGER: printf("%d\n", v.integer); break;
  5.     case POINTER: printf("%p\n", v.pointer); break;
  6.     }
  7. }

 

調用時

 

Code Snippet
  1. Value var;
  2. var = make_val_int(10);
  3. PrintVal(var);
  4. var = make_val_ptr(&var);
  5. PrintVal(var);

 

這麼做優點是,一份 8 bytes 記憶體 ( struct 大小 ) 可供多種不同資料型態使用,但相對的那些 make_val_xxx 與 PrintVal 必須自己動手刻。上面這技巧在較高階之程式語言(如 vb、autoit ),有「自動資料型態」( automation variant) 的也在用, OLE 、COM 也如此。

 

INPUT struct

 

另一種較佳的範例,大概屬 M$ 對 struct Input 之定義。

 

Code Snippet
  1. typedef struct tagINPUT {
  2.     enum {INPUT_MOUSE, INPUT_KEYBOARD, INPUT_HARDWARE}Type;
  3.     union {
  4.         MOUSEINPUT    mi;
  5.         KEYBDINPUT    ki;
  6.         HARDWAREINPUT hi;
  7.     };
  8. } INPUT, *PINPUT;

 

 其中 MOUSEINPUTKEYBDINPUTHARDWAREINPUT 三個又是各自之 struct。相對的若要存取一系列的 Input 操作,可考慮這麼做。

 

Code Snippet
  1. typedef struct {
  2.     enum {EventKeyPress, EventKeyRelease, 
  3.           EventMousePress, EventMouseRelease} EvenType;
  4.     union{
  5.         unsigned int KeyCode; // use for EventKeyxxx
  6.         struct { // use for EventMousexxx
  7.             int x, y;
  8.             unsigned ButtonCode;
  9.         };
  10.     };
  11. }InputEvent;

 

IEEE754 欄位分析

 

提醒一下,這功能可用 frexpldexp 完成。

union 、struct 拿來做浮點數欄位分析是件非常適合的事。首先,先寫一個從 10 進位無號數轉成 2 進位字串之副函式,同時具有指定寬度之功能。

 

Code Snippet
  1. char * to_binary(char * dst, uint32_t x, size_t width)
  2. {
  3.     uint32_t mask = 1U << (width-1);
  4.     size_t i=0;
  5.     while(mask){
  6.         dst[i] = '0' + ( (x&mask)!=0 );
  7.         ++i;
  8.         mask>>=1;
  9.     }
  10.     dst[i] = 0;
  11.     return dst;
  12. }

 

再來定義 union 及其 struct 欄位,欄位部份可參考 wiki

 

Code Snippet
  1. typedef union tagFloat{
  2.     float    val;
  3.     uint32_t hex;
  4.     struct {
  5.         uint32_t mantissa  :23; // bit[0:22]
  6.         uint32_t exponent  : 8; // bit[23:30]
  7.         uint32_t sign      : 1; // bit[31]
  8.     };
  9. }Float;

 

接下來就沒什麼技巧了,放上測試程式碼。

 

Code Snippet
  1. int main()
  2. {
  3.     char str_exp[30], str_man[30];
  4.     Float f;
  5.     f.val = -1.5;
  6.  
  7.     to_binary(str_exp, f.exponent, 8);
  8.     to_binary(str_man, f.mantissa, 23);
  9.     
  10.     printf("Dec         : %f\n", f.val);
  11.     printf("Hex         : %08x\n", f.hex);
  12.     printf("Sign        : %d\n", f.sign);
  13.     printf("Exponent    : %08x < %s > \n", f.exponent, str_exp);
  14.     printf("Mantissa    : %08x < %s > \n", f.mantissa, str_man);
  15.  
  16.     return 0;
  17. }

 

要做 bit 設定也行, 在 union 裡面可以額外再加上另一個 no-name struct ,  struct field bit0~bit31 : 1 < 如果願意這麼做的話..>。

32 bits 會了,自然 64 bits 之 double 也不成問題。

 

Endian

 

以上述之 IEEE754 而言,在 wiki 之說明是 sign 於第 31 bit, exponent 為 30~23 bit,但上述之例子是 mantissa 寫最前面、exponent 次之、sign 最後,原因是假設 client 端電腦為little endian。

一般用到 union,必須考慮到位元的「次序」關係時,就該考慮到 little endian / big endian 之不同。要判斷 big/little 方法很多,但注意到的,要判斷的話必須使用 macro,而不能使用 function。因 struct 之定義必須在編譯期前完成,故較建議使用 macro 完成。

解決這種問題第一種方式是直接由使用者「額外定義」,大概長這樣。

 

 

為考慮 big / endian ,下面這段供參考 < 手邊沒 big endian 機器可供驗證 >

 

Code Snippet
  1. #ifdef BIG_ENDIAN // for big endian define struct
  2. typedef union tagFloat{
  3.     float    val;
  4.     uint32_t hex;
  5.     struct {
  6.         uint32_t sign      : 1;
  7.         uint32_t exponent  : 8;
  8.         uint32_t mantissa  :23;
  9.     };
  10. }Float;
  11. #else // for little endian define struct
  12. typedef union tagFloat{
  13.     float    val;
  14.     uint32_t hex;
  15.     struct {
  16.         uint32_t mantissa  :23;
  17.         uint32_t exponent  : 8;
  18.         uint32_t sign      : 1;
  19.     };
  20. }Float
  21. #endif

 

另一種是由程式自動判斷,下面這段 code 供參考。

 

Code Snippet
  1. #include <stdint.h>
  2.  
  3. #define LITTLE_ENDIAN 0x41424344UL
  4. #define BIG_ENDIAN    0x44434241UL
  5. #define PDP_ENDIAN    0x42414443UL
  6. #define ENDIAN_ORDER  ('ABCD')
  7.  
  8. #if ENDIAN_ORDER==LITTLE_ENDIAN
  9. typedef union tagFloat{
  10.     float    val;
  11.     uint32_t hex;
  12.     struct {
  13.         uint32_t mantissa  :23;
  14.         uint32_t exponent  : 8;
  15.         uint32_t sign      : 1;
  16.     };
  17. }Float;
  18. #elif ENDIAN_ORDER==BIG_ENDIAN
  19. typedef union tagFloat{
  20.     float    val;
  21.     uint32_t hex;
  22.     struct {
  23.         uint32_t sign      : 1;
  24.         uint32_t exponent  : 8;
  25.         uint32_t mantissa  :23;
  26.     };
  27. }Float;
  28. #elif ENDIAN_ORDER==PDP_ENDIAN
  29. /* other define */
  30. #else
  31. /* other define */
  32. #endif

 

小結

 

(1) 在簡單的 POD 情況下,union 大多可用 pointer 與 bitwise 代替其操作,且速度通常比 union 還快。

(2) 在較複雜之 (資料型態結構) 上,為程式碼維護方便,較常使用 struct + union 做維護。

(3) 狀態機、語法分析,這兩個議題都可用 struct + union 完成 ( 也不只這兩種方式) ,於此不再示範。

(4) union 、struct,由於有 endian 問題與 padding 問題,使用上要特別小心,"hack" 就別做了。

(5) union 說穿了,是定義一份資料型態,可能有不同種性質,將這些性質打包起來罷了。

 

Reference

 

[1] IEEE754 in Wiki ( 繁中 )

[2] stack overflow - C macro definition to determine big endian or little endian machine

 

 

 

 

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