ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C语言操作符详解:从优先级到避坑实战,初学必看

C语言操作符详解:从优先级到避坑实战,初学必看 如果你是刚开始学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的结果是 -17 % -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 变 00 变 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 ba 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 加 1i先把 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 改成 01 0就是 1整个表达式就变成真了但数学上你根本不会拿 1 去比较。正确写法是a b b c。7.2 逻辑与、逻辑或、逻辑非逻辑与左右两个操作数都非 0结果才是 1否则为 0。||逻辑或只要有一个操作数非 0结果就是 1。!逻辑非把真假反过来非 0 变成 00 变成 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整个结果必然为 0B 不会执行A || B如果 A 非 0整个结果必然为 1B 不会执行这个特性非常有用。最常见的场景是判空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) 01 2 11 (2 1)结果是 8(1 2) 1*p*(p)先取值后移指针明确意图再决定a b c先算a b得 0 或 1a b b cif (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 在本地编译器里跑一遍跑完再回头记优先级表你会觉得那张表根本不是背出来的而是代码跑出来的规律。下一篇操作符详解下我会把条件操作符、逗号表达式、下标引用、函数调用和结构成员操作符一起串讲顺带补充一些和指针混在一起的优先级难题。等你把这上下两篇消化完再看实际项目里的复杂表达式就不会再心跳加速了。
返回列表