關於宏定義 Bin(n),LongToBin(n),LongToBin(0x##n##L)

eazpeng發表於2024-07-14

注:挖墳找到了 2013-12-25 在百度知道回答的這個問題,當時的賬號 Estrivr 丟失了,再將此內容轉載到此處,心血來潮再最佳化下解答。
連結1:https://zhidao.baidu.com/question/424150510.html?qbl=relate_question_0&word=%23define LongToBin(n)&dyTabStr=MCwxLDMsMiw1LDYsNCw3LDgsOQ%3D%3D
連結2:https://zhidao.baidu.com/question/1830121487019039060.html?fr=wwwt&word=%23define+LongToBin(n)&dyTabStr=MCwxLDMsMiw1LDYsNCw3LDgsOQ==

這個問題比較基礎,那時候人也比較青澀,所以描述有些生硬、偏差,以下為最佳化後的解答——

具體宏定義程式碼為(單引數宏定義),

  #define LongToBin(n)        \              // DEF 1
  (                           \
  ((n >> 21) & 0x80) |        \
  ((n >> 18) & 0x40) |        \
  ((n >> 15) & 0x20) |        \
  ((n >> 12) & 0x10) |        \
  ((n >>  9) & 0x08) |        \
  ((n >>  6) & 0x04) |        \
  ((n >>  3) & 0x02) |        \
  ((n      ) & 0x01)          \
  )
  
  #define Bin(n) LongToBin(0x##n##L)          // DEF 2
  1. 在 C 語言中,宏定義有一個作用是為了簡化一些書寫、增強程式碼可讀性(格式:#define 識別符號[(引數1,.....,引數n)] 被識別符號代表的字串),在這種情況下,識別符號往往比被標識的字串要簡潔,用起來像函式,其與函式的區別在於:宏定義在編譯之前、也就是預編譯階段,會完成展開替換——即字元文字的替換(原始碼實際上就是字元文字的集合)。上面的這兩個宏,也會先展開到具體呼叫的位置,展開後的程式碼才會進行編譯。
  2. 再複雜的宏定義,只要將其逐步展開到具體的位置就好理解了,只是這裡需要說明一下涉及到的一些不多用的符號——
    • " \ ":由於 C 語言的書寫規範需求,為方便閱讀,當一行程式碼需要換行時(通常因為一行太長),可以在行尾加上 \,表示下一行內容也是屬於本行內容(稱為 續行)。而對於宏定義的格式,需要識別符號、被識別符號同屬一行,因此用該符號告知編譯器定義內容要多行結合起來,具體到以上 DEF 1,即等價於以下寫法——
      #define LongToBin(n) (((n>>21)&0x80)|((n>>18)&0x40)|((n>>15)&0x20)|((n>>12)&0x10)|(n>>9)&0x08)|((n>>6)&0x04)|((n>>3)&0x02)|(n&0x01)) // 沒有換行的寫法
      
    • " ## ":是宏定義中的字元(串)連線符,即,將符號兩端的字元(串)接為一個整體,如以上程式碼 DEF 2 中,在呼叫 Bin(n) 時,若 n=11111111,該宏會展開為 LongToBin(0x##11111111##L) ,去掉連線符 ## 進而變為 LongToBin(0x11111111L),即 Bin(11111111) 等價於 LongToBin(0x11111111L),同樣 Bin(11001001) 等價於 LongToBin(0x11001001L)當然,如果 n=AbcDE,那 Bin(AbcDE) 也就是 LongToBin(0xAbcDEL),從格式上也是允許的,就像 A 中所述,宏在預編譯階段展開,只要格式沒有問題,編譯前不會有錯誤(只是再透過 DEF 1LongToBin(0xAbcDEL) 展開後,得到的字元文字沒有任何運算意義,到編譯時則會報錯)。
  3. 兩個宏的作用/目的是:將 8 位的十進位制數(或不帶雙引號的 8 個字元的字串,如 11111111 )轉為十六進位制數(如 0xFF),這樣從書寫形式上,就能將有 8 個位的字元文字(看起來像一個 Byte 的二進位制形式)與十六進位制相對應,完全是為了書寫、閱讀的友好性。比如 Bin(11111111) => 0xFFBin(11001001) => 0xC9Bin(10001000) => 0x88,這樣就可以在寫程式碼時很方便地觀察一個位元組裡每個位的情況,展開後的值又是正確的。
  4. 基於 3 中的目標,再看具體的實現方案,
    • 先將 8 位十進位制數轉化為 long 型數(32 bits,如 11111111 轉為 0x11111111L ,寫成二進位制是 0b 0001 0001 0001 0001 0001 0001 0001 0001,如 11001001 轉為 0x11001001L ,寫成二進位制則是 0b 0001 0001 0000 0000 0001 0000 0000 0001),這便是程式碼中 DEF2 的作用,暫且稱這一層展開的數為 中間數
    • 然後將 long 型的中間數的每 半個位元組 (4 bits)的最低位(非 0 即 1),按對應的高低順序,整合到最低的單個位元組(8 bits)的對應位上。想要實現這種轉換,long 型由高到低的有效位(半位元組最低位)分別需要右移 21、18、15、12、9、6、3 和 0 個位置,再 位與0xFF 達到保值的效果( 0xFF=0x80|0x40|0x20|0x10|0x08|0x04|0x02|0x01位與 0xFF 也便是分別 位與 等號右側的各個值後再 位或 起來)。這便是程式碼中 DEF 1 的作用。
    • 呼叫時,先展開 DEF 2 再展開 DEF 1
  5. 宏展開示例(含數值計算),
    // 11001001 -> 0xC9
    Bin(11001001)
    => 
    LongToBin(0x11001001L)					// 第一層展開
    => 
    (										\	// 第二層展開
    ((0x11001001L >> 21) & 0x80) |		\	// & 右側數值,發生資料型別轉換,char->long
    ((0x11001001L >> 18) & 0x40) |		\
    ((0x11001001L >> 15) & 0x20) |		\
    ((0x11001001L >> 12) & 0x10) |		\
    ((0x11001001L >>  9) & 0x08) |		\
    ((0x11001001L >>  6) & 0x04) |		\
    ((0x11001001L >>  3) & 0x02) |		\
    ((0x11001001L >>  0) & 0x01)			\
    )
    =>										// 以下為數值計算
    (																										\
    ((0b 0001 0001 0000 0000 0001 0000 0000 0001L >> 21) & 0b 0000 0000 0000 0000 0000 0000 1000 0000) |	    \
    ((0b 0001 0001 0000 0000 0001 0000 0000 0001L >> 18) & 0b 0000 0000 0000 0000 0000 0000 0100 0000) |	    \
    ((0b 0001 0001 0000 0000 0001 0000 0000 0001L >> 15) & 0b 0000 0000 0000 0000 0000 0000 0010 0000) |	    \
    ((0b 0001 0001 0000 0000 0001 0000 0000 0001L >> 12) & 0b 0000 0000 0000 0000 0000 0000 0001 0000) |	    \
    ((0b 0001 0001 0000 0000 0001 0000 0000 0001L >>  9) & 0b 0000 0000 0000 0000 0000 0000 0000 1000) |	    \
    ((0b 0001 0001 0000 0000 0001 0000 0000 0001L >>  6) & 0b 0000 0000 0000 0000 0000 0000 0000 0100) |	    \
    ((0b 0001 0001 0000 0000 0001 0000 0000 0001L >>  3) & 0b 0000 0000 0000 0000 0000 0000 0000 0010) |	    \
    ((0b 0001 0001 0000 0000 0001 0000 0000 0001L >>  0) & 0b 0000 0000 0000 0000 0000 0000 0000 0001)		\
    )
    =>
    (																									\
    ((0b 0000 0000 0000 0000 0000 0000 1000 1000L) & 0b 0000 0000 0000 0000 0000 0000 1000 0000) |		\
    ((0b 0000 0000 0000 0000 0000 0100 0100 0000L) & 0b 0000 0000 0000 0000 0000 0000 0100 0000) |		\
    ((0b 0000 0000 0000 0000 0010 0010 0000 0000L) & 0b 0000 0000 0000 0000 0000 0000 0010 0000) |		\
    ((0b 0000 0000 0000 0001 0001 0000 0000 0001L) & 0b 0000 0000 0000 0000 0000 0000 0001 0000) |		\
    ((0b 0000 0000 0000 1000 1000 0000 0000 1000L) & 0b 0000 0000 0000 0000 0000 0000 0000 1000) |		\
    ((0b 0000 0000 0100 0100 0000 0000 0100 0000L) & 0b 0000 0000 0000 0000 0000 0000 0000 0100) |		\
    ((0b 0000 0010 0010 0000 0000 0010 0000 0000L) & 0b 0000 0000 0000 0000 0000 0000 0000 0010) |		\
    ((0b 0001 0001 0000 0000 0001 0000 0000 0001L) & 0b 0000 0000 0000 0000 0000 0000 0000 0001)		    \
    )
    =>
    (								\
    (0x00000088 & 0x00000080) |		\
    (0x00000440 & 0x00000040) |		\
    (0x00002200 & 0x00000020) |		\
    (0x00011000 & 0x00000010) |		\
    (0x00088008 & 0x00000008) |		\
    (0x00440040 & 0x00000004) |		\
    (0x02200200 & 0x00000002) |		\
    (0x11001001 & 0x00000001)		\
    )
    =>
    (0x00000080 | 0x00000040 | 0x00000000 | 0x00000000 | 0x00000008 | 0x00000000 | 0x00000000 | 0x00000001)
    =>
    0x000000C9
    // 即,宏展開並數值運算後,形成以下對應關係,
    Bin(11001001) => 0x000000C9
    
  6. 應用場合,
    • 微控制器(尤其是 8 位機)開發過程中,一些暫存器的賦值,通常是一個位元組代表多個功能標誌,比如一組 GPIO 可能有 8 個埠,對應的輸入輸出狀態暫存器、埠電平暫存器、埠上下拉使能暫存器等,初始化過程中可以整體賦值,同時又可以看到每個標誌位的狀態,很直觀。
    • 只要是對單個位元組進行操作地場合,都可以使用這兩個宏,但由於其形式上不容易擴充套件到更大記憶體單元,單位元組之外的場景少用。
  7. 其他,
    • 按照其運算思路,| 可以替換為 +
    • 對於宏定義,預編譯階段只完成展開,即字元文字替換,而編譯器則通常會將 只有立即數參與的表示式 計算出結果(提升程式碼執行效率)。
      // 原始碼,
      unsigned char i = Bin(11001001);
      unsigned long l = Bin(11001001);
      
      // 預編譯後,
      unsigned char i = (((0x11001001L >> 21) & 0x80) | ((0x11001001L >> 18) & 0x40) | ((0x11001001L >> 15) & 0x20) | ((0x11001001L >> 12) & 0x10) | ((0x11001001L >>  9) & 0x08) | ((0x11001001L >>  6) & 0x04) | ((0x11001001L >>  3) & 0x02) | ((0x11001001L >>  0) & 0x01));		// 無換行
      unsigned long l = (((0x11001001L >> 21) & 0x80) | ((0x11001001L >> 18) & 0x40) | ((0x11001001L >> 15) & 0x20) | ((0x11001001L >> 12) & 0x10) | ((0x11001001L >>  9) & 0x08) | ((0x11001001L >>  6) & 0x04) | ((0x11001001L >>  3) & 0x02) | ((0x11001001L >>  0) & 0x01));		// 無換行
      
      // 編譯最佳化,
      unsigned char i = 0xC9;			// 發生截斷
      unsigned long l = 0x000000C9;
      
    • 其他實現方案(列舉方案可以用一下)
      參考連結:https://www.cnblogs.com/LittleTiger/p/4373887.html
      // 2. 直接按位元組移位後組合,呼叫時不如上述方案方便
      // 定義
      #define Bin(a, b, c, d, e, f, g, h) \
      ((a << 7) + (b << 6) + (c << 5) + (d << 4) + (e << 3) + (f << 2) + (g << 1) + (h << 0))
      // 呼叫
      unsigned char i = Bin(1,1,0,0,1,0,0,1);
      
      // 3. 列舉(列舉式宏定義)
      // 定義
      typedef enum {
          _0b00000000,
          _0b00000001,
          _0b00000010,
          _0b00000011,
           ··· ···
          _0b11111101,
          _0b11111110,
          _0b11111111,
      }BIN_VALUE;
      #define _0b00000000		0x00
      #define _0b00000001		0x01
      #define _0b00000010		0x02
      #define _0b00000011		0x03
        ··· ··· ··· ··· ··· ···
      #define _0b11111101		0xFD
      #define _0b11111110		0xFE
      #define _0b11111111		0xFF
      // 呼叫
      unsigned char i = _0b11001001;
      

相關文章