☰
C语言操作符详解:从优先级到避坑实战,初学必看
2026/10/3 14:55:49 网站建设 项目流程

如果你是刚开始学C语言,多半已经体会过:代码能看懂,但真让自己写一个带判断条件的表达式,结果总跟预期对不上。比如a = b << 2 + 1大家各执一词,有人说先算移位有人先说加法;再比如a++ + ++a这种经典题目,不同编译器出来的结果还不一样。这些东西的背后,全都指向同一个主题:操作符。这是《C语言初阶》系列里绕不开的第二道坎,也是后续指针、结构体嵌套表达式的基础。

这篇“操作符详解上”把 C 语言里最常用的算术、移位、位、赋值、单目、关系、逻辑七类操作符拆开讲,重点放在“为什么是这样”和“实际写代码时怎么避坑”,不是只丢给你一张优先级表。适合刚学完变量、输入输出、开始写分支和循环的初学者,也适合准备计算机二级或学校考试、想系统复习操作符优先级的同学。你只要能看懂int a = 1;这种最基本的声明,就能跟着往下走。

1. 操作符整体分类与优先级速览

1.1 操作符分类总览

很多人学操作符喜欢一个个硬背,我的建议反过来:先把 C 语言里有哪些操作符搞清楚,再去看细节。C 语言的操作符主要可以分成这几类:

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

这篇“上”先讲前七类,因为日常写表达式用得最多。条件操作符、逗号表达式、下标和结构成员这些放到“下”会更合适,它们经常和函数调用、指针、结构体语法混在一起,拆开讲不会把你绕晕。

1.2 优先级和结合性速记表

操作符的核心规则只有两个:优先级和结合性。优先级决定不同操作符谁先算,结合性决定相同优先级的操作符从左算还是从右算。

下面这张表是我整理的速记版,从高到低:

优先级操作符说明
高()[]->.后缀++--后缀表达式
!~+-*&sizeof(类型)前缀++--单目操作符
*/%乘法类
+-加法类
<<>>移位
<<=>>=关系比较
==!=相等比较
&按位与
^按位异或
|按位或
&&逻辑与
||逻辑或
?:条件操作符
低=+=-=等赋值操作符

结合性方面,绝大多数二元操作符是左结合,也就是从左往右算;但赋值操作符是从右往左结合,条件操作符也是右结合的。最典型的是a = b = c = 10,它的执行顺序是从右往左,把 10 依次赋给 c、b、a。

1.3 不确定就加括号

我见过太多人栽在1 << 2 + 1这种表达式上。根据优先级表,加减法的优先级高于移位,所以2 + 1会先计算,结果其实是1 << 3,等于 8。如果你想表达“1 左移 2 位,再加 1”,必须写成(1 << 2) + 1,结果才是 5。

所以我的核心建议第一条:写复杂表达式时,不要对着表硬推,直接加括号。加括号不是示弱,是把意图清清楚楚告诉编译器,也让后面维护代码的人少掉头发。

2. 算术操作符

2.1 五个基础算术操作符

算术操作符一共有五个:+-*/%。其中加、减、乘、除大家都熟悉,唯一要重点注意的是取模%。

取模%要求两个操作数都是整数,结果也是整数,它得到的是“整除后的余数”。比如7 % 2结果是 1,因为 7 除以 2 商 3 余 1。

下面这段代码我建议你亲手跑一遍:

#include <stdio.h> int main(void) { int a = 7; int b = 2; printf("%d\n", a + b); // 9 printf("%d\n", a - b); // 5 printf("%d\n", a * b); // 14 printf("%d\n", a / b); // 3 printf("%d\n", a % b); // 1 return 0; }

注意看最后一次除法a / b,输出是 3 而不是 3.5,因为两个整数相除,C 语言直接截断小数部分。

2.2 整数除法、浮点数除法和取模的坑

整数除法是初学者第一个容易翻车的地方。5 / 2得到的是 2,不是 2.5。哪怕你潜意思觉得“数学上应该是 2.5”,C 语言只看操作数类型:两个整数相除,结果就是整数。

如果希望保留小数,至少要让一边变成浮点数。常见的写法有三种:

double r1 = 5.0 / 2; // 2.500000 double r2 = (double)5 / 2; // 2.500000 double r3 = 5 / 2; // 2.000000,先整除再转double,还是2

第三种写法是错误的,它先做整数除法得到 2,再转成 double,结果依然是 2.0。这一点在笔试里特别爱考。

关于取模操作符还有一个细节:负数取模的结果符号问题。在主流编译器里,-7 % 2的结果是 -1,7 % -2的结果是 1。可以简单记忆为“余数的符号跟被除数一致”。不过早期 C 标准并没有完全统一这个行为,所以写代码时尽量不要依赖负数取模的符号,最好保持操作数是正数。

2.3 溢出问题与实用建议

溢出是算术操作里最容易被忽略的坑。有符号整数溢出在 C 语言中属于未定义行为,也就是说编译器的处理方式可以千奇百怪,你不能默认它会“溢出后回绕成负数”。例如INT_MAX + 1的结果不是固定的,甚至可能被优化成意外的东西。

无符号整数不一样,它做加减乘时是明确按 2 的 n 次方取模回绕的。也就是说无符号数永远不会溢出,它会“包回去”。

实际开发建议是:如果预料到数字可能比较大,优先考虑long long而不是贪图省事用int。尤其是做累加、求阶乘这类题,int满打满算也就二十多亿,很容易就爆了。

3. 移位操作符

3.1 左移和右移的二进制本质

移位操作符有两个:<<左移,>>右移。它们的操作对象是整数的二进制位。

左移x << k,就是把 x 的所有二进制位向左移动 k 位,右边空出来的位补 0,左边高位直接丢弃。每左移 1 位,数值上相当于乘以 2。所以3 << 2等于 12,因为 3 乘以 2 再乘以 2。

右移x >> k稍微复杂一点。对于无符号整数,右移时左边空出来的位补 0,叫逻辑右移。对于有符号整数,C 标准没有强制规定,大多数编译器采用算术右移:左边补符号位,也就是负数补 1,正数补 0。所以在写代码时,尽量不要对有符号负数做右移,因为结果依赖平台。

看个例子:

#include <stdio.h> int main(void) { unsigned char x = 1; printf("%u\n", x << 3); // 8 int y = -8; printf("%d\n", y >> 1); // 常见结果是 -4,但依赖编译器实现 return 0; }

3.2 移位操作符的优先级陷阱

移位操作符的优先级比算术加减法低,这一点非常反直觉。很多人写1 << 2 + 1,心中想的是“把1左移2位再加1”,但实际结果是1 << 3。

原因很简单:+的优先级高于<<,所以先算2 + 1,再移位。

表达式真实含义计算结果
1 << 2 + 11 << (2 + 1)8
(1 << 2) + 1(1 << 2) + 15

你要是问我怎么避免,答案还是那句:移位操作符配合其他运算符时,一律加括号。

3.3 移位在实际项目中的常见用途

移位最常见的用途是配合位操作符做标志位处理。比如一个状态变量,你想用位 3 表示某个开关状态,写成flags |= 1 << 3;既可以置位,又不影响其他位。

另一个用处是乘以或除以 2 的幂。不过现在编译器优化做得很好,x * 8和x << 3生成的机器码几乎一样,所以初学阶段不必刻意用移位替代乘法。

还需要强调一个安全点:移位的位数如果大于等于该类型的位数,属于未定义行为。比如 32 位 int,写x << 32是不可靠的。这类问题在老代码里很常见,自己写的时候要规避。

4. 位操作符

4.1 按位与、按位或、按位异或、按位取反

位操作符处理的是每一个二进制位,它们包括:

  • &按位与:两个位都是 1 时结果才是 1
  • |按位或:只要有一个是 1,结果就是 1
  • ^按位异或:两个位不同,结果是 1
  • ~按位取反:1 变 0,0 变 1

举个例子:

#include <stdio.h> int main(void) { unsigned char a = 0x0A; // 二进制 0000 1010 unsigned char b = 0x0C; // 二进制 0000 1100 printf("%X\n", a & b); // 0000 1000 -> 8 printf("%X\n", a | b); // 0000 1110 -> E printf("%X\n", a ^ b); // 0000 0110 -> 6 printf("%X\n", (unsigned char)~a); // 1111 0101 -> F5 return 0; }

注意最后一个~a我做了(unsigned char)强制转换,因为按位取反之前会发生整型提升,直接打印可能会得到一个大整数,初学者经常被这个现象吓到。整型提升的细节在指针和类型转换专题里会展开,这里先记住“取反后记得看类型”。

4.2 位操作经典应用:掩码、置位、清位、翻转

位操作不只是考试题里的计算题,实际开发中用处极大。

判断奇偶性可以用n & 1:如果二进制最低位是 1,数字就是奇数。这比n % 2在某些底层场景下更直观,性能也差不多。

取低 4 位可以用掩码:n & 0x0F。这个操作在寄存器处理里很常见。

置位、清位、翻转是三个基本操作:

flags |= 1 << 3; // 把第3位置1 flags &= ~(1 << 3); // 把第3位清0 flags ^= 1 << 3; // 把第3位翻转

很多人不理解为什么清位时要用~(1 << 3)而不是flags &= ~1 << 3。这里其实是“先算出只有第3位为1的掩码,再取反,得到除了第3位为1之外其他位都是1的掩码”,然后按位与,就能不动其他位置只清掉第3位。这个思路建议先在纸上画一遍二进制的位,必须自己推一次才能真懂。

还有个老玩法是异或交换两个变量:

a ^= b; b ^= a; a ^= b;

这个技巧看似炫酷,但实际可读性差,性能也没有优势,我不建议在正式项目里用它。它的价值在于帮你理解异或的性质:同一个数异或两次会还原。

4.3 位操作符与逻辑操作符的对比陷阱

&和&&,|和||是两对非常容易混淆的操作符,但它们完全不同。

1 & 2 // 0,因为 01 & 10 = 00 1 && 2 // 1,因为两边都非0 3 | 4 // 7 3 || 4 // 1

位操作符比较的是“每个对应的二进制位”,逻辑操作符比较的是“整个表达式的真假”。一个典型错误是有人写if (x & 1 == 0),以为判断 x 是偶数,实际上因为==优先级高于&,表达式变成x & (1 == 0),而1 == 0是 0,整个结果永远是 0。这里如果想让判断生效,必须写成(x & 1) == 0。这类优先级问题在后面的排查章节还会提到。

5. 赋值操作符

5.1 简单赋值与连续赋值

=是赋值操作符,不是数学上的等号。它的作用是把右边表达式的值赋给左边可修改的变量。

赋值操作符有一个关键特性:整个赋值表达式是有值的,值就是被赋的那个值。所以它可以嵌套在更大的表达式里。最经典的例子是:

while ((ch = getchar()) != EOF) { // ... }

这里ch = getchar()的赋值结果会与EOF比较,但因为!=优先级高于=,所以必须加括号,否则编译都会报错。

连续赋值是赋值操作符右结合性的体现:

int a, b, c; a = b = c = 10;

这个语句把 10 赋给 c,再把 c 的值赋给 b,再把 b 的值赋给 a,三个变量最终都是 10。它特别适合一段代码里把多个变量同时初始化。

5.2 复合赋值操作符

复合赋值操作符是“赋值和算术/位操作的组合写法”,包括+=-=*=/=%=<<=>>=&=|=^=等。

a += b等价于a = a + b,a <<= 2等价于a = a << 2。写成复合形式除了省几个字符,还有一个细微好处:左边的变量只写一次,语义更清晰,编译器也更容易优化。

注意事项:复合赋值表达式的优先级比较低,所以把它混合在复杂表达式里时,最好也用括号包住。比如x = y *= 2虽然合法,但读起来很费劲,不建议写。

5.3 赋值与等于的比较陷阱

初学阶段最常被坑的写法是:

if (x = 5) { // ... }

这里不是判断 x 是否等于 5,而是把 5 赋给 x,再判断赋值表达式的值是否为真。由于 5 非 0,条件恒为真,程序行为完全偏离预期。

防止这种错误有个老派但有效的技巧:把常量写在左边。

if (5 == x)

如果你手滑写成if (5 = x),编译器会直接报错,因为 5 不是一个可修改的左值。这个方法不能解决所有问题,但能拦截掉最常见的==写成=的隐患。另外现在主流编译器比如 GCC 加-Wall也会对if (x = 5)给出警告,编译时多看一眼警告,能省下很多调试时间。

6. 单目操作符

6.1 单目操作符全家桶

单目操作符的意思是“只需要一个操作数就能运算”的操作符,常见的有这么几个:

  • !逻辑非
  • -负号
  • +正号,基本没实际意义
  • &取地址
  • *解引用
  • sizeof计算大小
  • ~按位取反
  • ++--前置自增自减
  • (类型)强制类型转换

取地址和解引用在这里先混个脸熟,详细内容会在指针那篇展开。这一节我重点讲自增自减、sizeof 和强制类型转换。

6.2 自增自减的经典问题

i++和++i的区别,一句话说:i++先返回 i 的旧值,再把 i 加 1;++i先把 i 加 1,再返回 i 的新值。

直接看代码:

int i = 3; printf("%d\n", i++); // 输出 3,然后 i 变成 4 printf("%d\n", ++i); // i 先变成 5,输出 5

这里有一个大坑:把一个变量在同一表达式里多次自增自减,结果是不确定的。比如i++ + i++属于未定义行为,不同编译器可能给出不同结果,这种代码没有任何讨论价值,笔试考到了直接指出“未定义行为”就行。实际项目里也不要写这种“炫技”代码,就算能跑,也是埋雷。

*p++是另一个高频考点,它等价于*(p++),也就是先取指针 p 当前指向的值,再让 p 向后移动。这和(*p)++完全不一样,后者是先让 p 指向的那个变量自增。这个区别到了指针部分会反复出现,现在就建立条件反射:后缀++的优先级高于解引用*。

6.3 sizeof 的细节

sizeof是操作符,不是函数。它可以用来求一个类型或变量所占的字节数,返回值类型是size_t,打印时用%zu。

它的写法有点特殊:

sizeof(int) // 类型要加括号 sizeof x // 变量可以不加括号

初学者最容易记错的是第二点:对变量使用 sizeof 时,括号不是必须的。你写sizeof(x)也不会错,所以我通常建议统一都加括号,少踩规则坑。

另一个经典考点是 sizeof 不会真正执行括号里的表达式。看这里:

int a = 5; printf("%zu\n", sizeof(a++)); printf("%d\n", a); // 输出仍然是 5

因为sizeof只关心类型大小,不会去执行“自增”的副作用。这个规律有一个特例是变长数组,初学阶段可以不用管,但要知道不能默认 sizeof 一定不求值。

有一个最容易被误解的点是:sizeof求出来的是“字节数”,不是“元素个数”。求数组元素个数要写成sizeof(arr) / sizeof(arr[0]),这在后续刷题时是高频代码。

6.4 强制类型转换

强制类型转换(类型)表达式也是一个单目操作符,它可以把表达式的结果转换成指定类型。

重点还是刚才说的整数除法:

int a = 5, b = 2; double x = (double)a / b; // 2.5 double y = (double)(a / b); // 2.0

第一种是先把 a 转成 double,再除以 b,得到 2.5;第二种是先做整数除法得到 2,再转成 double。一字之差,结果完全不同,考试和面试都爱用这种对比。

7. 关系操作符与逻辑操作符

7.1 关系操作符返回值与连续比较问题

关系操作符包括><>=<===!=,它们的返回值只有两个:真返回 1,假返回 0。在 C 语言里,0 代表假,任何非 0 值都代表真,而关系操作符作为表达式时,只会返回 0 或 1。

关系操作符有个经典错误,叫“连续比较”。比如数学里写a < b < c,很多人以为 C 语言也是这样判断的。实际上 C 语言会从左往右先算a < b,结果是 0 或 1,再用这个 0 或 1 去和 c 比较。举个例子:

int a = 5, b = 3, c = 2; printf("%d\n", a > b > c);

先算5 > 3,结果是 1,再算1 > 2,结果是 0。如果把 c 改成 0,1 > 0就是 1,整个表达式就变成真了,但数学上你根本不会拿 1 去比较。正确写法是a > b && b > c。

7.2 逻辑与、逻辑或、逻辑非

&&逻辑与:左右两个操作数都非 0,结果才是 1,否则为 0。||逻辑或:只要有一个操作数非 0,结果就是 1。!逻辑非:把真假反过来,非 0 变成 0,0 变成 1。

看代码:

int x = 3; int y = 0; printf("%d\n", x && y); // 0 printf("%d\n", x || y); // 1 printf("%d\n", !x); // 0 printf("%d\n", !y); // 1

注意逻辑与和逻辑或的操作数会被隐含转换成“真假值”,它们本身不一定是 0 或 1,所以!100的结果是 0 而不是别的数。

7.3 短路求值

逻辑与和逻辑或都有“短路求值”的特性。看名字就能理解:一旦已经能确定结果,后面的表达式就不再求值。

  • A && B:如果 A 为 0,整个结果必然为 0,B 不会执行
  • A || B:如果 A 非 0,整个结果必然为 1,B 不会执行

这个特性非常有用。最常见的场景是判空:

if (p != NULL && strlen(p) > 5) { // ... }

如果 p 是空指针,第一个条件为假,后面strlen(p)根本不会执行,不会解引用空指针。反过来如果写if (strlen(p) > 5 && p != NULL)在某些场景下就可能崩溃。

除法也是一个例子:

if (b != 0 && a / b > 2)

当 b 等于 0 时,第一个条件为假,后面的除法不会执行,避免了除零错误。

7.4 逻辑操作符与位操作符的对比

我一直建议初学者做一张对比表,把这两类操作符彻底区分开。它们在考试和项目里都容易混。

操作符作用对象结果
&|^每个二进制位按位结果
&&||整体真或假0 或 1
~每个二进制位按位取反
!整体真或假0 或 1

举例:3 & 2的结果是 2,因为二进制011 & 010 = 010;但3 && 2的结果是 1,因为两个都非 0。~1的结果是一个很大的负数(具体看 int 位数),但!1的结果是 0。这两类操作符没有任何等价关系,忘了谁都会翻车。

8. 常见问题排查与避坑经验

8.1 优先级导致的常见错误场景

以下这几个场景我几乎每年都能见到,建议把它们当标准答案背下来:

代码逻辑上的真实含义纠正方式
x & 1 == 0x & (1 == 0),结果是 0(x & 1) == 0
1 << 2 + 11 << (2 + 1),结果是 8(1 << 2) + 1
*p++*(p++),先取值后移指针明确意图再决定
a < b < c先算a < b得 0 或 1a < b && b < c
if (x = 5)给 x 赋值,条件恒真if (x == 5)或if (5 == x)

排查这些问题时,我喜欢先加括号验证一下,再把每个子表达式用 printf 打印出来,对比手工推算和实际输出的差别。不要靠猜,也不要直接抄网上的结论,自己跑一遍比什么都靠谱。

8.2 调试操作符表达式的方法

当你面对一个“看起来很奇怪”的表达式时,建议按三步排查:

先把表达式拆开,用中间变量保存子结果。比如int r = a << 2 + 1可以先写成int s = a << (2 + 1);,再去推值,这样思路清晰多了。

然后用编译器的警告选项辅助排查,GCC 和 Clang 都可以用-Wall -Wextra,很多赋值等于的误写、优先级可疑的表达式都能被提醒。

最后是主动证明:写一个最简 demo,用几个已知数值去验证表达式的求值顺序。操作符的坑大多数可以通过边界值测试暴露出来,重点测 0、1、负数、最大值、最小值这些特殊输入。

8.3 实操练习题与验证

把这一篇的知识浓缩成三道题,建议先自己算,再写出代码验证。

第一题:表达式求值。

int a = 1, b = 2, c = 3; int x = a + b * c; // 7 int y = (a + b) * c; // 9 int z = a << 2 + 1; // 8

第二题:用位运算判断一个整数是否为奇数,并让第 3 位清零。

#include <stdio.h> int main(void) { int n = 15; if (n & 1) printf("奇数\n"); else printf("偶数\n"); n &= ~(1 << 3); printf("%d\n", n); // 15 -> 7 return 0; }

第三题:猜一下输出,再跑一遍看看是否和你预期一致。

#include <stdio.h> int main(void) { int i = 3; printf("%d %d\n", i++, ++i); return 0; }

这道题我要专门提醒:它属于未定义行为,不要在实战里这样写。如果在面试题里碰见,正确回答是“结果未定义,不讨论具体值”,而不是费劲去推测编译器会怎么算。

我个人的教学体会是,操作符这部分死记优先级表效果很有限,真正起作用的是“动手验证 + 形成条件反射”。文章里这些坑,最好每个都写一个小 demo 在本地编译器里跑一遍,跑完再回头记优先级表,你会觉得那张表根本不是背出来的,而是代码跑出来的规律。下一篇操作符详解下,我会把条件操作符、逗号表达式、下标引用、函数调用和结构成员操作符一起串讲,顺带补充一些和指针混在一起的优先级难题。等你把这上下两篇消化完,再看实际项目里的复杂表达式,就不会再心跳加速了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询