清风日常

Stay curious. Stay gentle.

听一会儿

4 首收藏
LOVE 2000 封面

LOVE 2000

遠野ひかる

0:004:23
首页/C Primer Plus/正文

C Primer Plus(第6版)· 第5章:运算符、表达式和语句

运算符描述怎样处理数据,表达式把这些操作组合起来,语句则把计算、修改和控制流程组织成程序。

文章目录 54 个章节

第5章:运算符、表达式和语句

运算符描述怎样处理数据,表达式把这些操作组合起来,语句则把计算、修改和控制流程组织成程序。

理解本章时,应同时关注三件事:表达式怎样组合、运算以什么类型进行、对变量的修改何时完成。

1. 用 while 重复执行任务

计算机的优势之一是重复执行相同规则。把需要变化的数据与固定处理步骤分开,就可以通过循环生成一组结果。

c
#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;
}

输出:

text
number square
     1      1
     2      4
     3      9
     4     16
     5     25

1.1 先判断,再执行

text
初始化 number = 1
        ↓
检查 number <= 5 ←────────┐
    ├─ 假 → 离开循环      │
    └─ 真 → 输出平方      │
               ↓         │
          number 加一 ───┘

while 的基本形式是:

text
while (条件表达式)
    一条语句

如果需要重复执行多条语句,用花括号把它们组成一个复合语句。

条件为零表示假,非零表示真。关系表达式如 number <= 5,成立时产生整数 1,不成立时产生整数 0。

由于先检查条件,循环体可能一次也不执行。上面的循环在 number 变成 6 后退出。

1.2 计数循环的三个要素

要素 本例代码 作用
初始状态 int number = 1; 确定从哪里开始
继续条件 number <= 5 确定哪些情况进入循环
状态更新 number = number + 1; 让程序逐步接近退出条件

如果遗漏更新,条件可能一直为真,反复打印同一个结果。循环中的值也可以来自下一次输入,不一定只能由计数器加一获得。

2. 赋值、对象与可修改左值

2.1 = 是保存操作

c
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 赋值表达式也产生值

c
int first;
int second;
int third;

first = second = third = 8;

赋值运算符按从右到左的方式结合,结构相当于:

c
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 中,整数除法向零截断:

text
 9 /  4 →  2
-9 /  4 → -2
 9 / -4 → -2
-9 / -4 →  2

向零截断不是向下取整,也不是四舍五入。

4.2 接收结果的变量不能反过来改变计算

c
double first = 9 / 4;
double second = 9.0 / 4;

first 是 2.0,second 是 2.25。第一条先算出整数 2,然后才转换为 double 保存。

同样:

c
double result = 3 / 5 * 10.0;

结构是 (3 / 5) * 10.0,左边整数除法先得到 0,最终结果是 0.0。后面的浮点常量不能补回此前丢失的小数部分。

4.3 避免无效除法

整数除法的除数不能为零。如果有符号商无法由结果类型表示,例如常见补码系统中的 INT_MIN / -1,同样会发生未定义行为。

浮点除法也应根据任务约束检查除数,不能把某个平台会产生无穷大的表现当作所有计算场景的处理方案。

5. 余数运算符 %

5.1 整除得到整份数,余数得到剩余部分

对非负秒数 3671:

c
int seconds = 3671;
int hours = seconds / 3600;
int minutes = seconds / 60 % 60;
int remaining_seconds = seconds % 60;

结果是 1 小时、1 分钟、11 秒。

% 的操作数必须是整数,不能直接对 double 使用它。数学库有处理浮点余数的函数,但那是另一套接口。

5.2 负数余数的符号

在除法有效且商可表示的前提下,整数满足:

text
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 优先级决定怎样组成表达式

c
result = 10 + 6 * 4;

由于乘法优先级高于加法,结构是 10 + (6 * 4),得到 34。如果需要先加法,应写成 (10 + 6) * 4,得到 64。

下面列出本章涉及的部分层级,从高到低排列:

层级 运算符示例 结合方向
后缀 value++、value--、函数调用 从左到右
一元与强制转换 ++value、--value、一元 + -、sizeof、(type) 从右到左
乘法层级 *、/、% 从左到右
加法层级 +、- 从左到右
关系比较 <、<=、>、>= 从左到右
相等比较 ==、!= 从左到右
赋值 = 从右到左

这不是全部运算符列表。用于分组的圆括号可以明确改变组合结构,不应仅靠记忆一张大表来写复杂表达式。

6.2 结合性决定同层运算怎样分组

text
24 / 4 * 2  → (24 / 4) * 2 → 12
24 / (4 * 2)               → 3

乘除同级,按从左到右的结合方向分析。赋值则从右到左结合,例如 a = b = 3。

这里的“从左到右”描述的是语法分组,不能推广为所有操作数都按从左到右的时间顺序求值。

6.3 优先级不能确定所有执行先后

c
result = left() + right() * 2;

这段表达式要求把 right() 的结果与 2 相乘,再用于外层加法,但不保证一定先调用 left() 或一定先调用 right()。

函数各实参的求值顺序也不能简单假定为从左到右。给某部分加括号,只能明确组合关系,不会自动建立所有操作之间的先后关系。

概念 回答的问题
优先级 不同运算符怎样结合操作数?
结合性 同一层级的运算符怎样分组?
求值顺序 操作数求值、函数调用、变量修改在时间上如何安排?

7. 递增与递减

7.1 修改变量,同时产生表达式结果

++ 使对象增加 1,-- 使对象减少 1。操作数必须是类型合适的可修改左值,不能给常量或普通加法结果使用它们。

当独立作为语句时:

c
count++;
++count;

两者都会使 count 加一。递减也有对应的前缀和后缀形式。

7.2 前缀使用新值,后缀提供旧值

c
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 条件里的递增会影响循环体

c
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 表达式后加分号形成表达式语句

c
count = 4;
++count;
printf("%d\n", count);

这些都是表达式语句。它们通过赋值、递增或输出完成可观察的工作。

3 + 4; 也是合法的表达式语句,但在这里计算结果被丢弃,没有完成有用的状态修改或输出,编译器可能发出警告。

int count; 是声明,不能因为也以分号结尾,就把它归为表达式语句。return 0; 则属于跳转语句。

8.3 空语句

c
;

单独一个分号是一条空语句。在语法上需要语句、但确实不需要执行任何内容时,可以使用它。

一个常见错误是在循环条件后无意间多写分号:

c
while (count < 5);

这里循环体就是空语句。后面看起来缩进得很整齐的代码,也不会自动成为它的循环体。

8.4 复合语句把多个步骤组成整体

c
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 圆括号内的控制表达式。

对本章这种顺序执行代码,前一个完整表达式中的求值与副作用,要在后一个完整表达式开始前完成。

因此:

c
count++;
printf("%d\n", count);

第二条语句能读取到递增后的值。前面 while (count++ < 3) 的例子,也正是因为控制表达式已经完成,循环体才会看到新值。

这可以借助“序列点”的概念理解。C11 还用“先序于”等关系描述求值之间的约束。初学时不需要背完整术语,但要知道括号、逗号分隔的实参和分号并不都具有相同的顺序作用。

9.3 不要依靠未定义行为推断结果

在本章采用的 C99/C11 语境下,下面的写法存在没有适当顺序约束的修改或读取冲突:

c
/* 以下是错误示例,不应执行或依赖结果。 */
y = n++ + n++;
printf("%d %d\n", n, n++);
result = n / 2 + n++;

这些属于未定义行为,不能只解释为“几个候选输出中随机选一个”,也不能保证运行结束后变量一定增加了几次。

只加分组括号不能解决这种冲突。应该把修改与使用拆成有明确顺序的步骤,例如:

c
int old_value = n;
++n;
printf("%d %d\n", old_value, n);

明确先保存旧值,再修改,再输出,才能表达稳定的意图。

10. sizeof、size_t 与 typedef

10.1 大小结果使用 size_t

c
int number = 0;
size_t bytes = sizeof number;

printf("%zu\n", bytes);

sizeof 按 C 字节计算大小,结果是无符号整数类型 size_t。操作数为类型名时写 sizeof(int);为普通对象表达式时,可以写 sizeof number。

10.2 普通非变长类型的操作数不被求值

c
int number = 3;
size_t bytes = sizeof(number++);

这里 number 仍是 3。对于这个普通 int 表达式,sizeof 只需要确定其类型大小,不会执行递增。编译器可能因此提醒这里的副作用不会发生。

涉及可变长数组类型时存在需要求值的情况,因此不要把它推广为“任何 sizeof 内部都绝对不求值”。

10.3 typedef 为类型起别名

c
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 有符号与无符号混合尤其要留意

c
int signed_value = -1;
unsigned int unsigned_value = 1U;

printf("%d\n", signed_value < unsigned_value);

结果是 0。这组相同等级的类型比较时,有符号值转换为 unsigned int,-1 得到对应的无符号大值,所以不能再按数学上的负一小于一推断。

若有符号类型等级更高,还要看它是否能表示无符号类型的全部值;能则转为该有符号类型,不能则使用相应的无符号类型。

11.4 保存结果时再转换为目标类型

c
int count = 12.9;
double amount = 12;

第一条在值可表示的前提下向零截断,得到 12;第二条把整数转换为浮点数。

可能丢失信息的情形包括:

  • 浮点转整数,丢弃小数部分;截断后的值超出范围时是未定义行为。
  • 精度较高的浮点值保存到较低精度类型,发生舍入。
  • 整数转换到无符号类型,按该目标类型的模数调整。
  • 整数转换到无法表示该值的有符号类型,在 C99/C11 下结果由实现定义,或产生实现定义的信号。

最后一项是整数转换规则,不应与有符号算术运算溢出的未定义行为混为一谈。

12. 显式强制类型转换

12.1 写法与作用范围

text
(目标类型) 表达式

例如:

c
double first = (double)9 / 4;
double second = (double)(9 / 4);

first 是 2.25;second 是 2.0。

第一条在除法之前把操作数转成浮点类型,改变了计算类型;第二条先完成整数除法,再转换已经截断的结果。

12.2 转换发生在哪里,会改变答案

c
int first = (int)(1.8 + 2.4);
int second = (int)1.8 + (int)2.4;

第一条先相加后截断,得到 4;第二条先分别截断再相加,得到 3。

强制转换表示程序员的转换意图,不是数据范围的验证,也不会让超范围操作自动变得安全。

13. 带参数的函数

13.1 用参数让同一段代码处理不同数据

c
#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");
}

输出:

text
***
*****
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 函数原型指导类型转换

c
void print_marks(int count);

原型告诉编译器:函数不返回值,接收一个 int 参数。

如果在看得见这个原型的位置传入 4.8,会按形参类型转换为 int,函数接收到 4。这种截断必须符合任务意图,并满足目标类型范围。

固定形参的转换与可变参数部分的默认提升不同。例如:

  • 传给已声明 int 形参的浮点值,按 int 参数要求转换。
  • 传给 printf 可变参数部分的 float,提升为 double;较窄整数进行整数提升。

在本章的 C99/C11 规则中,void print_marks(); 没有给出相同的参数类型信息,不能替代清楚的函数原型。

14. 循环里的累加与倍增

14.1 累加需要保存已有结果

c
int number = 1;
int total = 0;

while (number <= 5) {
    total = total + number;
    ++number;
}

最后 total 为 15。total 是累加器,初值为零,每轮在原有结果上加上新的一项。

如果在循环体内每次都先把 total 设为零,就会丢掉前面各轮的累计结果。

14.2 倍增与总量不是同一个变量

假设每轮数量是上一轮的两倍:

text
轮次      本轮数量      累计数量
 1           1             1
 2           2             3
 3           4             7
 4           8            15
 5          16            31

可以用 current 保存当前项、total 保存累计值:

c
current = current * 2;
total = total + current;

更新顺序必须与初始状态配合。循环执行前是否已处理第一项,会影响循环条件和总次数。

指数增长很快,变量类型要能容纳中间值和累计值。换成浮点类型可以扩大范围,但不能同时保证对所有大整数都保持精确。

15. 综合示例:反复拆分时长

下面的程序反复读取总秒数,拆成时、分、秒,并显示按小时计算的浮点结果。输入负数退出,输入非数字或遇到输入结束也会停止。

c
#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,其中两条结果行为:

text
01:01:11, 1.020 hours
00:00:00, 0.000 hours

这个程序把几条规则组合在一起:

  1. scanf 的返回值作为循环条件的一部分,避免读取失败后继续使用旧数据。
  2. 整数 / 计算整份数,% 计算剩余部分。
  3. (real) 在除法前取得浮点操作数,使 total_hours 保留小时的小数部分。
  4. typedef 表达类型别名,const 表达固定换算系数。
  5. 通过函数形参传入秒数,函数内部的局部变量与 main 中的变量各有自己的作用域。

示例按可由 int 表示的非负秒数处理;输入检查展示基本控制方式,完整的数值范围解析留待后续展开。

保存为 duration.c 后,可以在已经提供 cc 的 macOS 或类 UNIX 环境中编译运行:

sh
cc -std=c11 -Wall -Wextra duration.c -o duration
./duration
Aron 的清风日常

作者 Aron · 发布于 2026.07.19

愿这份记录能为你提供一点参考。发现问题,欢迎通过 GitHub 或 邮件交流。

如果这篇记录对你有用,留下一份喜欢。

← 所有文章继续阅读C Primer Plus(第6版)· 附录整理:语法与标准库速查 →
SEARCH MY NOTES

在记录里,找一点答案

搜索标题、标签与全文 · 按 Esc 关闭