摘要 :把
-5存进int,按字节读出来是fb ff ff ff;把0x11223344存进去,逐字节读出来是44 33 22 11。前者是补码,后者是字节序。本文从进制转换讲起,说明原码、反码、补码和取值范围,再讲大小端、浮点数的位布局、移位与位运算,最后用权限标志和统计二进制 1 的个数把位运算落到代码里。
文章目录
- [1. 同一个数的四种写法](#1. 同一个数的四种写法)
-
- [1.1 按权重展开](#1.1 按权重展开)
- [1.2 2 进制转 8 进制、16 进制](#1.2 2 进制转 8 进制、16 进制)
- [2. 负数存的是补码](#2. 负数存的是补码)
-
- [2.1 补码为什么能统一加减法](#2.1 补码为什么能统一加减法)
- [2.2 整型提升与截断](#2.2 整型提升与截断)
- [3. 多字节数据怎么排](#3. 多字节数据怎么排)
-
- [3.1 小端和大端](#3.1 小端和大端)
- [3.2 判断当前机器的字节序](#3.2 判断当前机器的字节序)
- [3.3 数据放在哪](#3.3 数据放在哪)
- [4. 浮点数怎么存](#4. 浮点数怎么存)
- [5. 移位](#5. 移位)
-
- [5.1 左移](#5.1 左移)
- [5.2 右移](#5.2 右移)
- [5.3 优先级](#5.3 优先级)
- [6. 位运算](#6. 位运算)
- [7. 位运算的四个用途](#7. 位运算的四个用途)
-
- [7.1 交换两个整数](#7.1 交换两个整数)
- [7.2 统计二进制中 1 的个数](#7.2 统计二进制中 1 的个数)
- [7.3 置位、清位、切换、检查](#7.3 置位、清位、切换、检查)
- [7.4 权限标志位](#7.4 权限标志位)
- [8. 优先级与常见误写](#8. 优先级与常见误写)
- [9. 小结](#9. 小结)
1. 同一个数的四种写法
2 进制、8 进制、10 进制、16 进制只是同一个数值的不同表示。以 15 为例:
text
15 的 2 进制:1111
15 的 8 进制:17
15 的 10 进制:15
15 的 16 进制:F
2 进制满 2 进 1,每一位只能是 0 或 1;8 进制满 8 进 1,每一位是 0 到 7;16 进制满 16 进 1,10 到 15 用 A 到 F 表示。
C 语言里的写法有前缀约定:0x 开头是十六进制,0 开头是八进制,没有前缀是十进制。010 的值是 8,不是 10;0x10 的值是 16。
c
#include <stdio.h>
int main(void)
{
int n = 15;
printf("%d %o %x\n", n, n, n);
printf("%d %d\n", 0x6b, 0153);
return 0;
}
输出:
text
15 17 f
107 107
%d 按十进制打印,%o 按八进制打印,%x 按十六进制打印。0x6b 和 0153 是同一个数 107 的两种写法。
1.1 按权重展开
任意进制都可以按权重展开:
N = ∑ i = 0 k d i × b a s e i N = \sum_{i=0}^{k} d_i \times base^{i} N=i=0∑kdi×basei
2 进制从右往左的权重是 2 0 , 2 1 , 2 2 , ... 2^0, 2^1, 2^2, \dots 20,21,22,...:
text
1101 = 1×2³ + 1×2² + 0×2¹ + 1×2⁰ = 8 + 4 + 0 + 1 = 13
10 进制转 2 进制用「除 2 取余,逆序读」。以 123 为例:
text
123 ÷ 2 = 61 余 1
61 ÷ 2 = 30 余 1
30 ÷ 2 = 15 余 0
15 ÷ 2 = 7 余 1
7 ÷ 2 = 3 余 1
3 ÷ 2 = 1 余 1
1 ÷ 2 = 0 余 1
余数从下往上读是 1111011,验算 64 + 32 + 16 + 8 + 2 + 1 = 123 64 + 32 + 16 + 8 + 2 + 1 = 123 64+32+16+8+2+1=123。
1.2 2 进制转 8 进制、16 进制
8 进制每一位最大是 7,二进制 111 就够表示,所以从右往左每 3 位分一组;16 进制每一位最大是 15,二进制 1111 就够表示,所以每 4 位分一组。高位不够一组时补 0。

01101011 按 4 位分组是 0110 1011,对应 6B,即 0x6B;按 3 位分组是 001 101 011,对应 153,即 0153。反向转换就是把每一位拆成 3 位或 4 位。
| 10 进制 | 2 进制 | 8 进制 | 16 进制 |
|---|---|---|---|
| 0 | 0000 | 0 | 0 |
| 1 | 0001 | 1 | 1 |
| 2 | 0010 | 2 | 2 |
| 3 | 0011 | 3 | 3 |
| 4 | 0100 | 4 | 4 |
| 5 | 0101 | 5 | 5 |
| 6 | 0110 | 6 | 6 |
| 7 | 0111 | 7 | 7 |
| 8 | 1000 | 10 | 8 |
| 9 | 1001 | 11 | 9 |
| 10 | 1010 | 12 | A |
| 11 | 1011 | 13 | B |
| 12 | 1100 | 14 | C |
| 13 | 1101 | 15 | D |
| 14 | 1110 | 16 | E |
| 15 | 1111 | 17 | F |
这张表值得记熟。读内存、看位掩码、写十六进制常量时,基本都是在这 16 个值之间来回换。
2. 负数存的是补码
有符号整数的二进制表示有三种:原码、反码、补码。最高位是符号位,0 表示正,1 表示负,其余是数值位。
正整数的原码、反码、补码都相同。负数的规则是:
- 原码:符号位为 1,数值位写绝对值。
- 反码:符号位不变,数值位按位取反。
- 补码:反码加 1。
以 8 位的 -5 为例:
| 表示 | 二进制 | 十六进制 |
|---|---|---|
| 原码 | 1000 0101 |
0x85 |
| 反码 | 1111 1010 |
0xFA |
| 补码 | 1111 1011 |
0xFB |

在 VS2026 和常见的 x86/x64 平台上,有符号整数在内存里存的是补码。32 位的 -5 补码是 11111111 11111111 11111111 11111011,写成十六进制就是 0xFFFFFFFB。
c
#include <stdio.h>
int main(void)
{
int n = -5;
printf("%x\n", (unsigned)n);
unsigned char *p = (unsigned char *)&n;
for (int i = 0; i < 4; i++)
{
printf("%02x ", p[i]);
}
printf("\n");
return 0;
}
输出:
text
fffffffb
fb ff ff ff
%x 把 n 按无符号十六进制打印,得到 fffffffb。逐字节读出来是 fb ff ff ff,最低地址放的是最低字节 fb,这是小端序,下一节会讲。
2.1 补码为什么能统一加减法
n 位补码把 2 n 2^n 2n 当作模,负数 x x x 存的是 2 n − ∣ x ∣ 2^n - |x| 2n−∣x∣。8 位的 -5 存成 256 − 5 = 251 256 - 5 = 251 256−5=251,也就是 0xFB;32 位的 -5 存成 2 32 − 5 2^{32} - 5 232−5,也就是 0xFFFFFFFB。
这样设计有三个直接结果:
- 0 只有一种表示。原码里
+0是0000、-0是1000,补码统一成0000。 - 负数比正数多一个。8 位补码的范围是 -128 到 127,
10000000按补码解读是 -128,它没有对应的 8 位原码。 - 减法可以变成加法。 a − b a - b a−b 等于 a a a 加上 − b -b −b 的补码,最高位的进位丢掉,结果正好正确。CPU 只需要加法器,不需要单独的减法电路。
| 类型(位宽) | 取值范围 | 备注 |
|---|---|---|
8 位有符号(signed char) |
-128 到 127 | 0x80 是 -128 |
8 位无符号(unsigned char) |
0 到 255 | |
32 位有符号(int) |
-2147483648 到 2147483647 | INT_MIN 到 INT_MAX |
32 位无符号(unsigned int) |
0 到 4294967295 | 0 到 UINT_MAX |
32 位补码的负数比正数多一个,所以 -INT_MIN 无法用 int 表示,属于有符号整数溢出,是未定义行为。求绝对值或取负时,要先确认不是 INT_MIN。
2.2 整型提升与截断
位运算和移位作用在整型提升(integer promotion)之后的类型上,不是操作数的原类型。char、short 这些窄于 int 的类型,参与运算前会先转成 int:有符号做符号扩展,无符号高位补 0。
c
#include <stdio.h>
int main(void)
{
unsigned char uc = 0xFF;
signed char sc = (signed char)0xFF;
printf("%d %d\n", (int)uc, (int)sc);
printf("%d\n", ~uc);
printf("%u\n", (unsigned char)~uc);
return 0;
}
输出:
text
255 -1
-256
0
uc 是 unsigned char,值 255;转成 int 时高位补 0,还是 255。sc 是 signed char,0xFF 按 8 位补码解读是 -1;转成 int 时做符号扩展,结果还是 -1。
~uc 先把 uc 提升为 int 的 0x000000FF,再取反得到 0xFFFFFF00,也就是 -256。想得到 8 位的结果,必须显式截断回 unsigned char,这时只保留低 8 位,结果是 0。
赋值或强制转换到窄类型时,规则是只保留低 n 位,高位丢弃。整型提升和常规算术转换的完整规则,在《操作符与表达式求值》一篇讲过,这里只需要记住:位运算的结果类型是提升后的类型。
3. 多字节数据怎么排
一个字节没有顺序问题。int、double 这类多字节数据在内存里占多个字节,就有「哪个字节放在低地址」的问题,这叫字节序(byte order)。
3.1 小端和大端
以 0x11223344 为例,地址从低到高:
| 地址 | 小端存储 | 大端存储 |
|---|---|---|
| 低地址 +0 | 44 |
11 |
| +1 | 33 |
22 |
| +2 | 22 |
33 |
| 高地址 +3 | 11 |
44 |

- 小端序(little-endian):低位字节放低地址,高位字节放高地址。
- 大端序(big-endian):高位字节放低地址,低位字节放高地址。
x86 和 x86-64 默认是小端,ARM 通常也跑小端。大端更符合人从高位读到低位的习惯,TCP/IP 协议规定的网络字节序(network byte order)就是大端;数据在网络上传输前要转成大端,收到后再转回主机字节序。
3.2 判断当前机器的字节序
判断当前机器是哪种字节序,取 int 值 1 的第一个字节即可:
c
#include <stdio.h>
int CheckSys(void)
{
int i = 1;
return *(unsigned char *)&i;
}
int main(void)
{
printf("%s\n", CheckSys() == 1 ? "小端" : "大端");
return 0;
}
输出:
text
小端
i 的 4 个字节是 01 00 00 00(小端)或 00 00 00 01(大端)。*(unsigned char *)&i 取最低地址的那 1 个字节,小端得到 1,大端得到 0。
字节序只描述「同一个对象内部的字节顺序」。不同变量之间的地址高低由内存分区和链接器决定,不能拿来类推字节序;地址还会被 ASLR 打乱,每次运行都可能不同。
3.3 数据放在哪
字节序讲的是同一对象内部的字节顺序,对象本身放在哪是另一件事。一个运行中的 C 程序,地址空间通常分成几块:
| 区域 | 存放内容 | 生命周期 | 说明 |
|---|---|---|---|
| 代码段(text) | 机器指令 | 整个运行期 | 只读 |
| 只读数据段(rodata) | 字符串字面量、const 全局变量 |
整个运行期 | 写入通常触发访问冲突 |
| 已初始化数据段(data) | 有初值的全局变量、静态变量 | 整个运行期 | 初值随可执行文件装入 |
| 未初始化数据段(bss) | 无初值或初值为 0 的全局变量、静态变量 | 整个运行期 | 加载时由系统清零 |
| 堆(heap) | malloc 等申请的内存 |
从申请到 free |
通常由低地址向高地址增长 |
| 栈(stack) | 局部变量、形参、返回地址 | 函数调用期间 | 通常由高地址向低地址增长 |
两个和本讲直接相关的结论:未初始化的全局变量是 0,因为 bss 段在加载时被清零;未初始化的局部变量是垃圾值,因为栈空间会被反复复用。地址的高低不能跨平台类推,ELF 和 Windows PE 的段排布不同,ASLR 还会打乱每次运行的地址。
大小端解决「一个对象内部的字节顺序」,内存分区解决「对象放在哪」,补码与位运算解决「每一位怎么解释」。三者合起来,才是完整的内存模型。动态内存的申请和释放,在《指针》一篇有完整说明。
4. 浮点数怎么存
整数按补码存,浮点数按 IEEE 754 的格式存。任意二进制浮点数可以写成:
V = ( − 1 ) S × M × 2 E V = (-1)^S \times M \times 2^E V=(−1)S×M×2E
其中 S S S 是符号位, M M M 是有效数字, E E E 是指数。float 和 double 的位分配不同:
| 类型 | 总位数 | 符号位 S | 指数 E | 有效数字 M |
|---|---|---|---|---|
float |
32 | 1 位 | 8 位 | 23 位 |
double |
64 | 1 位 | 11 位 | 52 位 |

以 9.0f 为例。 9 = 1001 2 = 1.001 × 2 3 9 = 1001_2 = 1.001 \times 2^3 9=10012=1.001×23,所以 S = 0 S = 0 S=0, E = 3 + 127 = 130 = 10000010 2 E = 3 + 127 = 130 = 10000010_2 E=3+127=130=100000102, M M M 的小数部分是 001。拼起来是:
text
0 10000010 00100000000000000000000
写成十六进制是 0x41100000。在小端机器上逐字节读出来是 00 00 10 41:
c
#include <stdio.h>
int main(void)
{
float f = 9.0f;
unsigned char *p = (unsigned char *)&f;
for (int i = 0; i < 4; i++)
{
printf("%02x ", p[i]);
}
printf("\n");
return 0;
}
输出:
text
00 00 10 41
同一块 4 字节内存,按 int 读是 1091567616,按 float 读是 9.0。类型决定这串位模式怎么解释:这是字节序和浮点格式要放在一起看的原因。
浮点数不能精确表示所有十进制小数。0.1f 存进去是一个近似值,比较两个浮点数时通常看差值是否小于一个误差范围,不要直接用 ==。float 大约有 7 位有效数字,double 大约有 15 到 16 位。
5. 移位
移位操作符只能用于整数,<< 是左移,>> 是右移。a << b 把 a 的补码位向左移动 b 位,a >> b 向右移动 b 位,a 本身不变。
5.1 左移
左移的规则是「左边丢弃,右边补 0」。对无符号类型,结果相当于乘以 2 b 2^b 2b,超出类型宽度的位丢掉,按模 2 n 2^n 2n 回绕。
c
#include <stdio.h>
int main(void)
{
int num = 10;
printf("%d %d\n", num << 1, num);
unsigned int u = 10;
printf("%u\n", u >> 1);
int neg = -8;
printf("%d %d\n", neg >> 1, (int)((unsigned)neg >> 1));
return 0;
}
输出:
text
20 10
5
-4 2147483644
10 << 1 是 20,num 本身还是 10。左移不会修改原变量,除非写 num <<= 1。
对有符号类型,左移的规则更严格:左操作数非负且结果不溢出时,结果等于乘以 2 b 2^b 2b;如果左操作数是负数,或者左移后超出 int 的表示范围,属于未定义行为。所以写移位时优先用无符号类型,或者先转成无符号再移。
5.2 右移
右移分两种:
- 逻辑右移:左边补 0,右边丢弃。
- 算术右移:左边补原来的符号位,右边丢弃。
无符号类型一定是逻辑右移,高位补 0。有符号类型在左操作数非负时,高位也补 0;左操作数为负时,标准把它交给实现定义,常见编译器在 x86 上做算术右移,高位补符号位。

上面的程序里,neg = -8,neg >> 1 得到 -4,说明当前环境做的是算术右移,高位补了符号位 1。把 neg 转成 unsigned 再右移,得到 2147483644,这是逻辑右移的结果。
移位还有两条边界:
- 移动的位数必须在 0 到类型宽度减 1 之间。
int是 32 位,移动 32 位或更多是未定义行为。 - 不要移动负数位。
num >> -1没有统一定义,不同编译器结果可能不同。
5.3 优先级
移位的优先级低于加减,高于关系运算。1 << 2 + 3 先算 2 + 3,再算 1 << 5,结果是 32;想先左移再加,必须写 (1 << 2) + 3,结果是 7。
位运算和比较混写也有同样的问题,下一节和最后一节会集中讲。
6. 位运算
位运算直接操作补码的每一位,操作数必须是整数。
| 操作符 | 名称 | 规则 |
|---|---|---|
& |
按位与 | 两位都为 1 才是 1,否则是 0 |
| ` | ` | 按位或 |
^ |
按位异或 | 两位相同为 0,不同为 1 |
~ |
按位取反 | 0 变 1,1 变 0 |
c
#include <stdio.h>
int main(void)
{
int a = -5;
int b = 7;
printf("%d %d %d %d\n", a & b, a | b, a ^ b, ~0);
return 0;
}
输出:
text
3 -1 -4 -1
负数参与位运算时,先写成补码,运算完再把结果转回原码。以 -5 和 7 为例,32 位补码是:
text
-5:11111111 11111111 11111111 11111011
7:00000000 00000000 00000000 00000111
按位与得到 00000000 00000000 00000000 00000011,也就是 3;按位或得到全 1,也就是 -1;按位异或得到 11111111 11111111 11111111 11111100,转回原码是 -4。~0 把 0 的所有位取反,得到全 1,也就是 -1。
| 表达式 | 结果 | 原因 |
|---|---|---|
-5 & 7 |
3 | 只保留两个数都为 1 的位 |
| `-5 | 7` | -1 |
-5 ^ 7 |
-4 | 不同的位为 1,结果是负数的补码 |
~0 |
-1 | 0 取反得到全 1 |
&、|、^ 也有复合赋值形式:a &= b、a |= b、a ^= b,分别等价于 a = a & b 等。
~ 是对提升后的类型逐位取反,不是对操作数原宽度取反。~ 的优先级很高,和强制类型转换同级。对 unsigned char 取反会先提升为 int,结果是负的 int,想要 8 位结果必须截断回 unsigned char,这一点在 2.2 节已经看过。
7. 位运算的四个用途
7.1 交换两个整数
不借助临时变量,可以用三次异或交换两个整数:
c
#include <stdio.h>
int main(void)
{
int a = 10;
int b = 20;
a ^= b;
b ^= a;
a ^= b;
printf("a = %d, b = %d\n", a, b);
return 0;
}
输出:
text
a = 20, b = 10
推导用到异或的三条性质:a ^ a = 0,a ^ 0 = a,异或满足交换律和结合律。第一步 a 变成 a ^ b;第二步 b = (a ^ b) ^ b = a;第三步 a = (a ^ b) ^ a = b。
这个技巧只对整数有效,而且要求 a、b 是两个不同的对象。如果传进来的是同一个变量,第一步 x ^= x 就把 x 清成 0,后两步也救不回来。实际代码里,交换两个变量还是用临时变量更清楚,编译器会把它优化好;异或交换主要用于理解位运算,不建议为了省一个变量而使用。
7.2 统计二进制中 1 的个数
n & (n - 1) 会消掉 n 最低位的 1。每执行一次,1 的个数减一,循环次数正好等于 1 的个数:
c
#include <stdio.h>
int CountOnes(unsigned int n)
{
int count = 0;
while (n != 0)
{
n = n & (n - 1);
count++;
}
return count;
}
int main(void)
{
printf("%d %d\n", CountOnes(10), CountOnes(0xFFFFFFFFu));
return 0;
}
输出:
text
2 32
10 的二进制是 1010,有两个 1;0xFFFFFFFF 是 32 个 1。这个算法叫 Brian Kernighan 算法,时间复杂度是 O ( k ) O(k) O(k), k k k 是 1 的个数,比固定循环 32 次更快。
为什么 n & (n - 1) 能消掉最低位的 1?n - 1 会把最低位的 1 变成 0,并把它右边的 0 全部变成 1;再和原来的 n 做与运算,最低位的 1 和它右边的位都被清零,更高的位保持不变。
统计位时先把值放进 unsigned,可以避开 INT_MIN - 1 这类有符号溢出。对 INT_MIN 直接做 n - 1 是未定义行为。
7.3 置位、清位、切换、检查
位下标从 0 开始,最低位是第 0 位。1 << n 生成一个只有第 n 位为 1、其余位为 0 的掩码:
c
#include <stdio.h>
int main(void)
{
int a = 13;
a = a | (1 << 4);
printf("a = %d\n", a);
a = a & ~(1 << 4);
printf("a = %d\n", a);
a = a ^ (1 << 1);
printf("a = %d\n", a);
if (a & (1 << 1))
{
printf("第 1 位是 1\n");
}
return 0;
}
输出:
text
a = 29
a = 13
a = 15
第 1 位是 1
| 目标 | 写法 | 说明 |
|---|---|---|
| 第 n 位置 1 | `a | = (1u << n)` |
| 第 n 位清 0 | a &= ~(1u << n) |
与上目标位为 0、其余位为 1 的掩码 |
| 第 n 位翻转 | a ^= (1u << n) |
目标位与 1 异或翻转,其余位与 0 异或不改 |
| 检查第 n 位 | if (a & (1u << n)) |
结果非 0 表示该位为 1 |
代码里用的是 1 << 4,因为 int 有 32 位,左移到第 4 位不会溢出。如果要处理第 31 位,写 1u << 31,用无符号左移,避免有符号左移溢出。
7.4 权限标志位
一个整数可以拆成多个 bit,每个 bit 表示一个开关。权限系统就是典型用法:
c
#include <stdio.h>
#define PERM_READ (1u << 0)
#define PERM_WRITE (1u << 1)
#define PERM_EXEC (1u << 2)
#define PERM_DELETE (1u << 3)
int main(void)
{
unsigned int perm = 0;
perm |= PERM_READ;
perm |= (PERM_WRITE | PERM_EXEC);
printf("perm = 0x%x\n", perm);
printf("read=%d write=%d exec=%d delete=%d\n",
(perm & PERM_READ) != 0, (perm & PERM_WRITE) != 0,
(perm & PERM_EXEC) != 0, (perm & PERM_DELETE) != 0);
perm &= ~PERM_WRITE;
perm ^= PERM_DELETE;
printf("perm = 0x%x\n", perm);
printf("read=%d write=%d exec=%d delete=%d\n",
(perm & PERM_READ) != 0, (perm & PERM_WRITE) != 0,
(perm & PERM_EXEC) != 0, (perm & PERM_DELETE) != 0);
return 0;
}
输出:
text
perm = 0x7
read=1 write=1 exec=1 delete=0
perm = 0xd
read=1 write=0 exec=1 delete=1

初始 perm = 0,置上读、写、执行三个权限后是 0x7(二进制 0111)。清掉写权限,再翻转删除权限,得到 0xd(二进制 1101)。
用宏定义每一位,比直接写 1、2、4、8 更清楚。1u 表示无符号的 1,左移不会碰到有符号溢出;PERM_READ | PERM_WRITE 可以一次设置多个位。
常见的位运算写法还有:
| 需求 | 写法 |
|---|---|
| 判断奇偶 | n & 1,1 是奇数,0 是偶数 |
| 取低 8 位 | n & 0xFF |
| 判断是不是 2 的幂 | n > 0 && (n & (n - 1)) == 0 |
| 清除最低位的 1 | n & (n - 1) |
判断 2 的幂时,括号不能省。== 的优先级高于 &,n & (n - 1) == 0 会被解析成 n & ((n - 1) == 0),结果不对。
8. 优先级与常见误写
位运算和比较、算术混写,是最容易出错的地方。下面是从高到低的一部分优先级:
| 从高到低 | 运算符 |
|---|---|
| 1 | ~、强制类型转换 |
| 2 | * / % |
| 3 | + - |
| 4 | << >> |
| 5 | < <= > >= |
| 6 | == != |
| 7 | & |
| 8 | ^ |
| 9 | ` |
| 10 | && |
| 11 | ` |
c
#include <stdio.h>
int main(void)
{
int n = 6;
printf("%d %d\n", n & 1 == 0, (n & 1) == 0);
printf("%d %d\n", 1 << 2 + 3, (1 << 2) + 3);
return 0;
}
输出:
text
0 1
32 7
n & 1 == 0 里 == 的优先级高于 &,实际是 n & (1 == 0),也就是 6 & 0,结果是 0。写成 (n & 1) == 0 才是「n 的最低位是不是 0」,结果是 1。
1 << 2 + 3 里加法优先于移位,实际是 1 << 5,结果是 32。写成 (1 << 2) + 3 才是 7。
规则很简单:位运算和比较、算术混写时,一律加括号。不要依赖优先级表去读别人的代码,也不要把自己的代码写成需要查表才能看懂的样子。
9. 小结
- 2、8、10、16 进制只是同一个数的不同写法;2 进制转 8 进制每 3 位一组,转 16 进制每 4 位一组,高位不够补 0。C 里
0x开头是十六进制,0开头是八进制。 - 有符号整数在常见平台上存补码:正数三码相同,负数反码是符号位不变、数值位取反,补码是反码加 1;补码按模 2 n 2^n 2n 表示负数,0 只有一种表示,负数比正数多一个,
INT_MIN取负会溢出。 - 多字节数据有字节序:小端低位字节放低地址,大端高位字节放低地址;x86/x64 默认小端,网络字节序是大端。
- 浮点数按 IEEE 754 存,
float是 1 位符号、8 位指数、23 位有效数字,double是 1 位、11 位、52 位;类型决定同一串位模式怎么解释。 - 左移右边补 0,右移分逻辑右移和算术右移;移动位数必须在 0 到类型宽度减 1 之间,优先用无符号类型。
- 位运算
&清位、|置位、^翻转、~取反;n & (n - 1)消掉最低位的 1,1u << n生成第 n 位的掩码。 - 位运算和比较、算术混写必须加括号:
(n & 1) == 0不是n & 1 == 0,(1 << 2) + 3不是1 << 2 + 3。