何时在C中使用位域?


73

关于“我们为什么需要使用位域”这个问题,在Google上搜索时,我发现位域用于标志。现在我很好奇

  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值。这是对的吗?

5
关于位域的所有事情都是实现定义的,永远不会?
ouah 2014年

Answers:


62

现在我很好奇,[标志]实际使用位域的唯一方法是吗?

不,标志不是唯一使用位域的方法。它们也可以用于存储大于一位的值,尽管标志更为常见。例如:

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位,整个对象也将存在。


您能否在“手动执行”部分进行更多说明?您为什么需要这样做?
Willwsharp

@Willwsharp我很乐意添加更多细节;你能告诉我你努力理解的那一部分吗?
埃里克·芬

我想我现在明白了,“手动执行”将尝试在没有支持结构的情况下提取数据,这就是为什么您必须自己进行一点操作的原因。正确?
Willwsharp

对,就是这样。我可以清除该语言,“手动”可能不够具体。
埃里克·芬

66

一个很好的资源是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;

这将占用与1相同的空间unsigned int,即4个字节。您可以在需要更多空间之前将32个1位字段放入结构中。

这相当于经典的家庭酿造位字段:

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

但是位字段的语法比较干净,请比较:

if (flags.is_keyword)

反对:

if (flags & IS_KEYWORD)

而且显然不容易出错。


2
好答案!在谈论位字段及其在内存中的大小时,应记住,c ++编译器将按以下方式分配内存中的位字段:相同类型的几个连续位字段成员将按顺序分配。一旦需要分配一种类型,它将与下一个逻辑存储块的开头对齐。下一个逻辑块将取决于您的处理器。一些处理器可以与8位边界对齐,而其他处理器只能与16位边界对齐。
2014年

6
下一个问题是:什么时候需要节省空间?几乎从不。除非您在非常有限的环境中使用,否则请避免使用位字段。
edmz 2014年

位域几乎从未用于标志,因为如果您需要添加另一个,则将破坏ABI
z

1
另外:它的行为更像布尔值:您可以编写flags.is_keyword == flags.is_extern(与比较((flags & IS_KEYWORD) == 0) == ((flags & IS_EXTERN) == 0))。:在另一方面,与传统的位域,您可以检查多个值与一个比较语句(flags & (IS_KEYWORD IS_EXTERN)) == IS_KEYWORD(这意味着IS_KEYWORD,但没有IS_EXTERN
gaborsch

1
@Yawar如果该结构是不透明的,则只能通过指针来处理它。在C语言中,指针的类型无关紧要,在C ++语言中,它仅影响名称修饰。因此,简短的答案是“否”,长的答案是“如果不透明,它就永远不会成为ABI的一部分。”
rioki

27

位域通用的另一个地方是硬件寄存器。如果您有一个32位寄存器,每个位都有特定含义,则可以用位域对其进行优雅地描述。

这样的位域本质上是特定于平台的。在这种情况下,可移植性无关紧要。


7
可移植性不仅适用于硬件。同一体系结构的不同编译器可能在位字段的排序上存在分歧。
詹德,2015年

11
需要注意的是,尽管我很少见到使用多个编译器的嵌入式项目。通常,您坚持一个项目。
CodeMonkey

1
如果硬件寄存器位于IP块中,并且IP块驱动程序用于多种体系结构,则将有多个编译器。也就是说,这并不像人们想的那样普遍。
亨里克

11

我们主要(尽管不是排他性地)将位字段用于标志结构-字节或字(或可能更大的东西),在其中我们尝试打包(通常是相关的)信息的小片段(通常是2状态)。

在这些情况下,使用位字段是因为它们正确地模拟了我们要解决的问题:我们要处理的实际上不是8位(或16位,24位或32位)数字,而是收集了8条(或16条或24条或32条)相关但截然不同的信息。

我们使用位字段解决的问题是“打包”信息具有可衡量的好处和/或“打包”信息不会带来任何损失的问题。例如,如果要通过8个引脚暴露1个字节,并且每个引脚的位都经过各自的总线,而该总线已经印刷在板上,以使其准确地到达了预期的位置,那么位字段就是理想的选择。“打包”数据的好处是可以一次性发送(如果总线的频率受到限制并且我们的操作依赖于其执行的频率,则很有用),并且“拆包”数据的代价是不存在(或存在但值得)。

另一方面,由于计算机体系结构通常的工作方式,在其他情况下(例如常规程序流控制),我们不将位字段用于布尔值。大多数常见的CPU不喜欢从内存中获取一位,而是喜欢获取字节或整数。他们也不喜欢处理位-他们的指令通常对整数,字,内存地址等较大的东西进行操作。

因此,当您尝试对位进行操作时,由您或编译器(取决于您使用的是哪种语言)来写出执行位屏蔽并剥离除您实际想要的信息以外的所有结构的其他操作操作。如果“打包”信息没有任何好处(大多数情况下没有),那么将位字段用于布尔值只会在代码中引入开销和噪音。


7

为什么我们需要使用位域?

当您要存储一些数据,这些数据的存储量小于字节时,可以使用“位”字段在结构上耦合这类数据。在嵌入式字中,如果任何寄存器的一个32位世界对于不同的字具有不同的含义,那么您也可以使用位字段使它们更具可读性。

我发现位字段用于标志。现在我很好奇,这是实际使用位域的唯一方法吗?

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

我们是否需要使用位字段来节省空间?

是。

据我了解,内存中仅占用1位,而不是整个unsigned int值。这是对的吗?

没有。内存只能以字节的倍数占用。


7

为了回答原始问题“何时在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类型和某些位域结构的联合。如果您阅读大量的嵌入式代码,则这些测试,设置和清除模式将很常见,并且可以轻松地在代码中发现它们。


5

一个很好的用法是实现一个块以与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位flot如何做到这一点,因为尾数必须具有52位,并且-在32位中,目标int具有32位)。

结论:正如概念和这些示例所示,这是一个很少使用的功能,因为它主要用于内部目的,而不是用于日常软件。


1
联合化的问题float:字节序。在相反的字节序机器中,所需的结构可能是struct _FP32 { unsigned int mantissa:23; unsigned int exponent:8; unsigned int sign:1; }。大于的位宽时,位字段定义不明确unsigned。由于aunsigned必须至少为16位,因此任何宽度大于16的宽度都会遇到可移植性问题-“对于64位浮点数如何做到这一点”尚不明确。
chux-恢复莫妮卡2014年

2
这个答案不是标准的C。允许编译器以任何想要的方式打包位字段,您不能依赖于它是最低有效优先且无填充的。
minexew '16

5

位字段可用于节省内存空间(但很少将位字段用于此目的)。它用于存在内存限制的地方。例如)在嵌入式系统中编程时。

但这仅在非常需要时才使用。

因为我们无法获得位字段的地址。因此,地址运算符不能与它们一起使用。


@ Jerfov2他们节省了大量空间。想象一下一个使用48位数字(其中有数百万个)的服务器应用程序。您要支付48GB的ram还是64GB的价格?您的客户想要哪一个?
探索

2

您可以使用它们来扩展包装的无符号类型的数量。普通情况下,您仅具有8,16,32,64 ...的幂,但是对于位域,您可以拥有所有幂。

struct a
{
    unsigned int b : 3 ;
} ;

struct a w = { 0 } ;

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

2

要回答部分问题,没有人回答:

整数而不是短裤

使用整数而不是短裤等的原因是,在大多数情况下,这样做不会节省空间。

现代计算机具有32或64位体系结构,即使您使用较小的存储类型(例如short),也将需要32或64位。

如果可以将较小的类型打包在一起,则较小的类型仅对节省内存有用(例如,较短的数组可能比int数组使用的内存少,因为可以将短裤更紧密地打包在一起。)在大多数情况下,使用位域时并非如此。

其他用途

位域最常用于标志,但是它们还有其他用途。例如,一种表示许多国际象棋算法中使用的国际象棋棋盘的方法是使用64位整数表示棋盘(8 * 8像素),并在该整数中设置标志以给出所有白色棋子的位置。另一个整数显示所有黑色棋子,等等。


注意:许多嵌入式处理器(2013年为每年100亿)使用8位和16位架构。C在那受欢迎。
chux-恢复莫妮卡2014年


1

如果这也是我们经常使用的值,那么我们不仅可以节省空间,而且还可以提高性能,因为我们不需要污染缓存。但是,缓存也是使用位字段的危险,因为对不同位的并发读取和写入将导致数据争用,对完全独立的位进行更新可能会用旧值覆盖新值。


1

位域更加紧凑,这是一个优势。

但不要忘记,打包结构比普通结构慢。由于程序员必须定义每个字段要使用的位数,因此它们的构造也更加困难。


0

我们为什么要使用int?占用多少空间?

我在其他任何答案中都没有提到的这个问题的一个答案是,C标准保证了对int的支持。特别:

位字段的类型应为_Bool的合格或不合格版本,signed int,unsigned int或其他一些实现定义的类型。

编译器通常允许其他位域类型,但不是必需的。如果您真的很担心可移植性,那么int是最佳选择。


0

在我们的项目中,我们使用它从给定的内存地址中提取Page表条目和Page目录条目:

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;

现在我们可以从以下地址访问PTE和PDE:
cout << tempAddress.PteIndex;

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.