位运算符的真实世界使用案例

280

以下是一些与实际应用有关的位运算符:

  • AND(&)
  • XOR(^)
  • NOT(~)
  • OR(|)
  • 左/右移位(<< / >>)

1
我还记得当我第一次了解它们时,似乎它们只用于低级编程...但自那以后,它们已经进入我的工具箱,我经常使用它们,包括在进行“高级”编程时。现在它们对我来说就像+和*一样。 - Oak
2
@Anon:在我看来,现实世界应该指的是除了位运算符最明显的低级编程之外的任何东西。 - Olivier Lalonde
位运算的实际应用 - phuclv
可以使用二进制异或运算在排列中找到缺失的数字:https://www.martinkysel.com/codility-permmissingelem-solution/ - Janac Meena
41个回答

9

大约三分钟前,我刚刚使用位异或(^)计算了与PLC的串行通信的校验和...


9

在当今现代语言的抽象世界中,使用位运算的并不多。文件IO是一个容易想到的例子,但那只是在已经实现的东西上进行位运算操作,而不是实现使用位运算的东西。然而,作为一个简单的例子,以下c#代码演示如何去除文件的只读属性(以便可以使用新的FileStream并指定FileMode.Create):

//Hidden files posses some extra attibutes that make the FileStream throw an exception
//even with FileMode.Create (if exists -> overwrite) so delete it and don't worry about it!
if(File.Exists(targetName))
{
    FileAttributes attributes = File.GetAttributes(targetName);

    if ((attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly)
        File.SetAttributes(targetName, attributes & (~FileAttributes.ReadOnly));

    File.Delete(targetName);
}

至于定制实现,下面是一个最近的例子: 我为我们分布式应用程序的安装之间创建了一个“消息中心”,用于发送安全消息。基本上,它类似于电子邮件,包括收件箱、发件箱、已发送等,但它还具有保证交付和已读回执的功能,因此除了“收件箱”和“已发送”之外,还有其他附属子文件夹。这相当于要求我通用地定义“收件箱”或“已发送文件夹”中的内容。对于已发送文件夹,我需要知道哪些已读,哪些未读。对于未读的内容,我需要知道哪些已接收,哪些未接收。我使用这些信息构建一个动态的where子句,过滤本地数据源并显示适当的信息。
以下是枚举的组成方式:
    public enum MemoView :int
    {
        InboundMemos = 1,                   //     0000 0001
        InboundMemosForMyOrders = 3,        //     0000 0011
        SentMemosAll = 16,                  //     0001 0000
        SentMemosNotReceived = 48,          //     0011
        SentMemosReceivedNotRead = 80,      //     0101
        SentMemosRead = 144,                //     1001
        Outbox = 272,                       //0001 0001 0000
        OutBoxErrors = 784                  //0011 0001 0000
    }

你看到这段代码的作用了吗?通过使用"与"(&)运算符与"Inbox"枚举值进行"与"运算,我知道"InboundMemosForMyOrders"在收件箱中。

下面是构建并返回定义当前选定文件夹视图的过滤器方法的简化版本:

    private string GetFilterForView(MemoView view, DefaultableBoolean readOnly)
    {
        string filter = string.Empty;
        if((view & MemoView.InboundMemos) == MemoView.InboundMemos)
        {
            filter = "<inbox filter conditions>";

            if((view & MemoView.InboundMemosForMyOrders) == MemoView.InboundMemosForMyOrders)
            {
                filter += "<my memo filter conditions>";
            }
        }
        else if((view & MemoView.SentMemosAll) == MemoView.SentMemosAll)
        {
            //all sent items have originating system = to local
            filter = "<memos leaving current system>";

            if((view & MemoView.Outbox) == MemoView.Outbox)
            {
                ...
            }
            else
            {
                //sent sub folders
                filter += "<all sent items>";

                if((view & MemoView.SentMemosNotReceived) == MemoView.SentMemosNotReceived)
                {
                    if((view & MemoView.SentMemosReceivedNotRead) == MemoView.SentMemosReceivedNotRead)
                    {
                        filter += "<not received and not read conditions>";
                    }
                    else
                        filter += "<received and not read conditions>";
                }
            }
        }

        return filter;
    }

非常简单,但是这是一个非常整洁的实现,它处于一种通常不需要位运算的抽象层级。

你必须使用位运算吗?布尔标志似乎可以胜任。或者你可以使用枚举,例如 SENT_READ。 - Gilbert

9

这是一个从字节格式的位图图像中读取颜色的示例

byte imagePixel = 0xCCDDEE; /* Image in RRGGBB format R=Red, G=Green, B=Blue */

//To only have red
byte redColour = imagePixel & 0xFF0000; /*Bitmasking with AND operator */

//Now, we only want red colour
redColour = (redColour >> 24) & 0xFF;  /* This now returns a red colour between 0x00 and 0xFF.

I hope this tiny examples helps....


在这种情况下,将redColour右移24位,怎么可能得到除了0以外的结果呢? - undefined

9
通常位运算比乘除法更快。因此,如果您需要将变量x乘以9,则可以使用x<<3 + x,而不是x*9,这样可以节省几个周期。如果此代码在ISR中,则可以节省响应时间。
同样,如果您想要使用数组作为循环队列,使用位运算处理环绕检查会更快(更优雅)。(数组大小应该是2的幂)。例如:如果您想要插入/删除,则可以使用tail = ((tail & MASK) + 1)代替tail = ((tail +1) < size) ? tail+1 : 0
另外,如果您想要一个错误标志来保存多个错误代码,则每个位都可以保存单独的值。您可以将其与每个单独的错误代码进行AND操作以进行检查。这在Unix错误代码中使用。
此外,n位位图可以是一种非常酷且紧凑的数据结构。如果您想要分配大小为n的资源池,我们可以使用n位来表示当前状态。

7

按位与运算符用于掩码/提取字节的某个部分。

1字节变量

 01110010
&00001111 Bitmask of 0x0F to find out the lower nibble
 --------
 00000010

特别是移位运算符(<< >>)经常用于计算。

5
似乎没有人提到固定点数学。(是的,我已经老了,好吧?)

5

位运算符对于长度为2的幂次方的数组循环非常有用。正如许多人所提到的,位运算符非常有用,并且在标志、图形、网络和加密中使用。不仅如此,它们还非常快速。我个人最喜欢的用途是在没有条件语句的情况下循环一个数组。假设您有一个以零为基础的数组(例如,第一个元素的索引为0),并且您需要无限循环它。无限循环是指从第一个元素到最后一个元素,然后返回到第一个元素。一种实现方法是:

int[] arr = new int[8];
int i = 0;
while (true) {
    print(arr[i]);
    i = i + 1;
    if (i >= arr.length) 
        i = 0;
}

这是最简单的方法,如果您想避免使用if语句,您可以使用取模方法,如下所示:
int[] arr = new int[8];
int i = 0;
while (true) {
    print(arr[i]);
    i = i + 1;
    i = i % arr.length;
}

这两种方法的缺点是模运算符很昂贵,因为它在整数除法后寻找余数。而第一种方法在每次迭代中都会运行一个语句。然而,使用位运算符,如果您的数组长度是2的幂,则可以轻松地通过使用&(按位与)运算符生成类似于0 .. length-1 的序列,像这样:i & length。因此,了解这一点后,上面的代码变成了:
int[] arr = new int[8];
int i = 0;
while (true){
    print(arr[i]);
    i = i + 1;
    i = i & (arr.length - 1);
}

这是它的运作原理。在二进制格式中,每个2的幂减去1后所得到的数字都只由1组成。例如,3的二进制为11,7的二进制为111,15的二进制为1111等等,你懂的。现在,如果你将任何数字与一个二进制全为1的数字进行&运算,会发生什么呢?假设我们这样做:
num & 7;

如果num小于或等于7,则结果将为num,因为与1进行&运算的每个位都是它本身。如果num大于7,则在&操作期间计算机会考虑7的前导零,这些前导零在&操作后当然仍然保持为零,只有尾部部分将保留。如在二进制中9 & 7的情况下,它看起来像:
1001 & 0111

结果将是0001,它在十进制中表示为1,并定位于数组的第二个元素。

你在文本中提到的“如果数组长度是2的幂”的备注应该放在第一句话中。这个技巧的使用有非常严重的限制。就我个人而言,我不会采用这种方法,因为代码比使用“if”或“mod”更难理解。 - Jan Doggen
@JanDoggen 您是正确的,我会把它放在第一句话中。至于数组是否为2的幂,在我的经验中,有很多次这种方法都奏效了。可能是因为它与网络和图形有关。 - user3552161
1
这篇文章的重点是展示位运算符在生成数字序列时非常有用,0、1、2、3、0、1、2... 这个序列只是我想到的第一个。 - user3552161

4

判断数字x是否为2的幂?(在算法中很有用,例如计数器递增时,只需执行对数次操作)

(x & (x - 1)) == 0

一个整数x的最高位是什么?(例如,这可以用于查找大于x的最小2的幂)
x |= (x >>  1);
x |= (x >>  2);
x |= (x >>  4);
x |= (x >>  8);
x |= (x >> 16);
return x - (x >>> 1); // ">>>" is unsigned right shift

如何找到整数 x 的最低位的 1?(这有助于找到可以被2整除的次数。)

x & -x

在C语言中,无符号右移是通过将左操作数强制转换为无符号类型来完成的。您最后的公式并不能找到最低位[集合],它只是检查X是否是2的幂。要找到最低位集合,请执行x&-x - Potatoswatter
嗯,你说得对,不知怎么回事,我用第一个片段替换了x和-x,感谢编辑。 - Dimitris Andreou

4

Base64编码是一个例子。Base64编码用于将二进制数据表示为可打印字符,以便通过电子邮件系统(和其他目的)发送。Base64编码将一系列8位字节转换为6位字符查找索引。位操作,移位,与运算,或运算,非运算非常有用,可以实现Base64编码和解码所需的位操作。

当然,这只是无数例子中的1个。


4

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接