找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1618

积分

0

好友

206

主题
发表于 5 天前 | 查看: 28| 回复: 0

在编程中,我们有时会遇到一些看似矛盾的现象。例如,在 C/C++ 语言中,-3>>1 的结果是 -2,而 3>>1 的结果是 1。但如果直接进行除法运算 -3/2,结果却是 -1。为何针对同一数值的“除以2”操作,右移和除法会得到不同的结果?这背后涉及计算机底层对整型数据的存储与运算规则。

补码:负数在计算机中的形态

要理解这个现象,首先需要知道负数在计算机中如何存储。我们通常使用的是补码表示法。简单来说,一个负数的补码,等于其绝对值的二进制表示“按位取反后再加一”。

-3 为例,我们可以用一小段代码来查看其在内存中的十六进制形态:

int main()
{
    int n = -3;
    printf("0x%x",n);
}

这段代码的输出结果是:

0xfffffffd

这个 0xfffffffd 就是 -3 在 32 位有符号整型中的补码形式。为什么采用这种形式?这源于计算机运算的溢出特性。例如,1 的十六进制是 0x1,为了让 1-1 相加等于 0,且考虑到 32 位寄存器中超出范围的进位会被丢弃,-1 就可以表示为 0xffffffff(因为 0xffffffff + 0x1 = 0x100000000,高位的 1 被丢弃,结果为 0)。

基于这个规则,32 位有符号整数的取值范围被分为两部分:正数从 00x7fffffff,负数从 0x800000000xffffffff。其中最高位(bit 31)被用作符号位:为 0 表示正数,为 1 表示负数。这也是补码设计带来的一个直观规律。

整型数的移位运算:为何 -3>>1 等于 -2

右移运算符(>>)的行为在有符号数和无符号数上有所不同。为了确凿地了解其过程,最直接的方法是观察编译器生成的汇编代码。这个过程通常分为三步:

  1. 编写测试用的 C 代码。
  2. 使用编译器(如 ARM 交叉编译工具链)编译该代码。
  3. 反汇编生成的可执行文件,查看对应的汇编指令。

我们编写以下测试函数,分别对有符号数和无符号数进行右移操作:

#include<stdio.h>
int shift(int a, int b)
{
    return (a >> b);
}

unsigned int shift_u(unsigned int a, unsigned int b)
{
    return (a >> b);
}

main(){
    int a = shift(-3, 1);
    unsigned int b = shift_u(3, 1);
    printf("[%d][%u]",a,b);
}

编译并反汇编后,可以看到核心的汇编指令差异:

  • 对于有符号数右移,编译器使用了 asr.w(算术右移)指令。
  • 对于无符号数右移,编译器使用了 lsr.w(逻辑右移)指令。

这两条指令的关键区别在于对符号位的处理。根据 ARM 官方文档的说明,asr(算术右移)指令在移位后,会将原始数值的最高位(即符号位)拷贝到结果的高位。而 lsr(逻辑右移)指令则简单地用 0 填充高位。

因此,-3 (0xfffffffd) >> 1 的过程如下:

  1. 0xfffffffd 的二进制位整体右移一位,得到 0x7ffffffe
  2. 由于原始符号位是 1asr 指令会将这个 1 填充回结果的最高位,最终得到 0xfffffffe

查看补码表可知,0xfffffffe 正是 -2 的补码形式。这就解释了为什么有符号数右移是“向下取整”(-1.5 向下取整为 -2)。

整型数的除法运算:为何 -3/2 等于 -1

既然右移可以实现除以 2 的幂次,那么为什么编译器不直接用移位来优化 -3/2 呢?因为 C 语言标准规定,整数除法应向零截断(即 truncate toward zero),这与右移的向下取整规则不同。

再次通过汇编来验证。编写除法测试代码:

#include<stdio.h>

int div(int a, int b)
{
    return (a / b);
}

unsigned int div_u(unsigned int a, unsigned int b)
{
    return (a / b);
}

main(){
        int a = div(-3, 2);
        unsigned int b = div_u(3, 2);
        printf("[%d][%d]",a,b);
}

在现代 ARMv8 架构上,反汇编可能直接看到 sdiv(有符号除)和 udiv(无符号除)指令。为了看清内部逻辑,可以观察 ARMv7 等没有硬件除法指令的架构生成的代码,编译器会用软件例程实现除法。

分析 __divsi3(有符号整型除法)例程的关键部分,可以发现其对有符号除法的处理流程如下:

  1. 记录符号:通过异或运算,记录被除数与除数的符号是否相同。
  2. 转为正数:将被除数和除数都转换为它们的绝对值(正数形式)。
  3. 执行无符号除法:调用与无符号数相同的除法算法,得到正数的商。
  4. 恢复符号:根据第一步记录的符号信息,如果原被除数与除数符号不同(即结果为负),则将商转换为负数。

因此,-3 / 2 的计算步骤是:

  1. 判断符号不同,结果应为负。
  2. -32 转换为正数 32
  3. 计算 3 / 2,得到商 1
  4. 由于结果应为负,将 1 转换为 -1

这就实现了“向零取整”(-1.5 向零截断为 -1)。一个边界情况是 0x80000000(即 -2147483648),它取负后仍是自身。按照上述流程,0x80000000 / 2 会先将 0x80000000 当作无符号数 2147483648 进行除以 2 操作,得到 0x400000001073741824),然后恢复负号,得到结果 -1073741824

总结

-3>>1-3/2 结果不同的根本原因在于二者遵循不同的数学规则:

  • 有符号数右移 (>>) :是算术右移,它保留符号位,结果等价于向下取整(floor)。
  • 有符号数除法 (/) :在 C/C++ 标准中规定为向零取整(truncate toward zero)。

这种差异源于底层指令的不同设计目标。移位是位操作,直接作用于二进制补码;而除法是算术运算,需要符合高级语言定义的数学语义。理解这些计算机基础原理,尤其是在涉及底层优化或跨平台开发时,能帮助开发者避免细微的错误,写出更稳健的代码。对于想深入探究汇编实现细节,例如除法算法如何用移位和加法模拟的读者,可以进一步研究相关C/C++编译器的底层库实现。

参考资料

[1] 代码里-3>>1是-2但3>>1是1,-3/2却又是-1,为什么?, 微信公众号:mp.weixin.qq.com/s/kblipkg3zujm-xFZI80pxw

版权声明:本文由 云栈社区 整理发布,版权归原作者所有。




上一篇:架构、性能、成本,千问Qwen 3.5如何实现大模型“三赢”?
下一篇:QBDI深度解析:动态二进制插桩原理与Arm64实战
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-2-23 10:26 , Processed in 0.922726 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表