第 05 章

第 5 章:算术、优先级与逻辑运算

本章对应长视频 4:45:33–5:55:57。讲清整数除法的截断与取模、表达式的优先级与结合性、前后缀自增自减、复合赋值、比较运算符与逻辑运算符,并解释为什么打印比较结果时必须加括号。

Chapter 5: Arithmetic, Precedence and Logic

Covers 4:45:33–5:55:57 of the long video. It explains integer division truncation and modulus, precedence and associativity, prefix and postfix increment, compound assignment, comparison operators and logical operators, and why printing a comparison requires parentheses.

首发于 2022.02.17中 / EN
系统课程cppoperatorsprecedenceinteger-divisionboolean-logic
语言
字号
目录
篇内目录

这一章在讲什么

第 5 章覆盖 4:45:33–5:55:57。前面两章把数据放进变量,这一章开始对数据做运算。核心不是"多背几个符号",而是理解两件事:

  • 整数的除法与取模和数学不一样:7 / 2 得 3,小数部分被直接丢掉。
  • 一个表达式里有多个运算符时,谁先算由优先级和结合性决定;读代码的人不该被迫去查表,所以要用括号把意图写清楚。

时间轴

时间内容
4:47:39加减乘除与取模;整数除法的截断
4:58:05优先级与结合性;运算符表
5:07:14用括号明确运算顺序
5:11:54前缀与后缀自增自减
5:23:30复合赋值运算符
5:31:42关系运算符与布尔结果
5:40:59逻辑运算符:与、或、非

算术运算

int number1 {2};
int number2 {7};
int result {0};

result = number1 + number2;   // 9
result = number2 - number1;   // 5
result = number1 - number2;   // -5
result = number1 * number2;   // 14

这些都是整数运算,结果仍然是整数。

整数除法:截断而不是四舍五入

result = number2 / number1;   // 7 / 2
std::cout << result << std::endl;   // 3

7 / 2 得到 3,不是 3.5。原因是右边两个操作数都是 int,结果是 int,只能保留整数部分。可以理解为"2 能完整装进 7 几次"——三次,剩下的 1 被丢掉。

想要小数结果,至少让一个操作数是浮点数:

double quotient = 7.0 / 2;   // 3.5

7 / 2.0、7.0 / 2、7.0 / 2.0 都行,编译器会把另一个也提升为 double 再相除。

取模:余数

result = number2 % number1;   // 7 % 2
std::cout << result << std::endl;   // 1

% 求的是整数除法的余数。31÷10=331 \div 10 = 3 余 1,所以 31 % 10 等于 1。

取模只对整数有意义。7.5 % 2 编译不过——浮点数的"余数"没有公认定义,C++ 直接用 std::fmod 处理。

常见用途:判断奇偶(n % 2 == 0)、循环取模做环形索引(index % size)、提取十进制各位(n % 10 取个位)。

优先级与结合性

一个表达式里有多个运算符时,需要两条规则:

  • 优先级(precedence):不同运算符谁先算。*、/、% 高于 +、-。
  • 结合性(associativity):同优先级时从左还是从右算。加减乘除都是从左到右。

看这个表达式:

int a {6}, b {4}, c {6}, d {9}, e {3}, f {2}, g {5};

int result = a + b * c - d / e - f + g;

按规则拆解:

  1. 先算乘除:b * c = 24,d / e = 3。
  2. 表达式变成 a + 24 - 3 - f + g,即 6 + 24 - 3 - 2 + 5。
  3. 从左到右:6+24=30,30-3=27,27-2=25,25+5=30。

结果 30。

与之相关的优先级顺序(从高到低):

优先级运算符结合性
高后缀 ++ --左
前缀 ++ --、一元 + -右
* / %左
+ -左
<< >>左
< <= > >=左
== !=左
&&左
低`
最低赋值 = += 等右

用括号表达意图

int result = a + (b * c) - (d / e) - f + g;   // 同样是 30

结果相同,但读代码的人不用查表就知道先算什么。课程的建议很明确:不要过度依赖优先级表,用括号把意图写出来。

括号更大的作用是改变运算顺序:

int result = (a + b) * c - d / e - f + g;
// (6+4)*6 - 3 - 2 + 5 = 60

现在加法先做,结果从 30 变成 60。

自增与自减

++ 加 1,-- 减 1。它们只能加减 1,不能加减 5 或 7。

后缀:先用后改

int value {5};
std::cout << value++ << std::endl;   // 5
std::cout << value << std::endl;     // 6

value++ 先取出当前值参与运算(这里是打印),语句结束后才把 value 加 1。所以第一行打印 5,第二行打印 6。

value-- 同理:

int value {5};
std::cout << value-- << std::endl;   // 5
std::cout << value << std::endl;     // 4

前缀:先改后用

int value {5};
std::cout << ++value << std::endl;   // 6
std::cout << value << std::endl;     // 6

++value 先把 value 加 1,再取出新值参与运算,所以两行都打印 6。

记忆方法:符号在后,旧值先出场;符号在前,新值先出场。

单独写一行时(value++; 或 ++value;),两者效果相同,只有把表达式的结果拿去用时才有区别。

复合赋值

+=、-=、*=、/=、%= 把"运算"和"赋值"合成一步:

int value {45};

value += 5;    // 等价于 value = value + 5;  → 50
value -= 5;    // 等价于 value = value - 5;  → 45
value *= 2;    // 等价于 value = value * 2;  → 90
value /= 3;    // 等价于 value = value / 3;  → 30
value %= 11;   // 等价于 value = value % 11; → 8

最后一步验证:30÷11=230 \div 11 = 2 余 8,所以结果是 8。

复合赋值不是必需的语法糖,但它把"对同一个变量做累积运算"这个意图写得更紧凑,读起来也更清楚。

关系运算符

关系运算符比较两个值,结果是 bool:

运算符含义
<小于
<=小于或等于
>大于
>=大于或等于
==等于
!=不等于
int number1 {45};
int number2 {60};

std::cout << std::boolalpha;
std::cout << "number1 < number2: " << (number1 < number2) << std::endl;   // true
std::cout << "number1 > number2: " << (number1 > number2) << std::endl;   // false
std::cout << "number1 == number2: " << (number1 == number2) << std::endl; // false
std::cout << "number1 != number2: " << (number1 != number2) << std::endl; // true

为什么必须加括号

把括号去掉:

std::cout << "number1 < number2: " << number1 < number2 << std::endl;

这段编译不过。原因在上一节的优先级表:<< 的优先级高于 <,编译器会先算 (std::cout << ... << number1),再拿它的结果去和 number2 比较——而 std::cout 没有定义 <,于是报错。

加括号 (number1 < number2) 先把比较算成一个 bool,再交给 cout 输出。

= 与 ==

if (number1 = number2) { }    // 赋值,不是比较
if (number1 == number2) { }   // 比较

单等号是赋值,双等号才是比较。if 里写单等号通常还能编译(赋值表达式的值被当作条件),但逻辑完全错了。有些编译器会给出警告,不要忽略它。

逻辑运算符

逻辑运算符作用于布尔值:

运算符名称何时为真
&&与两个操作数都为真
||或至少一个为真
!非操作数为假

真值表:

aba && ba || b
falsefalsefalsefalse
falsetruefalsetrue
truefalsefalsetrue
truetruetruetrue
bool a {true}, b {false}, c {true};

std::cout << (a && b) << std::endl;      // false
std::cout << (a && c) << std::endl;      // true
std::cout << (a && b && c) << std::endl; // false(只要有一个 false 就 false)

std::cout << (a || b) << std::endl;      // true
std::cout << (!a) << std::endl;          // false
std::cout << (!b) << std::endl;          // true

a && b && c 说明"与"可以链式使用:只要有一个假,整个表达式就是假;"或"则相反,只要有一个真就是真。

与关系运算符组合

关系运算的结果是 bool,逻辑运算接收的正是 bool,所以可以自然组合:

int d {45}, e {20}, f {11};

bool result = (d > e) && (d > f);   // true && true → true

括号同样重要:它既避免优先级问题,也让意图清楚。

补充: && 与 || 有"短路"行为:a && b 在 a 为假时不再计算 b,a || b 在 a 为真时不再计算 b。这在右边有副作用(比如函数调用)时非常关键,后面讲条件与循环时会经常用到。

本章结论

  • 7 / 2 得 3:整数除法截断,不四舍五入;要小数就让一个操作数变成浮点数。
  • % 求余数,只对整数有意义。
  • 乘除优先于加减;同优先级从左到右;括号可以改变顺序,也应当用来表达意图。
  • value++ 先用后加,++value 先加后用。
  • +=、-=、*=、/=、%= 把运算与赋值合成一步。
  • 关系运算得到 bool;打印比较结果时必须加括号,因为 << 的优先级更高。
  • && 全真才真,|| 一真即真,! 取反;它们可以链式组合,也可以接在关系运算后面。

理解检验

1. 7 / 2 和 7.0 / 2 分别得到什么?为什么?

参考答案:7 / 2 得到 3(int),7.0 / 2 得到 3.5(double)。整数除法中两个操作数都是 int,结果类型也是 int,小数部分被直接丢弃,只保留"2 能装进 7 三次"。只要有一个操作数是浮点数,另一个就会被提升为 double,除法按浮点规则进行,保留小数。

2. 计算 6 + 4 * 6 - 9 / 3 - 2 + 5,并写出每一步依据的优先级规则。

参考答案:先乘除(优先级高于加减):4 * 6 = 24,9 / 3 = 3(整数除法,恰好整除)。表达式变为 6 + 24 - 3 - 2 + 5。加减同级、左结合,从左到右依次计算:6+24=30,30-3=27,27-2=25,25+5=30。结果 30。

3. std::cout << (a < b); 去掉括号后为什么编译失败?

参考答案:<< 的优先级高于 <。去掉括号后表达式被解析为 ((std::cout << ...) < b):先执行流插入,而流插入表达式的结果类型是 std::ostream&,C++ 没有为 std::ostream 定义与整数比较的 < 运算符,因此编译报错。加上括号后,比较先算出一个 bool,再交给 cout,类型正确。

4. int v = 5; int a = v++; int b = ++v; 之后,v、a、b 各是多少?

参考答案:v++ 先把旧值 5 交给 a,再把 v 加 1,所以 a = 5、v = 6;接着 ++v 先把 v 加到 7 再交给 b,所以 b = 7、v = 7。最终 v = 7、a = 5、b = 7。口诀是"符号在后用旧值,符号在前用新值"。

5. value += 5 与 value = value + 5 完全等价吗?为什么课程仍然推荐前者?

参考答案:对内置整数类型,两者语义相同,结果一样。课程推荐 += 是因为它把"在自身基础上累加"这个意图压缩成一个符号,读起来更直接,也不容易在长表达式里写错变量名。对自定义类型(后面讲的类),+= 还可以避免一次不必要的临时对象,不过那是后续内容。

6. 写出 a、b、c 分别为 true、false、true 时,(a && b) || (!c) 的值,并说明计算过程。

参考答案:先算括号内:a && b 中 b 为 false,结果为 false;!c 中 c 为 true,取反为 false。再算 false || false,结果为 false。整个表达式只有当"a 与 b 同时为真"或"c 为假"时才成立,本例两者都不满足。

与前后章的关系

本章把"数据"变成了"表达式与判断",这是条件语句和循环的直接前提。第 6 章先处理另一件事:把输出排版好——宽度、对齐、进制、精度、布尔显示等操纵器,以及 <limits> 与 <cmath> 提供的工具。第 7 章再正式进入 if、else if、switch,把本章的比较与逻辑结果用于控制流程。

What this chapter covers

Chapter 5 spans 4:45:33–5:55:57. The previous two chapters put data into variables; this one starts operating on it. The point is not to memorise symbols but to grasp two facts:

  • Integer division and modulus are not like school arithmetic: 7 / 2 is 3, and the fraction is thrown away.
  • When an expression contains several operators, precedence and associativity decide the order; a reader should not have to consult a table, so parentheses should state the intent.

Timeline

TimeTopic
4:47:39Addition, subtraction, multiplication, division, modulus; integer truncation
4:58:05Precedence and associativity; the operator table
5:07:14Using parentheses to fix the order
5:11:54Prefix and postfix increment and decrement
5:23:30Compound assignment
5:31:42Relational operators produce booleans
5:40:59Logical operators: and, or, not

Arithmetic

int number1 {2};
int number2 {7};
int result {0};

result = number1 + number2;   // 9
result = number2 - number1;   // 5
result = number1 - number2;   // -5
result = number1 * number2;   // 14

All of these are integer operations; the result is an integer too.

Integer division truncates, it does not round

result = number2 / number1;   // 7 / 2
std::cout << result << std::endl;   // 3

7 / 2 is 3, not 3.5. Both operands are int, so the result is int and only the whole part survives. A useful reading is "how many whole times does 2 fit into 7" — three times, with 1 left over and discarded.

To get a fractional result, make at least one operand floating point:

double quotient = 7.0 / 2;   // 3.5

7 / 2.0, 7.0 / 2 and 7.0 / 2.0 all work; the compiler promotes the other operand to double before dividing.

Modulus: the remainder

result = number2 % number1;   // 7 % 2
std::cout << result << std::endl;   // 1

% returns the remainder of integer division. Since 31=3×10+131 = 3\times10 + 1, 31 % 10 is 1.

Modulus is defined only for integers; 7.5 % 2 does not compile, because a "remainder" for floating point has no accepted definition — use std::fmod there.

Common uses: parity (n % 2 == 0), ring indexing (index % size), and extracting decimal digits (n % 10 gives the last digit).

Precedence and associativity

Two rules settle an expression with several operators:

  • Precedence: which operator binds first. *, /, % bind tighter than +, -.
  • Associativity: within one precedence level, whether evaluation goes left to right or right to left. Arithmetic is left to right.

Consider:

int a {6}, b {4}, c {6}, d {9}, e {3}, f {2}, g {5};

int result = a + b * c - d / e - f + g;

Step by step:

  1. Multiplication and division first: b * c = 24, d / e = 3.
  2. The expression becomes a + 24 - 3 - f + g, i.e. 6 + 24 - 3 - 2 + 5.
  3. Left to right: 6+24=30, 30-3=27, 27-2=25, 25+5=30.

The result is 30.

The relevant precedence ladder, highest first:

PrecedenceOperatorsAssociativity
highpostfix ++ --left
prefix ++ --, unary + -right
* / %left
+ -left
<< >>left
< <= > >=left
== !=left
&&left
low||left
lowestassignment = += etc.right

Parentheses state intent

int result = a + (b * c) - (d / e) - f + g;   // still 30

Same result, but the reader does not need the table. The course's advice is explicit: do not lean on the precedence table; write the intent with parentheses.

Parentheses also change the order:

int result = (a + b) * c - d / e - f + g;
// (6+4)*6 - 3 - 2 + 5 = 60

Now the addition happens first and the answer moves from 30 to 60.

Increment and decrement

++ adds 1; -- subtracts 1. They can only change a value by one, never by five or seven.

Postfix: use first, change after

int value {5};
std::cout << value++ << std::endl;   // 5
std::cout << value << std::endl;     // 6

value++ yields the current value (5, printed here) and only increments after the statement finishes. Hence 5 then 6.

The same with --:

int value {5};
std::cout << value-- << std::endl;   // 5
std::cout << value << std::endl;     // 4

Prefix: change first, use after

int value {5};
std::cout << ++value << std::endl;   // 6
std::cout << value << std::endl;     // 6

++value increments first and then yields the new value, so both lines print 6.

The mnemonic: operator after, old value first; operator before, new value first.

On a line of its own (value++; or ++value;) the two are equivalent; the difference appears only when the expression's value is used.

Compound assignment

+=, -=, *=, /=, %= fuse the operation and the assignment:

int value {45};

value += 5;    // value = value + 5;  → 50
value -= 5;    // value = value - 5;  → 45
value *= 2;    // value = value * 2;  → 90
value /= 3;    // value = value / 3;  → 30
value %= 11;   // value = value % 11; → 8

The last step: 30÷11=230 \div 11 = 2 remainder 8, so the result is 8.

Compound assignment is not merely sugar; it makes "accumulate into this same variable" visible at a glance.

Relational operators

Relational operators compare two values and produce a bool:

OperatorMeaning
<less than
<=less than or equal
>greater than
>=greater than or equal
==equal
!=not equal
int number1 {45};
int number2 {60};

std::cout << std::boolalpha;
std::cout << "number1 < number2: " << (number1 < number2) << std::endl;   // true
std::cout << "number1 > number2: " << (number1 > number2) << std::endl;   // false
std::cout << "number1 == number2: " << (number1 == number2) << std::endl; // false
std::cout << "number1 != number2: " << (number1 != number2) << std::endl; // true

Why the parentheses are mandatory

Remove them:

std::cout << "number1 < number2: " << number1 < number2 << std::endl;

This does not compile. By the ladder above, << binds tighter than <, so the compiler first evaluates (std::cout << ... << number1) and then tries to compare that result with number2 — but std::ostream has no < against an integer, hence the error.

With (number1 < number2) the comparison is evaluated first into a bool, which cout can print.

= versus ==

if (number1 = number2) { }    // assignment, not comparison
if (number1 == number2) { }   // comparison

A single = assigns; == compares. Writing = inside an if often still compiles (the assigned value becomes the condition) while being logically wrong. Some compilers warn; do not ignore it.

Logical operators

Logical operators work on booleans:

OperatorNameTrue when
&&andboth operands are true
||orat least one is true
!notthe operand is false

Truth table:

aba && ba || b
falsefalsefalsefalse
falsetruefalsetrue
truefalsefalsetrue
truetruetruetrue
bool a {true}, b {false}, c {true};

std::cout << (a && b) << std::endl;      // false
std::cout << (a && c) << std::endl;      // true
std::cout << (a && b && c) << std::endl; // false (one false is enough)

std::cout << (a || b) << std::endl;      // true
std::cout << (!a) << std::endl;          // false
std::cout << (!b) << std::endl;          // true

a && b && c shows that "and" chains: one false makes the whole expression false; "or" is the mirror image, one true makes it true.

Combined with relational operators

Relational operators produce bool, and logical operators consume bool, so they compose naturally:

int d {45}, e {20}, f {11};

bool result = (d > e) && (d > f);   // true && true → true

Parentheses matter here as well, both for precedence and for readability.

Supplement: && and || short-circuit: a && b skips evaluating b when a is false, and a || b skips b when a is true. This becomes important when the right-hand side has side effects such as a function call, a situation that appears often once conditions and loops arrive.

Chapter summary

  • 7 / 2 is 3: integer division truncates rather than rounds; make one operand floating point to keep the fraction.
  • % returns the remainder and is defined only for integers.
  • Multiplication and division bind before addition and subtraction; equal precedence evaluates left to right; parentheses can change the order and should express intent.
  • value++ yields the old value then increments; ++value increments then yields the new one.
  • +=, -=, *=, /=, %= fuse operation and assignment.
  • Relational operators produce bool; printing a comparison needs parentheses because << binds tighter.
  • && needs both true, || needs one, ! negates; all compose with relational results.

Comprehension check

1. What do 7 / 2 and 7.0 / 2 produce, and why?

Reference answer: 7 / 2 gives 3 (int); 7.0 / 2 gives 3.5 (double). In integer division both operands are int, so the result type is int and the fraction is discarded — "2 fits into 7 three whole times". With one floating-point operand, the other is promoted to double and the division follows floating-point rules.

2. Evaluate 6 + 4 * 6 - 9 / 3 - 2 + 5, naming the rule used at each step.

Reference answer: Multiplication and division first (higher precedence than addition): 4 * 6 = 24 and 9 / 3 = 3 (exact integer division). The expression becomes 6 + 24 - 3 - 2 + 5. Addition and subtraction share a level and associate left to right: 6+24=30, 30-3=27, 27-2=25, 25+5=30. The result is 30.

3. Why does std::cout << (a < b); fail to compile without the parentheses?

Reference answer: << has higher precedence than <, so std::cout << ... < b parses as ((std::cout << ...) < b). The insertion expression yields std::ostream&, and there is no < operator comparing std::ostream with an integer, so the compiler reports an error. With parentheses the comparison runs first and produces a bool, which cout can print.

4. After int v = 5; int a = v++; int b = ++v;, what are v, a and b?

Reference answer: v++ hands the old value 5 to a, then increments, so a = 5, v = 6. Then ++v increments to 7 and hands that to b, so b = 7, v = 7. Final values: v = 7, a = 5, b = 7. The rule: operator after the variable uses the old value, operator before uses the new one.

5. Is value += 5 exactly equivalent to value = value + 5? Why does the course still prefer it?

Reference answer: For built-in integer types the two are semantically identical and produce the same result. The course prefers += because it compresses "accumulate into this same variable" into one symbol, which reads more directly and avoids mistyping the variable name in a long expression. For user-defined types (classes, later in the course) += can also avoid a temporary object, though that belongs to a later chapter.

6. With a, b, c equal to true, false, true, evaluate (a && b) || (!c) and show the steps.

Reference answer: Inside the parentheses: a && b is false because b is false; !c is false because c is true. Then false || false is false. The whole expression holds only when "a and b are both true" or "c is false", and neither holds here.

How it connects

This chapter turns data into expressions and decisions, the direct prerequisite for conditionals and loops. Chapter 6 first handles a different matter: laying out output — width, alignment, bases, precision and boolean display — together with the tools in <limits> and <cmath>. Chapter 7 then moves into if, else if and switch, putting the comparisons and logical results of this chapter to work in control flow.

来源