12.表达式求值
1. 表达式求值的顺序一部分是由操作符的优先级和结合性决定。
2. 有些表达式的操作数在求值的过程中可能需要转换为其它类型。
12.1 : 隐式类型转换(整型提升)
1. C语言的整型算术运算总是至少以缺省(默认)整型类型的精度来进行的。
2. 为了获得这个精度,表达式中的字符(char)和短整型(short)操作数在使用之前被转换为普通整型(char为1字节,int为4字节),这种转换称为整型提升,这里的转换只是计算时临时转换一下,变量本身不会转换类型。
整型提升的意义:
表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器(register)的长度
因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。
通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令中可能由这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整形值,都必须先转换为 int 或 unsigned int ,然后才能送入CPU去执行运算。
整型提升是按照变量的数据类型的符号位来提升的
(一个有符号的数字,在二进制序列里面就把最高位规定为符号位)
//整型提升 #include <stdio.h> int main() { // 存放时截断,使用时整型提升 char c1 = 5; //00000000000000000000000000000101 -- 整型5的32为bit位(四个字节:32位) //00000101 -- char类型5的8为bit位(一个字节:8位)(截断) //00000101 -- c1 char c2 = 127; //00000000000000000000000001111111 - 整型 //01111111 -- c2 char c3 = c1 + c2; //计算时要整型提升:整型提升是按照变量的数据类型的符号位来提升的 //char类型是有符号的char,此时高位就是它的符号位,为 0,所以 补 0 到 32位, // //00000101 --》00000000000000000000000000000101 // 相加 //01111111 --》00000000000000000000000001111111 // 从 char 变成 int //得 00000000000000000000000010000100 -- 32位 // // 要把32位放进8位得char中,再进行截断 //00000000000000000000000010000100 --》10000100 //所以 c3 = 10000100 // 这时打印 c3 ,会打印 -124 //原因: // %d - 以10进制的形式打印有符号的整数 // 因为 c3 是 char 类型的,不是 %d 要的整型,所以这里也要进行整型提升 // c3 = 10000100 ,char类型是有符号的char,此时高位就是它的符号位,为 1 // 所以 补 1 到 32位, // (无符号整形提升,直接高位补0) // 10000100 --》 // 11111111111111111111111110000100 -- 补码 // 11111111111111111111111110000011 -- 反码(补码 - 1) // 10000000000000000000000001111100 -- 原码(反码按位取反) // 得 打印时得 -124 printf("%d\n", c3); return 0; }
(一定程度上)证明 整形提升 的存在
(有符号数字和无符号数字:
10010100,如果这是有符号数字,最高位就是符号位,不是有效数字
如果这是无符号数字,最高位就是有效数字,表示 2的7次方)
(截断:
以char类型为例,截断会直接获取二进制序列最低的8位,即一个字节,其它位全部丢弃)
//证明 整形提升: #include <stdio.h> //%d - 以10进制的形式打印有符号的整数 //%u - 以10进制的形式打印无符号的整数 int main() { char c = 1; printf("%u\n", sizeof(c)); printf("%u\n", sizeof(+c)); printf("%u\n", sizeof(-c)); return 0; }
12.2 :算术转换
1. 如果某个操作符的各个操作数属于不同的类型(>=4个字节),那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换(向上转换),这里的转换只是计算时临时转换一下,变量本身不会转换类型。2. 如果某个操作数的类型在下面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运算。
3. 警告: 算术转换要合理,要不然会有一些潜在的问题,如精度丢失。
12.3 :操作符的属性
复杂表达式的求值有三个影响的因素:
1. 操作符的优先级
2. 操作符的结合性
3. 是否控制求值顺序
两个相邻的操作符先执行哪个?取决于他们的优先级。如果两者的优先级相同,取决于他们的结合性。
操作符优先级
(图中从高到下,优先级从高到低)
操作符 |
描述 |
用法示例 |
结果类型 |
结合性 |
是否控制求值顺序 |
() |
聚组 |
(表达式) |
与表达式同 |
N/A |
否 |
() |
函数调用 |
rexp(rexp,...,rexp) |
rexp |
L-R |
否 |
[ ] |
下标引用 |
rexp[rexp] |
lexp |
L-R |
否 |
. |
访问结构成员 |
lexp.member_name |
lexp |
L-R |
否 |
-> |
访问结构指针成员 |
rexp->member_name |
lexp |
L-R |
否 |
++ |
后缀自增 |
lexp ++ |
rexp |
L-R |
否 |
-- |
后缀自减 |
lexp -- |
rexp |
L-R |
否 |
! |
逻辑反 |
! rexp |
rexp |
R-L |
否 |
~ |
按位取反 |
~ rexp |
rexp |
R-L |
否 |
+ |
单目,表示正值 |
+ rexp |
rexp |
R-L |
否 |
- |
单目,表示负值 |
- rexp |
rexp |
R-L |
否 |
++ |
前缀自增 |
++ lexp |
rexp |
R-L |
否 |
-- |
前缀自减 |
-- lexp |
rexp |
R-L |
否 |
* |
间接访问 |
* rexp |
lexp |
R-L |
否 |
& |
取地址 |
& lexp |
rexp |
R-L |
否 |
sizeof |
取其长度,以字节表示 |
sizeof rexp sizeof(类 型) |
rexp |
R-L |
否 |
(类型) |
类型转换 |
(类型) rexp |
rexp |
R-L |
否 |
*
乘法
rexp * rexp
rexp
L-R
否
/
除法
rexp / rexp
rexp
L-R
否
%
整数取余
rexp % rexp
rexp
L-R
否
+
加法
rexp + rexp
rexp
L-R
否
-
减法
rexp - rexp
rexp
L-R
否
<<
左移位
rexp << rexp
rexp
L-R
否
>>
右移位
rexp >> rexp
rexp
L-R
否
>
大于
rexp > rexp
rexp
L-R
否
>=
大于等于
rexp >= rexp
rexp
L-R
否
<
小于
rexp < rexp
rexp
L-R
否
<=
小于等于
rexp <= rexp
rexp
L-R
否
==
等于
rexp == rexp
rexp
L-R
否
!=
不等于
rexp != rexp
rexp
L-R
否
&
位与
rexp & rexp
rexp
L-R
否
^
位异或
rexp ^ rexp
rexp
L-R
否
|
位或
rexp | rexp
rexp
L-R
否
&&
逻辑与
rexp && rexp
rexp
L-R
是
||
逻辑或
rexp || rexp
rexp
L-R
是
? :
条件操作符
rexp ? rexp : rexp
rexp
N/A
是
=
赋值
lexp = rexp
rexp
R-L
否
+=
以...加
lexp += rexp
rexp
R-L
否
-=
以...减
lexp -= rexp
rexp
R-L
否
*=
以...乘
lexp *= rexp
rexp
R-L
否
/=
以...除
lexp /= rexp
rexp
R-L
否
%=
以...取模
lexp %= rexp
rexp
R-L
否
<<=
以...左移
lexp <<= rexp
rexp
R-L
否
>>=
以...右移
lexp >>= rexp
rexp
R-L
否
&=
以...与
lexp &= rexp
rexp
R-L
否
^=
以...异或
lexp ^= rexp
rexp
R-L
否
|=
以...或
lexp |= rexp
rexp
R-L
否
,
逗号
rexp,rexp
rexp
L-R
是
(运用这些操作符时,要尽量写得通俗易懂,可以使用括号来确定计算顺序,形成唯一路径,避免写成垃圾代码,不同编译器会有不同结果)
查看汇编语言观察垃圾代码的运行
//垃圾代码: #include <stdio.h> int main() { int i = 1; int ret = (++i) + (++i) + (++i); printf("%d\n", ret); printf("%d\n", i); return 0; }
(可见VS的运行顺序是:先算三次自增,再进行两次相加,其它编译器就不一定了)
总结:
我们写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题的
练习:
补充:sizeof 和 strlen 的对比
1. sizeof 是 操作符 ;strlen 是 库函数
2. sizeof 计算的是占用内存的大小,单位是字节,不关注内存中存放的具体数据
3. strlen 是求字符串长度的,只能针对字符串,会寻找字符串中的 \0,统计的是字符串中 \0 之前出现的字符的个数
使用函数完成整型函数的打印、元素逆置、初始化
//创建一个整形数组,完成对数组的操作 // //实现函数init() 初始化数组为全0 //实现print() 打印数组的每个元素 //实现reverse() 函数完成数组元素的逆置。 //要求:自己设计以上函数的参数,返回值。 #include <stdio.h> //实现print() 打印数组的每个元素 void print(int* arr, int sz) { //输出: int j = 0; for (j = 0; j < sz; j++) { printf("%d ", arr[j]); } //换行: printf("\n"); } //实现reverse() 函数完成数组元素的逆置。 void reverse(int* arr, int sz) { int i = 0; for (i = 0; i < (sz / 2); i++) //循环条件??? //6个元素调3次,5个元素掉两次 //sz/2 肯定够用 { int* left = arr + i; int* right = arr + sz - 1 - i; // 使用指针移位,类似下标,易混 // 让左右指针慢慢往中间靠 int tmp = *left; *left = *right; *right = tmp; // 获取指针值是用 * } } //实现函数init() 初始化数组为全0 void init(int* arr, int sz) { //arr:首元素地址 ; sz为几就循环几次 int i = 0; for (i = 0; i < sz; i++) { int* tmp = arr + i; //创建指针变量存放指针 *tmp = 0; //取指针中的值,并赋为0 } } int main() { int arr[5] = { 1,2,3,4,5 }; int sz = sizeof(arr) / sizeof(arr[0]); //元素个数 //实现print() 打印数组的每个元素 print(arr, sz); //实现reverse() 函数完成数组元素的逆置。 reverse(arr, sz); print(arr, sz); //实现函数init() 初始化数组为全0 init(arr, sz); print(arr, sz); return 0; }