
前言顺序结构是最基本的程序控制结构语句按书写顺序从上到下执行。这个描述对语句层面成立但一到表达式层面就容易产生误解——很多人以为从左到右书写就从左到右求值。C 标准并不这么规定。需要区分两个概念运算符优先级precedence决定表达式如何加括号结合性associativity决定同优先级运算符如何分组而求值顺序order of evaluation决定子表达式按什么次序计算。三者互相独立。f() g()的优先级很清楚加法但先调用f还是先调用g标准不作保证。关系和逻辑运算符是条件判断的基础。、、、、、!产生bool、||、!组合条件并具有短路特性其中和||的左侧一定先于右侧求值这是标准明确规定的少数顺序之一。本文讲清顺序结构、求值顺序、关系与逻辑运算符的语义和优先级并列出常见写法错误。一、语句层面的顺序结构不加分支和循环时语句按出现顺序执行。每条语句以分号结束声明、表达式、函数调用都算语句。// 适用于 C17#include iostreamint main(){int a 0;a a 1; // 先执行a a * 2; // 后执行此时 a 为 2std::cout a \n;return 0;}函数调用是一个顺序点意义上的分界被调用函数执行完毕控制权返回后下一条语句才开始。但函数实参之间的求值顺序不是从左到右这一点下一节展开。二、优先级不等于求值顺序优先级只解决表达式怎么分组。比如a b * c一定解析为a (b * c)但a、b、c三个子表达式谁先算标准没有规定。C17 明确了部分运算符的顺序赋值运算符的右侧先于左侧移位的左侧先于右侧下标表达式方括号内的部分先于方括号外。但大多数二元运算符包括的操作数仍然是无顺序unsequenced的先后不确定标准不保证一个完成后再开始另一个。函数实参之间在 C17 起是不确定顺序indeterminately sequenced不交错但先后不定。如果两个无顺序的子表达式修改同一个对象就是 UB。// 适用于 C17#include iostreamint f(){std::cout f ;return 1;}int g(){std::cout g ;return 2;}int main(){const int x f() g(); // 输出顺序可能是 f g 或 g fstd::cout x \n;return 0;}这段代码的结果x一定是 3但打印顺序不保证。如果需要固定顺序就拆成两条语句// 适用于 C17const int a f();const int b g();const int x a b;赋值运算符在 C17 起规定右操作数先于左操作数求值。函数调用表达式中后置表达式先于实参求值但实参之间仍是不确定顺序。逗号运算符则是明确从左到右左侧先求值且其副作用完成后才求右侧。三、关系运算符关系运算符比较两个值返回bool运算符含义示例小于a b小于等于a b大于a b大于等于a b等于a b!不等于a ! b关系运算符是左结合的且优先级高于、||低于算术运算符。所以a 1 b * 2等价于(a 1) (b * 2)。不能把比较串起来写。a b c解析为(a b) c先得到bool再拿这个bool0 或 1与c比较几乎永远不是想要的结果。正确写法是a b b c。浮点数用比较通常不可靠因为十进制小数在二进制里往往无法精确表示运算还会累积舍入误差。需要比较时通常判断差值是否小于一个容差容差的选取取决于业务对精度的要求。四、逻辑运算符与短路逻辑与、||逻辑或、!逻辑非把操作数按是否为零转成布尔值aba ba \\b!afalsefalsefalsefalsetruefalsetruefalsetruetruetruefalsefalsetruefalsetruetruetruetruefalse短路规则a ba为假时b不求值结果直接为假。a || ba为真时b不求值结果直接为真。!a总是求值。短路不仅是优化更是正确性的一部分。经典的链表遍历写法while (p ! nullptr p-next ! nullptr)依赖短路p为空时不会去读p-next。短路也意味着右侧的副作用可能不发生。ok || (count, false)这类写法可读性很差而且count是否自增取决于ok不应出现在正常代码里。五、优先级与结合性从高到低常用运算符大致如下优先级运算符结合性高!、~、一元/-、、--右高*、/、%左中、-左中、、、左中低、!左低左更低\\最低?:、赋值右由此可以推出几个容易错的式子!a b是(!a) b。a || b c是a || (b c)。x 1 0是x (1 0)几乎恒为 0。a b c是a (b c)先把比较结果赋给a。写条件时把括号补全比背诵优先级表可靠。六、完整的条件判断示例// 适用于 C17#include iostreamint main(){int score 0;std::cout score: ;if (!(std::cin score)) {std::cerr invalid input\n;return 1;}if (score 0 || score 100) {std::cout out of range\n;} else if (score 90) {std::cout A\n;} else if (score 60) {std::cout B\n;} else {std::cout C\n;}return 0;}这里先判断范围再判断等级else if的顺序保证了每个分数只命中一个分支。如果把score 90放在最前面而不先排除超过 100 的情况105 分也会被判成 A。七、逗号运算符与 if 初始化逗号运算符是少数明确规定求值顺序的运算符左侧先求值其副作用完成后才求右侧整个表达式的结果是右侧的值。它常被用来在一条语句里做多件事但滥用会降低可读性。// 适用于 C17int i 0;int j (i, i * 2); // 先 i 得到 1再算 1 * 2C17 还给if和switch增加了初始化语句可以把先取得某个值再判断写在一个作用域里// 适用于 C17#include map#include stringvoid check(const std::mapstd::string, int m, const std::string key){if (auto it m.find(key); it ! m.end()) {// it 在这里可见离开 if 后销毁}}初始化的求值先于条件判断这是标准规定的顺序不存在先判断再初始化的可能。常见坑点❌ 条件里写if (x 5)把比较写成了赋值。✅ 写if (x 5)对常量可用5 x的写法让编译器报错。❌ 链式比较if (0 x 10)。✅ 写if (x 0 x 10)。❌ 认为f() g()一定先调用f。✅ 顺序不确定需要固定顺序就拆语句。❌ 用代替做空指针保护。✅才有短路两侧都会求值。❌ 对浮点数直接用判断相等。✅ 用容差比较或改用整数/定点表示。❌ 依赖a || b中b一定执行。✅ 左侧为真时b不求值必须执行就单独写一条语句。❌ 在else if链中把宽条件放在窄条件前面。✅ 从最严格的条件开始或先排除非法范围。❌ 把!的优先级想得比比较低。✅!a b是(!a) b需要时加括号。总结概念作用易错点顺序结构语句自上而下执行与表达式求值顺序不同优先级决定表达式如何分组不等于求值先后关系运算产生bool不能链式比较逻辑运算组合条件短路右侧可能不执行求值顺序子表达式何时计算多数不确定勿依赖写条件判断时先想清楚哪些情况非法、哪些情况正常再用关系运算符把边界写出来用、||组合。括号和短路规则用对绝大多数逻辑错误都能在编码阶段避免。