Skip to content
Gains Summary
Main Navigation 首页 / Home
C++ 编程 / C++ Programming
系统与高性能 / Systems & Performance
Web 开发 / Web Development
人工智能 / Artificial Intelligence
工业软件 / Industrial Software
其他内容 / Other Topics
C++ 编程 / C++系统与性能 / SystemsWeb 开发 / Web人工智能 / AI工业软件 / Industrial

外观

Sidebar Navigation

← C++ 编程 / C++ Programming

基础语法与编译模型 / Language & Compilation

1. C++ 语言基础完整学习手册 / Complete C++ Language Basics Guide

2. C++ 语言基础与程序运行 / C++ Language Basics and Program Execution

3. C++ 程序是怎样做出来的:从源码到可执行文件 / From Source to Executable

4. 值、类型与对象:C++ 程序中的第一层现实 / Values, Types, and Objects

5. 表达式、运算符与控制流 / Expressions, Operators, and Control Flow

6. 函数、作用域与重载 / Functions, Scope, and Overloading

7. 数组、指针与字符串:从地址到借用范围 / Arrays, Pointers, and Strings

8. 引用、const 与类型推导:从复制到借用 / References, const, and Type Deduction

9. 让数据说清楚自己:枚举、结构体、联合体与类型别名 / Enums, Structs, Unions, and Aliases

10. 一个头文件怎样影响整个程序:预处理、头文件与命名空间 / Preprocessor, Headers, and Namespaces

11. 基础 I/O、错误与调试:让程序会观察自己的失败 / Basic I/O, Errors, and Debugging

本页目录

表达式、运算符与控制流 / Expressions, Operators, and Control Flow ​

本文属于 C++ 语言基础模块。

我们不会从运算符表开始背。

我们先观察一个程序,再把“它为什么这样运行”拆成值、类型、求值、副作用和控制流几个问题。

1. 先预测:程序到底会打印什么 ​

先看一个小程序:

cpp
#include <iostream>

bool ready() {
    std::cout << "ready()\n";
    return false;
}

bool load() {
    std::cout << "load()\n";
    return true;
}

int main() {
    if (ready() && load()) {
        std::cout << "done\n";
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

很多初学者第一次看到它时会猜:

text
ready()
load()
1
2

但实际输出只有:

text
ready()
1

原因不是编译器“聪明地删掉了”load()。

原因是 && 有一个明确的语言规则:

text
左边为 false
  |
  v
整个 && 已经不可能为 true
  |
  v
右边不再求值
1
2
3
4
5
6
7

这里的“求值”是本文的第一个重要词。

求值可以先理解成:

程序按照语言规则计算一个表达式,并可能读取对象、修改对象或调用函数。

ready() 被求值,所以它打印文字。

load() 没有被求值,所以它没有打印文字。

把 && 改成 &:

cpp
if (ready() & load()) {
    std::cout << "done\n";
}
1
2
3

再次运行,通常会看到:

text
ready()
load()
1
2

这两个运算符看起来只差一个字符,但它们不是同一类东西:

text
&&
  +-- 逻辑与
  +-- 面向真假
  +-- 有短路求值

&
  +-- 按位与
  +-- 面向整数的二进制位
  +-- 通常需要两边都求值
1
2
3
4
5
6
7
8
9

这就是为什么表达式不能只看“最后结果”。

我们还必须问:

text
它的类型是什么?
它会不会修改对象?
它会不会调用函数?
它的左右两边是否都执行?
它最终改变了哪条控制流?
1
2
3
4
5

2. 本文先建立一张地图 ​

本文会从最容易观察的地方逐步深入:

text
一个表达式
  |
  +-- 产生什么值?
  |
  +-- 具有什么类型?
  |
  +-- 是否产生副作用?
  |
  +-- 子表达式按什么顺序求值?
  |
  v
控制流决定哪些表达式执行
  |
  v
编译器把它转换成比较、跳转和循环
  |
  v
CPU 按指令和分支预测执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

这里有三个层次:

text
语言层
  +-- C++ 规定表达式和控制结构的含义

编译器层
  +-- 把这些含义转换成中间表示和机器指令

处理器层
  +-- 执行寄存器运算、比较、跳转和内存访问
1
2
3
4
5
6
7
8

三层不能混成一句话。

例如:

if 会生成一条 je 指令。

这句话有时在某个平台、某个优化级别下成立,但它不是 C++ 语言保证的事实。

更准确的说法是:

C++ 的 if 表达一个条件分支;编译器通常会把它实现为条件跳转、条件移动或其他等价机器代码。

3. 本文基础词小注释 ​

3.1 表达式是什么 ​

表达式是由名字、字面量、运算符和函数调用组合出来的代码片段。

cpp
2 + 3
count
count + 1
read_value()
1
2
3
4

它们都可以被求值。

有些表达式主要产生值:

cpp
2 + 3
1

有些表达式还会产生副作用:

cpp
++count
1

副作用是对程序外部可观察状态的改变,例如:

text
修改变量
写文件
打印输出
调用系统调用
分配或释放资源
1
2
3
4
5

3.2 运算符是什么 ​

运算符是用来组合或操作值的符号。

cpp
left + right
1

这里 + 是运算符,left 和 right 是操作数。

有些运算符只有一个操作数:

cpp
!ready
++count
1
2

有些运算符有两个操作数:

cpp
left + right
1

有些运算符看起来像运算符,实际上还会影响控制流:

cpp
left && right
condition ? a : b
1
2

3.3 语句是什么 ​

语句是程序执行过程中的一个动作单位。

cpp
count = 3;
++count;
return count;
1
2
3

表达式加分号,常常可以形成表达式语句。

if、while、for 和 return 也是语句形式。

3.4 控制流是什么 ​

控制流是程序执行路径经过哪些语句、跳过哪些语句、重复哪些语句。

text
开始
  |
  v
判断条件
  |
  +-- true  --> 路径 A
  |
  +-- false --> 路径 B
1
2
3
4
5
6
7
8

顺序执行、分支、循环和函数调用共同组成一个程序的控制流。

3.5 类型为什么会影响表达式 ​

2 + 3 和 2.0 + 3.0 都像是在做加法,但它们的操作数类型不同:

cpp
2 + 3        // int
2.0 + 3.0    // double
1
2

类型会影响:

text
如何解释比特
使用哪种运算
结果是什么类型
是否发生转换
哪些写法被允许
1
2
3
4
5

4. 从一个表达式拆出五个问题 ​

看下面的代码:

cpp
int count = 3;
int result = count + 2;
1
2

不要只说“结果是 5”。

可以按五个问题拆解:

text
1. 表达式是什么?
   count + 2

2. 它的类型是什么?
   int

3. 它的值是什么?
   5

4. 它是否修改了对象?
   没有

5. 它何时被求值?
   初始化 result 时
1
2
3
4
5
6
7
8
9
10
11
12
13
14

再看:

cpp
int result = count++;
1

这里就不同了:

text
读取 count 的旧值
  |
  v
把旧值作为表达式结果
  |
  v
count 自增
1
2
3
4
5
6
7

结果值和对象的新状态不是同一个问题。

cpp
int count = 3;
int result = count++;
1
2

执行后:

text
result == 3
count  == 4
1
2

这也是后置自增容易让人困惑的原因。

5. 算术运算:先区分整数和浮点数 ​

5.1 整数除法不是小数除法 ​

cpp
#include <iostream>

int main() {
    std::cout << 7 / 3 << '\n';
    std::cout << 7.0 / 3.0 << '\n';
}
1
2
3
4
5
6

结果通常类似:

text
2
2.33333
1
2

第一行两个操作数都是整数。

所以执行整数除法,结果也是整数。

第二行至少有浮点操作数,使用浮点运算。

text
7 / 3
  |
  +-- int / int
  +-- 结果为 int
  +-- 小数部分被截去

7.0 / 3.0
  |
  +-- double / double
  +-- 结果为 double
1
2
3
4
5
6
7
8
9
10

5.2 类型转换可能悄悄发生 ​

cpp
int count = 7;
double average = count / 3;
1
2

很多人以为 average 会得到 2.333...。

实际上,除法在赋值前就已经完成:

text
count / 3
  |
  +-- int / int
  +-- 得到 2
  |
  v
再把 2 转成 double
  |
  v
average == 2.0
1
2
3
4
5
6
7
8
9
10

如果想得到浮点结果:

cpp
double average = static_cast<double>(count) / 3;
1

static_cast<double> 是显式转换。

它告诉读者和编译器:

我知道这里要把 count 当成 double 参与运算。

5.3 加法、减法和乘法 ​

cpp
int a = 7;
int b = 3;

int sum = a + b;
int difference = a - b;
int product = a * b;
1
2
3
4
5
6

这些运算的结果类型会受操作数类型影响。

当两个操作数都是 int 时,通常使用整数运算。

但是整数也有范围。

cpp
#include <limits>

int maximum = std::numeric_limits<int>::max();
1
2
3

如果继续给有符号整数加一,不能把它简单理解成“自动变成更大的数”。

有符号溢出可能导致未定义行为。

这里的重点不是背上限,而是理解:

text
类型不是装数字的无限盒子
  |
  v
类型规定表示范围和运算规则
1
2
3
4

5.4 余数 % ​

cpp
int remainder = 17 % 5;
1

17 / 5 的整数商是 3。

17 % 5 的余数是 2。

余数经常用于:

cpp
if (value % 2 == 0) {
    // value 是偶数
}
1
2
3

但要注意负数的结果也应通过小实验确认,不要凭小学数学的“余数必须为正”想当然。

6. 比较和逻辑:从值进入控制流 ​

比较表达式通常产生 bool:

cpp
int score = 86;
bool passed = score >= 60;
1
2

常见比较运算符:

text
<    小于
<=   小于或等于
>    大于
>=   大于或等于
==   相等
!=   不相等
1
2
3
4
5
6

结果只有两种:

text
true
false
1
2

但是不要因此把比较看成“只是计算”。

比较结果经常决定下一条控制流:

text
score >= 60
  |
  +-- true  --> 通过路径
  |
  +-- false --> 不通过路径
1
2
3
4
5

6.1 if 读取的是条件的真假 ​

cpp
if (score >= 60) {
    std::cout << "pass\n";
}
1
2
3

执行步骤:

text
计算 score >= 60
  |
  v
得到 true 或 false
  |
  v
true 执行大括号内语句
false 跳过大括号内语句
1
2
3
4
5
6
7
8

在 C++ 中,整数也可以转换成真假:

cpp
if (count) {
    // count 不为 0 时进入
}
1
2
3

虽然合法,但初学阶段最好把意图写清楚:

cpp
if (count != 0) {
}
1
2

6.2 ! 是逻辑非 ​

cpp
bool ready = false;
if (!ready) {
    std::cout << "not ready\n";
}
1
2
3
4

! 把真变假,把假变真。

6.3 && 是有条件的逻辑与 ​

cpp
if (ptr != nullptr && *ptr > 0) {
    use(*ptr);
}
1
2
3

这里左边不只是一个条件。

它还是右边能够安全执行的前置条件:

text
ptr != nullptr ?
  |
  +-- false --> 停止,不读取 *ptr
  |
  +-- true  --> 可以继续读取 *ptr
1
2
3
4
5

这就是短路求值的工程价值。

6.4 || 也会短路 ​

cpp
if (cached() || load_from_disk()) {
    use_value();
}
1
2
3

如果 cached() 返回 true,右侧不会执行。

text
cached() == true
  |
  v
整个 || 已经为 true
  |
  v
不调用 load_from_disk()
1
2
3
4
5
6
7

因此把函数调用放在逻辑表达式中时,要把副作用考虑进去。

7. && 和 &:一个小实验看懂区别 ​

创建 logic.cpp:

cpp
#include <iostream>

bool left() {
    std::cout << "left\n";
    return false;
}

bool right() {
    std::cout << "right\n";
    return true;
}

int main() {
    std::cout << "logical:\n";
    bool a = left() && right();

    std::cout << "bitwise:\n";
    bool b = left() & right();

    std::cout << a << ' ' << b << '\n';
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

先用最简单的命令编译:

bash
g++ logic.cpp -o logic
1

g++ 是 C++ 编译器命令。

logic.cpp 是输入源码。

-o logic 指定输出程序叫 logic。

运行:

bash
./logic
1

你会看到逻辑与部分只调用 left(),按位与部分通常调用两个函数。

这次实验要观察的不是最后 a 和 b 都可能为 false。

真正重要的是:

text
结果相同
  不代表
执行过程相同
1
2
3

8. 优先级、结合性和求值顺序不是一回事 ​

看这段:

cpp
int result = 2 + 3 * 4;
1

结果是 14,因为乘法在语法分组上优先于加法:

text
2 + (3 * 4)
1

加括号可以明确意图:

cpp
int result = 2 + (3 * 4);
1

如果想先加:

cpp
int result = (2 + 3) * 4;
1

8.1 优先级解决什么 ​

优先级回答:

代码如何被分组?

cpp
a + b * c
1

通常被解析为:

text
a + (b * c)
1

8.2 求值顺序解决什么 ​

求值顺序回答:

已经分好组之后,哪些子表达式先执行?

不要把这两个问题混为一谈。

函数调用的多个实参尤其容易引起误解:

cpp
consume(next(), next());
1

不要假定左边的 next() 一定先执行。

如果先后顺序重要,就拆开:

cpp
int first = next();
int second = next();
consume(first, second);
1
2
3

拆开以后,顺序不仅清楚,调试器也更容易观察。

9. 自增、自减和副作用 ​

9.1 前置自增 ​

cpp
int count = 3;
int result = ++count;
1
2

执行过程:

text
count 从 3 变成 4
  |
  v
表达式结果为 4
1
2
3
4

最终:

text
count  == 4
result == 4
1
2

9.2 后置自增 ​

cpp
int count = 3;
int result = count++;
1
2

执行过程:

text
先产生旧值 3
  |
  v
result 得到 3
  |
  v
count 再变成 4
1
2
3
4
5
6
7

最终:

text
count  == 4
result == 3
1
2

9.3 为什么不要写复杂自增表达式 ​

cpp
int i = 0;
// int result = i++ + i++;
1
2

这类代码把同一个对象的多次修改和读取压在一起。

更好的写法:

cpp
int left = i++;
int right = i++;
int result = left + right;
1
2
3

这里每一步都变成可观察的语句。

代码更长了几行,但推理成本大幅下降。

10. 条件运算符和赋值表达式 ​

10.1 条件运算符 ​

cpp
int absolute = value >= 0 ? value : -value;
1

它可以读成:

text
如果 value >= 0
  |
  +-- 选择 value
  |
  +-- 否则选择 -value
1
2
3
4
5

只有被选择的分支会被求值:

cpp
bool ok = condition ? safe_value() : fallback_value();
1

不要把复杂业务流程塞进三目运算符。

10.2 赋值本身也是表达式 ​

cpp
int a;
int b;
a = b = 0;
1
2
3

赋值从右向左结合,所以先让 b 变成 0,再把结果赋给 a。

但下面的代码非常容易是错误:

cpp
if (status = read_status()) {
}
1
2

它的意思是“先赋值,再把结果转成 bool”。

如果确实需要在条件中初始化,使用带初始化语句的 if:

cpp
if (auto status = read_status(); status.ok()) {
    use(status);
}
1
2
3

这会把 status 的作用域限制在 if 和 else 内。

11. 位运算:把整数看成一组位 ​

位运算不是逻辑运算的“高级写法”。

它直接操作整数表示中的位。

cpp
#include <cstdint>

std::uint8_t flags = 0;
constexpr std::uint8_t read = 1u << 0;
constexpr std::uint8_t write = 1u << 1;
1
2
3
4
5

可以把四位简化画成:

text
bit:       3 2 1 0
read:      0 0 0 1
write:     0 0 1 0
read|write 0 0 1 1
1
2
3
4

设置位:

cpp
flags |= read;
1

检查位:

cpp
bool can_read = (flags & read) != 0;
1

清除位:

cpp
flags &= static_cast<std::uint8_t>(~read);
1

位运算常见于权限、硬件寄存器、压缩状态和协议字段。

12. if:把条件变成两条路径 ​

cpp
int score = 86;

if (score >= 90) {
    std::cout << "A\n";
} else if (score >= 60) {
    std::cout << "pass\n";
} else {
    std::cout << "fail\n";
}
1
2
3
4
5
6
7
8
9

控制流图:

text
             score >= 90 ?
             /          \
          true          false
           |              |
           v              v
          A         score >= 60 ?
                         /       \
                      true       false
                       |           |
                       v           v
                     pass        fail
1
2
3
4
5
6
7
8
9
10
11

每个 else if 都是对前一个条件为假的路径继续判断。

12.1 大括号是控制流的边界 ​

即使只有一行,也建议保留大括号:

cpp
if (ready) {
    start();
}
1
2
3

否则后续添加日志时容易出现视觉误读:

cpp
if (ready)
    start();
    log(); // 不属于 if
1
2
3

13. switch:按离散状态选择 ​

cpp
enum class Command {
    start,
    stop,
    status
};

switch (command) {
case Command::start:
    start();
    break;
case Command::stop:
    stop();
    break;
case Command::status:
    print_status();
    break;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

switch 适合有限的离散状态。

没有 break 时,程序可能继续执行下一个 case。

cpp
case 'y':
case 'Y':
    confirm();
    break;
1
2
3
4

如果是故意贯穿,也可以写:

cpp
[[fallthrough]];
1

14. 循环:重复执行,但必须有进展 ​

14.1 while ​

cpp
int count = 3;

while (count > 0) {
    std::cout << count << '\n';
    --count;
}
1
2
3
4
5
6

执行路径:

text
初始化 count=3
  |
  v
检查 count > 0
  |
  +-- false --> 结束
  |
  +-- true  --> 循环体
                 |
                 v
              count--
                 |
                 +---- 回到条件
1
2
3
4
5
6
7
8
9
10
11
12
13

14.2 do while ​

cpp
do {
    read_once();
} while (should_retry());
1
2
3

它至少执行一次循环体。

14.3 for ​

cpp
for (int i = 0; i < 3; ++i) {
    std::cout << i << '\n';
}
1
2
3

它可以理解为:

cpp
int i = 0;
while (i < 3) {
    std::cout << i << '\n';
    ++i;
}
1
2
3
4
5

14.4 范围 for ​

cpp
int values[]{10, 20, 30};

for (int value : values) {
    std::cout << value << '\n';
}
1
2
3
4
5

这里的 value 是每个元素的副本。

修改原元素:

cpp
for (int& value : values) {
    value *= 2;
}
1
2
3

只读而不复制:

cpp
for (const int& value : values) {
    std::cout << value << '\n';
}
1
2
3

14.5 半开区间 ​

text
[0, size)
  |
  +-- 包含 0
  +-- 不包含 size
1
2
3
4

数组有三个元素时,合法下标是:

text
0, 1, 2
1

错误:

cpp
for (std::size_t i = 0; i <= values.size(); ++i) {
    use(values[i]);
}
1
2
3

正确:

cpp
for (std::size_t i = 0; i < values.size(); ++i) {
    use(values[i]);
}
1
2
3

15. break、continue 和 return ​

cpp
for (int value : values) {
    if (value < 0) {
        continue;
    }

    if (value == target) {
        break;
    }

    process(value);
}
1
2
3
4
5
6
7
8
9
10
11
text
value < 0
  |
  +-- true  --> continue,跳到下一轮
  |
  +-- false --> 继续
                 |
                 v
              value == target
                 |
                 +-- true  --> break,结束循环
                 +-- false --> process(value)
1
2
3
4
5
6
7
8
9
10
11

return 不只是结束循环,而是结束整个当前函数。

16. CPU 怎样看待 if 和循环 ​

cpp
if (x < y) {
    A();
} else {
    B();
}
1
2
3
4
5

抽象地看,编译器会形成:

text
比较 x 和 y
  |
  +-- 条件为真  --> A
  |
  +-- 条件为假  --> B
1
2
3
4
5

循环则可能接近:

text
loop:
    比较条件
    条件不满足 -> exit
    执行循环体
    跳回 loop
exit:
1
2
3
4
5
6

实际汇编可能使用条件跳转、条件移动、循环展开或向量化。

这些是实现选择。

语言层只要求可观察行为符合规则。

17. 分支预测和性能 ​

现代 CPU 会猜测条件分支:

text
程序提供分支
  |
  v
CPU 猜下一条路径
  |
  +-- 猜对 --> 继续执行
  |
  +-- 猜错 --> 丢弃部分工作并重新取指
1
2
3
4
5
6
7
8

但正确顺序是:

text
先写出正确、清楚的控制流
  |
  v
用测试验证语义
  |
  v
用 benchmark 或 profiler 找到热点
  |
  v
再讨论分支预测
1
2
3
4
5
6
7
8
9
10

18. 一个完整实验:条件、循环和汇编 ​

创建 branch.cpp:

cpp
#include <iostream>

int main() {
    for (int value : {2, 3, 4, 5}) {
        if (value % 2 == 0) {
            std::cout << value << " is even\n";
        } else {
            std::cout << value << " is odd\n";
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11

先编译:

bash
g++ branch.cpp -o branch
1

再运行:

bash
./branch
1

输出应体现奇偶判断。

然后生成汇编:

bash
g++ -S branch.cpp -o branch.s
1

先找比较和跳转,不必试图一次读懂所有指令。

19. 常见误解 ​

误解一:括号里的每个函数都会执行 ​

&&、|| 和条件运算符都可能跳过子表达式。

误解二:优先级就是执行顺序 ​

优先级首先决定语法分组。

误解三:& 是 && 的缩写 ​

一个是按位运算,一个是逻辑短路运算。

误解四:循环写出来就会自动结束 ​

循环必须有条件变假或主动退出的路径。

误解五:i <= size 可以访问最后一个元素 ​

size 通常是一过尾位置,不能解引用。

误解六:编译器把所有 if 都变成跳转 ​

不一定,可能使用条件移动或其他等价形式。

20. 自测题 ​

问题一 ​

为什么 false && load() 不调用 load()?

**答案:**左侧已经为假,逻辑与不可能为真,右侧不需要求值。

问题二 ​

int value = 7 / 3; 为什么不是 2.333?

**答案:**两个操作数都是 int,先执行整数除法。

问题三 ​

++count 和 count++ 的主要差别是什么?

**答案:**前者先修改再产生新值,后者先产生旧值再修改。

问题四 ​

为什么不要依赖 consume(next(), next()) 的先后?

**答案:**多个实参的求值先后不能简单按书写顺序假定。

问题五 ​

为什么 i < size 常比 i <= size 正确?

**答案:**合法下标是半开区间 [0, size)。

21. 本篇总结 ​

表达式不只是“算一个数”。

它可能:

text
产生值
  |
  +-- 具有类型
  +-- 触发转换
  +-- 修改对象
  +-- 调用函数
  +-- 影响控制流
1
2
3
4
5
6
7

控制流不只是 if 和 for 的语法。

它最终会影响:

text
编译器如何组织基本块
  |
  v
CPU 如何比较和跳转
  |
  v
程序哪些状态会被读取或修改
1
2
3
4
5
6
7

最值得保留的判断顺序是:

text
先问值
  |
  v
再问类型
  |
  v
再问副作用
  |
  v
再问求值顺序
  |
  v
最后问控制流和性能
1
2
3
4
5
6
7
8
9
10
11
12
13

上一篇:类型、变量与初始化 | 下一篇:函数、作用域与重载

最后更新于:

Pager
上一篇4. 值、类型与对象:C++ 程序中的第一层现实 / Values, Types, and Objects
下一篇6. 函数、作用域与重载 / Functions, Scope, and Overloading

持续记录,持续成长

Copyright © Tidenflow