【整活】C++不用加减乘除模运算符实现加减乘除模

Tuesday, February 21, 2023
本文共1205字
3分钟阅读时长

⚠️本文是作者P3troL1er原创,首发于https://peterliuzhi.top/posts/%E6%95%B4%E6%B4%BBc++%E4%B8%8D%E7%94%A8%E5%8A%A0%E5%87%8F%E4%B9%98%E9%99%A4%E6%A8%A1%E8%BF%90%E7%AE%97%E7%AC%A6%E5%AE%9E%E7%8E%B0%E5%8A%A0%E5%87%8F%E4%B9%98%E9%99%A4%E6%A8%A1/。商业转载请联系作者获得授权,非商业转载请注明出处!

Always tell the truth. That way, you don’t have to remember what you said. — Mark Twain

最近在上数字逻辑,想起以前做过的一道算法题(不用加法符实现两数相加),突发奇想想要纯用位运算实现一下加减乘除模

以前最多实现加减乘,对除法一直没想到纯粹位运算的方式,今天算是“圆梦”了

注意,我写的这几个函数只能用于整数(包括int、long、long long还有他们的unsigned形式),使用C++主要是可以使用模板实现泛型,如果纯用C可能就需要转成void*之类的比较复杂的操作(或者使用宏中的##连接符实现重载)

下面是具体的代码,如果您发现了bug可以在下面的评论告诉我~

template <typename NUM>
NUM add(NUM a, NUM b){
    while (b != 0)
    {
        // 筛选出a中位为1的同时b中位为1,然后左移一位当作进位
        NUM tmp = (a & b) << 1;
        a ^= b;
        b = tmp;
    }
    return a;
}

template <typename NUM>
NUM minus(NUM a, NUM b){
    while (b != 0)
    {
        // 筛选出a中位为0的同时b中位为1,然后左移一位当作进位
        NUM tmp = ((~a) & b) << 1;
        a ^= b;
        b = tmp;
    }
    return a;
}

template <typename NUM>
NUM multiply(NUM a, NUM b){
    bool sign = false;
    if (a < 0)
    {
        sign = !sign;
        a = add(~a, 1);
    }
    if (b < 0)
    {
        sign = !sign;
        b = add(~b, 1);
    }
    // 有一个等于0直接返回
    if (a == 0 || b == 0)
    {
        return 0;
    }
    
    NUM sum = 0; // 记录结果
    char cnt = 0; // 记录左移多少位
    while (b != 0)
    {
        // 如果这位是1,就要将a左移cnt位并加上
        if (b & 0x1)
        {
            sum = add(sum, a << cnt);
        }
        b >>= 1;
        ++cnt;
    }
    // 处理符号
    if (sign)
    {
        return add(~sum, 1);
    }else
    {
        return sum;
    }
}

template <typename NUM>
NUM* basic_divide(NUM a, NUM b, NUM* ret){
    if (a == 0)
    {
        ret[0] = 0;
        ret[1] = 0;
        return ret;
    }else if (b == 0)
    {
        ret[0] = a;
        ret[1] = 0;
        std::cerr << "dividing 0!\n";
        return ret;
    }
    
    bool sign = false;
    if (a < 0)
    {
        sign = !sign;
        a = add(~a, 1);
    }
    if (b < 0)
    {
        sign = !sign;
        b = add(~b, 1);
    }
    NUM result = 0;
    NUM max = b;
    char cnt = 1;
    while ((b << 1) < a)
    {
        b <<= 1;
        ++cnt;
    }
    
    while (cnt > 0)
    {
        if (a >= b)
        {
            a = minus(a, b);
            result = (result << 1) ^ 0x1;
        }else
        {
            result <<= 1;
        }
        b >>= 1;
        --cnt;
    }
    if (sign)
    {
        ret[0] = add(~result, 1);
        ret[1] = add(~a, 1);
    }else
    {
        ret[0] = result;
        ret[1] = a;
    }
    return ret;
}

template <typename NUM>
NUM divide(NUM a, NUM b){
    NUM result[2] {0};
    return basic_divide(a, b, result)[0];
}

template <typename NUM>
NUM mod(NUM a, NUM b){
    NUM result[2] {0};
    return basic_divide(a, b, result)[1];
}

由于模板函数的定义和声明不能分开,所以这些代码都得放在头文件中,当然也可以放在cpp文件中然后在头文件包含它:

#ifndef __ARITH_ARITH_H__
#define __ARITH_ARITH_H__
#include "arith.cpp"
#endif /* __ARITH_ARITH_H__ */

加法的思路就是用且运算获取同为1的位,然后左移一位作为进位,一直while循环直到进位为0

减法的思路就是用复杂的位运算获取被减数为0而减数为1的位,然后左移一位作为借位,一直while循环直到借位为0

乘法的思路就是将其中一个乘数看作数个权值的和,比如3 = 0b11 = 2^0 + 2^1,而一个数乘以2的n次方可以看作向左移动n位,然后将他们全部加起来就是结果了(乘法很可能溢出……在汇编中mul指令会把结果放在两个寄存器中,我比较懒,这么搞比较复杂,就没有搞)

除法的思路就是左移除数让其刚好再移一位就大于被除数,然后被除数减去它,并在结果推进一个1,这时候被除数的总位数一定会少一位(因为除数*2 > 被除数,那么被除数 - 除数一定会小于被除数 » 1),然后让除数右移一位,如果此时被除数小于除数,就在结果推进一个0,然后再右移除数……本质上是模拟竖式除法运算。这个思路可以一次性得出结果和余数。

笔者的思路可能不是最高效的,也可能有一定问题,如果您读完本文及代码,有一些疑问或建议,欢迎在评论区指出!谢谢!