【问题标题】:Bit Manipulation and Flags位操作和标志
【发布时间】:2018-08-27 10:29:34
【问题描述】:

https://i.imgur.com/VU56Rwn.png

答: 当打开的手册页说: 指定的标志由以下值组成:

       O_RDONLY        open for reading only
       O_WRONLY        open for writing only
       ...

这意味着我们应该在这样的标志之间使用逻辑或:O_RDONLY || O_WRONLY 来指定我们想要的权限组合。

B:为了指示不同的选项,我们使用位标志(而不是字符或整数)以节省空间。

C:对位标志执行操作很快。

D:系统调用中使用的位标志在库文件中定义。

E:命令 chmod 使用八进制定义的位标志常量,因为有八种可能的权限状态。

如果它是一个好的系统库,我知道库文件中没有定义位标志。它们通常是标头中的常量或#defines,而不是编译的对象,如果那是“库文件”所指的话。但是,我不明白它们是如何节省空间的,位标志毕竟不是整数吗?

【问题讨论】:

  • 请不要张贴代码、输出或作业的图片,至少有礼貌在您的问题中写作业,这样我们就不必访问外部资源。
  • || ORing 标志显然是错误的
  • 不,标志是按位或运算的,而不是你所展示的逻辑。所以O_RDONLY | O_WRONLY。此类标志应作为位保存在单个变量中。
  • ||| 是不同的东西。前者是按位或,后者是逻辑或运算符。
  • 您不要结合 O_RDONLY 和 O_WRONLY 标志来获取读写权限。相反,您使用 O_RDWR。只有这三个(O_RDONLY 或 O_WRONLY 或 O_RDWR)之一。这 3 个值中的任何一个都可以与文档中提到的其他标志(O_CREAT、O_DIRECT 等)结合使用。

标签: c bitflags


【解决方案1】:

首先,||| 是不同的运算符。

| 是按位 OR 运算符,它对每一位进行 OR 运算,您会得到 结果。

|| 是逻辑或运算符,如果左侧为真,则返回真或 右边为真,否则为假。

如果是位标志,你应该使用|,比如O_RDONLY | O_WRONLY

B:为了指示不同的选项,我们使用位标志(而不是字符或整数)以节省空间。

说实话,我认为这种措辞有点误导。好东西 关于位标志,是你可以打包在一个 int 中,主要是 32 位长,向上 到 32 个具有 ON/OFF 语义的不同值。所以需要的功能 这类选项,只需使用单个int 即可获得多个选项 来自调用者,因为单个位代表这样的开/关属性。如果 bit 为 1,则属性为 ON,否则为 OFF。

在合同中,当您不使用位标志时,该函数必须采用 每个选项的单独变量,如果你有很多选项,你需要 函数声明中有很多变量。所以你可以“节省空间”,如果你 改用位标志。

考虑一下:

// lot's of other has_property_x cases

int somefunc(int has_property_a, int has_property_b, int has_property_c, ...)
{
    if(has_property_a)
        do_something_based_on_a();
    if(has_property_b)
        do_something_based_on_b();
    ....
}

void foo(void)
{
    somefunc(1, 1, 0, 1, 1);
}

这不是很有效,很难阅读,编码很痛苦, 总体来说不是一个好的设计。

但是如果你使用位标志,你可以在函数中保存很多变量:

// lot's of other HAS_PROPERTY_X cases

#define HAS_PROPERTY_A 1
#define HAS_PROPERTY_B 2
#define HAS_PROPERTY_C 4
...

int somefunc(int flags)
{
    if(flags & HAS_PROPERTY_A)
        do_something_based_on_a();
    if(flags & HAS_PROPERTY_B)
        do_something_based_on_b();
    ...
}

void foo(void)
{
    somefunc(HAS_PROPERTY_A | HAS_PROPERTY_B | HAS_PROPERTY_E);
}

更加紧凑和可读。

【讨论】:

    【解决方案2】:

    什么是位标志的简要概述

    位标志是定义一组某种类型的常量,通常是各种类型的选项。位标志通常定义为十六进制常量,其目的是将位运算符与这些常量一起使用,以便通过使用位运算符从全部常量集中创建一些子集。

    按位运算符是诸如|(按位或)、&(按位与)、^(按位异或)和~(按位非)等运算符,它们逐位执行运算符指定的布尔逻辑运算对这两个值生成一个新值。位运算符不同于逻辑运算符,例如||(逻辑或)、&&(逻辑与),它们与计算结果为布尔值 true(非零)或 false(零)的表达式一起使用。

    使用 C 预处理器 define 指令创建按位标志的典型定义示例如下:

    #define  ITM_FLAG_EXAMPLE1  0x00000001L   // an example bitwise flag
    #define  ITM_FLAG_EXAMPLE2  0x00000002L   // another example flag
    #define  ITM_FLAG_JUMP01    0x00040000L   // another example
    #define  ITM_FLAG_JUMP02    0x00080000L   // another example
    

    此外,现代 C 编译器也将允许使用 enum 类型。

    typedef enum { ITEM_FLAG_1 = 1, ITEM_FLAG_2 = 2, ITEM_FLAG_3 = 4 } ItemType;
    
    ItemType AnItem = ITEM_FLAG_1;    // defining a variable of the type
    ItemType AnItem2 = ITEM_FLAG_1 | ITEM_FLAG_2;  // defining a second variable
    

    enum { ITEM_FLAG_1 = 1, ITEM_FLAG_2 = 2, ITEM_FLAG_3 = 4 } ItemType;
    
    enum ItemType AnItem = ITEM_FLAG_1;    // defining a variable of the type
    enum ItemType AnItem2 = ITEM_FLAG_1 | ITEM_FLAG_2;  // defining a second variable
    

    以及如何使用它们的一些示例:

    unsigned long ulExample = ITM_FLAG_EXAMPLE2;   // ulExample contains 0x00000002L
    unsigned long ulExamplex = ITM_FLAG_EXAMPLE1 | ITM_FLAG_EXAMPLE2;  // ulExamplex contains 0x00000003L
    unsigned long ulExampley = ulExamplex & ITM_FLAG_EXAMPLE2;  // ulExampley contains 0x00000002L
    

    请参阅此博客帖子 Intro to Truth Tables & Boolean Algebra,其中描述了各种布尔代数运算和此 Wikipedia topic on Truth Tables

    使用 Bitflags 的一些注意事项

    使用位标志时可能会遇到一些问题,并且需要注意一些事项。

    • 使用位标志定义的常量为零,未设置位,可能会导致问题
    • 按位运算与逻辑运算混合在一起可能会出现缺陷

    一般来说,使用定义为 0 而不是非零值的位标志会导致错误。大多数程序员都希望位标志有一个非零值,表示某种集合中的成员资格。定义为零的位标志是一种违反预期的行为,在按位运算中使用时可能会导致意想不到的后果和行为。

    将位标志变量的按位运算与逻辑运算符结合使用时,强制特定运算符优先级的括号通常更容易理解,因为它不需要读者知道C operator precedence table

    关于已发布的问题

    在提供库时,通常会有一个或多个包含文件伴随库文件,以提供许多所需的项目。

    • 库提供的函数的函数原型
    • 库使用的类型的变量类型声明和定义
    • 用于控制库函数行为的操作数和标志的特殊常量

    位标志是一种为函数接口提供选项的历史悠久的方式。位标志有几个很好的特性,它们对 C 程序员很有吸引力。

    • 易于通过接口传输的紧凑表示
    • 非常适合布尔代数运算和集合操作
    • 自然适合 C 位运算符来执行这些操作

    位标志节省空间,因为标志的名称可能很长且具有描述性,但可以编译为单个无符号值,例如 unsigned short 或 unsigned long 或 unsigned char。

    使用位标志的另一个好处是,当对表达式中的常量使用按位运算符时,大多数现代编译器会将按位运算作为编译表达式的一部分进行计算。因此,现代编译器将在 O_RDONLY | O_WRONLY 等表达式中采用多个按位运算符,并在编译源代码时执行按位或运算,并将表达式替换为计算后的表达式的值。

    在大多数计算机体系结构中,按位运算符是使用加载数据的寄存器执行的,然后执行按位运算。对于 32 位架构,使用 32 位变量包含一组位自然适合 CPU 寄存器,就像在 64 位架构中一样,使用 32 位或 64 位变量包含一组位自然适合寄存器。这种自然契合允许对相同变量进行多次按位运算,而无需从 CPU 缓存或主内存中进行提取。

    C 的位运算符几乎总是有一个 CPU 机器指令类似物,因此 C 位运算符具有几乎完全相同的 CPU 操作,因此编译器生成的结果机器代码非常高效。

    通过使用unsigned long 传递32 个不同的标志或使用unsigned long long 将64 个不同的标志传递给函数,可以很容易地看到位标志的紧凑表示。通过使用数组偏移和位标志方法以及一组 C 处理器宏或一组函数来操作数组,unsigned char 的数组可用于传递更多标志。

    一些可能的例子

    位运算符与用于集合的逻辑运算符非常相似,并且用位标志表示集合效果很好。如果您有一个包含操作数的集合,其中一些不应该与其他一些标志一起使用,那么使用按位运算符和位掩码可以很容易地查看是否指定了两个冲突标志。

    #define FLAG_1  0x00000001L     // a required flag if FLAG_2 is specified
    #define FLAG_2  0x00001000L     // must not be specified with FLAG_3
    #define FLAG_3  0x00002000L     // must not be specified with FLAG_2
    
    int func (unsigned long ulFlags)
    {
        // check if both FLAG_2 and FLAG_3 are specified. if so error
        // we do a bitwise And to isolate specific bits and then compare that
        // result with the bitwise Or of the bits for equality. this approach
        // makes sure that a check for both bits is turned on.
        if (ulFlags & (FLAG_2 | FLAG_3) == (FLAG_2 | FLAG_3)) return -1;
    
        // check to see if either FLAG_1 or FLAG_3 is set we can just do a
        // bitwise And against the two flags and if either one or both are set
        // then the result is non-zero.
        if (ulFlags & (FLAG_1 | FLAG_3)) {
            // do stuff if either or both FLAG_1 and/or FLAG_3 are set
        }
    
        // check that required option FLAG_1 is specified if FLAG_2 is specified.
        // we are using zero is boolean false and non-zero is boolean true in
        // the following. the ! is the logical Not operator so if FLAG_1 is
        // not set in ulFlags then the expression (ulFlags & FLAG_1) evaluates
        // to zero, False, and the Not operator inverts the False to True or
        // if FLAG_1 is set then (ulFlags & FLAG_1) evaluates to non-zero, True,
        // and the Not operator inverts the True to False. Both sides of the
        // logical And, &&, must evaluate True in order to trigger the return.
        if ((ulFlags & FLAG_2) && ! (ulFlags & FLAG_1)) return -2;
        // other stuff
    }
    

    例如,请参阅Using select() for non-blocking sockets,了解使用位标志的标准socket() 接口的简要概述,以及使用类似于使用位标志的集合操作抽象的示例select() 函数。 socket 函数允许通过使用fcntl() 函数和select() 函数具有关联的函数/宏集(FD_SET()、@987654350)的位标志来设置各种特征,例如非阻塞@ 等),它提供了一个抽象,用于指示要在 select() 中监视哪些套接字句柄。我并不是要暗示select() 套接字集是位图,尽管在最初的 UNIX 中,我相信它们是位图。然而select() 的抽象设计及其相关实用程序提供了一种可以用位标志实现的集合。

    对包含按位标志的变量的评估也可以更快、更容易、更高效、更易读。例如,在使用某些已定义标志调用的函数中:

    #define ITEM_FLG_01  0x0001
    #define ITEM_FLG_02  0x0002
    #define ITEM_FLG_03  0x0101
    #define ITEM_FLG_04  0x0108
    #define ITEM_FLG_05  0x0200
    
    #define ITEM_FLG_SPL1 (ITEM_FLG_01 | ITEM_FLG_02)
    

    可能有switch()这样的声明:

    switch (bitwiseflags & ITEM_FLG_SPL1) {
        case ITEM_FLG_01 | ITEM_FLG_02:
            // do things if both ITEM_FLG_01 and ITEM_FLG_02 are both set
            break;
        case ITEM_FLG_01:
            // do things if ITEM_FLG_01  is set
            break;
        case ITEM_FLG_02:
            // do things if ITEM_FLG_02 is set
            break;
        default:
            // none of the flags we are looking for are set so error
            return -1;
    }
    

    您可以使用与上面相同的定义来做一些简短的表达式,例如以下。

    // test if bitwiseflags has bit ITEM_FLG_5 set and if so then call function
    // doFunc().
    (bitwiseflags & ITEM_FLG_5) == ITEM_FLG_5 && doFunc();
    

    附录:用于真正大型位标志集的技术

    请参阅此答案 Creating bitflag variables with large amounts of flags or how to create large bit-width numbers,以了解一种处理大型集合的方法,该方法大于位标志的 32 位或 64 位变量。

    【讨论】:

    • 我不认为这个switch 声明是这样工作的,请参阅ideone.com/Gg5Kjs 或者我错过了什么?
    • @Pablo 取决于这些位标志是#define 值还是const int O_RDONLY = 0x001; 之类的值。如果#define,那么在预处理步骤期间,O_RDONLY 的文本将被转换为表示的常量值,编译器将只看到诸如case 0x0001 | 0x0002: 之类的常量。但如果使用const int 或类似名称,我会看到编译器错误。
    • 我不明白你想说什么。使用const int A = 1,我得到error: case label does not reduce to an integer constant。当我执行#define A 或将它们放入enum 时,它会编译。但是default 的情况被执行了。在您的代码中,case O_RDONLY | O_WRONLY 当且仅当设置了这两个位且未设置其他位时才会执行。如果设置了除它们之外的任何其他位,则不会执行 case 块。
    • @Pablo 我会相信你对enum 的看法,因为我还没有尝试过。你对其他一切都是正确的。你有什么问题?
    • O_RDONLY 可以为零(在大多数系统上)所以,开关不会编译。
    猜你喜欢
    • 2022-04-09
    • 1970-01-01
    • 2019-08-15
    • 2019-09-08
    • 1970-01-01
    • 1970-01-01
    • 2022-11-17
    • 2016-02-05
    相关资源
    最近更新 更多