本文是《C语言期末速成》系列第 3 篇。上一章我们认识了数据,这一章学”怎么摆弄数据”——运算符与表达式。全篇围绕一句话:表达式一定有数值。搞清楚每个运算符”最后得出什么值”,你就掌握了本章的一切。


上一篇C语言期末速成 2:数据类型与进制

考点清单

  • 表达式一定有数值——这是理解一切运算符的钥匙
  • 关系(比较)表达式的结果只能是 1(真)或 0(假)a < b < c 连写不是区间判断,要用 && 连接
  • / 两边都是整数时结果是整数;% 两边必须都是整数,结果取余数
  • 赋值表达式值为最左边的变量值;赋值的左边只能是一个变量
  • 复合赋值要先”补括号”再算:a *= 2+3 等价于 a = a * (2+3)
  • 自增自减:++a 先加后用,a++ 先用后加
  • 逗号表达式值取最右边那个表达式的值,优先级最低
  • 强制类型转换写法是 (int)a,类型一定要带括号
  • 优先级口诀:! > 算术 > 关系 > && > || > 赋值 > 逗号

一、算术运算符

+ - * 分别对应数学上的加、减、乘,想必各位都比较熟悉了,在此不再展开。而除法以及取余运算在 C 语言中有一些特殊的地方,需要特别注意。

1.1 整数除法:/ 的两种命运

/ 的行为取决于两边的类型

3 / 2     结果 1      /* 两边都是整数 → 结果取整数部分(商),丢弃余数 */
3 / 2.0   结果 1.5    /* 有一边是小数 → 结果就是小数 */
3.0 / 2   结果 1.5

这里隐藏着 C 的一个机制:除法运算要求两边类型一致,不一致时会做隐式转换(见本章第四节)。3 / 2.0 会先把 3 转成 3.0 再除,所以结果是小数。

这个坑是考试重灾区:5/2=27/3=2-7/2=-3(C 里整型除法向 0 截断)。写程序想保留小数,务必让其中一边是小数,例如 n/2.0(double)n/2

1.2 取余 %:只认整数

%取余数,不是除号!而且两边必须是整数,出现小数就非法。

7 % 3     结果 1     /* 7 = 2×3 + 1 */
10 % 2    结果 0     /* 能被整除 */
5 % 5     结果 0

它的用处极广:判断奇偶 n % 2 == 0、判断整除 n % 5 == 0、取个位 n % 10。第 6 章循环里你会频繁遇到它。

余数的符号(加深)% 的结果符号跟随被除数(% 左边的数)。左边是正数,余数就是正数;左边是负数,余数就是负数:

100 % 124     100 % -124
-100 % 12-4     -100 % -12-4

这个规律在改错题里偶尔出现,注意别想当然以为”余数永远非负”。

1.3 % 取个位 / 逆序输出(经典)

n % 10 拿到个位,n / 10 丢掉个位——这两步组合,能逐位拆解一个整数:

int n = 1234;
printf("%d", n % 10);   /* 4:个位 */
n = n / 10;             /* 123 */
printf("%d", n % 10);   /* 3:十位 */

二、关系(比较)运算符

六个关系运算符用来比较两个值,结果只有 1(真)或 0(假)

运算符含义例子(a=3, b=4)
<小于a < b → 1
<=小于等于b <= 4 → 1
>大于a > b → 0
>=大于等于b >= 4 → 1
==等于a == b → 0
!=不等于a != b → 1

关系表达式的值一定是 1 或 0——C 语言没有专门的”逻辑类型”,任何判断真假的地方都是非 0 即真,0 即假

2.1 常见坑:a < b < c 不是数学连写

数学上的 0 < x < 10 在 C 里不是区间判断,而是从左到右一步步算:

int x = 5;
0 < x < 10   →   (0 < x) < 101 < 101   /* 碰巧为真 */

0 < x 先算出 0 或 1,再拿这个 0/1 去和 10 比。要表达”x 在 0 和 10 之间”,必须用逻辑与连接(第 5 章):0 < x && x < 10

2.2 字符也能比较:比的是 ASCII 码

字符比较实际是 ASCII 码大小比较(第 2 章):'a' < 'b'97 < 98 → 1。判断”是不是数字字符”就靠它:

ch >= '0' && ch <= '9'   /* 判断 ch 是不是数字字符 */

2.3 ===:一个等号之差(必考)

  • ==比较:判断两边是否相等;
  • =赋值:把右边塞给左边。
if (x == 5)   /* 比较:x 等于 5 吗? */
if (x = 5)    /* 赋值:把 5 塞给 x,表达式值恒为 5(非 0)→ 条件恒为真! */

x = 5 作为条件表达式永远为真(5 非 0),这是改错题最爱挖的坑(第 5 章还会再考)。

2.4 浮点数不要用 == 精确比较(加深)

0.1 + 0.2 == 0.3   /* 结果是 0(假)! */

浮点数在二进制里大多存不精确(0.1 无法用二进制有限小数表示),所以比较两个浮点是否”相等”,要用差的绝对值小于一个很小的数

fabs(a - b) < 1e-6   /* 近似相等就认为相等 */

三、赋值运算符:= 与它的家族

3.1 赋值的方向与规则

  • 赋值表达式 a = b = 5值是最左边变量 a 的值,即 5;
  • 常量不可以赋值5 = a 非法,左边必须是变量;
  • 赋值的左边只能是一个变量a + b = c 非法。

3.2 定义时的”连续赋值”陷阱(必考)

int x = y = 10;   /* 错!定义变量时不可以连续赋值,y 还没定义 */
int x, y;
x = y = 10;       /* 对!定义完成后可以连续赋值 */

原因是:定义语句里 y 还未被声明,y = 10y 无类型可依,编译器直接报错。

3.3 赋值时的隐式类型转换

不同类型赋值会自动转换:

int x = 7.7;      /* 对:x 是 int,取整 → 7(小数部分被丢弃) */
float y = 7;      /* 对:y 是 float,→ 7.0 */

规律:把”高精度”赋给”低精度”会丢失信息(7.7 → 7),把”低精度”赋给”高精度”则自动补零(7 → 7.0)。int x = 7.7; 合法,x 就是 7——这是三种”取整丢小数”的情形之一,第四节汇总。

TIP

虽然高精度赋值给低精度是合法的,但在实际程序设计中应该尽量避免,或者显式的给出类型转换,以免产生歧义。(你不觉得 int x = 7.7; 这种写法很魔丸吗~~~)

3.4 复合赋值运算符

+=-=*=/=%= 等是”自身运算后写回”的简写:a += b 等价于 a = a + b

必考陷阱:右边先整体算完,再和左边运算。换句话说,先在心里给右边打上括号

int a = 2;
a *= 2 + 3;        /* 等价于 a = a * (2 + 3) = 2 × 5 = 10 */

而不是 a = a * 2 + 3。同理:

int b = 6;
b %= 4 + 1;        /* b = b % (4+1) = 6 % 5 = 1 */

口诀:复合赋值,右边先算,补上括号再动手。


四、自增自减

4.1 前置 vs 后置:一个字的差别

a = 5

写法表达式的值运算结束后 a
++a6(先加,再用)6
a++5(先用,再加)6
--a44
a--54

背诵口诀:++ 在前,先加后用;++ 在后,先用后加。

要理解本质,把它想成两步操作的组合:

  • ++a:第 1 步给 a 加 1,第 2 步把新值作为表达式结果;
  • a++:第 1 步把当前 a 的值作为表达式结果,第 2 步再给 a 加 1。

无论前置还是后置,两步都执行完后,变量 a 的值都一样(都加了 1)——不同的只是”表达式那一刻拿到的值”。

int a = 5;
int m = ++a;   /* m = 6,a = 6 */
int n = a++;   /* n = 6,a = 7 */

4.2 混在表达式里的连环坑

单独写 a++;++a; 效果相同,但塞进表达式就差很多:

int a = 3;
printf("%d %d\n", a++, a++);   /* 别这样写!结果依赖求值顺序,不同编译器可能不同 */

这类”同一个变量在一个表达式里被多次自增”的写法属于未定义行为,考试要么不考,要么只考最简单的单步推导。遇到时按”先取旧值用,再自增”的顺序手推即可。


五、强制类型转换与隐式转换

5.1 强制类型转换:(类型) 表达式

(int)a       /* 对:类型必须带括号 */
int(a)       /* 错:这种函数式写法在 C 中非法 */

最经典的区别题

(int)(a + b)   /* 先算 a+b,再把结果转成 int */
(int)a + b     /* 只把 a 转成 int,再加 b,b 不受影响 */
double d = 7.8;
int i1 = (int)d;     /* 7:d 取整 */
int i2 = (int)(d+0.2); /* 8:(7.8+0.2)=8.0 转 int */

5.2 三种”取整丢小数”的情形(必考汇总)

  1. 赋值给整型:int a = 1.6;a = 1
  2. 强制转换:(int)1.61
  3. 整数除法:1 / 203 / 21

这三者都是”把小数部分直接丢掉”,注意和四舍五入区分——第 4 章会给出实现四舍五入的公式。

5.3 隐式转换规则(加深)

不写 (int) 时,C 也会自动转换类型,规则按”低优先级 → 高优先级”进行,直觉就是**“小的往上凑,凑到能放下为止”**:

char/short → int → unsigned → long → float → double
  • 整数运算时,charshort 先提升为 int(称为”整型提升”);
  • 混合运算时,低精度转高精度:'a' + 1'a' 先变成 97 再加;
  • 所以 3 + 2.53 会先变 3.0,结果是 5.5
char c = 'a';          /* 97 */
printf("%d\n", c + 1); /* 98:c 提升为 int 再加 */

六、逗号表达式:优先级最低的”顺序执行”

逗号表达式 (表达式1, 表达式2, ...) 的规则:

  1. 从左到右依次求值各表达式;
  2. 整个表达式的值 = 最后一个表达式的值。
int x = (2, 3, 4);    /* 值 = 4,x = 4 */

但注意加不加括号,命运完全不同:

int z = (2, 3, 4);    /* 整个括号内是逗号表达式,z = 4 */
int z = 2, 3, 4;      /* 逗号优先级最低:先赋值 z = 2,再逗号隔开 */

第二行里,因为赋值 = 的优先级高于逗号,所以 z = 2 先执行,后面 , 3, 4 只是被逗号连接、不赋值——z 是 2。这就是”逗号运算符级别最低”的威力。

表达式结果
(2, 3, 4)4
z = (2, 3, 4)z 为 4
z = 2, 3, 4z 为 2

七、优先级与结合性总表(加深)

考试不要求背整张表,但要求知道相对次序和几个特殊案例。下表按优先级从高到低排列(越靠上越先算):

优先级运算符说明
最高() [] -> .函数调用、下标、成员
! ++ -- - (类型) * &单目运算
* / %乘除取余
+ -加减
<< >>移位
< <= > >=关系比较
== !=相等比较
&位与
^位异或
|位或
&&逻辑与
||逻辑或
?:条件(三目)
= += *=赋值(右结合)
最低,逗号

几条必须记住的结论:

  • 赋值优先级倒数第二,逗号最低
  • !(逻辑非)优先级很高,比算术运算符还高:!a + 1(!a) + 1
  • 关系运算低于算术:a + b > c(a+b) > c
  • == 低于 <a < b == c(a < b) == c
  • 三个位逻辑的次序& > ^ > |,且都排在关系、相等之后、逻辑 &&/|| 之前。所以 a & 1 == 1 会先算 1 == 1 再位与,想判断最低位必须写 (a & 1) == 1
  • 赋值是右结合a = b = c 从右往左,即 a = (b = c)
  • 不确定时加括号最保险——括号永远最高优先级,可读性也最好。

易错点 / 陷阱

  1. 5/2 算成 2.5:两边都是整数,结果取整数部分,是 2。
  2. % 当除号% 是取余,两边必须整数;7%2=1 不是 3。
  3. int x = y = 10; 当合法:定义时不能连续赋值,先分开定义。
  4. a *= 2+3 算成 a=a*2+3:复合赋值右边先算,等价于 a*(2+3)
  5. a++++a 混用:记住”在后先用后加”。
  6. int(3.9) 当强制转换:正确写法是 (int)3.9,括号在类型上。
  7. (int)a+b(int)(a+b) 混淆:前者只转 a
  8. z = 2, 3, 4 认为 z 是 4:逗号优先级比赋值低,先执行 z=2
  9. a < b < c 当数学区间:先算 a<b 得 0/1,再和 c 比;区间判断要写 a<b && b<c
  10. == 写成 =x = 5 是赋值,表达式值恒为 5(非 0),条件永远为真。

自测小练习

  1. 求值 7 / 27 % 27 / 2.0
  2. int a = 2; a *= 3 + 4; 之后 a 是多少?
  3. int a = 8; 依次执行 a++a 为几?++a 后呢?
  4. int a = 3, b = 4, c = 5;c = (a, b, c)c = a, b, c 两种写法下 c 的值。
  5. int x = 9.9; x 是多少?(int)9.9 呢?9/4 呢?
  6. int a = 3, b = 4, c = 5;a < b < c(a < b) == 1 的值。
参考答案
  1. 7/2 = 3(整数除法)、7%2 = 1(余数)、7/2.0 = 3.5
  2. a *= 3+4 等价于 a = a*(3+4) = 2×7 = 14
  3. a++a = 9;再 ++aa = 10。(单步操作后变量都是加 1 的结果)
  4. c = (a,b,c)c = 5c = a, b, c → 逗号优先级最低,先 c = a,所以 c = 3
  5. x = 9(赋值取整);(int)9.9 = 99/4 = 2(整数除法)。
  6. a < b < c:先算 a<b 得 1,再算 1 < c 得 1;(a<b) == 11 == 1 得 1。

下一篇C语言期末速成 4:格式化输入输出

评论区