news 2026/10/7 8:20:50

《C语言操作符详解:位运算、进制转换与表达式求值的 9 个高频考点》

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
《C语言操作符详解:位运算、进制转换与表达式求值的 9 个高频考点》

《C语言操作符详解:位运算、进制转换与表达式求值的 9 个高频考点》

学完 C 基础之后,大多数人对+ - * /已经驾轻就熟,可一旦碰到<< >> & | ^ ~、原码反码补码,就开始发懵。其实这些并不神秘,它们只是逼着你回头想清楚一件事——一个整数在内存里,究竟是以什么模样存着的。

比如:

  • 负数-7 % 2到底得几?符号跟着谁走?
  • num >> 1对负数来说,左边补 0 还是补 1?
  • 十进制转二进制,除 2 取余之后,余数正着读还是倒着读?
  • 不创建临时变量,怎么交换两个整数?
  • 13要把它二进制里的某一位「置 1」「置 0」,怎么写?

这些问题,归根到底都是「底层表示」没吃透。

这篇博客不追求讲深,只讲一件事:C 语言里和二进制、位运算、类型转换有关的操作符,到底怎么用、坑在哪。我会把知识点拆成卡片逐一讲解——操作符分类、进制转换、原码反码补码、移位操作符、位操作符、负数的除法与取模、整型提升、算术转换、逗号表达式。


1:操作符分类(先认全这家人)

核心

C 语言的运算符一共可以分成 13 类,先混个脸熟,尤其是位操作符和移位操作符这两类「二进制专属」的。

分类操作符
算术操作符+-*/%
移位操作符<<>>
位操作符&|^~
赋值操作符=+=-=*=/=%=<<=>>=&=|=^=
单目操作符!++--&*+-~sizeof(类型)
关系操作符>>=<<=!===
逻辑操作符&&||
条件操作符? :
逗号表达式,
下标引用[]
函数调用()
结构成员访问.->

坑点

💡移位操作符和位操作符的操作数,只能是整数(不能是浮点数)。这是它们和算术操作符最本质的区别,也是很多人第一次编译报错的来源。


2:进制转换(把数字「翻译」过来)

核心

计算机只认二进制,但我们习惯十进制,所以「翻译」能力是位运算的前提。四种进制对同一个数15的表示:

进制表示C 语言写法
二进制11110b1111
八进制17017(前缀 0)
十进制1515
十六进制F0xF(前缀 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~701101011→0153
二进制 → 十六进制从右往左4 位一组,每组换成 0~F01101011→0x6b

💡 原理:2³ = 8,2⁴ = 16,所以 3 位二进制恰好对应 1 位八进制,4 位二进制恰好对应 1 位十六进制。

坑点

余数别读反。除 2 取余得到的是「从低位到高位」的余数序列,所以最终要倒着读。这也是手算二进制最容易错的一步。


3:原码、反码、补码(负数的三种「面孔」)

核心

同一个整数,存在三种编码:原码、反码、补码。

  • 原码:最高位是符号位(0 正 1 负),其余位是绝对值的二进制。
  • 反码:符号位不变,其余位按位取反。
  • 补码:反码+1。

最省心的一条规则:正整数的原码、反码、补码完全一样。只有负数才需要折腾。

例:-1(32 位)

原码:10000000000000000000000000000001反码:11111111111111111111111111111110补码:11111111111111111111111111111111

细节

整数在内存中存的是补码。为什么?三个理由:

  1. 补码让符号位和数值域可以统一处理;
  2. 加减法可以统一成加法(CPU 只有加法器,减法本质是加一个负数);
  3. 补码和原码互相转换,运算过程完全相同(都是「取反 +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。

坑点

两个最容易踩的坑:

  1. -7 / 2是-3不是-4——向零取整,别当成向下取整;
  2. %的符号跟被除数,不跟除数——-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

规则两条:

  1. 若有操作数低于int,先做整型提升;
  2. 否则向等级更高的类型转换;等级相同时若一有符号一无符号,则转成无符号。

坑点

有符号和无符号混用是头号陷阱,经典错误:

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
算术转换类型不同向高等级转;有符号和无符号别混用
逗号表达式从左到右求值,取最后一个值;优先级最低,赋值时要加括号
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 8:20:27

ponytail 插件怎么用?轻量级代码片段管理与快速注入工具实战指南

1. 从“ponytail”这个词说起&#xff1a;它到底是什么第一次看到“ponytail”这个项目标题&#xff0c;很多人脑子里蹦出来的第一反应大概是发型——马尾辫。没错&#xff0c;字面意思确实是马尾辫&#xff0c;但作为一个项目名、一个插件名&#xff0c;它显然不是让你去研究怎…

作者头像 李华
网站建设 2026/10/7 8:20:02

AnyPS5存档系统完全解析:libSceSaveData如何在PC上读写PS5存档文件

AnyPS5存档系统完全解析&#xff1a;libSceSaveData如何在PC上读写PS5存档文件 【免费下载链接】AnyPS5 Tool for automatic PS5 executables porting to Linux and Windows 项目地址: https://gitcode.com/GitHub_Trending/an/AnyPS5 如果你想在电脑上玩移植的PS5游戏&…

作者头像 李华
网站建设 2026/10/7 8:18:39

[HNCTF 2022 Week1]fmtstrre

格式化字符串漏洞利用读取特定地址内容 原思路的wp地址&#xff1a;https://www.nssctf.cn/note/set/13425里面有讲解位置参数的用法平台&#xff1a;NSSCTF 方向&#xff1a;Pwn 知识点&#xff1a;格式化字符串 难度&#xff1a;入门一、信息获取 可以先checksec一下NX、SHST…

作者头像 李华
网站建设 2026/10/7 8:18:26

PCB屏蔽罩图纸制作全攻略:从设计参数到实操避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华