运算符
优先级与结合性
位运算
数学库函数
赋值运算符
自增和自减运算符
案例: 计算 ISBN-13 书号校验位
C语言的特点在于更加强调表达式的使用
表达式是由变量、常量和运算符所构成的
C拥有丰富的运算符集合, 包括:
算术运算符
关系运算符
逻辑运算符
赋值运算符
自增和自减运算符
其他运算符
五个二元算术运算符 (两个操作数): + 加、- 减、* 乘、/ 除、% 求余
两个一元算术运算符 (一个操作数): + 一元加、- 一元减
一元运算符只需要一个操作数:
i = +1;
j = -i;
一元+运算符什么也不做, 它主要用于强调数字常数是正数
i % j是i除以j时的余数
二元算术运算符(%除外)允许使用整数或浮点操作数, 并允许混合
当int和float/double操作数混合时, 结果的类型为float/double
/和%运算符 /和%运算符使用需要特别注意:
当两个操作数都是整数时, /会截断结果: 值1 / 2是0, 而不是0.5
%运算符要求整数作为操作数; 任一操作数不是整数, 程序将无法编译
使用零作为/或%的右操作数会导致未定义的行为
/和%与负操作数一起使用时的行为在C89中由实现定义
C99中, 除法结果总是向零截断, 并且i % j的值与i有相同符号
C标准故意不指定语言的某些部分
未指定部分是因为C语言更强调效率, 常常意味着与硬件行为相匹配
避免编写依赖于由具体实现所定义的行为的程序
i + j * k 是先加再乘, 还是先乘再加? 加括号可以消歧; 省略括号时, C 用运算符优先级规则来确定含义
算术运算符 的优先级:
最高: + - (一元)
其次: * / % (二元)
最低: + - (二元)
示例:
i + j * k |
等价于 | i + (j * k) |
|---|---|---|
-i * -j |
等价于 | (-i) * (-j) |
+i + j / k |
等价于 | (+i) + (j / k) |
表达式有多个相同优先级操作符, 考虑运算符的结合性
如果运算符从左到右结合的, 则称它为左结合运算符
二元算术运算符*, /, %, +, -都是左结合的:
i - j – k |
等价于 | (i - j) - k |
|---|---|---|
i * j / k |
等价于 | (i * j) / k |
如果运算符从右向左结合, 则它是右结合的运算符
一元算术运算符(+和-)都是右结合的:
-+i等价于-(+i)
#include <stdio.h>
int main(void) {
printf("9 / 4 * 4 = %d\n", 9 / 4 * 4);
printf("9 * 4 / 4 = %d\n", 9 * 4 / 4);
return 0;
}
运行结果:
9 / 4 * 4 = 8
9 * 4 / 4 = 9
同样的数字、同样的运算符, 顺序不同结果不同——这就是优先级与结合性的实际影响
9 / 4 * 4: 左结合, 先算 9/4 整除得 2, 再 ×4 得 8; 9 * 4 / 4: 先得 36, 再 /4 得 9
罪魁祸首是整数除法的截断: 9/4 悄悄丢掉了小数部分 (下一课深入)
拿不准时加括号, 让意图明确: (9 / 4) * 4 或 9 * 4 / 4
先自己心算下面表达式的值——不用编译器, 想清楚再翻页:
10 + 20 % 7 * 3
2 + 3 * 4 / 2 % 3
-10 / 3
(10 + 4) / (3 + 1)
9 - 3 / 2 + 1
-5 - -3
-5 + 6 * 4 % 5
10 - 4 % 3
每题问自己两个问题: 先算谁? (优先级) 同级的从左还是从右? (结合性) 两个 - 挨着是什么意思? (一元负号)
| 表达式 | 值 | 为什么 |
|---|---|---|
10 + 20 % 7 * 3 |
28 |
% 高于 +; (20%7)*3 = 18 |
2 + 3 * 4 / 2 % 3 |
2 |
长链同级, 全部左结合: ((3*4)/2)%3 = 0 |
-10 / 3 |
-3 |
向零截断 (不是 -4!) |
(10 + 4) / (3 + 1) |
3 |
括号管顺序, 管不了截断: 14/4 |
9 - 3 / 2 + 1 |
9 |
先截断: 3/2 = 1, 再 9-1+1 |
-5 - -3 |
-2 |
(-5) - (-3); 第二个 - 是一元负号 |
-5 + 6 * 4 % 5 |
-1 |
先 6*4%5 = 4, 再 -5+4 |
10 - 4 % 3 |
9 |
% 比 - 高 |
第 1、8 题: 优先级——% 高于 + -
第 2 题: 结合性——同级长链全部从左往右
第 3、4、5 题: 截断三连——负数向零、括号救不了、中间截断
第 6、7 题: 一元负号——-3 是一个数, 不是减号
拿不准就加括号, 或拆成多行——程序是给人读的
整数在内存里是二进制 (第 2 周看过 32 位图案); 位运算逐位操作, 把 int 当成 32 根小开关
| 运算符 | 含义 | 示例 (12 = 1100, 10 = 1010) |
结果 |
|---|---|---|---|
& |
按位与: 都是 1 才为 1 | 12 & 10 = 1000 |
8 |
| |
按位或: 有 1 就为 1 | 12 | 10 = 1110 |
14 |
^ |
按位异或: 不同才为 1 | 12 ^ 10 = 0110 |
6 |
~ |
按位取反: 0 变 1, 1 变 0 | ~12 (32 位) |
-13 (补码) |
手算方法: 两个数对齐写成二进制, 竖着一列一列算:
1100 (12)
& 1010 (10)
----
1000 (8)
~ 是唯一的一元位运算符: 32 位全翻转, 结果按补码解读是负数
& | 是一个符号, 别和两个符号的 && || (逻辑与或, 第 4 周) 搞混
<< 左移, >> 右移: 所有位整体挪动, 空出来的位置补 0
| 表达式 | 二进制过程 | 结果 | 规律 |
|---|---|---|---|
12 << 2 |
1100 → 110000 |
48 |
乘 4 (\(2^2\)) |
12 >> 2 |
1100 → 11 |
3 |
整除 4 (\(2^2\)) |
1 << 4 |
1 → 10000 |
16 |
第 4 位亮起 |
左移 \(n\) 位 = 乘 \(2^n\); 右移 \(n\) 位 = 整除 \(2^n\) (对正数; 负数右移是实现定义的, 别依赖)
用途举例: n & 1 判奇偶 (结果是 1 就是奇数); n << 1 是乘 2 的快捷写法
了解即可——知道"位运算在动二进制"就够了, 后面复合赋值会用到它们的缩写
+ - * / % 就这么多, 要开方、幂、对数、三角? 调用 C 的数学库函数: 先 #include <math.h>
| 函数 | 含义 |
|---|---|
abs(x) / fabs(x) |
整数 / 浮点数绝对值 |
sqrt(x) |
x 的平方根 |
pow(x, y) |
x 的 y 次方 |
exp(x) / log(x) |
e 的 x 次方 / 自然对数 |
log10(x) |
常用对数 |
sin(x) / cos(x) / tan(x) |
三角函数 (x 为弧度) |
返回值都是 double, 打印用 %f
下一页: 动手用一用
#include <stdio.h>
#include <math.h>
int main(void) {
printf("sqrt(9) = %f\n", sqrt(9.0));
printf("pow(2, 10) = %f\n", pow(2.0, 10));
printf("fabs(3 - 5.5) = %f\n", fabs(3 - 5.5));
printf("sqrt(2) = %.3f\n", sqrt(2));
printf("pow(2, 0.5) = %.3f\n", pow(2, 0.5)); /* 0.5 次方 = 开方 */
return 0;
}
运行结果:
sqrt(9) = 3.000000
pow(2, 10) = 1024.000000
fabs(3 - 5.5) = 2.500000
sqrt(2) = 1.414
pow(2, 0.5) = 1.414
函数调用是表达式: sqrt(9.0) 可以直接放进 printf 里算
pow(2, 0.5) 就是 \(\sqrt{2}\)——0.5 次方 = 开方
返回 double 用 %f 打印; 控制小数位用 %.3f
书背面条形码下方的 ISBN-13: 前 12 位数字, 末位是校验位 (识别抄写或录入错误)
计算校验位流程:
第1、3、5、7、9、11位 (奇数位) 数字求和
第2、4、6、8、10、12位 (偶数位) 数字求和
总数 = 奇数位和 + 3 × 偶数位和
校验位 = (10 - 总数 mod 10) mod 10
/* Computes an ISBN-13 check digit (e.g., the textbook 978-7-04-030276-9) */
#include <stdio.h>
int main(void)
{
int d1, d2, d3, d4, d5, d6, d7, d8, d9, d10, d11, d12;
int odd_sum, even_sum, total;
printf("Enter the first twelve digits of an ISBN: ");
scanf("%1d%1d%1d%1d%1d%1d%1d%1d%1d%1d%1d%1d",
&d1, &d2, &d3, &d4, &d5, &d6, &d7, &d8, &d9,
&d10, &d11, &d12);
odd_sum = d1 + d3 + d5 + d7 + d9 + d11;
even_sum = d2 + d4 + d6 + d8 + d10 + d12;
total = odd_sum + 3 * even_sum;
printf("Check digit: %d\n", (10 - total % 10) % 10);
return 0;
}
教材 ISBN 978-7-04-030276-9 的校验位计算示例:
奇数位和: \(9 + 8 + 0 + 0 + 0 + 7 = 24\)
偶数位和: \(7 + 7 + 4 + 3 + 2 + 6 = 29\)
总数: \(24 + 3 \times 29 = 111\)
\(111\) mod \(10 = 1\)
校验位: \((10 - 1)\) mod \(10 = 9\) ✓
课间试试: 翻到课本封底, 验算 ISBN-13
isbn.c
简单赋值: 把值存进变量 (=); 复合赋值: 更新已有变量的值 (+=)
v = e先评估表达式e的值, 然后将值复制到v中
e可以是常量、变量或更复杂的表达式:
i = 5; /* i is now 5 */
j = i; /* j is now 5 */
k = 10 * i + j; /* k is now 55 */
如果 v 和 e 的类型不同, 在赋值时 e 的值将转换为 v 的类型:
int i;
float f;
i = 72.99f; /* i is now 72 */
f = 136; /* f is now 136.0 */
在许多编程语言中, 赋值是一个语句; 而在 C 中, 赋值操作的结果类似运算符, 就像运算符 + 一样
赋值表达式 v = e 的值是赋值后 v 的值
表达式 i = 72.99f 的值是 \(72\), 而不是 \(72.99\)
i = j = 50 + 3;
由于 赋值 是一个运算符, 因此可以将多个赋值链接在一起:
i = j = k = 0;
= 运算符是右结合的, 所以这个赋值等价于
i = (j = (k = 0));
使用变量的旧值来计算其新值的赋值很常见
例如: i = i + 2;
使用 += 复合赋值运算符, 可改写为:
i += 2; /* same as i = i + 2; */
还有其他九个复合赋值运算符, 包括: -= *= /= %= 和位运算的复合形式(^= &= >>= <<= |=), 如 i &= j 等价于 i = i & j
| v += e | 将v加e, 将结果存储在v中 |
|---|---|
| v -= e | 将v减e, 将结果存储在v中 |
| v *= e | 将v乘以e, 将结果存储在v中 |
| v /= e | 将v除以e, 将结果存储在v中 |
| v %= e | 计算v除以e的余数, 将结果存储在v中 |
v += e与v = v + e可能不等价
一是运算符优先级:
i *= j + k 与 i = i * j + k 不等价
在极少数情况下, v += e 不同于 v = v + e 因为 v本身有副作用(如v为++i或i++等)
使用复合赋值运算符时, 不要交换构成复合运算符的两个字符
虽然i =+ j能编译通过, 但意义不同, 它相当于 i = (+j), 它只是将j的值复制到i中
对变量最常见的两种操作是 自增(加 1)和 自减(减 1)
可以使用复合赋值运算符进行自增和自减
C 特别提供了 ++ 自增 和 -– 自减 运算符
++ 运算符将操作数加1
-- 运算符将操作数减1
有前缀运算符(++i和–-i)或后缀运算符(i++和i--)
i = i + 1;
j = j - 1;
i += 1;
j -= 1;
i = 1;
printf("i is %d\n", ++i); /* prints "i is 2" */
printf("i is %d\n", i); /* prints "i is 2" */
i = 1;
printf("i is %d\n", i++); /* prints "i is 1" */
printf("i is %d\n", i); /* prints "i is 2" */
i = 1;
printf("i is %d\n", --i); /* prints "i is 0" */
printf("i is %d\n", i); /* prints "i is 0" */
i = 1;
printf("i is %d\n", i--); /* prints "i is 1" */
printf("i is %d\n", i); /* prints "i is 0" */
评估表达式++i (前缀自增) 的结果是i + 1, 副作用是i自增1:
评估表达式i++ (后缀自增) 的结果是i, 副作用是i随后自增1:
++i表示立即自增i, 而i++表示暂时使用i的旧值, 稍后再自增i
--运算符与++运算符相似:
i = 1;
j = 2;
k = ++i + j++;
i = i + 1;
k = i + j;
j = j + 1;
i = 1;
j = 2;
k = i++ + j++;
当++或--在同一个表达式中多次使用时, 结果通常很难理解, 如:
最后一条语句等价于:
i, j和k的最终值分别为 2, 3和4.
相反, 执行语句
i、j和k的值最终为2、3和3
多个++/--混用容易写错, 实际编程中尽量分行写
C 有一个不寻常的规则, 即任何表达式都可以用作语句
例如 ++i;: i首先递增, 然后获取i的新值, 然后丢弃
由于它的值被丢弃了, 除非表达式有副作用, 否则将表达式用作语句几乎没有意义
一个无意的编码错误很容易会创建一个 什么都不做 的表达式语句, 如 i = j; 误编写为 i + j;
++i;
i = 1; /* useful */
i--; /* useful */
i * j - 1; /* not useful */
1 / 2 的值是多少?7 % 2 呢?9 % 3 呢?
-i * -j 等价于什么?i * j / k 按什么顺序计算?
i += 2 等价于什么?i *= j + k 等价于 i = i * j + k 吗?
i = 1; j = i++; 执行后 i 和 j 分别是多少?
sqrt(16) 需要包含哪个头文件?