目录
1.二进制
我们常见的二进制、八进制、十进制、十六进制就是对数值的不同表示形式。比如15在这四个进制中的表达形式:
15的二进制 1111
15的八进制 17
15的十进制 15
15的十六进制 F
首先来看一下我们最熟悉的十进制,对于十进制我们有很多常 识:
- 十进制中满十则进一,低位变成0
- 十进制中的各位数字都是由0-9组成的
那么二进制也满足相同的特点:
- 二进制满二进一,低位变成0
- 二进制中的各位数字都是由0-1组成的
1.1 二进制转十进制
在十进制中,123表示的是一百二十三。为什么会是这样的值呢?这是在十进制中的每一位都是有权重的,十进制从右到左对应个位、十位、百位……,分别对应的权重是10^0、10^1、10^2…… 求出的值为十进制数字,如下图所示:
二进制和十进制类似的,以15的二进制:1111 为例,求出的值为十进制数字,如下图所示:
1.1.1 10进制转二进制
十进制转二进制的计算方式是:用2一直除十进制数字求余数,直到十进制数字为0,然后倒取余数,就是二进制数字了。
还是十进制数字15为例:
1.2 二进制转八进制
八进制的夜特点:
- 八进制满八进一,低位变成0
- 八进制的各位数字都是由0-7组成的
二进制转八进制。从右到左,每3个二进制位转换为一个八进制位,不够三个的二进制可以直接换算。
例如,将二进制的1111,转换为八进制就是:017。0开头的数字会被当做八进制。
1.3 二进制转十六进制
十六进制的特点:
- 十六进制满十六进一,低位变成0
- 十六进制的各位数字都是由0-9和a-f组成的。a-f表示的是10-15
二进制转十六进制,从右到左,每4个二进制位转换为一个十六进制,不够4个二进制位的直接换算。
例如,将二进制的1101011,转换为十六进制就是:0x6b。十六进制表示的时候前面会加0x。
2.原码、反码、补码
整数的二进制有三种表示方法:原码、反码和补码。这里只有整数,不涉及小数
三种表示方式均有符号位和数值位两部分。在二进制序列中,最高位被当做符号位,剩余的都是数值位。符号位用0表示“正”,用1表示负”。
正整数的原、反、补码都相同。无符号整数可以按照正整数理解。
负数的原、反、补码都各不相同。如下:
- 原码:直接将数值位按照正负数的形式翻译成二进制,得到的就是原码。
- 反码:将原码的符号位不变,数值位依次按位取反,得到的就是反码。
- 补码:反码+1,就得到补码
对于整型来说:数据存放内存中其实存放的是补码。使用补码,可以将符号位和数值域统一处理。
3.移位操作符
移位操作符的操作数只能是整数。
3.1左移操作符
移位规则:左边抛弃,右边补0。
例如,二进制1111向左移一位。执行了移位操作符之后,num的值不会变。
3.2右移操作符
右移操作符运算分为两种:
- 逻辑右移:左边用0填充,右边丢弃
- 算数右移:左边用原该值的符号位填充,右边丢弃。
VS2022使用的算数右移。以15的二进制1111,执行了右移操作符之后,num的值不会变。
4.位操作符:&、|、^、~
位操作符有:
& //按位与 对应的二进制位,有0则为0,两个同时为1才为1
| //按位或 对应的二进制位上有1则为1.两个同时为0才为0
^ //按位异或 对应的二进制上相同则为0,相异则为1
~ //按位取反 符号位和数值位都按位取反(二进制的0变为1,二进制的1变为0)
注:它们的操作数必须是整数。
举例,num1=-3,num2=5,求它们的按位与、按位或、按位异或、取反
按位异或是具有交换律的,并且0和任何数进行异或,都是这个数本身。如下所示:
5.逗号表达式
表达式1,表达式2,表达式3……表达式N
逗号表达式,就是用逗号隔开的多个表达式。从左向右依次执行,整个表达式的结果是最后一个表达式的结果,并且这些表达式也能够改变其他变量的值。
6.下标方位[]、函数调用()
6.1 []下标引用操作符
操作数:一个数组名+一个索引值
int arr[10]; //创建数组
arr[9]=10; //使用下标引用操作符
[ ]的两个操作数是arr和10
6.2 函数调用操作符
接受一个或者多个操作数:第一个操作数是函数名。剩余的操作数就是传递个函数的参数。
用来计算数组元素个数的 sizeof 不是函数,是操作符。它的()可以省略,省略之后得到的结果,和加上()的结果一致。
7.操作符的属性:优先级、结合性
C语言的操作符有两个重要的属性:优先级、结合性。这两个属性决定了表达式求值的计算顺序。
7.1 优先级
优先级指的是,如果一个表达式包含多个运算符,哪个运算符应该优先执行。各种运算符的优先级是不一样的。
3 + 4 * 5;
以表达式 3+4*5 为例,里面既有加法运算符 + ,又有乘法运算符 * 。由于乘法的优先级高于加法,所以先计算 4*5。
( 3 + 4 ) * 5;
但是在这个表达式中,会先计算 3+4 ,这是因为 () 的优先级高于乘法运算符 * 。
7.2 结合性
如果运算符的优先级相同,就不能确定先计算哪个了。这是胡就需要看结合性了,则根据运算符是左结合(从左向右执行),还是右结合(从右向左执行),决定执行顺序。
5 * 6 /2
以这个表达式为例,* 和 / 的优先级相同,它们都是左结合运算符,所以先计算 5 * 6 ,再计算 /2。
详细操作符的优先级和结合性,请看下表:
参考网址:https://zh.cppreference.com/w/c/language/operator_precedence
8.表达式求值
8.1整型提升
C语言中整形算数运算总是至少缺省整形类型的精度来进行的。
为了获得这歌精度,在表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换被称为整型提升。
如何进行整型提升?
- 有符号整型提升按照变量的数据类型的符号位来提升的
- 无符号整型提升,高位补0.
下面来演示一下有符号的整型提升
//负数的整型提升
char c1= -1;
变量c的二进制(补码)中只有八个比特为:11111111
因为 char 为有符号的 char
所以整型提升的时候,高位补充符号位,即为1
提升之后:11111111111111111111111111111111
//正数的整型提升
char c1= 1;
变量c的二进制(补码)中只有八个比特为:00000001
因为 char 为有符号的 char
所以整型提升的时候,高位补充符号位,即为0
提升之后:00000000000000000000000000000001
8.2 算数转换
如果某个操作符的各个操作数属于不同的类型,那么除非一个操作数的转换为另一个操作数的类型,否则操作就无法进行。
下面的层次体系就被称为寻常算术转换。如果某个操作数的类型在上面这个列表中排名靠后,那么首先要转换为另外一个操作数的类型后执行运算。
long double
double
float
unsigned long int
long int
unsigned int
int
8.3 问题表达式解析
表达式1:
c + --c;
在这个表达式中,操作符的优先级只能决定 -- 的运算在 + 的前面,但是我们没有办法得知,+ 操作符的的左操作符的获取,是在右操作数的之前还是之后求值,是有歧义的。
表达式2
int fun()
{
static int count = 1;
return ++count;
}
int main()
{
int answer;
answer = fun() - fun() * fun();
printf("%d",answer);
return 0;
}
这个代码看着没有什么问题,也能够求出结果。
在上述代码中我们只能通过优先级得知先算乘法,再算减法。但是,函数的调用先后顺序无法通过操作符的优先级确定。
表达式3
int main()
{
int i = 1;
int ret = (++i) + (++i) + (++i);
printf("%d",ret);
printf("%d", i);
return 0;
}
在这段代码中,在执行第一个 + 的时候,第三个 ++ 是否执行,这个不确定。因为依靠操作符的优先级和结合性无法确定第一个 + 和第三个 ++ 的先后顺序。
8.4 总结
即便有了操作符的优先级和结合性,我们写出的表达式依然有可能不能通过操作符的属性确定唯一的计算路径。那这个表达式就是存在潜在风险的,要避免写这种表达式。