
写在前面操作符不是靠死记优先级学会的学 C 语言到操作符这一章很多人的第一反应就是优先级表好长、记不住。可真正让人在写代码时踩坑的往往不是优先级本身而是位运算那一堆、|、^、、——看着都认识一上手算二进制就懵再加上原码、反码、补码这套负数表示更是绕成一团。这篇复盘我按先打地基、再上操作符、最后聊那些玄学的求值顺序的顺序把操作符这一讲重新捋了一遍配上图争取一次讲明白。一、操作符的分类先看清全貌C 语言的操作符教材里一般分这么几类先整体扫一眼心里有个地图算术操作符、-、*、/、%移位操作符、位操作符、|、^、~赋值操作符、、-、*、/、%、、、、|、^单目操作符!、、--、、*、、-、~、sizeof、(类型)关系操作符、、、、、!逻辑操作符、||条件操作符? :逗号表达式,下标引用[]函数调用()结构成员访问.、-算术、赋值、逻辑、条件这些之前都讲过了今天重点啃和二进制相关的移位、位运算以及它们的地基——进制转换和原反补码。二、二进制与进制转换位运算的地基我们常听到 2 进制、8 进制、10 进制、16 进制它们到底是什么说白了只是同一个数值的不同表示形式罢了。就拿数值15来说它在四种进制下长这样二进制1111八进制17十进制15十六进制F两个小约定先记住十六进制前面写0x八进制前面写0比如0x6b是十六进制0153是八进制。2.1 二进制转十进制看权重我们先从最熟的 10 进制讲起10 进制满 10 进 1每一位都是 0~9 的数字。二进制也一样满 2 进 1每一位都是 0~1 的数字。那十进制123为什么是一百二十三因为每一位有权重——从右往左是个位、十位、百位权重是10^0、10^1、10^2……。二进制也是同样的道理只不过每一位的权重变成了2^0、2^1、2^2……。看图就懂了11011×8 1×4 0×2 1×113。2.2 十进制转二进制反过来十进制转二进制就是用 2 反复除、取余数倒着排。比如 15 除以 2一路余 1、1、1、1倒过来就是1111。2.3 二进制转八进制、十六进制转八进制八进制每一位是 0~7写成二进制最多 3 位就够7 的二进制是111。所以从二进制右边低位开始每 3 位换算成 1 个八进制位不够 3 位直接换算。例01101011→0153。转十六进制十六进制每一位是 0~9、a~f写成二进制最多 4 位f 的二进制是1111。所以从右边低位开始每 4 位换算成 1 个十六进制位。例01101011→0x6b。三、原码、反码、补码负数在内存里长啥样整数的二进制表示有三种原码、反码、补码。有符号整数的三种表示都分符号位和数值位两部分二进制序列里最高位当符号位其余是数值位。符号位 0 表示正、1 表示负。正整数的原码、反码、补码都相同负整数三种表示各不相同原码直接按正负翻译成二进制反码符号位不变其余位按位取反补码反码 1。而补码再变回原码也是取反 1运算过程一模一样。IMG2为什么内存里存的是补码因为用补码可以把符号位和数值域统一处理加法和减法也能统一CPU 只有加法器而且补码和原码互转的运算过程相同不需要额外的硬件电路。这一条是理解后面位运算结果会正负乱跳的关键。四、移位操作符 和 左移、右移操作数只能是整数。4.1 左移操作符规则就一句左边抛弃、右边补 0。#include stdio.h int main() { int num 10; int n num 1; printf(n %d\n, n); // 20 printf(num %d\n, num); // 10原值不变 return 0; }注意num 1只是算了个结果num本身没变。左移一位等价于乘以 2。4.2 右移操作符右移分两种行为不一样逻辑右移左边用0填充右边丢弃算术右移左边用原值的符号位填充右边丢弃。#include stdio.h int main() { int num 10; int n num 1; printf(n %d\n, n); // 5 printf(num %d\n, num); // 10 return 0; }IMG3⚠️ 警告移位运算符不要移动负数位这是标准未定义行为。比如num -1就是错的。五、位操作符 | ^ ~位操作符有四个按位与、|按位或、^按位异或、~按位取反。同样操作数必须是整数。IMG4直接上代码#include stdio.h int main() { int num1 -3; int num2 5; printf(%d\n, num1 num2); // 5 printf(%d\n, num1 | num2); // -3 printf(%d\n, num1 ^ num2); // -8 printf(%d\n, ~0); // -1 return 0; } 这里记住一个异或的好用结论任何数和 0 异或是它自己任何数和它自己异或是 0a^a0、a^0a。5.1 一道变态面试题不创建临时变量交换两个整数不给第三个变量怎么交换a和b用异或#include stdio.h int main() { int a 10; int b 20; a a ^ b; b a ^ b; // b (a^b)^b a a a ^ b; // a (a^b)^a b printf(a %d b %d\n, a, b); return 0; }原理就靠a^a0、a^0a这两条一步步消掉了。这个方法面试常问但实际工程里可读性差慎用。5.2 练习1求二进制中 1 的个数方法一除 2 取余对负数不友好会死循环int count 0; while (num) { if (num % 2 1) count; num num / 2; }方法二逐位和1i按位与要循环 32 次int count 0; for (int i 0; i 32; i) { if (num (1 i)) count; }方法三num num (num - 1)每次消掉最低位的一个 1有几个 1 就循环几次最优int count 0; while (num) { count; num num (num - 1); }n (n-1)这个技巧很经典它会把 n 的二进制里最低位的一个 1 变成 0。方法三虽然难想但效率最高面试加分项。5.3 练习2把某一位置 0 或置 1把 13 的二进制第 5 位改成 1再改回 0#include stdio.h int main() { int a 13; // 0000...01101 a a | (1 4); // 第5位置10000...11101 printf(a %d\n, a); // 29 a a ~(1 4); // 第5位置00000...01101 printf(a %d\n, a); // 13 return 0; }套路记牢置 1 用| (1n)置 0 用 ~(1n)。六、单目操作符单目操作符的特点是只有一个操作数!、、--、、*、、-、~、sizeof、(类型)。其中取地址和*解引用这两个留到学指针时再讲。七、逗号表达式exp1, exp2, exp3, … expN逗号表达式就是用逗号隔开的多个表达式从左向右依次执行整个表达式的结果是最后一个表达式的结果。int a 1; int b 2; int c (a b, a b 10, a, b a 1); // c 是多少c 13b a1 121 13逗号表达式在while里能把多步操作挤成一句while (a get_val(), count_val(a), a 0) { // 业务处理 }八、下标[] 和 函数调用()这两个也是操作符[]下标引用操作数是一个数组名 一个索引。arr[9] 10里[]的两个操作数就是arr和9。()函数调用第一个操作数是函数名剩下的是传参。test2(hello bit.)里的()就是函数调用操作符。#include stdio.h void test1() { printf(hehe\n); } void test2(const char *str) { printf(%s\n, str); } int main() { test1(); // 这里的 () 就是函数调用操作符 test2(hello bit.); return 0; }九、结构成员访问. 和 -C 语言只有 char、short、int、float、double 这些内置类型还不够——想描述一个学生、一本书单一类型搞不定于是有了结构体这种自定义类型。结构是一些值的集合这些值叫成员变量每个成员可以是不同类型标量、数组、指针甚至别的结构体。9.1 结构的声明struct Stu { char name[20]; // 名字 int age; // 年龄 char sex[5]; // 性别 char id[20]; // 学号 }; // 分号不能丢定义和初始化struct Stu s1 {zhangsan, 20}; // 普通初始化 struct Stu s2 {.age 20, .name lisi}; // 指定顺序初始化9.2 成员的访问直接访问用.结构体变量.成员名比如p.x。间接访问拿到的是指向结构体的指针时用-结构体指针-成员名比如ptr-x。#include stdio.h struct Point { int x; int y; }; int main() { struct Point p {3, 4}; struct Point *ptr p; ptr-x 10; ptr-y 20; printf(x %d y %d\n, ptr-x, ptr-y); return 0; }十、优先级与结合性操作符有两个重要属性决定了表达式求值的顺序优先级一个表达式里有多个运算符时谁先算。比如3 4 * 5乘法优先级高先算4*5。结合性两个运算符优先级相同时从左到右还是从右到左。大部分是左结合少数如赋值是右结合。比如5 * 6 / 2*和/同级左结合先算5*6再/2。优先级从高到低记住大概即可其他查表圆括号()自增/自减--单目/-乘*除/加减-关系 赋值圆括号优先级最高所以想改变顺序、又不想背表加括号最省心。十一、表达式求值那些玄学的结果11.1 整型提升C 语言里整型算术运算至少以 int 的精度进行。表达式里比 int 短的 char、short运算前会先被转成 int这叫整型提升。为什么因为 CPU 的整型运算器ALU操作数长度一般就是 int两个 char 相加实际执行前也得先转成 int 的标准长度否则 CPU 不好直接算。提升规则有符号整数按符号位提升负数高位补 1正数补 0无符号整数高位补 0。char c1 -1; // 补码 11111111 // 提升高位补符号位 1 → 11111111 11111111 11111111 11111111 char c2 1; // 补码 00000001 // 提升高位补 0 → 00000000 00000000 00000000 0000000111.2 算术转换如果操作符两边的操作数类型不同就得先把排名靠后的转成另一边的类型再算。层次从高到低long double double float unsigned long int long int unsigned int int11.3 问题表达式别写这种未定义的代码就算有优先级和结合性有些表达式的求值顺序依然无法唯一确定结果就是未定义行为不同编译器结果不同。举几个反例a * b c * d e * f; // 优先级只能保证 * 比 早但第三个 * 和第一个 谁先执行不确定 c --c; // -- 在 前但 的左操作数何时取值未知结果有歧义 answer fun() - fun() * fun(); // 三个 fun() 的调用顺序无法确定 int i 1; int ret (i) (i) (i); // GCC 和 VS 结果不同结论就一句话即使懂了优先级和结合性也别写出那种靠猜的复杂表达式。括号一加、拆成几步又清楚又安全何乐不为。总结最后收个尾把这一讲的骨架拎出来进制二进制满 2 进 1转十进制看权重转八/十六进制靠分组3 位 / 4 位。原反补负数内存里存的是补码取反1符号位 0 正 1 负。移位左移左丢右补 0右移分逻辑补 0和算术补符号位别移负数位。位运算置 0 / 提取位、|置 1、^翻转与交换、~取反记住a^a0、n(n-1)消最低位 1。求值优先级结合性决定顺序但别写靠猜的复杂表达式。一句话收尾位运算不难难的是把二进制那套地基先打牢。拓展学习青蛙跳台阶 / 汉诺塔上节递归的拓展可用位运算或递归继续练深入理解整数在内存中的补码存储与大小端字节序