C/C++位域(Bit-fields)之我見

 

            分類:                C plusplusc語言4634人閱讀評論(23)收藏舉報

前言

很早想說說這個問題了,經常也會有很多公司拿位域出來考人,呵呵要真的想弄清楚還要一點點的分析。

這裏先看看網宿的一道筆試題目,這道題目我之前是複製網上的,結果不對,修改了一下,可以正確運行了,謝謝(imafish_i )提醒:

    1. //假設硬件平臺是intel x86(little endian)   
    2.  
    3. typedef unsigned int uint32_t;  
    4. void inet_ntoa(uint32_t in)   
    5. {   
    6.     char b[18];   
    7.     register char *p;   
    8.     p = (char *)∈   
    9. #define UC(b) (((int)b)&0xff)   
    10.     sprintf(b, "%d.%d.%d.%d/n", UC(p[0]), UC(p[1]), UC(p[2]), UC(p[3]));   
    11.     printf(b);   
    12. }   
    13. int main()   
    14. {   
    15.    inet_ntoa(0x12345678); 
    16.    inet_ntoa(0x87654321);   
    17. }  

有點難度的一道題目,其實理解的也很簡單。

位域(Bit-fields)分析

位域是c++和c裏面都有的一個概念,但是位域有一點要注意的有很多問題我們一樣樣的看:

大端和小端字節序


這個很簡單,就是起始點該怎麼確定。
先看一個程序:

  1. union {
  2.     struct 
  3.     {
  4.         unsigned char a1:2;
  5.         unsigned char a2:3;
  6.         unsigned char a3:3;
  7.     }x;
  8.     unsigned char b;
  9. }d;
  10. int main(int argc, char* argv[])
  11. {
  12.     d.b = 100;
  13.     return 0;
  14. }

那麼x的a1,a2,a3該怎麼分配值,100的二進制是:0110 0100,那麼a1到a3是不是就是依次取值恩?
不是!
我們先看看100分配位的低端是左邊的0還是右邊的0?很明顯是右邊的0,那麼我們再看a1到a3的分配是從低端到高端的
那麼,對應的應該是
<<<<<<--內存增大
a3   a2  a1
011  001 00


內存增大之所以這麼寫是因爲,011是在高位!
而不是通常認爲的的:
a1   a2  a3
011  001 00

還有一個情況多見就是一個二進制的數字轉化爲點分十進制數值,如何進行,這裏涉及到大端還是小端的問題,上面沒有涉及,主要是因爲上面是一個字節,沒有這個問題,多個字節就有大端和小端的問題了,或許我們應該記住這一點就是,在我們的計算機上面,大端和小端都是以字節爲準的,當然嚴格來說更應該以位爲準不是嗎?具體可以參考維基百科上面的一篇文章,他給出了一個以位爲準的大小端序的圖:

http://en.wikipedia.org/wiki/Endianess

下面研究字節爲單位的大小端序,繼續看代碼吧,如下:

  1. int main(int argc, char* argv[])
  2. {
  3.     int a = 0x12345678;
  4.     char *p = (char *)&a;
  5.     char str[20];
  6.     sprintf(str,"%d.%d.%d.%d", p[0], p[1], p[2], p[3]);
  7.     printf(str);
  8.     return 0;
  9. }

這個程序假設是小端字節序,那麼結果是什麼?
我們看看應該怎麼放置呢?
每個字節8位,0x12345678分成4個字節,就是從高位字節到低位字節:12,34,56,78,那麼這裏該怎麼放?如下:
---->>>>>>內存增大
78 56 34 12

因爲這個是小端,那麼小內存對應低位字節,就是上面的結構。

接下來的問題又有點迷糊了,就是p怎麼指向,是不是指向0x12345678的開頭--12處?不是!12是我們所謂的開頭,但是不是內存

的開始處,我們看看內存的分佈,我們如果瞭解p[0]到p[1]的操作是&p[0]+1,就知道了,p[1]地址比p[0]地址大,也就是說p的地址

也是隨內存遞增的!

12 ^ p[3]
    |
34 | p[2]
    |
56 | p[1]
    |
78 | p[0]
內存隨着箭頭增大!同時小端存儲也是低位到高位在內存中的增加!
這樣我們知道了內存怎麼分佈了

那麼:

  1. sprintf(str,"%d.%d.%d.%d", p[0], p[1], p[2], p[3]);

str就是這個結果了:
120.86.52.18

那麼反過來呢?

  1. int main(int argc, char* argv[])
  2. {
  3.     int a = 0x87654321;
  4.     char *p = (char *)&a;
  5.     char str[20];
  6.     sprintf(str,"%d.%d.%d.%d", p[0], p[1], p[2], p[3]);
  7.     printf(str);
  8.     return 0;
  9. }

依舊是小端,8位是一個字節那麼就是這樣的啦:

87 ^ p[3]
     |
65 | p[2]
    |
43 | p[1]
    |
21 | p[0]

結果是:
33.67.101.-121
爲什麼是負的?因爲系統默認的char是有符號的,本來是0x87也就是135,大於127因此就減去256得到-121
那麼要正的該怎麼的弄?
如下就是了:

  1. int main(int argc, char* argv[])
  2. {
  3.     int a = 0x87654321;
  4.     unsigned char *p = (unsigned char *)&a;
  5.     char str[20];
  6.     sprintf(str,"%d.%d.%d.%d", p[0], p[1], p[2], p[3]);
  7.     printf(str);
  8.     return 0;
  9. }

用無符號的!
結果:
33.67.101.135

位域的符號(正負)

看完大端和小端以後,再看看位域的取值的問題,上面我們談到了一些,首先就是位域是按照位來取值的跟我們的int是32位char是8

位一樣,很簡單,但是,要注意一點就是位域也有正負,指有符號屬性的,就是最高位表示的,也會涉及到補碼這個一般被認爲非常

噁心的東西,看看程序吧:

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <string.h>
  4. int main(int argc, char** argv)
  5. {
  6.     union
  7.     {
  8.         struct
  9.         {
  10.             unsigned char a:1;
  11.             unsigned char b:2;
  12.             unsigned char c:3;
  13.         }d;
  14.         unsigned char e;
  15.     } f;
  16.     f.e = 1;
  17.     printf("%d/n",f.d.a);
  18.     return 0;
  19. }

<小端>
那麼輸出是什麼?
換一下:

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <string.h>
  4. int main(int argc, char** argv)
  5. {
  6.     union
  7.     {
  8.         struct
  9.         {
  10.             char a:1;
  11.             char b:2;
  12.             char c:3;
  13.         }d;
  14.         char e;
  15.     } f;
  16.     f.e = 1;
  17.     printf("%d/n",f.d.a);
  18.     return 0;
  19. }

輸出又是什麼?

小端的話,那麼,再d.a上面分得1,而這個是無符號的char,那麼前者輸出是1,沒有問題,第二個輸出是-1,哈哈。
爲什麼?
第二個是無符號的,就一個位分得1,那麼就是最高位分得1,就是負數,負數用的補碼,實際的值是取反加1,就是0+1=1,再取符

號負數,就是-1.

整型提升

最後的打印是用的%d,那麼就是對應的int的打印,這裏的位域肯定要提升,這裏有一點,不管是提升到有符號還是無符號,都是自

己的符號位來補充,而不改變值的大小(這裏說的不改變值大小是用相同的符號屬性來讀取),負數前面都補充1,正數都是用0來補充

,而且也只有這樣才能保證值不變,比如,char提升到int就是前面補充24個char的最高位,比如:

  1. char c = 0xf0;
  2. int p = c;
  3. printf("%d %d/n",c,p);

輸出:-16 -16
p實際上就是0xfffffff0,是負數因此就是取反加1得到
c是一個負數那麼轉化到x的時候就是最高位都用1來代替,得到的數不會改變值大小的。
再看:

  1. char c = 0xf0;
  2. unsigned int x = c;
  3. printf("%u/n",x);

得到的結果是4294967280,也就是0xfffffff0,記住,無符號用%u來打印。

地址不可取


最後說的一點就是位域是一個字節單元裏面的一段,是沒有地址的!

附錄

最後附上《The C Book》這本書的一段說法:
While we're on the subject of structures, we might as well look at bitfields. They can only be declared inside a

structure or a union, and allow you to specify some very small objects of a given number of bits in length. Their

usefulness is limited and they aren't seen in many programs, but we'll deal with them anyway. This example should

help to make things clear:

  1. struct {
  2.       /* field 4 bits wide */
  3.       unsigned field1 :4;
  4.       /*
  5.        * unnamed 3 bit field
  6.        * unnamed fields allow for padding
  7.        */
  8.       unsigned        :3;
  9.       /*
  10.        * one-bit field
  11.        * can only be 0 or -1 in two's complement!
  12.        */
  13.       signed field2   :1;
  14.       /* align next field on a storage unit */
  15.       unsigned        :0;
  16.       unsigned field3 :6;
  17. }full_of_fields;

Each field is accessed and manipulated as if it were an ordinary member of a structure. The keywords signed and

unsigned mean what you would expect, except that it is interesting to note that a 1-bit signed field on a two's

complement machine can only take the values 0 or -1. The declarations are permitted to include the const and

volatile qualifiers.

The main use of bitfields is either to allow tight packing of data or to be able to specify the fields within some

externally produced data files. C gives no guarantee of the ordering of fields within machine words, so if you do

use them for the latter reason, you program will not only be non-portable, it will be compiler-dependent too. The

Standard says that fields are packed into ‘storage units’, which are typically machine words. The packing order, and

whether or not a bitfield may cross a storage unit boundary, are implementation defined. To force alignment to a

storage unit boundary, a zero width field is used before the one that you want to have aligned.

Be careful using them. It can require a surprising amount of run-time code to manipulate these things and you can

end up using more space than they save.

Bit fields do not have addresses—you can't have pointers to them or arrays of them.

最後

瞭解了這些我想網宿的那道題目也很簡單了。希望大家指正。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章