《C语言操作符详解:位运算、进制转换与表达式求值的 9 个高频考点》
学完 C 基础之后,大多数人对+ - * /已经驾轻就熟,可一旦碰到<< >> & | ^ ~、原码反码补码,就开始发懵。其实这些并不神秘,它们只是逼着你回头想清楚一件事——一个整数在内存里,究竟是以什么模样存着的。
比如:
- 负数
-7 % 2到底得几?符号跟着谁走? num >> 1对负数来说,左边补 0 还是补 1?- 十进制转二进制,除 2 取余之后,余数正着读还是倒着读?
- 不创建临时变量,怎么交换两个整数?
13要把它二进制里的某一位「置 1」「置 0」,怎么写?
这些问题,归根到底都是「底层表示」没吃透。
这篇博客不追求讲深,只讲一件事:C 语言里和二进制、位运算、类型转换有关的操作符,到底怎么用、坑在哪。我会把知识点拆成卡片逐一讲解——操作符分类、进制转换、原码反码补码、移位操作符、位操作符、负数的除法与取模、整型提升、算术转换、逗号表达式。
1:操作符分类(先认全这家人)
核心
C 语言的运算符一共可以分成 13 类,先混个脸熟,尤其是位操作符和移位操作符这两类「二进制专属」的。
| 分类 | 操作符 |
|---|---|
| 算术操作符 | +-*/% |
| 移位操作符 | <<>> |
| 位操作符 | &|^~ |
| 赋值操作符 | =+=-=*=/=%=<<=>>=&=|=^= |
| 单目操作符 | !++--&*+-~sizeof(类型) |
| 关系操作符 | >>=<<=!=== |
| 逻辑操作符 | &&|| |
| 条件操作符 | ? : |
| 逗号表达式 | , |
| 下标引用 | [] |
| 函数调用 | () |
| 结构成员访问 | .-> |
坑点
💡移位操作符和位操作符的操作数,只能是整数(不能是浮点数)。这是它们和算术操作符最本质的区别,也是很多人第一次编译报错的来源。
2:进制转换(把数字「翻译」过来)
核心
计算机只认二进制,但我们习惯十进制,所以「翻译」能力是位运算的前提。四种进制对同一个数15的表示:
| 进制 | 表示 | C 语言写法 |
|---|---|---|
| 二进制 | 1111 | 0b1111 |
| 八进制 | 17 | 017(前缀 0) |
| 十进制 | 15 | 15 |
| 十六进制 | F | 0xF(前缀 0x) |
细节
① 其他进制 → 十进制:按权展开。
每一位数字乘上它对应的权重,再相加。
二进制1101=1*8+1*4+0*2+1*1=13② 十进制 → 二进制:除基取余法(余数倒着写)。
125÷2=62余162÷2=31余031÷2=15余115÷2=7余17÷2=3余13÷2=1余11÷2=0余1结果:1111101(余数从下往上读)③ 二进制 ↔ 八进制 / 十六进制:分组法。
| 转换 | 规则 | 示例 |
|---|---|---|
| 二进制 → 八进制 | 从右往左3 位一组,每组换成 0~7 | 01101011→0153 |
| 二进制 → 十六进制 | 从右往左4 位一组,每组换成 0~F | 01101011→0x6b |
💡 原理:2³ = 8,2⁴ = 16,所以 3 位二进制恰好对应 1 位八进制,4 位二进制恰好对应 1 位十六进制。
坑点
余数别读反。除 2 取余得到的是「从低位到高位」的余数序列,所以最终要倒着读。这也是手算二进制最容易错的一步。
3:原码、反码、补码(负数的三种「面孔」)
核心
同一个整数,存在三种编码:原码、反码、补码。
- 原码:最高位是符号位(0 正 1 负),其余位是绝对值的二进制。
- 反码:符号位不变,其余位按位取反。
- 补码:反码+1。
最省心的一条规则:正整数的原码、反码、补码完全一样。只有负数才需要折腾。
例:-1(32 位)
原码:10000000000000000000000000000001反码:11111111111111111111111111111110补码:11111111111111111111111111111111细节
整数在内存中存的是补码。为什么?三个理由:
- 补码让符号位和数值域可以统一处理;
- 加减法可以统一成加法(CPU 只有加法器,减法本质是加一个负数);
- 补码和原码互相转换,运算过程完全相同(都是「取反 +1」),不需要额外硬件电路。
💡 例:
1 - 1,CPU 实际算的是1 + (-1)的补码:0000...0001 + 1111...1111 = 1 0000...0000,舍去最高位进位,得到 0。
坑点
负数补码 = 原码「取反 +1」,不是「直接 +1」。很多人算-1的补码时,把原码直接 +1,这是错的,必须先把非符号位取反。
4:移位操作符(<<和>>)
<<左移操作符>>右移操作符核心
- 左移
<<:左边丢弃、右边补 0。 - 右移
>>:分两种——逻辑右移(左边补 0)和算术右移(左边补符号位)。
细节
左移:
intnum=10;// 0000...01010intn=num<<1;// 0000...10100 = 20// 注意:num 本身不变,移位表达式只是返回移位后的结果💡 左移 1 位相当于 ×2(在不溢出、不丢有效位的前提下)。
右移:到底补 0 还是补符号位,取决于编译器,常见的 MSVC、GCC 都采用算术右移。
intnum=-1;// 补码全 1:1111...1111// 逻辑右移:左边补 0 → 0111...1111(一个很大的正数)// 算术右移:左边补符号位 1 → 仍是 1111...1111(还是 -1)💡 算术右移 1 位约等于 ÷2,且负数右移后仍然是负数。
坑点
⚠️ 不要移动负数位,这是标准未定义行为。
num>>-1;// error:未定义行为,别写💡 移动位数 ≥ 操作数位宽(比如 int 移 32 位)同样是未定义行为。
5:位操作符(&|^~)
&// 按位与:全 1 才是 1|// 按位或:有 1 就是 1^// 按位异或:相同为 0,不同为 1~// 按位取反:1 变 0,0 变 1核心
这四位是二进制世界里最实用的「开关工具」。操作数必须是整数。
细节
① 经典面试题:不创建临时变量,交换两个整数。
// 异或交换法(不溢出,最优雅)a=a^b;b=a^b;// b = a^b^b = aa=a^b;// a = a^b^a = b💡 依赖三条性质:
x ^ x = 0、x ^ 0 = x、异或满足交换律和结合律。
// 加减法(注意溢出风险)a=a+b;b=a-b;a=a-b;② 求二进制中 1 的个数——三种写法对比。
// 方法1:取余除二(❌ 负数会出错,num%2 对负数是 -1)while(num){if(num%2==1)count++;num=num/2;}// 方法2:逐位与 1(✅ 正确,但必须循环 32 次)for(inti=0;i<32;i++)if(num&(1<<i))count++;// 方法3:n & (n-1)(⭐ 最优,循环次数 = 1 的个数)while(num){count++;num=num&(num-1);// 每次消灭最右侧的 1}💡
n & (n-1)还能判断 2 的幂:n > 0 && (n & (n-1)) == 0。
③ 二进制位「置 1」「置 0」。
把13(00001101)的第 5 位(从 0 开始编号,即1<<4)置 1 再置 0:
inta=13;a=a|(1<<4);// 置 1:00001101 | 00010000 = 00011101 = 29a=a&~(1<<4);// 置 0:00011101 & 11101111 = 00001101 = 13坑点
💡记忆口诀:置 1 用
|,置 0 用& ~,消 1 用n & (n-1)。
置 0 时千万别漏了~,a & (1 << 4)只会得到「只有那一位可能为 1」的结果,而不是把原位清零。
6:负数的除法与取模(/和%的隐藏规则)
核心
/除法:向零取整(直接砍掉小数部分,不是四舍五入)。%取模:结果的符号跟随被除数(左操作数)。
细节
printf("%d\n",7/2);// 3(不是 3.5,也不是 4)printf("%d\n",-7/2);// -3(向零取整,不是 -4)printf("%d\n",7%2);// 1printf("%d\n",-7%2);// -1(符号跟随 -7)printf("%d\n",7%-2);// 1(符号跟随 7)两者由恒等式牢牢绑定:(a / b) * b + a % b == a。
💡 浮点数不能用
%,要用<math.h>里的fmod,符号规则和整数%一致:fmod(-7.5, 2.0) == -1.5。
坑点
两个最容易踩的坑:
-7 / 2是-3不是-4——向零取整,别当成向下取整;%的符号跟被除数,不跟除数——-7 % 2 == -1,但7 % -2 == 1。
7:整型提升(Integer Promotion)
核心
凡是比int小的整型(char、signed char、unsigned char、short、unsigned short),在参与表达式运算时,都会被自动「提升」成int(如果int装不下,则提升为unsigned int)。
细节
为什么要有整型提升?因为 CPU 的通用寄存器和运算器都是按int宽度设计的,用比int更小的类型运算反而更慢,所以编译器统一先提升再算。
chara=3,b=127;intn=a+b;// a、b 先提升为 int,再相加printf("%d\n",sizeof(a+b));// 4(a+b 是 int,不是 char)坑点
符号位扩展是整型提升最大的坑。多数平台上char是有符号的,值为负时提升会「扩展符号位」。
chara=0xb6;// 二进制 10110110,作为 signed char 是负数inti=a;// 整型提升:高位全部补符号位 1,i 仍是负数printf("%d\n",i);💡 口诀:小于 int 先提升,提升时负数补符号位、正数补 0。
8:算术转换(Arithmetic Conversion)
核心
当二元操作符(+ - * /等)的两个操作数类型不同时,会进行「寻常算术转换」,把两者统一成同一个更宽的类型再运算。
细节
转换等级由低到高(简化版):
char/short→int→unsignedint→long→unsignedlong→longlong→unsignedlonglong→float→double→longdouble规则两条:
- 若有操作数低于
int,先做整型提升; - 否则向等级更高的类型转换;等级相同时若一有符号一无符号,则转成无符号。
坑点
有符号和无符号混用是头号陷阱,经典错误:
inta=-1;unsignedintb=1;if(a>b)// a 被转成 unsigned int,变成 4294967295printf("a > b\n");// 居然输出这一行!elseprintf("a <= b\n");💡
-1转成unsigned int后是0xFFFFFFFF(42 亿多),远大于 1,所以a > b为真——与直觉相反。
9:逗号表达式(Comma Expression)
核心
用逗号把多个表达式连起来,从左到右依次求值,整个表达式的值是最后一个表达式的值。
inta=3,b=5;intc=(a,b,a+b);// 依次算 a、b、a+b,最终 c = 8细节
- 逗号是优先级最低的运算符;
- 常用于
for循环里一次更新多个变量:
for(inti=0,j=10;i<j;i++,j--){...}坑点
逗号优先级低于赋值,所以a = 1, 2, 3不等于a = (1, 2, 3):
inta;a=1,2,3;// 等价于 (a = 1), 2, 3 → a = 1a=(1,2,3);// a = 3💡 想用逗号给变量「一次赋多个值」时,记得加括号,否则只有第一个赋值生效。
写在最后
回头看这 9 张卡片,它们其实都在回答同一件事:C 语言里和二进制、类型转换有关的操作符,本质上都是在跟「内存里的补码」和「类型系统」打交道。
- 进制转换看不懂?那是你不清楚「权」是什么;
- 原反补分不清?记住「正数三码合一、负数取反加一、内存存补码」;
- 移位规则记不住?左移补 0,右移看编译器(多半算术右移补符号位);
- 位运算不会用?口诀就三句:置 1 用
|,置 0 用& ~,消 1 用n & (n-1); - 类型转换老踩坑?记住「小于 int 先提升,有符号和无符号别混用,逗号记得加括号」。
把底层表示吃透了,这些坑自然就填平了。
6 道自测题(检验你是否真看懂了)
Q1:下面代码输出什么?为什么?
intnum=-1;printf("%d\n",num>>1);Q2:执行下面三行后,a的值是多少?它等价于什么运算?
inta=13;a=a|(1<<4);Q3:-7 / 2和-7 % 2分别等于多少?符号各由谁决定?
Q4:char a = 0xb6;后执行int i = a;,i的值是正还是负?为什么?
Q5:int a = -1; unsigned int b = 1;那么a > b是真还是假?为什么?
Q6:执行int a; a = 1, 2, 3;后,a的值是多少?如果想得到 3 该怎么写?
Q1:
- 若编译器采用算术右移(MSVC/GCC 默认):左边补符号位 1,结果仍是全 1,输出-1。
- 若采用逻辑右移:左边补 0,输出
0x3FFFFFFF(很大的正数)。 - 常见环境下答案是-1,考点是「右移对负数补符号位」。
Q2:1 << 4 = 16,13 | 16 = 29。等价于把 13 二进制的第 5 位(从 0 编号)置 1。考点是「置 1 用|」。
Q3:
-7 / 2 == -3(向零取整),符号由两个操作数共同决定(异号得负)。-7 % 2 == -1,符号只跟随被除数-7。- 验证恒等式:
(-7/2)*2 + (-7%2) = -6 + (-1) = -7✓。
Q4:
- 是负数。
0xb6(10110110)在多数平台上char是有符号的,最高位是符号位 1,所以a是负数;赋给int时发生整型提升,高位补符号位 1,i仍是负数。考点是「整型提升的符号位扩展」。
Q5:
- 真。
a > b中a(int -1)被算术转换成unsigned int,变成0xFFFFFFFF(4294967295),远大于 1,所以为真。考点是「有符号和无符号混用」的经典陷阱。
Q6:
a = 1。因为逗号优先级低于赋值,a = 1, 2, 3等价于(a = 1), 2, 3,后两个值被求值后丢弃。想得到 3 应写成a = (1, 2, 3);。
📌 快速记忆卡
| 知识点 | 一句话总结 |
|---|---|
| 进制转换 | 转十进制按权展开;十进制除基取余倒着读;二转八 3 位一组、二转十六 4 位一组 |
| 原反补 | 正数三码合一;负数取反加一;内存存补码 |
左移<< | 左边丢弃、右边补 0,约等于 ×2 |
右移>> | 逻辑补 0 / 算术补符号位;移负数位 = 未定义 |
| 位操作符 | &全 1 为 1,|有 1 为 1,^相异为 1,~取反;操作数必须整数 |
| 置位口诀 | 置 1 用|,置 0 用& ~,消 1 用n & (n-1) |
| 除法取模 | /向零取整;%符号随被除数;浮点数用fmod |
| 整型提升 | 小于 int 先提升,负数补符号位、正数补 0 |
| 算术转换 | 类型不同向高等级转;有符号和无符号别混用 |
| 逗号表达式 | 从左到右求值,取最后一个值;优先级最低,赋值时要加括号 |