运算符、表达式和语句
掌握C语言运算符体系——算术/关系/逻辑/赋值/自增自减/逗号运算符,以及表达式求值与类型转换规则,能正确运用while循环构建简单程序
学习目标
- 能正确使用算术、关系、逻辑、赋值和自增自减运算符,区分
++i(前缀)和i++(后缀)的表达式值差异 - 能解释 C 语言表达式求值中的类型转换规则——整型提升、寻常算术转换和赋值转换——并预测混合类型表达式的结果
- 能用
while循环配合自增运算符实现一个"打印 1 到 10 及其平方值"的程序,并回答:如果把while (n < 11)写成while (n <= 11),输出会多什么?
机制总览
运算符、表达式和语句:机制路径
- 1
为什么 C 的运算像一条"加工流水线"?
想象一条巧克力工厂的流水线:原料从传送带左边进来,经过混合、加热、冷却、切割、包装五道工序,右边出来的就是成品。每道工序的输入是上一道的输出,工序之间有固定的先后顺序——你先称量再混合,而不是反过来。
- 2
C 运算符全览——一句话把常用运算符说清楚
在进入细节之前,先把本章涉及的五大类运算符摊开看一遍。这张表就是你的"运算流水线机器清单"——运算符、含义、示例、优先级,一目了然:
- 3
表达式与类型转换——当 int 遇上 float
当表达式混合不同类型时,C 会按运算符规则转换操作数。 类型转换(type conversion) 需要结合类型等级和可表示范围判断。
章级决策实验
运算符、表达式和语句:机制与证据
切换《运算符、表达式和语句》的三个关键教学阶段,先解释机制,再用运行与失败证据验证结论。
选择推理阶段
当前阶段 · 为什么 C 的运算像一条"加工流水线"?
想象一条巧克力工厂的流水线:原料从传送带左边进来,经过混合、加热、冷却、切割、包装五道工序,右边出来的就是成品。每道工序的输入是上一道的输出,工序之间有固定的先后顺序——你先称量再混合,而不是反过来。
可核验证据
用 -Wall -Wextra -pedantic 构建本节示例,再以正常值、边界值和失败输入核对「为什么 C 的运算像一条"加工流水线"?」的实际行为。
学完《运算符、表达式和语句》后,应能从输入和前置条件推导状态变化,并用可重复的构建、运行或边界测试证明结果。
失效—证据矩阵
运算符、表达式和语句:失效与核验
为什么 C 的运算像一条"加工流水线"?
典型失效
若只记语法而忽略「为什么 C 的运算像一条"加工流水线"?」的类型范围、求值顺序或资源边界,输入变化后可能出现未定义行为或错误结果。
核验证据
用 -Wall -Wextra -pedantic 构建本节示例,再以正常值、边界值和失败输入核对「为什么 C 的运算像一条"加工流水线"?」的实际行为。
C 运算符全览——一句话把常用运算符说清楚
典型失效
若只记语法而忽略「C 运算符全览——一句话把常用运算符说清楚」的类型范围、求值顺序或资源边界,输入变化后可能出现未定义行为或错误结果。
核验证据
用 -Wall -Wextra -pedantic 构建本节示例,再以正常值、边界值和失败输入核对「C 运算符全览——一句话把常用运算符说清楚」的实际行为。
表达式与类型转换——当 int 遇上 float
典型失效
若只记语法而忽略「表达式与类型转换——当 int 遇上 float」的类型范围、求值顺序或资源边界,输入变化后可能出现未定义行为或错误结果。
核验证据
用 -Wall -Wextra -pedantic 构建本节示例,再以正常值、边界值和失败输入核对「表达式与类型转换——当 int 遇上 float」的实际行为。
为什么 C 的运算像一条"加工流水线"?
想象一条巧克力工厂的流水线:原料从传送带左边进来,经过混合、加热、冷却、切割、包装五道工序,右边出来的就是成品。每道工序的输入是上一道的输出,工序之间有固定的先后顺序——你先称量再混合,而不是反过来。
在 C 语言里,表达式由运算符和操作数组成。a + b * c 按优先级解析为 a + (b * c);这说明语法分组,不等于规定所有操作数的运行时求值先后。只有标准明确的排序规则(例如 &&、||、条件运算符选中分支、逗号运算符)才能约束副作用顺序。
如果没有这套规则呢? 3 + 4 * 5 的结果可能是 35(从左算)也可能是 23(先乘后加)——同一行代码在不同机器上跑出不同答案。更可怕的是,++i 和 i++ 如果混在一起用,你会拿到意料之外的中间值。这一章就教你掌握 C 运算流水线上每一台机器的脾气——让每个表达式都按你预想的方式出结果。
C 运算符全览——一句话把常用运算符说清楚
在进入细节之前,先把本章涉及的五大类运算符摊开看一遍。这张表就是你的"运算流水线机器清单"——运算符、含义、示例、优先级,一目了然:
记不住没关系——全章讲完你可能只用得到括号。但理解每类运算符的语义是写出正确表达式的第一步。下面逐一拆解。
算术运算符——整数除法向零截断
↡完成加减乘除和求余运算的符号:+(加/正号)、-(减/负号)、*(乘)、/(除)、%(求余/模运算)。 看起来跟你小学数学课上的符号一样,但 C 里有两条关键差异:
-
整数除法向零截断,不是数学上的 floor。
5 / 2等于 2,-5 / 2等于 -2(C99 起明确)。除数为零或最小有符号整数除以 -1 且结果不可表示时,行为未定义。 -
%求余运算符只能用于整数。10 % 3的结果是 1——10 除以 3 商 3 余 1。%很好记——"百分号求余,结果是除法剩下的那点不能整除的部分"。
#include <stdio.h>
int main(void) {
printf("5 / 2 = %d\n", 5 / 2); // 2(整数除法,截断)
printf("5.0 / 2 = %.1f\n", 5.0 / 2); // 2.5(浮点除法)
printf("-5 / 2 = %d\n", -5 / 2); // -2(往零截断)
printf("10 %% 3 = %d\n", 10 % 3); // 1(求余)
printf("10 %% 2 = %d\n", 10 % 2); // 0(整除,无余数)
return 0;
}关系与逻辑运算符——"是真是假"的判断器
↡用于比较两个值大小关系的运算符:小于、小于等于、大于、大于等于、等于、不等于。结果是一个整数值:关系成立为 1、不成立为 0。 和 ↡对真/假条件进行组合的运算符:&&(逻辑与,两边都真才真)、||(逻辑或,任一边真就真)、!(逻辑非,真变假、假变真)。 的作用是"判断条件的真假"——它们在 if 和 while 的条件里是主力。
C99 提供 _Bool,<stdbool.h> 还定义 bool、true、false 宏。关系与逻辑表达式的结果类型仍是 int,值为 0 或 1;作为控制条件时,标量值 0 为假,非零为真。
#include <stdio.h>
int main(void) {
int a = 5, b = 3;
printf("a > b = %d\n", a > b); // 1(5 大于 3,真)
printf("a < b = %d\n", a < b); // 0(5 小于 3,假)
printf("a == 5 = %d\n", a == 5); // 1(a 等于 5,真)
printf("a != 5 = %d\n", a != 5); // 0(a 等于 5,不"不等于")
// 逻辑运算符:组合多个条件
printf("a>0 && b>0 = %d\n", a > 0 && b > 0); // 1(两边都真)
printf("a>0 && b<0 = %d\n", a > 0 && b < 0); // 0(一边假就全假)
printf("a<0 || b>0 = %d\n", a < 0 || b > 0); // 1(任一边真就真)
printf("!(a > 0) = %d\n", !(a > 0)); // 0(真变假)
return 0;
}自增自减运算符——C 语言最"狡猾"的符号
↡一元运算符:++i(前缀递增,先加后用)、i++(后缀递增,先用后加)、--i(前缀递减)、i--(后缀递减)。两者对变量最终值的修改相同,但表达式的返回值不同——这是初学者最容易搞混的点。 可能是 C 语言里最让人困惑的两个符号。它们都只做一件事——让变量加 1 或减 1——但前缀和后缀的区别在于表达式返回的是新值还是旧值。
用代码感受一下这个差异:
#include <stdio.h>
int main(void) {
int i = 5, j = 5;
int new_i = ++i; // 前缀:修改后产生值 6
printf("++i: i=%d, 表达式值=%d\n", i, new_i);
int old_j = j++; // 后缀:产生旧值 5,再完成递增
printf("j++: 表达式值=%d, j=%d\n", old_j, j);
return 0;
}赋值与复合赋值运算符
= 把右侧转换后的值存入左侧对象。↡把二元运算与赋值组合的运算符,如加等于、减等于、乘等于。左操作数只求值一次,运算后再按左侧类型转换并写回,因此不能把有副作用的左侧机械展开成普通赋值。 能明确表达“读取、运算、写回”:
#include <stdio.h>
int main(void) {
int n = 10;
n += 5; // 等价于 n = n + 5; → n 变成 15
n -= 3; // 等价于 n = n - 3; → n 变成 12
n *= 2; // 等价于 n = n * 2; → n 变成 24
n /= 4; // 等价于 n = n / 4; → n 变成 6
n %= 4; // 等价于 n = n % 4; → n 变成 2
printf("n = %d\n", n); // 输出 2
return 0;
}复合赋值运算符读起来也很自然:n += 5 读作"n 增加 5"。
逗号运算符——让你在一行里做多件事
↡优先级最低的二元运算符,写法和逗号一样:表达式1, 表达式2。先求值左边的表达式、丢弃结果,再求值右边的表达式、把它作为整个逗号表达式的结果。主要用于 for 循环里放多个初始化/更新语句。 , 优先级在 C 所有运算符中最低。它先求值左边的表达式(丢弃其结果),再求值右边,整个逗号表达式的结果就是最右边表达式的结果。
int x, y;
x = (y = 3, y + 1); // 先执行 y=3(y 变成 3),再计算 y+1=4
// x 的值是 4,y 的值是 3最常见的用处是在 for 循环里同时初始化或更新两个变量(这是下两章的内容,先留个印象):
for (i = 0, j = 10; i < j; i++, j--) { /* ... */ }表达式与类型转换——当 int 遇上 float
当表达式混合不同类型时,C 会按运算符规则转换操作数。↡把值从一种类型映射到另一种类型的规则集合,包括整数提升、通常算术转换、赋值转换和显式转换。转换可能改变符号、范围或精度,并不保证避免信息丢失。 需要结合类型等级和可表示范围判断。
C 的类型转换分两阶段:
-
整型提升:
char和short在表达式里首先被自动提升为int(如果int表示范围不够,则升为unsigned int)。所以char c = 'A'; c + 1是按 int 来算的,结果是 66。 -
通常算术转换: 提升后若类型不同,先处理浮点等级;纯整数则比较转换等级、符号和可表示范围,选择共同类型。混合
signed与unsigned时结果尤其不能用简单“精度阶梯”概括:
5 / 2.0 中 5 可转换为 double,结果为 2.5;但 -1 < 1u 可能先把 -1 转为无符号类型,从而得到与直觉相反的比较结果。转换规则是语言定义的共同类型选择,不是“永不丢数据”承诺。
#include <stdio.h>
int main(void) {
char ch = 'A';
int n = 5;
double d = 2.0;
printf("ch code = %d\n", ch); // 执行字符集中的编码值
printf("n / 3 = %d\n", n / 3); // 1(整数除法,截断)
printf("n / d = %.2f\n", n / d); // 2.50(int → double,浮点除法)
printf("n / 3.0 = %.2f\n", n / 3.0); // 1.67(int → double,浮点除法)
return 0;
}强制类型转换——你说了算
当你需要显式地把一种类型转换成另一种类型(而不是靠 C 自动帮你做),用 ↡一元运算符 (类型名)表达式,它显式地把表达式的值转换为指定类型。与隐式转换不同,cast 是你手动控制的——你清楚自己在做什么、接受可能的精度损失。:
int a = 5, b = 2;
double result;
result = a / b; // result = 2.0(先整数除法得 2,再赋值时转为 double)
result = (double)a / b; // result = 2.5(a 先被 cast 成 double,浮点除法)
result = (double)(a / b); // result = 2.0(先整数除法得 2,再 cast 成 double)强制类型转换的写法是 (目标类型)表达式。(double)a / b 先把 a 转为 double;(double)(a / b) 则先进行整数除法。显式写 cast 只表达意图,不会让越界转换自动变安全。
把运算符和 while 循环串起来
有了运算符基础,就可以用 while 循环来反复执行一段代码了。while 是 C 里最基本的结构之一。
↡C 语言的入口条件循环:while (条件) { 循环体 }。先检查条件——条件为真则执行循环体,执行完后回到条件检查;条件为假则跳过整个循环体、往下执行。循环体可能一次都不执行。 的语法很简单:
while (条件表达式) {
// 循环体:条件为真时反复执行
// 每次执行完回到条件检查
}循环体如果只有一条语句,花括号 {} 可以省略,但强烈建议总是写花括号——它能防止后续添加代码时忘了加花括号而产生的 bug。
下面用 Stepper 一步步展示 while 循环如何配合自增运算符工作:
动手:while 循环一步步跑起来
猜一猜:下面这段代码运行到最后,
i的值是多少?答案不是 9!通过 Stepper 一步步揭晓。
第一步:初始化条件变量
变量 i 先被初始化为 0。这一步只是把"计数器"归零——while 循环本身还没开始执行。i = 0 这行写在循环外面,只执行一次。确保循环前条件变量有明确的值是写出正确循环的第一步。
下面是一段完整可运行的 while 循环程序——打印 1 到 10 及其平方值:
#include <stdio.h>
int main(void) {
int n = 1;
printf("数字\t平方\n");
printf("----\t----\n");
while (n < 11) {
printf("%d\t%d\n", n, n * n);
n++; // 每次循环把 n 加 1——忘了这行就死循环!
}
return 0;
}输出:
数字 平方
---- ----
1 1
2 4
3 9
4 16
5 25
6 36
7 49
8 64
9 81
10 100
注意 while (n < 11) 这个写法——当 n = 10 时满足条件、进入循环;n 变成 11 时 11 < 11 不成立、循环结束。n <= 10 和 n < 11 在这个场景下效果一样,但 n < 11 明确告诉你"这条循环跑到 10 为止"。
代码详解:四个核心场景逐段拆解
场景一:自增自减的完整示例——前缀 vs 后缀的全景
#include <stdio.h>
int main(void) {
int a = 5, b = 5;
int result;
// 前缀递增:先加后用
result = ++a;
printf("++a: a=%d, result=%d\n", a, result); // a=6, result=6
// 后缀递增:先用后加
result = b++;
printf("b++: result=%d, b=%d\n", result, b); // result=5, b=6
// 单独使用时效果相同
int x = 3, y = 3;
++x;
y++;
printf("单独使用: x=%d, y=%d\n", x, y); // x=4, y=4
return 0;
}关键区别只在表达式有赋值或函数调用时才体现——把 ++a 的结果赋给 result,result 得到新值 6;把 b++ 的结果赋给 result,result 得到旧值 5。如果单独写一行 ++i; 或 i++;,两者对 i 的效果完全一样——都让 i 加 1。 这个区别让小白很慌,但搞清楚"表达式值"和"变量值"是两个概念之后就简单了。
场景二:复合赋值——用 while 循环做累加
#include <stdio.h>
int main(void) {
int sum = 0;
int i = 1;
while (i <= 10) {
sum += i; // 等价于 sum = sum + i
i++;
}
printf("1+2+...+10 = %d\n", sum); // 输出 55
return 0;
}这段代码把复合赋值和 while 循环结合起来——sum += i 每次把当前 i 的值累加到 sum 上,i++ 推动循环前进。循环体只有两行,却完成了"从 1 加到 10"的计算——运算符和循环配合得当,几行代码就能做很多事。
场景三:类型转换陷阱——赋值窄化与精度丢失
#include <stdio.h>
int main(void) {
double pi = 3.1415926535;
float f = pi; // double → float,精度丢失
int n = pi; // double → int,小数截断
printf("pi (double) = %.10f\n", pi); // 3.1415926535
printf("f (float) = %.10f\n", f); // 3.1415927410(丢了精度!)
printf("n (int) = %d\n", n); // 3(小数全丢)
// 除法中的类型差异
printf("5/2 = %d\n", 5/2); // 2(整数除法)
printf("5/2.0 = %.1f\n", 5/2.0); // 2.5(5转double,浮点除法)
printf("5.0/2 = %.1f\n", 5.0/2); // 2.5(2转double,浮点除法)
return 0;
}float 只有约 6-7 位有效数字——把 double 的 10 位有效数字全部塞进 float,后面的就被丢掉了。int 赋值截断更粗暴——3.99 也照样变成 3。理解了这些"数据在类型之间转移时可能被截断/丢失精度",你就能避免因为"数学上是对的但计算机上错了"而抓狂。
场景四:逗号运算符与 cast 的组合用法
#include <stdio.h>
int main(void) {
int i = 5;
double d = 3.14159;
int result;
// 逗号运算符:两个表达式顺序执行,结果是右边
// i, i*2 的结果是 10(i*2 的值)
printf("(i, i*2) = %d\n", (i, i * 2)); // 10
// cast 运算符:显式转换
result = (int)d; // double → int,截断
printf("(int)%.5f = %d\n", d, result); // 3
// 逗号和 cast 组合
int x;
x = (i++, (int)(d + 0.6)); // i 先变 6,再算 (int)3.74 = 3
printf("x = %d, i = %d\n", x, i); // x=3, i=6
return 0;
}(i++, (int)(d + 0.6)) 这个表达式里:先用逗号让 i++ 把 i 从 5 变成 6(丢弃旧值),再算 d + 0.6 = 3.74、cast 成 (int) 得 3——整个表达式的值是 3。逗号运算符在实际中用得不多,但在 for 循环的初始化和更新段里几乎必不可少。
容易踩的坑
小结
- C 的五大类运算符各有明确的语义:算术(含截断除法和求余)、关系与逻辑(结果是整数 1/0)、自增自减(前缀新值、后缀旧值)、复合赋值(运算+赋值缩写)、逗号(顺序执行,取最右)
++i返回新值(先加后用),i++返回旧值(先用后加)——单独使用时效果相同,有赋值或传参时表达式值不同- 混合类型表达式按整型提升 + 寻常算术转换自动升级;赋值时反向截断(窄化,可能丢数据);cast 显式控制转换
- while 循环遵循"检查条件 → 执行循环体 → 更新条件 → 回到检查"四步;条件不成立时循环体一次都不执行
- 最易踩的坑:同一变量多次自增自减(未定义行为)、while() 后多了分号(空循环体死循环)、
=写成==(赋值当比较)
练习
问题 1(改代码型) 下面程序有两处导致死循环的问题。找出并修正,再说明退出后的 n。
#include <stdio.h>
int main(void) {
int n = 1;
while (n <= 10); {
printf("%d^2 = %d\n", n, n * n);
}
printf("n 最终值 = %d\n", n);
return 0;
}问题 2(问答型) 写出前三行的确定结果,并说明为什么后两行都没有可移植答案。
#include <stdio.h>
int main(void) {
int i = 3, j = 3;
printf("① 5/2 = %d\n", 5 / 2);
printf("② 5.0/2 = %.1f\n", 5.0 / 2);
printf("③ 10%%3 = %d\n", 10 % 3);
printf("④ ++i = %d, i = %d\n", ++i, i);
printf("⑤ j++ = %d, j = %d\n", j++, j);
return 0;
}问题 3(综合实现型) 用 while 打印 1 到 100 中的 7 的倍数,要求以 += 更新循环变量,并解释为什么不会发生有符号溢出。
名词解释
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 算术运算符(arithmetic operator)
完成加减乘除和求余运算的符号:
+(加/正号)、-(减/负号)、*(乘)、/(除)、%(求余/模运算)。它们和小学数学符号长得一样,但 C 里整数除法会截断(丢掉小数部分)、%求余只能用于整数。详见本章「算术运算符」一节和 COperatorTable 速查表。- 关系运算符(relational operator)
用于比较两个值大小关系的运算符:
<(小于)、<=(小于等于)、>(大于)、>=(大于等于)、==(等于)、!=(不等于)。关系表达式的结果是整数:关系成立返回 1,不成立返回 0。==和=是两回事——一个是"比大小"、一个是"把值塞进去"。详见本章「关系与逻辑运算符」一节。- 逻辑运算符(logical operator)
对真/假条件进行组合的运算符:
&&(逻辑与,两边都真才真)、||(逻辑或,任一边真就真)、!(逻辑非,真变假、假变真)。C 里没有单独的"布尔类型"——非零就是真、0 就是假,逻辑运算符的结果也是整数 1 或 0。详见本章「关系与逻辑运算符」一节。- 自增运算符(++)和自减运算符(--)
只让变量加 1 或减 1 的一元运算符。前缀(++i):先加,表达式的值是新值。后缀(i++):先返回旧值,再加。两者对变量的最终影响完全相同——单独写
i++;和++i;效果一样。只有在被赋值或当函数参数时,"表达式值"的差异才体现出来。详见本章「自增自减运算符」一节和 IncrementTimelineDiagram Stepper 图示。- 复合赋值运算符
把二元运算和写回组合起来,如
+=、-=、*=。左操作数只求值一次,结果再转换为左侧类型,因此带副作用的左侧不能机械展开成普通赋值。- 逗号运算符(comma operator)
优先级最低的二元运算符:表达式1, 表达式2。从左到右顺序求值——先算左边、丢弃结果,再算右边、以右边结果为整个表达式的结果。最常见用法是在 for 循环的初始化和更新段里同时做多件事(如
for (i=0, j=10; ...; i++, j--))。详见本章「逗号运算符」一节。- 类型转换(type conversion)
值在不同类型间映射的规则,包括整数提升、通常算术转换、赋值转换和显式转换。共同类型由等级、符号和可表示范围共同决定,可能改变数值或精度。
- 强制类型转换(cast)
一元运算符
(目标类型)表达式,显式地把表达式的值转换成指定类型。cast 优先级很高——(double)a / b是把 a 先转成 double 再做除法,(double)(a / b)是先做整数除法再把结果转 double(已经丢掉的小数再转也回不来)。详见本章「强制类型转换」一节。- while 循环
C 语言的入口条件循环结构:
while (条件) { 循环体 }。先检查条件→条件为真则执行循环体→回到条件检查→条件为假则退出。循环体可能一次都不执行(如果条件一开始就不成立)。条件变量必须在循环体内被更新——忘了更新就会死循环。详见本章「while 循环」一节和 WhileLoopFlowDiagram Stepper 图示。
原版目录概念补充核对
以下条目补齐官方目录中容易被示例主线掩盖的概念。它们不重复罗列目录,而是明确每项概念的机制、适用边界和验收证据。
取模:机制、边界与证据
在《运算符、表达式和语句》的官方单元 cpr-05 中,取模连接本章第 3 组知识约束。学习时要同时说明它接受什么输入、改变什么状态、在何种边界失效;再以本章示例的编译诊断、固定输入输出或失败用例复核结论,不能只记术语名称。