第5章:运算符、表达式和语句
运算符描述怎样处理数据,表达式把这些操作组合起来,语句则把计算、修改和控制流程组织成程序。
理解本章时,应同时关注三件事:表达式怎样组合、运算以什么类型进行、对变量的修改何时完成。
1. 用 while 重复执行任务
计算机的优势之一是重复执行相同规则。把需要变化的数据与固定处理步骤分开,就可以通过循环生成一组结果。
#include <stdio.h>
int main(void)
{
int number = 1;
printf("number square\n");
while (number <= 5) {
printf("%6d %6d\n", number, number * number);
number = number + 1;
}
return 0;
}
输出:
number square
1 1
2 4
3 9
4 16
5 25
1.1 先判断,再执行
初始化 number = 1
↓
检查 number <= 5 ←────────┐
├─ 假 → 离开循环 │
└─ 真 → 输出平方 │
↓ │
number 加一 ───┘
while 的基本形式是:
while (条件表达式)
一条语句
如果需要重复执行多条语句,用花括号把它们组成一个复合语句。
条件为零表示假,非零表示真。关系表达式如 number <= 5,成立时产生整数 1,不成立时产生整数 0。
由于先检查条件,循环体可能一次也不执行。上面的循环在 number 变成 6 后退出。
1.2 计数循环的三个要素
| 要素 | 本例代码 | 作用 |
|---|---|---|
| 初始状态 | int number = 1; |
确定从哪里开始 |
| 继续条件 | number <= 5 |
确定哪些情况进入循环 |
| 状态更新 | number = number + 1; |
让程序逐步接近退出条件 |
如果遗漏更新,条件可能一直为真,反复打印同一个结果。循环中的值也可以来自下一次输入,不一定只能由计数器加一获得。
2. 赋值、对象与可修改左值
2.1 = 是保存操作
count = count + 1;
这表示读取 count 的旧值,计算加一后的结果,再把新值存回 count。
= 不表示数学上的相等关系。要比较两个值是否相等,使用 ==。
2.2 赋值左边为什么有要求
| 概念 | 含义 | 示例 |
|---|---|---|
| 对象 | 程序中可以保存值的一块存储区域 | 一个整型变量的存储对象 |
| 左值 | 能够标识一个对象的表达式 | 变量名、数组元素表达式等 |
| 可修改左值 | 满足可修改条件的左值 | 普通 int 变量 count |
| 表达式的值 | 对表达式求值获得的结果 | count + 1 计算出的数值 |
赋值运算符的左操作数必须是可修改左值。
例如,声明 int count = 3; 后,count = 4; 合法;3 = count; 和 (count + 1) = 4; 都不合法,因为左边没有给出可修改的对象位置。
const int limit = 10; 中的 limit 能标识对象,是左值,但不能通过普通赋值修改。数组本身也不能像普通整数变量那样整体赋值。
不要把“左值”理解为“只能写在等号左边”。同一个变量在右侧也能提供当前值,例如 total = count;。
2.3 赋值表达式也产生值
int first;
int second;
int third;
first = second = third = 8;
赋值运算符按从右到左的方式结合,结构相当于:
first = (second = (third = 8));
在这个相同类型的例子中,内层保存 8 并产生值 8,供外层继续使用,最后三个变量都是 8。
如果各层类型不同,赋值转换也会影响下一层获得的值。赋值表达式提供的是赋值转换之后的结果,不能认为原始右值会跳过中间类型直接传到最外层。
3. 基本算术运算符
| 运算符 | 用途 | 示例 |
|---|---|---|
+ |
加法 | total + extra |
- |
减法 | total - used |
* |
乘法 | length * width |
/ |
除法 | distance / time |
% |
整数余数 | seconds % 60 |
一元 - |
对数值取负 | -temperature |
一元 + |
表达正号,并参与相应类型规则 | +count |
二元运算符需要两个操作数,一元运算符需要一个操作数。left - right 中的减号是二元运算符;-right 中的是一元运算符。
C 没有幂运算符。平方通常写 x * x,立方写 x * x * x;^ 是按位异或,不能用来表示乘方。更一般的幂运算可使用数学库函数 pow。
选择运算符后,还要检查类型和可表示范围。有符号整数的加、减、乘等操作若产生无法表示的结果,可能发生未定义行为;一元取负也不能保证对所有负整数都安全。
4. 整数除法与浮点除法
4.1 运算对象决定除法类型
| 表达式 | 结果 | 原因 |
|---|---|---|
9 / 4 |
2 |
两个操作数都是整数,执行整数除法 |
9.0 / 4 |
2.25 |
另一个操作数转换为相应浮点类型 |
9 / 4.0 |
2.25 |
执行浮点除法 |
9.0 / 4.0 |
2.25 |
两边都是 double |
在 C99/C11 中,整数除法向零截断:
9 / 4 → 2
-9 / 4 → -2
9 / -4 → -2
-9 / -4 → 2
向零截断不是向下取整,也不是四舍五入。
4.2 接收结果的变量不能反过来改变计算
double first = 9 / 4;
double second = 9.0 / 4;
first 是 2.0,second 是 2.25。第一条先算出整数 2,然后才转换为 double 保存。
同样:
double result = 3 / 5 * 10.0;
结构是 (3 / 5) * 10.0,左边整数除法先得到 0,最终结果是 0.0。后面的浮点常量不能补回此前丢失的小数部分。
4.3 避免无效除法
整数除法的除数不能为零。如果有符号商无法由结果类型表示,例如常见补码系统中的 INT_MIN / -1,同样会发生未定义行为。
浮点除法也应根据任务约束检查除数,不能把某个平台会产生无穷大的表现当作所有计算场景的处理方案。
5. 余数运算符 %
5.1 整除得到整份数,余数得到剩余部分
对非负秒数 3671:
int seconds = 3671;
int hours = seconds / 3600;
int minutes = seconds / 60 % 60;
int remaining_seconds = seconds % 60;
结果是 1 小时、1 分钟、11 秒。
% 的操作数必须是整数,不能直接对 double 使用它。数学库有处理浮点余数的函数,但那是另一套接口。
5.2 负数余数的符号
在除法有效且商可表示的前提下,整数满足:
a == (a / b) * b + a % b
在 C99/C11 的向零截断规则下:
| 表达式 | 商 | 余数 |
|---|---|---|
11 / 5、11 % 5 |
2 |
1 |
11 / -5、11 % -5 |
-2 |
1 |
-11 / 5、-11 % 5 |
-2 |
-1 |
-11 / -5、-11 % -5 |
2 |
-1 |
非零余数与被除数同号,不由除数的正负决定。能整除时,余数为零。
因此,C 的 % 对负数未必与数学里总取非负结果的模运算约定一致。
5.3 常见用途
- 把总量拆成整份和余量,例如小时、分钟、秒。
- 检查是否整除,例如
month % 3 == 0。 - 判断奇偶,例如
number % 2 == 0。 - 周期性分组或轮转,但要额外考虑负值与范围约束。
整数余数也不能使用零除数;有符号商不可表示时,相关的 % 操作同样不合法。
6. 优先级、结合性与求值顺序
6.1 优先级决定怎样组成表达式
result = 10 + 6 * 4;
由于乘法优先级高于加法,结构是 10 + (6 * 4),得到 34。如果需要先加法,应写成 (10 + 6) * 4,得到 64。
下面列出本章涉及的部分层级,从高到低排列:
| 层级 | 运算符示例 | 结合方向 |
|---|---|---|
| 后缀 | value++、value--、函数调用 |
从左到右 |
| 一元与强制转换 | ++value、--value、一元 + -、sizeof、(type) |
从右到左 |
| 乘法层级 | *、/、% |
从左到右 |
| 加法层级 | +、- |
从左到右 |
| 关系比较 | <、<=、>、>= |
从左到右 |
| 相等比较 | ==、!= |
从左到右 |
| 赋值 | = |
从右到左 |
这不是全部运算符列表。用于分组的圆括号可以明确改变组合结构,不应仅靠记忆一张大表来写复杂表达式。
6.2 结合性决定同层运算怎样分组
24 / 4 * 2 → (24 / 4) * 2 → 12
24 / (4 * 2) → 3
乘除同级,按从左到右的结合方向分析。赋值则从右到左结合,例如 a = b = 3。
这里的“从左到右”描述的是语法分组,不能推广为所有操作数都按从左到右的时间顺序求值。
6.3 优先级不能确定所有执行先后
result = left() + right() * 2;
这段表达式要求把 right() 的结果与 2 相乘,再用于外层加法,但不保证一定先调用 left() 或一定先调用 right()。
函数各实参的求值顺序也不能简单假定为从左到右。给某部分加括号,只能明确组合关系,不会自动建立所有操作之间的先后关系。
| 概念 | 回答的问题 |
|---|---|
| 优先级 | 不同运算符怎样结合操作数? |
| 结合性 | 同一层级的运算符怎样分组? |
| 求值顺序 | 操作数求值、函数调用、变量修改在时间上如何安排? |
7. 递增与递减
7.1 修改变量,同时产生表达式结果
++ 使对象增加 1,-- 使对象减少 1。操作数必须是类型合适的可修改左值,不能给常量或普通加法结果使用它们。
当独立作为语句时:
count++;
++count;
两者都会使 count 加一。递减也有对应的前缀和后缀形式。
7.2 前缀使用新值,后缀提供旧值
int first = 4;
int second = 4;
int old_value = first++;
int new_value = ++second;
执行完这两条初始化后:
| 对象 | 值 |
|---|---|
first |
5 |
old_value |
4 |
second |
5 |
new_value |
5 |
“后缀提供旧值”只描述这个递增表达式的结果,不表示整个程序要等到任意后续位置才修改变量。
7.3 条件里的递增会影响循环体
int count = 0;
while (count++ < 3) {
printf("%d\n", count);
}
这里输出 1、2、3。第一次比较使用旧值 0,但是进入循环体前,递增已经完成,所以输出的是 1。
| 条件求值前 | 用于比较的旧值 | 条件结果 | 求值后 count |
是否进入循环体 |
|---|---|---|---|---|
0 |
0 |
真 | 1 |
是 |
1 |
1 |
真 | 2 |
是 |
2 |
2 |
真 | 3 |
是 |
3 |
3 |
假 | 4 |
否 |
最后一次失败的条件判断也会执行递增,所以循环结束时 count 是 4。
若改为 while (++count < 3),同样从 0 开始,循环体只输出 1、2,最后 count 是 3。
7.4 复杂时拆开写
对初学者而言,把主要操作分成“判断、处理、更新”通常更容易检查边界。
递增写法的紧凑程度不等于程序效率,编译器可以对许多等价形式作相同优化。选择写法时应优先确保含义清楚。
8. 表达式、语句与块
8.1 表达式可以组合,也可能产生修改
| 形式 | 含义 |
|---|---|
4 |
常量表达式 |
count |
通过变量取得当前值 |
count + 4 |
算术表达式 |
count = 4 |
赋值表达式,保存并产生相应值 |
count > 4 |
关系表达式,产生 0 或 1 |
printf("Done\n") |
函数调用表达式,具有返回结果 |
本章的数值表达式都能产生值,但不能把“所有表达式都有可用的值”作为绝对规则。调用返回 void 的函数也是表达式,却没有可供数值运算使用的结果。
8.2 表达式后加分号形成表达式语句
count = 4;
++count;
printf("%d\n", count);
这些都是表达式语句。它们通过赋值、递增或输出完成可观察的工作。
3 + 4; 也是合法的表达式语句,但在这里计算结果被丢弃,没有完成有用的状态修改或输出,编译器可能发出警告。
int count; 是声明,不能因为也以分号结尾,就把它归为表达式语句。return 0; 则属于跳转语句。
8.3 空语句
;
单独一个分号是一条空语句。在语法上需要语句、但确实不需要执行任何内容时,可以使用它。
一个常见错误是在循环条件后无意间多写分号:
while (count < 5);
这里循环体就是空语句。后面看起来缩进得很整齐的代码,也不会自动成为它的循环体。
8.4 复合语句把多个步骤组成整体
while (count < 5) {
printf("%d\n", count);
++count;
}
花括号中的内容作为一个复合语句交给 while。没有花括号时,while 只控制紧随其后的那一条语句。
缩进用于帮助阅读,实际范围由语法和花括号决定。块中还可以包含声明,不应把块限定理解为只能放若干条表达式语句。
9. 副作用、完整表达式与顺序约束
9.1 副作用不一定是坏事
| 表达式 | 产生的结果 | 副作用 |
|---|---|---|
3 + 4 |
数值 7 |
没有修改对象或输出 |
count = 4 |
赋值后的值 | 改变 count |
count++ |
递增前的值 | 改变 count |
printf("Hi\n") |
输出成功时为输出字符数 | 向输出流写数据 |
副作用是语言对状态修改、输入输出等行为的称呼,通常正是程序要完成的任务。
9.2 完整表达式结束时形成重要边界
完整表达式不是更大表达式的子表达式。例如:
- 表达式语句中、分号之前的整个表达式。
while圆括号内的控制表达式。
对本章这种顺序执行代码,前一个完整表达式中的求值与副作用,要在后一个完整表达式开始前完成。
因此:
count++;
printf("%d\n", count);
第二条语句能读取到递增后的值。前面 while (count++ < 3) 的例子,也正是因为控制表达式已经完成,循环体才会看到新值。
这可以借助“序列点”的概念理解。C11 还用“先序于”等关系描述求值之间的约束。初学时不需要背完整术语,但要知道括号、逗号分隔的实参和分号并不都具有相同的顺序作用。
9.3 不要依靠未定义行为推断结果
在本章采用的 C99/C11 语境下,下面的写法存在没有适当顺序约束的修改或读取冲突:
/* 以下是错误示例,不应执行或依赖结果。 */
y = n++ + n++;
printf("%d %d\n", n, n++);
result = n / 2 + n++;
这些属于未定义行为,不能只解释为“几个候选输出中随机选一个”,也不能保证运行结束后变量一定增加了几次。
只加分组括号不能解决这种冲突。应该把修改与使用拆成有明确顺序的步骤,例如:
int old_value = n;
++n;
printf("%d %d\n", old_value, n);
明确先保存旧值,再修改,再输出,才能表达稳定的意图。
10. sizeof、size_t 与 typedef
10.1 大小结果使用 size_t
int number = 0;
size_t bytes = sizeof number;
printf("%zu\n", bytes);
sizeof 按 C 字节计算大小,结果是无符号整数类型 size_t。操作数为类型名时写 sizeof(int);为普通对象表达式时,可以写 sizeof number。
10.2 普通非变长类型的操作数不被求值
int number = 3;
size_t bytes = sizeof(number++);
这里 number 仍是 3。对于这个普通 int 表达式,sizeof 只需要确定其类型大小,不会执行递增。编译器可能因此提醒这里的副作用不会发生。
涉及可变长数组类型时存在需要求值的情况,因此不要把它推广为“任何 sizeof 内部都绝对不求值”。
10.3 typedef 为类型起别名
typedef double real;
real distance = 12.5;
这里 real 是 double 的类型别名,distance 仍然是 double 类型。
typedef 不创建变量,也不是预处理文本替换,更不会凭空创建一种拥有独立运算规则的新数值类型。size_t 这类名称也便于标准接口适应不同实现的底层类型。
11. 自动类型转换
11.1 先提升较窄整数
char、short 及其相关较窄整数在许多算术表达式中会进行整数提升:
- 如果
int能表示其全部取值,提升为int。 - 否则,提升为
unsigned int。
因此,即使两个变量都是 short,它们相加的计算类型也不一定仍是 short。结果再存回原变量时,还可能发生下一次转换。
11.2 混合算术选择公共类型
| 表达式 | 普通算术转换中的认识 |
|---|---|
int 与 double 相加 |
整数转为 double,按相应浮点类型计算 |
float 与 double 相加 |
float 转为 double |
float 与 float 相加 |
不会仅因做算术,就按默认实参提升规则把它们都变成 double |
| 有符号与无符号整数混合 | 必须结合整数等级与可表示范围判断 |
对于实浮点类型参与的这些普通算术,公共类型依次考虑 long double、double、float。
只有整数时,先做整数提升,再处理符号和等级。不能单凭 sizeof,或一条“较小的都变成较大的”规则解决所有情况。
11.3 有符号与无符号混合尤其要留意
int signed_value = -1;
unsigned int unsigned_value = 1U;
printf("%d\n", signed_value < unsigned_value);
结果是 0。这组相同等级的类型比较时,有符号值转换为 unsigned int,-1 得到对应的无符号大值,所以不能再按数学上的负一小于一推断。
若有符号类型等级更高,还要看它是否能表示无符号类型的全部值;能则转为该有符号类型,不能则使用相应的无符号类型。
11.4 保存结果时再转换为目标类型
int count = 12.9;
double amount = 12;
第一条在值可表示的前提下向零截断,得到 12;第二条把整数转换为浮点数。
可能丢失信息的情形包括:
- 浮点转整数,丢弃小数部分;截断后的值超出范围时是未定义行为。
- 精度较高的浮点值保存到较低精度类型,发生舍入。
- 整数转换到无符号类型,按该目标类型的模数调整。
- 整数转换到无法表示该值的有符号类型,在 C99/C11 下结果由实现定义,或产生实现定义的信号。
最后一项是整数转换规则,不应与有符号算术运算溢出的未定义行为混为一谈。
12. 显式强制类型转换
12.1 写法与作用范围
(目标类型) 表达式
例如:
double first = (double)9 / 4;
double second = (double)(9 / 4);
first 是 2.25;second 是 2.0。
第一条在除法之前把操作数转成浮点类型,改变了计算类型;第二条先完成整数除法,再转换已经截断的结果。
12.2 转换发生在哪里,会改变答案
int first = (int)(1.8 + 2.4);
int second = (int)1.8 + (int)2.4;
第一条先相加后截断,得到 4;第二条先分别截断再相加,得到 3。
强制转换表示程序员的转换意图,不是数据范围的验证,也不会让超范围操作自动变得安全。
13. 带参数的函数
13.1 用参数让同一段代码处理不同数据
#include <stdio.h>
void print_marks(int count);
int main(void)
{
int times = 3;
print_marks(times);
print_marks(times + 2);
printf("times: %d\n", times);
return 0;
}
void print_marks(int count)
{
while (count > 0) {
printf("*");
--count;
}
printf("\n");
}
输出:
***
*****
times: 3
13.2 实参与形参
| 位置 | 术语 | 本例含义 |
|---|---|---|
print_marks(times) |
实参 | 调用时给出的表达式,其值为 3 |
print_marks(times + 2) |
实参 | 表达式计算得到 5 |
void print_marks(int count) |
形参 | 函数内部接收值的变量 count |
C 通过值传递参数。这里将调用方算出的值交给形参对象,函数内减少 count,不会修改 main 中的 times。
每次调用都按本次实参取得新的形参值。不能认为第一次调用把形参减为零,第二次就一定从零开始。
13.3 函数原型指导类型转换
void print_marks(int count);
原型告诉编译器:函数不返回值,接收一个 int 参数。
如果在看得见这个原型的位置传入 4.8,会按形参类型转换为 int,函数接收到 4。这种截断必须符合任务意图,并满足目标类型范围。
固定形参的转换与可变参数部分的默认提升不同。例如:
- 传给已声明
int形参的浮点值,按int参数要求转换。 - 传给
printf可变参数部分的float,提升为double;较窄整数进行整数提升。
在本章的 C99/C11 规则中,void print_marks(); 没有给出相同的参数类型信息,不能替代清楚的函数原型。
14. 循环里的累加与倍增
14.1 累加需要保存已有结果
int number = 1;
int total = 0;
while (number <= 5) {
total = total + number;
++number;
}
最后 total 为 15。total 是累加器,初值为零,每轮在原有结果上加上新的一项。
如果在循环体内每次都先把 total 设为零,就会丢掉前面各轮的累计结果。
14.2 倍增与总量不是同一个变量
假设每轮数量是上一轮的两倍:
轮次 本轮数量 累计数量
1 1 1
2 2 3
3 4 7
4 8 15
5 16 31
可以用 current 保存当前项、total 保存累计值:
current = current * 2;
total = total + current;
更新顺序必须与初始状态配合。循环执行前是否已处理第一项,会影响循环条件和总次数。
指数增长很快,变量类型要能容纳中间值和累计值。换成浮点类型可以扩大范围,但不能同时保证对所有大整数都保持精确。
15. 综合示例:反复拆分时长
下面的程序反复读取总秒数,拆成时、分、秒,并显示按小时计算的浮点结果。输入负数退出,输入非数字或遇到输入结束也会停止。
#include <stdio.h>
typedef double real;
void print_duration(int total_seconds);
int main(void)
{
int seconds = 0;
printf("Enter seconds; a negative value quits:\n");
while (scanf("%d", &seconds) == 1) {
if (seconds < 0) {
return 0;
}
print_duration(seconds);
printf("Enter the next value:\n");
}
printf("Input ended.\n");
return 0;
}
void print_duration(int total_seconds)
{
const int seconds_per_minute = 60;
const int seconds_per_hour = 3600;
int hours = total_seconds / seconds_per_hour;
int minutes = total_seconds / seconds_per_minute % 60;
int seconds = total_seconds % seconds_per_minute;
real total_hours = (real)total_seconds / seconds_per_hour;
printf("%02d:%02d:%02d, %.3f hours\n",
hours, minutes, seconds, total_hours);
}
依次输入 3671、0、-1,其中两条结果行为:
01:01:11, 1.020 hours
00:00:00, 0.000 hours
这个程序把几条规则组合在一起:
scanf的返回值作为循环条件的一部分,避免读取失败后继续使用旧数据。- 整数
/计算整份数,%计算剩余部分。 (real)在除法前取得浮点操作数,使total_hours保留小时的小数部分。typedef表达类型别名,const表达固定换算系数。- 通过函数形参传入秒数,函数内部的局部变量与
main中的变量各有自己的作用域。
示例按可由 int 表示的非负秒数处理;输入检查展示基本控制方式,完整的数值范围解析留待后续展开。
保存为 duration.c 后,可以在已经提供 cc 的 macOS 或类 UNIX 环境中编译运行:
cc -std=c11 -Wall -Wextra duration.c -o duration
./duration

