簡體   English   中英

"何時在 C 中使用位域?"

[英]When to use bit-fields in C?

關於“為什么我們需要使用位域”的問題,在谷歌上搜索我發現位域用於標志。 現在很好奇,

  1. 它是實際使用位域的唯一方法嗎?
  2. 我們是否需要使用位域來節省空間?

書中定義位域的方法:

struct {
    unsigned int is_keyword : 1; 
    unsigned int is_extern :  1; 
    unsigned int is_static : 1;
} flags;
  1. 為什么我們使用int?
  2. 占用了多少空間?

我很困惑為什么我們使用int ,而不是short或小於int的東西。

  1. 據我了解,內存中只占用了 1 位,而不是整個 unsigned int 值。 這是正確的嗎?

一個很好的資源是C 中的位字段

根本原因是減少使用的大小。 例如,如果你寫:

struct {
    unsigned int is_keyword; 
    unsigned int is_extern; 
    unsigned int is_static;
} flags;

您將至少使用3 * sizeof(unsigned int)或 12 個字節來表示 3 個小標志,這應該只需要 3 位。

所以如果你寫:

struct {
    unsigned int is_keyword : 1; 
    unsigned int is_extern : 1; 
    unsigned int is_static : 1;
} flags;

這使用了與一個unsigned int相同的空間,所以是 4 個字節。 在需要更多空間之前,您可以將 32 個一位字段放入結構中。

這有點相當於經典的自制位字段:

#define IS_KEYWORD 0x01
#define IS_EXTERN  0x02
#define IS_STATIC  0x04
unsigned int flags;

但是位域語法更清晰,比較:

if (flags.is_keyword)

反對:

if (flags & IS_KEYWORD)

並且顯然更不容易出錯。

現在我很好奇,[是標志] 位域實際使用的唯一方式嗎?

不,標志不是使用位域的唯一方式。 它們也可用於存儲大於一位的值,盡管標志更為常見。 例如:

typedef enum {
    NORTH = 0,
    EAST = 1,
    SOUTH = 2,
    WEST = 3
} directionValues;

struct {
    unsigned int alice_dir : 2;
    unsigned int bob_dir : 2;
} directions;

我們是否需要使用位域來節省空間?

位字段確實節省了空間。 它們還允許一種更簡單的方法來設置非字節對齊的值。 我們可以使用與在struct設置字段相同的語法,而不是位移位和使用按位運算。 這提高了可讀性。 使用位域,你可以寫

directions.alice_dir = WEST;
directions.bob_dir = SOUTH;

但是,要在沒有位字段的一個int (或其他類型)的空間中存儲多個獨立值,您需要編寫如下內容:

#define ALICE_OFFSET 0
#define BOB_OFFSET 2
directions &= ~(3<<ALICE_OFFSET); // clear Alice's bits
directions |= WEST<<ALICE_OFFSET; // set Alice's bits to WEST
directions &= ~(3<<BOB_OFFSET);   // clear Bob's bits
directions |= SOUTH<<BOB_OFFSET;  // set Bob's bits to SOUTH

可以說,提高位字段的可讀性比在這里和那里保存幾個字節更重要。

為什么我們使用int? 占用了多少空間?

占用了整個int的空間。 我們使用int是因為在很多情況下,它並不重要。 如果對於單個值,您使用 4 個字節而不是 1 個或 2 個,您的用戶可能不會注意到。 對於某些平台,大小確實更重要,您可以使用占用較少空間的其他數據類型( charshortuint8_t等)。

據我所知,內存中只占用了 1 位,而不是整個 unsigned int 值。 這是正確的嗎?

不,那是不正確的。 整個unsigned int將存在,即使您只使用它的 8 位。

位域常見的另一個地方是硬件寄存器。 如果您有一個 32 位寄存器,其中每個位都有一定的含義,您可以用位域來優雅地描述它。

這樣的位域本質上是特定於平台的。 在這種情況下,可移植性無關緊要。

我們主要(盡管不是唯一地)將位域用於標志結構 - 字節或字(或可能更大的事物),我們試圖在其中打包微小(通常是 2 狀態)(通常是相關的)信息。

在這些場景中,使用位字段是因為它們正確地模擬了我們正在解決的問題:我們正在處理的實際上並不是一個 8 位(或 16 位或 24 位或 32 位)數字,而是8 條(或 16 條、24 條或 32 條)相關但不同的信息的集合。

我們使用位域解決的問題是“打包”信息具有可衡量的好處和/或“解包”信息沒有懲罰的問題。 例如,如果您通過 8 個引腳暴露 1 個字節,並且每個引腳的位通過已經印在板上的自己的總線,以便它准確地引導到它應該到達的位置,那么位域是理想的。 “打包”數據的好處是可以一次性發送(如果總線的頻率有限並且我們的操作依賴於它的執行頻率,這很有用),而“解包”數據的懲罰是不存在(或存在但值得)。

另一方面,由於計算機體系結構通常的工作方式,我們在其他情況下(如正常程序流控制)不使用布爾值的位字段。 大多數常見的 CPU 不喜歡從內存中獲取一位——他們喜歡獲取字節或整數。 他們也不喜歡處理位——他們的指令通常對更大的東西進行操作,比如整數、字、內存地址等。

因此,當您嘗試對位進行操作時,取決於您或編譯器(取決於您使用的語言)編寫執行位掩碼的附加操作並剝離除您實際想要的信息之外的所有內容的結構操作。 如果“打包”信息沒有任何好處(在大多數情況下,沒有好處),那么將位字段用於布爾值只會在您的代碼中引入開銷和噪音。

回答原始問題»何時在 C 中使用位域?« ...根據 Brian Hook 的“編寫可移植代碼”一書(ISBN 1-59327-056-9,我閱讀了德語版 ISBN 3-937514-19 -8)和個人經驗:

永遠不要使用 C 語言的位域習語,而要自己動手。

許多實現細節是特定於編譯器的,尤其是與聯合相結合的情況下,不同的編譯器和不同的字節序不能保證事情的發展。 如果您的代碼必須是可移植的並且將針對不同的體系結構和/或使用不同的編譯器進行編譯,那么請不要使用它。

我們在將代碼從帶有一些專有編譯器的小端微控制器移植到另一個帶有 GCC 的大端微控制器時遇到了這種情況,這並不有趣。 :-/

這就是我從那時起使用標志(主機字節順序 ;-) )的方式:

# define SOME_FLAG        (1 << 0)
# define SOME_OTHER_FLAG  (1 << 1)
# define AND_ANOTHER_FLAG (1 << 2)

/* test flag */
if ( someint & SOME_FLAG ) {
    /* do this */
}

/* set flag */
someint |= SOME_FLAG;

/* clear flag */
someint &= ~SOME_FLAG;

不需要與 int 類型和一些位域結構的聯合。 如果您閱讀大量嵌入式代碼,那么這些測試、設置和清晰的模式將變得很常見,並且您很容易在代碼中發現它們。

為什么我們需要使用位域'?

當您想存儲一些可以存儲小於字節的數據時,可以使用位字段在結構中耦合這些數據。 在嵌入字中,當任何寄存器的一個 32 位世界對不同的字具有不同的含義時,您也可以使用位文件來使它們更具可讀性。

我發現位字段用於標志。 現在我很好奇,這是實際使用位域的唯一方法嗎?

不,這不是唯一的方法。 您也可以以其他方式使用它。

我們是否需要使用位域來節省空間?

是的。

據我所知,內存中只占用了 1 位,而不是整個 unsigned int 值。 這是正確的嗎?

不。 內存只能以字節的倍數占用。

位域可用於節省內存空間(但為此目的使用位域的情況很少見)。 它用於存在內存限制的地方。 例如)在嵌入式系統中編程時。

但是只有在非常需要時才應該使用它。

因為我們不能有位域的地址。 所以地址運算符&不能與它們一起使用。

一個很好的用法是實現一個塊來轉換到和從 base64 或任何未對齊的數據結構。

struct {
    unsigned int e1:6;
    unsigned int e2:6;
    unsigned int e3:6;
    unsigned int e4:6;
} base64enc; //I don't know if declaring a 4-byte array will have the same effect.

struct {
    unsigned char d1;
    unsigned char d2;
    unsigned char d3;
} base64dec;

union base64chunk {
    struct base64enc enc;
    struct base64dec dec;
};

base64chunk b64c;
//you can assign 3 characters to b64c.enc, and get 4 0-63 codes from b64dec instantly.

這個例子有點幼稚,因為 base64 還必須考慮空終止(即一個沒有長度l的字符串,因此l % 3 是 0)。 但作為訪問未對齊數據結構的示例。

另一個示例:使用此功能將TCP 數據包標頭分解為其組件(或您要討論的其他網絡協議數據包標頭),盡管它是一個更高級且最終用戶較少的示例。 一般來說:這對於 PC 內部、SO、驅動程序、編碼系統很有用。

另一個例子:分析一個float

struct _FP32 {
    unsigned int sign:1;
    unsigned int exponent:8;
    unsigned int mantissa:23;
}

union FP32_t {
    _FP32 parts;
    float number;
}

(免責聲明:不知道應用它的文件名/類型名稱,但在 C 中這是在頭文件中聲明的;不知道如何為 64 位浮標完成此操作,因為尾數必須有 52 位和 -在 32 位目標中 - 整數有 32 位)。

結論:正如概念和這些示例所示,這是一個很少使用的功能,因為它主要用於內部目的,而不是用於日常軟件。

您可以使用它們來擴展包裝的無符號類型的數量。 通常,您只有 8,16,32,64... 的冪,但您可以擁有位域的所有冪。

struct a
{
    unsigned int b : 3 ;
} ;

struct a w = { 0 } ;

while( 1 )
{
    printf("%u\n" , w.b++ ) ;
    getchar() ;
}

要回答沒有其他人回答的問題的一部分:

整數不是短褲

使用 int 而不是 short 等的原因是,在大多數情況下,這樣做不會節省空間。

現代計算機具有 32 或 64 位體系結構,即使您使用更小的存儲類型(例如 short),也需要 32 或 64 位。

較小的類型僅在您可以將它們打包在一起時才用於節省內存(例如,短數組可能比 int 數組使用更少的內存,因為數組中的 short 可以更緊密地打包在一起)。 對於使用位域的大多數情況,情況並非如此。

其他用途

位域最常用於標志,但它們還有其他用途。 例如,在許多國際象棋算法中表示棋盤的一種方法是使用 64 位整數來表示棋盤(8*8 像素)並在該整數中設置標志以給出所有白色棋子的位置。 另一個整數顯示所有黑色棋子等。

為了利用內存空間,我們可以使用位域。

據我所知,在現實世界的編程中,如果我們需要,我們可以使用布爾值而不是將其聲明為整數然后制作位字段。

如果它也是我們經常使用的值,不僅可以節省空間,還可以獲得性能,因為我們不需要污染緩存。 然而,緩存也是使用位字段的危險,因為對不同位的並發讀取和寫入會導致數據競爭,而對完全獨立位的更新可能會用舊值覆蓋新值。

位域更加緊湊,這是一個優勢。

但是不要忘記壓縮結構比普通結構慢。 它們也更難構建,因為程序員必須定義每個字段使用的位數。這是一個缺點

為什么我們使用int? 占用了多少空間?

我在其他任何答案中都沒有提到這個問題的一個答案是,C 標准保證支持 int。 具體來說:

位域的類型應為 _Bool、signed int、unsigned int 或其他一些實現定義類型的限定或非限定版本。

編譯器通常允許額外的位域類型,但不是必需的。 如果你真的關心可移植性,int 是最好的選擇。

在我們的項目中,我們使用它從給定的內存地址中提取頁表條目和頁目錄條目:

union VADDRESS {
    struct {
        ULONG64 BlockOffset : 16;
        ULONG64 PteIndex : 14;
        ULONG64 PdeIndex : 14;
        ULONG64 ReservedMBZ : (64 - (16 + 14 + 14));
    };

    ULONG64 AsULONG64;
};

現在假設,我們有一個地址: union VADDRESS tempAddress; tempAddress.AsULONG64 = 0x1234567887654321; union VADDRESS tempAddress; tempAddress.AsULONG64 = 0x1234567887654321;

現在我們可以從這個地址訪問 PTE 和 PDE:
cout<<tempAddress.PteIndex;

如今,微控制器 (MCU) 具有外圍設備,例如 I\/O 端口、ADC、DAC,以及板載芯片和處理器。 在 MCU 與所需的外圍設備一起可用之前,我們將通過連接到微處理器的緩沖地址和數據總線來訪問我們的一些硬件。 指針將設置為設備的內存地址,如果設備看到它的地址以及 r\/w 和可能的芯片選擇,它將被訪問。 很多時候,我們希望訪問設備上的單個或一小組位。

"

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM