游乐游手机版
首页/AI热点日报/热点详情

计算机系统数值类型编码运算转换原理解析

类型:热点整理2026-07-12
计算机系统采用补码编码表示有符号整数,位运算包括左移、逻辑右移和算术右移。理解数值类型的二进制表示和类型转换原理,对避免运算错误及防范因类型转换引发的内存越界等安全漏洞至关重要。

前言:深入理解数值类型,避免编程陷阱

在日常编程中,数值类型numeric types)是我们打交道最多的数据类型,可能没有之一。除了最熟悉的 int,还有 longfloatdouble 等。正因太熟悉,我们往往不会深究它们的底层原理。因为平时的工作中,知道个大概,也够用了。

但,在某些业务场景下,比如金融业务,数值运算不准确会带来灾难性的后果。这时,你就必须清楚数值类型的二进制表示、截断、转型等原理,否则很难保证运算结果的正确性。

另外,数值类型也是一个容易被黑客攻击的点,考虑如下一段代码:

// C++
/* Declaration of library function memcpy */
void *memcpy(void *dest, void *src, size_t n);
/* Kernel memory region holding user-accessible data */
#define KSIZE 1024
char kbuf[KSIZE];
/* Copy at most maxlen bytes from kernel region to user buffer */
int copy_from_kernel(void *user_dest, int maxlen) {
    /* Byte count len is minimum of buffer size and maxlen */
    int len = KSIZE < maxlen ? KSIZE : maxlen;
    memcpy(user_dest, kbuf, len);
    return len;
}

如果你熟悉数值类型的原理,一定会敏锐察觉出第 10 行存在 intsize_t 的类型转换。在 64 位系统中,size_t 通常被定义为 unsigned long 类型,如果攻击者在调用 copy_from_kernel 时,特意传入一个负数的 maxlen,转型到 memcpy 中的 n 将会是一个很大的正数,从而导致了内存拷贝的越界!

数值类型是计算机编程的基础,用的很多,也很重要,理解它的底层原理,有助于写出正确的代码,避免一些意料之外的错误

每个计算机系统都有固定的 word size ,也即常说的 xx 位,它也是指针的大小,跟虚拟内存相关,比如一个 w 位系统上的应用程序,最多能够访问 2^w byte 大小的虚拟内存。

最常用的是 32 位和 64 位系统,某些数值类型在它们之上会有些差异,比如 long 类型在 32 位系统上是 32 bit 大小,在 64 位系统上是 64 bit 大小。 考虑如今 64 位系统逐渐成为主流,本文会以它作为基础,进行数值类型的介绍

小提示: 当你在不同平台上编写可移植代码时,务必注意 long 等类型的大小差异。使用 int32_tint64_t 等固定宽度类型更安全。

整数:二进制编码与取值范围详解

在计算机系统中,整数可以分成 无符号unsigned)整数和 有符号signed)整数两大类,这之下,按照类型表示的 bit 位大小,又可细分成 8 位的 char/byte/int8、16 位的 short/int16、32 位的 int/int32 和 64 位的 long/int64,它们的取值范围如下:

类型最小值最大值
[signed] char-128127
unsigned char0255
short-32,76832,767
unsigned short065,535
int−2,147,483,6482,147,483,647
unsigned int04,294,967,295
long−9,223,372,036,854,775,8089,223,372,036,854,775,807
unsigned long018,446,744,073,709,551,615

死记这个表不容易,下面我们将试图从二进制编码层面去理解它。

二进制编码:计算机如何存储整数

整数在计算机系统上都是以二进制存储的,对于一个 w 位的整数,它的二进制表示写成:

x = x_{w-1} x_{w-2} ... x_1 x_0,其中 x_i 取值 0 或 1。

无符号编码(Unsigned Encodings)

在二进制表示的基础上,无符号编码是这样:

B2U(x) = Σ_{i=0}^{w-1} x_i * 2^i

比如,w = 4 场景下的一些例子:

由上述可知,无符号编码无法表示负数,因此只能表示无符号整数。为了表示有符号整数,还要探寻另一种编码方式。

原码编码(True Form Encodings)

为了区分正数和负数,很容易想到使用一个 bit 位作为 符号位,0 表示正数,1 表示负数。在无符号编码的基础上,使用最高位作为符号位,其他位含义不变,得出 原码编码 形式:

B2T_original(x) = (-1)^{x_{w-1}} * Σ_{i=0}^{w-2} x_i * 2^i

比如,w = 4 场景下的一些例子:

虽然原码编码方式简单直观,但它还存在两个问题:

  • (1)存在两种编码形式
    原码编码方式下,0 存在两种编码形式,0000 和 1000。同一个整数值,有两种编码,对计算机系统来说没什么意义,反而是一种浪费。
  • (2)带负数的加法运算不正确
    原码编码方式下,两个正数的加法没问题,一旦带上负数,结果就出错了:

所以,原码编码方式,注定不会被使用。

补码编码(Two's-complement Encodings)

于是,补码编码被发明,它也是建立在无符号编码的基础上,仍然取最高位为符号位,编码方式是这样:

B2T(x) = -x_{w-1} * 2^{w-1} + Σ_{i=0}^{w-2} x_i * 2^i

它与无符号编码的唯一区别是,最高位的取值从 +2^{w-1} 变成了 -2^{w-1}

比如,w = 4 场景下的一些例子:

补码编码很巧妙地解决了原码编码的两个问题:

  • 首先,0 在补码编码下只有一种编码形式,即 0000。
  • 此外,带负数的加法运算,也正确了。

因为补码编码的简单和正确性,目前,几乎所有的计算机系统,都采用补码编码来表示有符号整数

小提示: 补码编码中,一个 w 位有符号整数的范围是 [-2^{w-1}, 2^{w-1}-1]。例如 32 位 int 的范围是 -2147483648 到 2147483647,最小值比最大值绝对值大 1,正是因为这个最高位的权重是负数。

位运算:取反、与、或、异或与移位操作

位运算主要包含 取反异或移位 等几种,我们在业务开发时用得比较少,但如果你有阅读开源代码的习惯,就会经常发现它们的踪迹。如果碰巧对位运算不熟悉,那么阅读这些代码,就同读天书一般。

取反(~)、与(&)、或(|)、异或(^)的规则比较简单:

移位运算,可以分成 左移右移 两种,其中,右移又可分为 逻辑右移算术右移

左移(<<)运算,是对二进制整数,向左移若干位,高位丢弃,低位补零。也即,对 x 左移 k 位,得到 x * 2^k

比如,对 int i = -1 左移 10 位,会得到 i = -1024 的结果:

// Ja va语言
public static void main(String[] args) {
    int i = -1;
    System.out.println("Before << , i's value is " + i);
    System.out.println("i's binary string is " + Integer.toBinaryString(i));
    i <<= 10;
    System.out.println("After << , i's value is " + i);
    System.out.println("i's binary string is " + Integer.toBinaryString(i));
}
// 输出结果:
Before << , i's value is -1
i's binary string is 11111111111111111111111111111111
After << , i's value is -1024
i's binary string is 11111111111111111111110000000000

在 C/C++ 中,两种右移操作符都是 >>,对无符号整数用的是逻辑右移,对有符号整数用的是算术右移;在 Ja va 中,逻辑右移的操作符是 >>>,算术右移的操作符是 >>。为了方便区分,下文统一用 Ja va 的表示方法。

逻辑右移(>>>)运算,是对二进制整数,向右移若干位,高位补零,低位丢弃。也即,对 x 逻辑右移 k 位,得到 x / 2^k(无符号除法)。

比如,对 int i = -1 逻辑右移 10 位,会得到 i = 4194303 的结果:

// Ja va语言
public static void main(String[] args) {
    int i = -1;
    System.out.println("Before >>> , i's value is " + i);
    System.out.println("i's binary string is " + Integer.toBinaryString(i));
    i >>>= 10;
    System.out.println("After >>> , i's value is " + i);
    System.out.println("i's binary string is " + Integer.toBinaryString(i));
}
// 输出结果:
Before >>> , i's value is -1
i's binary string is 11111111111111111111111111111111
After >>> , i's value is 4194303
i's binary string is 1111111111111111111111

算术右移(>>)运算,是对二进制整数,向右移若干位,高位补符号位,低位丢弃。也即,对 x 算术右移 k 位,得到 floor(x / 2^k)(向负无穷取整)。

比如,对 int i = -1 算术右移 10 位,仍会得到 i = -1 的结果:

// Ja va语言
public static void main(String[] args) {
    int i = -1;
    System.out.println("Before >> , i's value is " + i);
    System.out.println("i's binary string is " + Integer.toBinaryString(i));
    i >>= 10;
    System.out.println("After >> , i's value is " + i);
    System.out.println("i's binary string is " + Integer.toBinaryString(i));
}
// 输出结果:
Before >> , i's value is -1
i's binary string is 11111111111111111111111111111111
After >> , i's value is -1
i's binary string is 11111111111111111111111111111111

目前为止,介绍移位运算的原理时,我们都默认 k < w,如果 k >= w 会怎样?

比如,x左移 w 位,结果会是 0 吗:

// Ja va语言
public static void main(String[] args) {
    int i1 = -1;
    System.out.println("Before << 31, i1's value is " + i1);
    System.out.println("i1's binary string is " + Integer.toBinaryString(i1));
    i1 <<= 31;
    System.out.println("After << 31, i1's value is " + i1);
    System.out.println("i1's binary string is " + Integer.toBinaryString(i1));
    int i2 = -1;
    System.out.println("Before << 32, i2's value is " + i2);
    System.out.println("i2's binary string is " + Integer.toBinaryString(i2));
    i2 <<= 32;
    System.out.println("After << 32, i2's value is " + i2);
    System.out.println("i2's binary string is " + Integer.toBinaryString(i2));
}
// 输出结果:
Before << 31, i1's value is -1
i1's binary string is 11111111111111111111111111111111
After << 31, i1's value is -2147483648
i1's binary string is 10000000000000000000000000000000
Before << 32, i2's value is -1
i2's binary string is 11111111111111111111111111111111
After << 32, i2's value is -1
i2's binary string is 11111111111111111111111111111111

上述例子中,w = 32,我们发现 k = 31 时,结果还符合预期;当 k = 32 时,结果不是 0,而是 -1,也即相当于 k = 0 时的结果。

原因是这样,w 位整数 x,当执行 x << k 时,实际执行的是 x << (k % w)。所以,当 i2 << 32 时,实际是 i2 << 32 % 32 = i2 << 0

右移操作也遵循同样的规则,也即 x >> k = x >> (k % w)x >>> k = x >>> (k % w)

小提示: 在编写可移植代码时,尽量避免移位量等于或超过类型的位宽,因为不同语言/硬件的行为可能不符合预期。推荐在移位前用 k % w 手动取模处理。

常见问题:补码、移位与编码疑虑

  • Q1:为什么补码编码能表示负数且加法正确?
    A1:补码的最高位权重为负数(-2^{w-1}),这使得正数和负数可以统一在一个线性表示中。加法器电路可以按无符号加法处理,结果自然正确(只需忽略进位)。
  • Q2:左移 32 位为什么等于没移?
    A2:因为 Ja va/C 等语言对移位操作会先对移动位数取模 w(w 为位宽)。对于 32 位 int,32 % 32 = 0,所以相当于没移动。这是为了提高硬件效率。
  • Q3:算术右移和逻辑右移的区别是什么?什么时候用哪个?
    A3:算术右移高位补符号位(正数补0,负数补1),用于有符号整数除法;逻辑右移高位补0,用于无符号整数或位操作。Ja va中 >> 是算术右移,>>> 是逻辑右移。
  • Q4:原码编码为什么不被采用?
    A4:原码有两个缺点:0 有两种表示(浪费编码空间);带负数的加法需要特殊处理,否则结果错误。补码完美解决了这两个问题。

结尾:掌握数值类型,写出健壮代码

数值类型虽然基础,但其背后的二进制编码和位运算原理却蕴含着精巧的设计。理解这些底层机制,不仅能帮助你避免诸如溢出、类型转换错误等常见陷阱,还能在阅读高性能代码或进行安全审计时更加得心应手。希望这篇教程能够让你对数值类型有一个更深入、更系统的认识,在未来的编程实践中写出更健壮、更高效的代码。

来源:https://m.elecfans.com/article/2077205.html

相关热点

继续查看同栏目近期热点。

延伸阅读

补充最近整理过的热点入口。