C语言学习笔记——运算符与表达式

C语言学习笔记——运算符与表达式
Kanwuqing数据类型那篇里,
我们让 5/2 得出了 0.000000 这个莫名其妙的结果,
当时说”格式符和类型不匹配”. 但还有个更基础的问题, 考虑如下代码:
1 |
|
上面这段代码里, c 是 double,
printf 也用 %f 输出, 按理说该打印
2.5. 可实际结果是 2.000000 —— 问题出在
5/2 这一步, 它根本没算出 2.5
这就轮到运算符了
1 C 的运算符
打开任何一本教材的运算符表, 密密麻麻几十行, 第一反应是这牛魔要背到什么时候. 其实它们分成几类, 而且大部分早就会用, 只是不知道名字:
| 类别 | 运算符 | 干什么 |
|---|---|---|
| 算术 | + - * / % |
加减乘除取余 |
| 关系 | > < >= <= == != |
比大小, 结果是”真/假” |
| 逻辑 | && \|\| ! |
把几个条件连起来 |
| 赋值 | = 以及 += -= *= /= 等 |
往变量里放值 |
| 自增自减 | ++ -- |
加一、减一 |
| 逗号 | , |
把几件事串成一串 |
| 条件 | ?: |
二选一 |
| 位运算 | & \| ^ ~ << >> |
直接操作二进制位 |
| 其他 | sizeof () [] .
-> & * |
取大小、取地址等 |
这一篇只讲前面七类, 位运算留到
预处理与位运算 那篇, 因为它需要先熟练运用二进制
*注意,
sizeof是运算符, 不是函数. 原因是它可以不加括号 ——sizeof x合法, 而函数调用f x一定报错. 并且它不需要#include任何头文件. 这也是为什么 C89 的 32 个关键字里没有sizeof(它是运算符, 关键字是另一回事) 却又能当关键字用的原因一般而言, 在Dev-C++里以粗体显示的都是关键字
2 算术运算符
+ - * 三个没什么好说的, 和小学学的一样. 但
/ 不一样:
1 | printf("%d\n", 5 / 2); // 2, 不是 2.5 |
两个整数相除, 结果一定是整数, 无论是否用实型存读, 小数部分会被直接扔掉. 这就是整数除法. 想要小数, 就得让其中一个变成小数:
1 | 5 / 2 // 2 两个都是 int |
这就是数据类型那篇里
%f打出0.000000的根源: 先按整数除法得 2, 再被当成浮点数读出来
“扔掉”的具体规则是朝 0 截断, 即朝着绝对值减小的方向截断, 用人话说就是闭着眼睛抹掉小数点后面的部分
1 | printf("%d\n", 7 / 2); // 3 |
-3.5 朝零截断得 -3. 这一点常考
2.1 % 取余
% 求的是余数, 只能用于整数:
1 | printf("%d\n", 7 % 3); // 1 |
写成 7.0 % 3 会直接编译不过:
1 | error: invalid operands to binary % (have 'double' and 'int') |
负数的 % 是另一个坑. 规则是:
结果的符号跟”被除数”一致:
1 | printf("%d\n", -5 % 3); // -2 |
注意第二行, 被除数是 5(正), 所以结果是正的 2 —— 哪怕除数是负数. 这条规则背下来就行
%在考试里最常见的用途有两个: 判断整除(n % 2 == 0)和取出各位数字(n % 10得个位, 再来一次得百位)
3 关系运算符: 结果是个整数
> < >= <= == != 用来比较,
但它们的结果不是”真/假”两个字, 而是整数 1 和
0:
1 | printf("%d\n", 3 > 2); // 1 |
在 C 里, 0 表示假, 非 0 表示真. 关系运算只会产出 1 或 0, 但判断的时候”任何非 0 值”都算真
举个能体现这一点的例子:
1 | if (5) printf("真"); // 5 是非0, 所以为真, 会打印 |
3.1 两个经典错误
第一个: 把 == 写成 =
1 | if (a == 5) // 判断 a 是不是 5 |
= 是赋值, == 才是比较.
写错了编译器多半不报错(只可能有 warning), 但逻辑全错
第二个: 连写比较
数学上可以写
1 | printf("%d\n", 1 < 2 < 3); // 1 |
看第二行: 3 > 2 先算出 1, 然后变成
1 > 1, 结果是 0. 明明 3 既大于 2 也大于 1,
结果却是假
正确写法必须用逻辑运算连起来:
1 | if (1 < x && x < 10) // 对 |
4 逻辑运算符与短路求值(重点)
&&(与)、||(或)、!(非).
三个都要注意
4.1 结果也是 1 和 0
1 | printf("%d\n", 3 && 5); // 1, 不是 5! |
! 有个有意思的性质: 双重否定还原成 1,
不是原值:
1 | printf("%d\n", !!5); // 1, 不是 5 |
4.2 短路求值
这是重点, 也是考试爱考的地方. &&
左边的值如果是假, 右边根本不执行; || 左边如果是真,
右边也不执行.
看个例子:
1 | int i = 0; |
i 还是 0 —— 因为
0 && ... 左边已经是假, 整个式子必然为假,
右边那个 i = 99 压根没被执行
同理:
1 | int i = 0; |
左边已经是真, 右边不执行
这个特性的实际价值在于: 它让
if (p != NULL && *p > 0)这种写法变得安全 —— 如果p是空指针, 左边为假, 右边*p就不会被求值, 不会崩溃. 短路求值是在”有意利用”的, 不是巧合, 指针相关的会在后续章节中具体展开这类题几乎必考, 问法一般是”执行后
i的值是多少”, 或者”printf会不会执行”. 判断方法就是从左往右看, 一旦能定下整体真假, 后面的全部跳过
5 赋值运算符
= 是赋值. 有个性质容易忽略:
赋值表达式本身也有值, 值就是赋进去的那个东西:
1 | int m, n; |
所以 m = n = 5 能工作, 它是 m = (n = 5),
右侧先算
5.1 复合赋值
+= -= *= /= %= 这五个是简写:
| 简写 | 等价于 |
|---|---|
a += 5 |
a = a + 5 |
a -= 5 |
a = a - 5 |
a *= 5 |
a = a * 5 |
a /= 5 |
a = a / 5 |
a %= 5 |
a = a % 5 |
还有个容易搞混的地方: a /= 5 是否丢小数取决于
a 的类型:
1 | int b = 7; b /= 2; // b 变成 3, 整数除法丢小数 |
复合赋值不改变类型, 只是简写. 该丢的还是丢
注意: 如果
=漏掉写成 m + 3 不会报错, 但是由于这仅仅是个算式, 没有任何赋值操作, 导致变量的值不会改变
6 自增自减: 坑最多的一节
++ 是加一, -- 是减一.
单独用时颠过来倒过去随便用, 均会改变变量的值:
1 | c++; // c 加 1 |
但一旦混进表达式里, 前后缀的区别就出来了:
1 | int a = 5; |
1 | int a = 5; |
- 后缀
a++, 加在后: 先用再加. 表达式取的是”加之前”的值 - 前缀
++a, 加在前: 先加再用. 表达式取的是”加之后”的值
两者结束时 a 都是 6,
区别只在”表达式的值”上
6.1 未定式
tmd高数还在追我
下面这种写法结果是不确定的, 别记答案:
1 | int i = 5; |
看起来像 5 + 6 = 11,
但语言标准根本没规定先算哪一边.
换个编译器、换个优化等级, 结果就可能不一样.
这在标准里叫未定义行为(undefined behavior)
原因: C 标准只规定”表达式整体的值”, 不规定”各部分的求值顺序”. 一个变量在同一条表达式里被改两次, 就是踩了这条规则的线
类似的还有
f(i++, i++),a[i] = i++等. 考试基本不考这种题, 因为它没有唯一答案; 真考了, 那就是出题人自己的问题真考到就按直觉来吧, 毕竟出题人估计也没动啥脑子
7 逗号运算符
逗号能把好几个表达式串成一串, 整个式子的值等于最后一个表达式的值:
1 | int c = (1, 2, 3); |
前面两个算了但白算, 只取最后那个
它能做点有用的事, 比如在一个表达式里干几件事:
1 | int x, y; |
注意别和函数参数里的逗号搞混:
printf("%d", a, b)里的逗号是参数分隔符, 不是逗号运算符. 只有当逗号出现在”表达式”的位置上, 它才是运算符逗号运算符的优先级是最低的, 比赋值还低. 所以
int c = 1, 2, 3;和你想的完全不同 —— 它等价于(int c = 1), 2, 3;,c就是 1. 想要 3 必须加括号
8 条件运算符(三目)
?: 是 C 里唯一的三目运算符, 形式是:
1 | 条件 ? 成立时的值 : 不成立时的值 |
记法: 条件(成立吗)?成立就直接上:不成立再说
用法直白:
1 | printf("%d\n", 3 > 2 ? 100 : 200); // 100 |
最经典的用法是求最大值:
1 | int m = 5, n = 9; |
有一个隐藏特点: 两个分支的类型不同时, 会发生类型提升:
1 | printf("%f\n", 1 ? 2 : 3.5); // 2.000000 |
2 本来是 int, 但因为另一个分支是
double(3.5), 整个表达式的类型被提升成 double.
所以 sizeof 得 8 而不是 4
*9 位运算
位运算直接操作二进制位, 和前面那些”算数值”的运算符不是一个思路. 它本身属于[预处理与位运算]那一篇, 但优先级表里绕不开它, 所以先认识一下
只要知道每个位运算符怎么算即可, 具体应用场景不涉及考查
一共 6 个: & | ^
~ << >>
& 按位与: 两位都是 1 才得 1
1 | 6 = 00000110 |
| 按位或: 有一位是 1 就得 1
1 | 6 = 00000110 |
^ 按位异或:
两位不同才得 1
1 | 6 = 00000110 |
~ 按位取反: 0 变 1, 1 变 0.
注意它作用在补码上, 结果是负数
1 | printf("%d\n", ~6); // -7 |
<< 左移、>>
右移: 整体挪动若干位, 若空出位置则用0补全
1 | printf("%d\n", 1 << 3); // 8, 相当于 1 * 2^3 |
实用场景. 位运算在考试和实际代码里主要有这么几种用法:
1 | n & 1 // 判断奇偶: 得 0 是偶数, 得 1 是奇数 |
异或还能不借助临时变量交换两个数:
1 | int p = 5, q = 9; |
两个坑: 1.
~6等于 -7 不是 249 —— 因为int是有符号的, 取反后按补码解释就是负数. 想得到无符号的结果要用unsigned2. 右移负数的结果由实现决定 ——-8 >> 1在我的编译器上得 -4, 但标准没规定补位补 0 还是补 1. 别写这种代码
10 优先级与结合性
到这儿所有常用运算符都认识了, 现在才能谈优先级, 否则一上来就背表没有意义
优先级: 谁先算; 结合性: 优先级相同时从左还是从右算
先看优先级(从高到低). ★ 是考试常考的, 其余了解即可:
| 级别 | 运算符 | 说明 | 重要度 |
|---|---|---|---|
| 1 | () [] ->
. |
括号、下标、成员 | ★ |
| 2 | ! ~ ++ --
+(正) -(负) *(解引用)
&(取址) sizeof (类型) |
单目运算 | ★ |
| 3 | * / % |
乘除余 | ★ |
| 4 | + - |
加减 | ★ |
| 5 | << >> |
移位 | ★ |
| 6 | < <= > >= |
关系 | ★ |
| 7 | == != |
相等性 | ★ |
| 8 | & |
按位与 | |
| 9 | ^ |
按位异或 | |
| 10 | \| |
按位或 | |
| 11 | && |
逻辑与 | ★ |
| 12 | \|\| |
逻辑或 | ★ |
| 13 | ?: |
三目 | ★ |
| 14 | = 及复合赋值 |
赋值 | ★ |
| 15 | , |
逗号 | ★ |
结合性只有两个方向:
- 从右到左: 单目运算符(级别2)、三目(13)、赋值(14)
- 从左到右: 其余全部
单目运算符的”右结合”是什么意思
这里最容易绕晕, 因为单目运算符只有一个操作数, 那”左右”从何谈起?
关键前提:
结合性只在”同一个运算符连着出现好几次”时才有意义. 比如
10 - 3 - 2 里有两个 -, 先算哪个就影响结果,
这才需要规则
单目运算符天生不存在这种歧义. -a
只有一个操作数 a, 没得选. 就算连着写, 比如
!!a, 也只能是 !(!a) —— 因为
(!a)! 根本不是合法的 C 语法. 所以标准里给单目标”从右到左”,
其实是因为只需要从右往左逐层剥开,
并没有真正的”选择”
拿 - -a 看(中间加空格, 免得被当成 --):
1 | printf("%d\n", - -a); // a=5 时得 5 |
解析成 -(-a): 先算最里层的
-a, 再套外层的负号. 这就是”从右到左”的含义 ——
从右往左一层层套, 而不是”右边的先算完”
所以你别被下面这种例子误导:
1 printf("%d\n", -a - b); // -8, a=5 b=3这里的两个减号根本不是同一个运算符! 第一个是单目负号(级别 2), 第二个是减法(级别 4). 它们优先级不同, 按优先级就定死了是
(-a) - b, 压根轮不到结合性出场你看到”像是左结合”, 其实那是优先级在起作用. 结合性只解决”同级运算符连着出现”的问题
对比一下双目的 -(级别 4), 那个才有真正的结合性:
1 | printf("%d\n", 10 - 3 - 2); // 5, 不是 9 |
两个 - 同级, 这时才靠结合性决定: 左结合, 所以是
(10-3)-2 = 5. 要是右结合就成了
10-(3-2) = 9
赋值是从右到左的, 所以 m = n = 5 才能work:
1 | m = n = 5 // 右结合 -> m = (n = 5) |
如果是左结合, 会变成 (m = n) = 5, 也就是”给
m=n 这个结果赋值”, 那是非法的
一句话总结: 优先级管”不同级谁先算”, 结合性管”同级连着写谁先算”, 单目的”右结合”只是”从右往左剥”的描述
10.1 几个容易记错的地方
1.&& 比 ||
优先级高
1 | printf("%d\n", 1 || 0 && 0); // 1 |
先算 0 && 0 得 0, 再算 1 || 0 得
1
2.! 优先级很高, 别漏括号
! 是级别 2, 比所有双目运算符都高. 拿 a = 6
试:
1 | printf("%d\n", !(a & 1)); // 1 先算 a&1 得 0, 再取非得 1 |
同一个式子的两行结果完全不同 —— 加不加括号, 算的是两码事
顺便注意,
!(3 > 2)和!3 > 2这两个碰巧都是 0(前者!1=0, 后者0>2=0), 用它俩举例是看不出差别的. 所以别拿它记规则, 老老实实加括号最省事
*3.位运算的优先级低于关系运算
== 是级别 7, & 是级别 8, 所以
& 反而后算:
1 | if (a & 1 == 0) // 陷阱! 先算 1 == 0 得 0, 整个变成 a & 0, 恒为假 |
拿 a = 6 看: 前者先算 1 == 0 得 0, 再算
6 & 0 得 0(假), 于是永远进不去 if;
后者算 6 & 1 得 0, 再算 0 == 0 得 1(真),
正确判定为偶数
*4.优先级和求值顺序是两件事
优先级决定”谁跟谁结合”, 但不决定”谁先算完”. f() + g()
里哪个先执行是未指定的. 这个区别在考试里偶尔以概念题出现
*11 表达式的类型与隐式转换
一个表达式算出来是什么类型, 由参与运算的类型决定:
1 | 1 / 2 // int -> 0 |
规则是向”精度高”的方向靠: int 和
double 混算, 结果一定是 double.
这叫类型提升
常见的高低顺序:
1 | char -> short -> int -> unsigned -> long -> float -> double -> long double |
其实本质上就两条: 纯整数运算就是整数运算, 该丢小数就丢; 一旦混入浮点, 结果就是浮点
12 一个完整的例子: 简易计算器
把这篇的东西串起来:
写一个能算加减乘除和取余的计算器. 要求:
- 一次读入一个算式, 形如
8*3, 也就是两个整数中间夹一个运算符 - 支持
+-*/%五种 - 除法要处理三件事: 除数为 0 时报错; 能整除就输出整数; 不能整除要输出小数(不是截断成整数)
- 遇到不认识的运算符要提示
示例输入输出见下
示例代码
1 |
|
这个程序里用到了本篇的不少东西:
%判断能否整除(a % b == 0), 决定输出整数还是小数(double)a / b强转, 避免整数除法丢小数&&虽然没直接出现, 但else if的链式判断是同一个思路op == '+'用的是==不是=%%是printf里输出百分号的办法
注意
scanf("%d%c%d", &a, &op, &b)这个写法.%c会读入任何字符(包括空格), 所以输入必须是8*3这样紧挨着的. 如果写成8 * 3,%c读到的是空格而不是*, 就会走进最后的else. 这是%c的老毛病, 想跳过空白得写" %c"(前面加个空格)
优先级那张表不用背, 但加减乘除、关系、逻辑、赋值的相对高低要有印象: 算术 > 关系 > 逻辑 > 赋值 > 逗号(这条链也不用背, 多看看有感觉就行). 实在记不清就加括号
