前言:我个人认为,C语言的两个比较有特色的点,(1)在于存在指针。(2)可以执行位运算
因为这两种特色,c能够对底层数据,作出修改和使用,这也是人们又把C称为底层语言的原因
今天主要讲讲C中的位运算
位运算的符号(&,|,^,~,<<,>>)
位运算符 | 解释说明 | 优先级(按大小分为4个等级) | 结合方向 | 运算规则说明 |
&(双目) | 按位与 | 3 | 置左向右 | 全1为1,有0为0 |
|(双目) | 按位或 | 5 | 置左向右 | 有1为1,全0为0 |
^(双目) | 按位异或 | 4 | 置左向右 | 相异则1,相同则0 |
~(单目) | 按位取反 | 1 | 置右向左 | 1变0,0变1 |
<< | 左移 | 2 | 置左向右 | 数据左移 |
>> | 右移 | 2 | 置左向右 | 数据右移 |
先看按位或运算 |
unsigned char aa=10;
char bb = -10;
char
cc=aa|bb;
printf("看看cc的值是多少%d\n",cc);
先理论计算一下 cc的值 。unsigned char aa 的二进制值; 0000 1010
|(按位或)
char bb的二进制值; 1000 1010
char cc 最终得到的值为; 1000 1010 =-10(10进制)
看看输出结果:
啥??为什么是-2,这又引出另外一个问题,负整数在内存中都是以补码的形式存储的。
要弄清楚补码,就得先知道反码
如 bb
原码(注意不是源码,而是原码,即按二进制+最高位的符号位) | 反码 | 反码规则 | 补码 | 补码规则 | |
1000 1010 (bb即-10) | 1111 0101 | 反码(即符号位不动,其他数据Bit,按位取反) | 1111 0110 | 在反码的基础上,加上1 | |
0000 1010 (aa即10) | 0000 1010 | 0000 1010 | |||
aa|bb的结果 | 1111 1110 | ||||
-2(1000 0010) | 1111 1101 | 1111 1110 |
因为表格有点问题:故最后三行,不能将补码,写在合适的位置。注意看最后一行-2的补码,和aa|bb的结果。于是问题就解决了。
总结的知识点
1)aa|bb的结果可以断定,按位或运算时,如果参与运算的数有有符号数。则符号位也要参与运算。也就是说,所有bit位都参与按位或运算
2)我查看了,部分资料描述道,正整数的补码和反码都是原码。感觉很啰嗦,而且这样记的话,也很容易出错。我们只需记住,正整数是是按原码存储在内存中的。
3)我们研究下,aa|bb的结果是如何被程序判定为-2的。cc=aa|bb=1111 1110,char cc声明时被定义为有符号数。故此时程序便去查看最高bit位 =1。于是判定为负数,1111 1110就是该数字的补码 。我们将这个补码转换为原码
首先符号位不动 ,数据位-1 =(1111 1110 -0000 0001)=1111 1101 ——》数据段取反=1000 0010 =-2
4)再看3)中我标绿的部分,请大家把 char cc声明修改为 unsigned char试试输出结果又会有什么不同。
再看结果:
也就是说,如果我们把按位或运算的值赋值给某个值,程序会根据这个值的类型来判断结果
5)根据第四条,我们又修改代码 printf("看看cc的值是多少%d\n",(aa|bb)); 看看结果:
结论:当采取这种方式时,程序会自己创建一个临时变量来存储aa|bb的值,这个临时变量的默认是有符号型。
说明:本人使用的是codeblock 中的 GUN GCC编译器,而且都是默认设置。如果运算结果是和编译器及设置存在关系,请大佬指证一下。
此时又有同学提出问题,你这算的都是同一个类型的数据,如果不同数据类型,如char|float该怎么办?我们试一试
unsigned char aa=10;
float bb = -10;
char cc=(aa|bb);
printf("看看aa|bb的值是多少%d\n",(aa|bb));
printf("看看aa|bb的值是多少%d\n",(aa|bb));
不过此时我还没打算放弃;我们将计算结果强行显示转换一下。
char cc = (char)(aa|bb);
结果如下图:也是不行。
于是我们只能修改为 float cc=(aa|bb); 看结果
结果依然是错误的。最后说结论:位运算只能是int (包括long short unsigned )char
两种类型。
那么问题又来了,当参与位运算的两个数分别为char和int类型,编译器该怎么转换
其中运算中(包含算术运算和位运算,其中位运算,准确的来说是双目位运算)的类型自动转换遵循如下规则:
1:char型、short型转换为int型(包括unsigned char, unsigned short),float型转换为double型;
2:相同类型的操作数作算术运算,其结果为同一类型,即5/2 = 2而不是2.5;
3:不同类型的操作数经规则1转换后仍为不同类型,则其中级别低的类型自动转换为级别高的类型后再进行运算;级别高低如下:
char < short < int < unsigned int < long < unsigned long < float < double
其中,需要注意的是int型数据和unsigned int运算时,int会先转换为unsigned int,再参与运算。(c语言中整形常量默认为int型,比如-2,实型常量为double型,比如3.2)
总结:隐式转换的优先级遵守以下两个规则:
1)短类型(指的是所占字节短)优先级<长类型(指的是所占字节长)
2)有符号类型<有符号类型
3)整形中 int(有符号+有符号)<long
总结:遇到数据隐式转换时,先用规则1,来评判。规则1不能解决时,采用规则2来解决
第3)点非常有必要单独拿出来讲讲,C99或C11标准都没有强制规定各类数据所占的字节数。
总体上遵守以下规则即可:
char < short <= int<= long <= float < double;
各类数据所占字节数;与编译器的位数直接相关,
常用类型 | 延伸类型 | 16bit平台(编译器) | 32Bit平台(编译器) | 64Bit平台(编译器) |
char | unsigned char | 1 | 1 | 1 |
short | unsigned short int | 2 | 2 | 2 |
int | unsigned int | 2 | 4 | 4 |
long | long int unsigned int | 4 | 4 | 8 |
long long(两个) | long long int | 8 | 8 | 8 |
float | / | 4 | 4 | 4 |
double | / | 8 | 8 | 8 |
移位运算:
移位运算的两个大前提;
** 目前C中的移位运算只针对int(整形)和char(字符型,本质上也是整形)对double和float则不能执行移位运算。(多说一句,对于int数组和char数组只能对数组中单个元素执行移位操作)
**目前C规范只对unsigned类型,作出了强制规定。对有符号数则没有强制规定移位操作需要遵守的规范,目前各个编译器采取了不同的方案,具体看下文介绍。
我看过很多资料和书籍,很多是直接开始讲移位运算。很多都没有讲清楚两个前提,
1)“所谓左移<<,右移>>”是建立在书写习惯的基础上的。不提这一点前提的话,直接讲左移和右移都是很不负责任的讲法。
首先看我们的书写习惯
十进制数字 10 我们是先写1(高位),再写0(低位)。
如果用unsigned char类型存储
二进制表示为 :0000 1010 ,注意看我们这也是把高位写在左边 ,低位写在右边。
当执行左移时,其实就是高位侧移出。
当执行右移时,其实就是低位侧移出。
2)
另外还有一个概念,算术移位和逻辑移位
*逻辑移位:右移时,移除的地位丢弃,左边空出来高位的位置0填充。左移时移除的高位丢弃,空出的低位用0填充。
unsigned char num_1= 0x80;
for(int i=0;i<15;i++)
{ if(i<7)
{
num_1=num_1>>1; //右移一位,
printf("看看num_1左移一位后的值%x\n",num_1);
/*测试按位或*/
}
if(i>7)
{
num_1=num_1<<1; //左移一位,
printf("看看num_1左移一位后的值%x\n",num_1);
}
};
查看结果:
算术移位:右移时,移除丢弃,空出高位用符号位填充。左移时符号位不动,只移动数据段,移出丢弃,空出低位填充0。
当char num_1=10,为正数时,执行算术移位时的操作,
0000 1010 >>3位=xxx 0 0001;
xxx代表空出来的高位,看前面的规则,使用符号位填充,符号位是0 则=000 0 0001;此时细心的同学发现了。当有符号数=正数时,执行算术右移时和逻辑移位好像没有区别。
0000 0001<<3 = 0000 1xxx,符号位不动,空出低位使用0填充=0000 1000
我们再看 0000 0001执行逻辑移位时:
0000 0001<<7 (执行逻辑移位)=1000 0000 则变成负数了。