☰
C语言运算符的优先级与结合性详解及一些错误讲解
2026/10/15 4:24:15 网站建设 项目流程

前言


*p++、a = b = c、x & 0xFF == 0、i = i++ + 1——这些表达式有一个共同点:它们的含义完全取决于优先级(precedence)与结合性(associativity),而其中好几个的真实含义和你的直觉相反。


C 语言有 40 多个运算符,优先级表长到没人能背全。但经验丰富的 C 程序员并不靠背表,而是掌握三条策略:



  1. 记住影响最大的几组优先级关系;

  2. 遇到不确定就加括号;

  3. 能拆成多条语句就拆开,尤其要避免依赖求值顺序。


本文系统梳理优先级与结合性,重点讲解那些"写得出来但含义反直觉"的表达式,并给出可运行的验证代码。


一、优先级与结合性是什么



  • 优先级(precedence):决定哪个运算符先与操作数结合。优先级高的先算。

  • 结合性(associativity):当两个同优先级运算符相邻时,决定从哪边开始结合。分左结合(left-to-right)和右结合(right-to-left)。


以a - b - c为例,-是左结合,所以等价于(a - b) - c,而不是a - (b - c)。


以a = b = c为例,=是右结合,所以等价于a = (b = c)——这正是连续赋值能工作的原因。


二、优先级总表(常用部分)


从高到低排列:




优先级运算符说明结合性



1()[]->.函数调用、下标、成员左

2!~++--+-*&(type)sizeof一元运算符(含强制类型转换)右

3*/%乘除取模左

4+-加减左

5<<>>移位左

6<<=>>=关系左

7==!=相等性左

8&按位与左

9^按位异或左

10`\`按位或

11&&逻辑与左

12`\\`

13?:条件运算符右

14=+=-=等赋值右

15,逗号左



2.1 记忆口诀(旧版)


老教材有一个口诀:「括号成员第一;全体单目第二;乘除余三,加减四;移位五,关系六;等于不等排第七;位与异或和位或,三五之后八九十;逻辑与或十二起;条件十三,赋值十四;逗号最低。」


口诀只用于快速定位,真正写代码时请加括号。


三、八个最容易出错的表达式


3.1*p++


前缀/后缀++优先级(2)高于一元*(2,但后缀更紧),实际解析为*(p++)。


#include <stdio.h>

int main(void)
{
int arr[3] = {10, 20, 30};
int *p = arr;

int v = *p++; // 等价于 *(p++),v = 10,p 指向 arr[1]
printf("v = %d, *p = %d\n", v, *p); // v = 10, *p = 20

return 0;
}



写法解析效果



*p++*(p++)取当前值,指针后移

(*p)++明确值加一,指针不动

*++p*(++p)指针先移,再取值

++*p++(*p)值加一,指针不动



3.2x & 0xFF == 0


这是最经典的优先级陷阱。==(优先级 7)高于&(优先级 8),所以解析为x & (0xFF == 0),即x & 0,结果恒为 0。


#include <stdio.h>

int main(void)
{
unsigned int x = 0x100;

// ❌ 错误:等价于 x & (0xFF == 0) = x & 0 = 0
if (x & 0xFF == 0)
printf("low byte is zero (wrong logic)\n");
else
printf("else branch\n"); // 这里会执行

// ✅ 正确:加括号
if ((x & 0xFF) == 0)
printf("low byte is zero\n");

return 0;
}

3.3a = b = c


=是右结合,解析为a = (b = c)。这是合法且常用的写法。


int a, b, c;
a = b = c = 5; // 全部为 5

但要注意类型截断:


int i;
char ch;
i = ch = 300; // ❌ ch 先被截断为 300 & 0xFF = 44,i 得到 44 而非 300

3.4i = i++ + 1


这是未定义行为,不是优先级问题。同一表达式中i既被修改又被读取,中间没有序列点。


int i = 0;
i = i++ + 1; // ❌ UB

正确写法:


i = i + 1; // ✅

3.5printf("%d", a ? b : c)与嵌套?:


?:优先级(13)很低,右结合。常见错误是把赋值写进条件分支:


int a = 1, b = 2, c = 3;

// 解析为 (a ? b : c) = 5?不,?: 不是左值
// 实际:a ? b : (c = 5) —— 若 a 为真,c 不会被赋值
a ? b : c = 5;

// ✅ 用括号明确
a ? b : (c = 5);

3.6sizeof与表达式


sizeof是运算符(优先级 2,右结合),不是函数。sizeof(int)中的括号是类型语法的一部分。


int x = 5;

size_t s1 = sizeof x; // 4,不需要括号(但对变量可省)
size_t s2 = sizeof(int); // 4
size_t s3 = sizeof x + 1; // (sizeof x) + 1 = 5,不是 sizeof(x + 1)!

// ❌ 常见错误:以为会算 sizeof(x + 1)
// ✅ 明确写成 sizeof(x + 1)

3.7 移位与加法的组合


+(优先级 4)高于<<(优先级 5)。


int a = 1, b = 2;

// 解析为 (1 + 2) << 3 = 24,不是 1 + (2 << 3) = 17
int r = a + b << 3;

// ✅ 明确意图
int r2 = (a + b) << 3;
int r3 = a + (b << 3);

3.8 逗号运算符


逗号(优先级最低)会丢弃左侧的值,只返回右侧。


int a = (1, 2, 3); // a = 3

// ⚠️ 函数实参中的逗号是分隔符,不是逗号运算符
f(1, 2, 3); // 传三个参数

// 如果想传一个逗号表达式的值,要加括号
f((1, 2, 3)); // 传一个参数,值为 3


注意:int arr[2][3]里的逗号也是分隔符,不要混淆。



四、结合性的实际影响


4.1 左结合 vs 右结合的差异


#include <stdio.h>

int main(void)
{
int a = 10, b = 3, c = 2;

// 减法左结合:((10 - 3) - 2) = 5
printf("%d\n", a - b - c); // 5

// 赋值右结合:a = (b = (c = 7))
a = b = c = 7;
printf("%d %d %d\n", a, b, c); // 7 7 7

return 0;
}

4.2 指针声明的"结合性"错觉


int *a, b;中*只作用于a,b是int。这不是优先级问题,而是声明符(declarator)语法。


int *a, b; // a 是 int*,b 是 int
int *a, *b; // 两个都是 int*

五、实战:用编译器帮你抓优先级错误


GCC/Clang 提供-Wparentheses,能捕获大部分可疑的优先级写法。


gcc -Wall -Wextra -Wparentheses -O2 -o test test.c

-Wall已经包含-Wparentheses,但显式写出更清晰。


#include <stdio.h>

int main(void)
{
unsigned int x = 0x100;

// 编译时会警告:suggest parentheses around comparison in operand of '&'
if (x & 0xFF == 0) { }

return 0;
}

编译输出类似:


warning: suggest parentheses around comparison in operand of '&' [-Wparentheses]

5.1 用单元测试固定语义


#include <stdio.h>
#include <assert.h>

int main(void)
{
int arr[3] = {1, 2, 3};

// 固定 *p++ 的语义,防止误改
int *p = arr;
assert(*p++ == 1);
assert(*p == 2);

// 固定 sizeof 的语义
int x = 5;
assert(sizeof x + 1 == 5);
assert(sizeof(x + 1) == sizeof(int));

// 固定移位优先级
assert((1 + 2) << 3 == 24);

printf("all assertions passed\n");
return 0;
}

把语义写成断言,比注释可靠得多。


常见坑点


坑点 1:&、|、^与==、!=的优先级关系反直觉。位运算符优先级低于相等性运算符,这是 C 语言设计上被广泛批评的一点。


// ❌ 全部需要括号
if (x & MASK == 0) { }
if (x | FLAG == FLAG) { }

// ✅
if ((x & MASK) == 0) { }
if ((x | FLAG) == FLAG) { }

坑点 2:&&比||优先级高,导致条件分组错误。


// 解析为 a || (b && c)
if (a || b && c) { }

// ✅ 显式表达意图
if (a || (b && c)) { }
if ((a || b) && c) { }

坑点 3:移动/取值与自增混用。


int arr[5] = {0};
int *p = arr;

// ⚠️ 需要加括号表达真实意图
int v = *p++; // 取 arr[0],p 后移
int w = (*p)++; // arr[1] 自增

坑点 4:-的二元与一元歧义。


int a = 5, b = 3;

int r1 = a - -b; // 6,二元减 + 一元负
int r2 = a--b; // ❌ 词法解析为 a-- b,语法错误

坑点 5:多重赋值中的截断。


int i;
char c;
i = c = 1000; // c 被截断,i 得到的是截断后的值

坑点 6:条件运算符中遗漏括号导致赋值位置错误。


int a = 1, b = 2, c = 3;

// ⚠️ 实际是 a ? b : (c = 5)
a ? b : c = 5;

// ✅ 明确
a ? b : (c = 5);

坑点 7:把->、.与*混用。


struct Node { int val; };
struct Node n = {1};
struct Node *p = &n;

// ✅ 两种等价写法,但优先级不同
int x1 = (*p).val; // 必须加括号
int x2 = p->val; // -> 优先级高,无需括号

总结




表达式实际解析建议



*p++*(p++)加括号或拆开

x & 0xFF == 0x & (0xFF == 0)(x & 0xFF) == 0

a = b = ca = (b = c)可用,注意类型截断

a + b << 3(a + b) << 3加括号

a ? b : c = 5a ? b : (c = 5)加括号

sizeof x + 1(sizeof x) + 1加括号

i = i++ + 1UB拆成两条语句



三条黄金实践:



  1. 拿不准就加括号。括号不花钱,bug 很贵。

  2. 不要在一个表达式里堆超过两个运算符。拆成多条语句更易读。

  3. 开启-Wall -Wextra并认真对待-Wparentheses警告。


优先级表值得看,但更值得养成的是"用括号显式表达意图"的习惯——因为代码是给人读的,编译器只是顺便执行它。




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

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

立即咨询