C 语言部分操作符详解 -- 进制转换,原码、反码、补码,位操作符,逗号表达式,操作符的优先级和结合性,整型提升,算术转换
目录
1. 操作符分类
2. 二进制和进制转换
2.1 2 进制和 10 进制的相互转换
2.2 2 进制和 8 进制、16 进制的转换
3. 原码、反码、补码
4. 位操作符
5. 逗号表达式
6. 下表访问[]、函数调用()
8. 操作符的属性:优先级、结合性
9. 表达式求值
9.1 整型提升
9.2 算术转换
1. 操作符分类
C 语言中很很多操作符,根据功能可以将操作符分为很多类,比如算数操作符、位操作符、赋值操作符、关系操作符、逻辑操作符等等。关系操作符、逻辑操作符以及条件操作符在https://blog.csdn.net/CSQCSQCC/article/details/137715007 中已经有所介绍,算数操作符以及部分单目操作符在https://blog.csdn.net/CSQCSQCC/article/details/148639715中已经有所介绍。
2. 二进制和进制转换
2 进制、8 进制、10 进制、16 进制是数值的不同表示形式。
15的2进制:1111
15的8进制:17
15的10进制:15
15的16进制:F
10 进制就是满 10 进 1,每一位都是 0-9 的数组组成。相对的二进制就是满 2 进 1,每一位都是 0-1 的数字组成。
2.1 2 进制和 10 进制的相互转换
10 进制的每一位是权重,10 进制的数字从右往左是个位、十位、百位...,分别每一位的权重是 10 的 0 次方,10 的 1 次方,10 的 2 次方...
2 进制和 10 进制类似,只不过 2 进制的每一位权重不一样,从右往左分别是 2 的 0 次方,2 的 1 次方,2 的 2 次方...
现在将 10 进制的 125 转为 2进制,则将 125 依次除以 2,每次除以 2 的余数就是 2 进制中从低数值位到高数值位的值。
如上图,最后得到的二进制就是从下到上的余数。
2 进制转成 10 进制,就是将 2 进制每一位的值乘以每一位的权重然后相加即可。
2.2 2 进制和 8 进制、16 进制的转换
8 进制的数字每一位是 0-7 的数字组成,写成 2 进制就是 3 位 2 进制数字,比如 8 进制的 7 就是 2 进制的 111,所以在 2 进制转 8 进制的时候,从低位开始,每 3 位转换成一个 8 进制数字即可,最后一组不够 3 位则将高位看做 0 即可。
如上图,01 转成 8 进制的 1,101 转为 8 进制的 5,011 转为 8 进制的 3,所以 2 进制的 01101011 就会转成 8 进制的 153。
16 进制的数字每一位是 0-9 以及 a-f 的数字组成,和 8 进制相似,16 进制写成 2 进制就是 4 位 2 进制数字,所以在 2 进制转 16 进制的时候,从低位开始,每 4 位转换成一个 16 进制数字即可,最后一组不够 4 位则将高位看做 0 即可。
如上图,0110 转成 16 进制的 6,1011 转为 16 进制的 b,所以 2 进制的 01101011 就会转成 16 进制的 6b。
3. 原码、反码、补码
整数的 2 进制表示方法有三种,原码、反码、补码。
有符号整数的三种表示方法均有符号位和数值位,2 进制序列中,最高位的 1 位是被当做符号位,0 表示“正”,1 表示“负”,剩余的都是数值位。
(1)正整数的原码、反码、补码都相同。
(2)负整数的原码、反码、补码各不相同。
原码:将数值按照正负数的形式翻译成 2 进制得到的就是原码。
反码:原码的符号位不变,其他位依次按位取反得到的就是反码。
补码:反码 + 1 就得到的是补码。反码取反然后 + 1 得到的就是原码。
在计算机系统中,数值一律用补码来表示和存储。因为使用补码,可以将符号位和数值域统一处理;同时,加法和减法也可以统一处理,此外,补码和原码相互转换,其运算过程是相同的,不需要额外的硬件电路。
4. 位操作符
操作符 | 名称 | 描述 |
---|---|---|
& | 按位与 | 对两个操作数逐位进行与运算 |
| | 按位或 | 对两个操作数逐位进行或运算 |
^ | 按位异或 | 对两个操作数逐位进行异或运算 |
~ | 按位取反 | 对两个操作数逐位进行取反运算 |
<< | 左移 | 将操作数的 bit 位左移指定位数 |
>> | 右移 | 将操作数的 bit 位右移指定位数 |
注:位操作符的操作数只能是整数。
#include <stdio.h>
int main()
{int num1 = -3;// -3 存在 int a 中,是 4 个字节,32 个 bit 位// 源码:1000 0000 0000 0000 0000 0000 0000 0011// 反码:1111 1111 1111 1111 1111 1111 1111 1100// 补码:1111 1111 1111 1111 1111 1111 1111 1101int num2 = 5;// 5 存在 int a 中,是 4 个字节,32 个 bit 位,整数原码,反码,补码都一样// 源码:0000 0000 0000 0000 0000 0000 0000 0101// 反码:0000 0000 0000 0000 0000 0000 0000 0101// 补码:0000 0000 0000 0000 0000 0000 0000 0101// -3 补码:1111 1111 1111 1111 1111 1111 1111 1101// 5 补码:0000 0000 0000 0000 0000 0000 0000 0101printf("%d\n", num1 & num2);// 补码:0000 0000 0000 0000 0000 0000 0000 0101// 反码:0000 0000 0000 0000 0000 0000 0000 0101// 源码:0000 0000 0000 0000 0000 0000 0000 0101 -- 5printf("%d\n", num1 | num2);// 补码:1111 1111 1111 1111 1111 1111 1111 1101// 反码:1000 0000 0000 0000 0000 0000 0000 0010// 原码:1000 0000 0000 0000 0000 0000 0000 0011 -- -3printf("%d\n", num1 ^ num2);// 补码:1111 1111 1111 1111 1111 1111 1111 1000// 反码:1000 0000 0000 0000 0000 0000 0000 0111// 原码:1000 0000 0000 0000 0000 0000 0000 1000 -- -8printf("%d\n", ~0);// 0 补码: 0000 0000 0000 0000 0000 0000 0000 0000// 补码取反: 1111 1111 1111 1111 1111 1111 1111 1111 -- 补码// 1000 0000 0000 0000 0000 0000 0000 0000 -- 反码// 1000 0000 0000 0000 0000 0000 0000 0001 -- 原码 -- -1return 0;
}
例:不创建临时变量,实现两个整数的交换。
// 方法1:
#include <stdio.h>
int main()
{int a = 3;int b = 5;printf("交换前: a = %d, b = %d", a, b);a = a + b; // a 为 a 和 b 的和b = a - b; // b = a 和 b 的和减去 a,则 b = aa = a - b; // 上一步中,b 已经等于 a 了,再用 a 和 b 的和减去 a 得到 bprintf("交换后: a = %d, b = %d", a, b);return 0;
}// 该方法在 a + b 超过整型的最大值时,就会出现溢出的情况,计算出来的值不对
// 方法2:异或
#include <stdio.h>
int main()
{int a = 3;int b = 5;printf("交换前: a = %d, b = %d", a, b);a = a ^ b;b = a ^ b; // b = a ^ b ^ b = a;a = a ^ b; // a = a ^ b ^ a = b;printf("交换后: a = %d, b = %d", a, b);return 0;
}// a ^ a = 0; a ^ 0 = a;
左移操作符 " << ":左边抛弃,右边补0。
#include <stdio.h>
int main()
{int num = 10;int n = num << 1;printf("n= %d\n", n);printf("num= %d\n", num);return 0;
}
右移操作符 " >> ":(1)逻辑右移:左边用 0 填充,右边丢弃。(2)算数右移:左边用原该值的符号位填充,右边丢弃。
右移到底是算数右移还是逻辑右移是取决于编译器的实现,常见的编译器都是算数右移。
算数右移:
逻辑右移:
注:对于移位运算符,不要移动负数位,这个是标准未定义的。
5. 逗号表达式
逗号表达式就是用逗号隔开多个表达式。逗号表达式从左往右依次执行,整个表达式的结果是最后一个表达式的结果。
#include <stdio.h>int main()
{int a = 1;int b = 2;int c = (a > b, a = b + 10, a, b = a + 1); //逗号表达式printf("c=%d", c);return 0;
}
在上述逗号表达式中,从左向右计算,a 的值被更新为 12,b 的值被更新为 13,则 c 等于最后一个表达式的结果,所以 c = b = 13。
逗号表达式在 while 循环中使用可以减少代码冗余。
a = get_val();
count_val(a);
while (a > 0)
{//业务处理//...a = get_val();count_val(a);
}// 如果使⽤逗号表达式,改写为如下代码,可以减少代码冗余
while (a = get_val(), count_val(a), a>0)
{//业务处理
}
6. 下表访问[]、函数调用()
(1)下标引用操作符 []:该操作符的操作数为一个数组名和一个索引值。
int arr[10]; // 创建数组。
arr[9] = 10; // 使用下标引⽤操作符。
(2)函数调用操作符 ():接收一个或者多个操作数,第一个操作数是函数名,剩余的操作数就是传递给函数的参数。
#include <stdio.h>
void test1()
{printf("hehe\n");
}
void test2(const char* str)
{printf("%s\n", str);
}
int main()
{test1(); // ()作为函数调⽤操作符。test2("hello XiaoC."); // ()就是函数调⽤操作符。return 0;
}
8. 操作符的属性:优先级、结合性
C 语言的操作符有两个重要的属性:优先级和结合性,这两个属性决定了表达式求值的计算顺序。
(1)优先级:如果一个表达式包含多个运算符,运算符的优先级决定哪个运算符应该优先执行。
3 + 4 * 5;
上述表达式中既有加法运算也有乘法运算,由于乘法运算优先级高于加法,所以先计算乘法。
(2)结合性:如果两个运算符优先级相同,优先级没办法决定哪个先计算,根据运算符左结合还是右结合,决定执行顺序。⼤部分运算符是左结合(从左到右执⾏),少数运算符是右结合(从右到左执⾏),⽐如赋值运算符( = )。
5 * 6 / 2;
上述乘法和除法的优先级相同,都是左结合运算符,所以从左到右执行。
9. 表达式求值
9.1 整型提升
C 语言中整型算数运算总是至少以缺省整型类型的精度来进行的,为了获取这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升。
整型提升规则:(1)有符号整数提升是按照变量的数据类型的符号位来提升的。(2)无符号整型提升,高位补0。
#include <stdio.h>int main()
{char a = 3;char b = 127;char c = a + b;printf("c = %d", c);return 0;
}
这里解释为什么上述结果为 126。
#include <stdio.h>int main()
{char a = 3;// 00000011 -- a在内存中存储的补码char b = 127;// 01111111 -- b在内存中存储的补码char c = a + b;// 当a和b进行运算时要进行整型提升// 00000000 00000000 00000000 00000011 -- a整型提升后在内存中存储的补码// 00000000 00000000 00000000 01111111 -- b整型提升后在内存中存储的补码// 00000000 00000000 00000000 10000010 -- a+b运算的结果在内存中存储的补码// 10000010 -- c在内存中存储的补码printf("c = %d", c);// 使用%d进行打印的时候也是需要进行整型提升的,c为有符号字符,所以整型提升时按照符号为进行提升// 11111111 11111111 11111111 10000010 -- c整型提升后的补码// 10000000 00000000 00000000 01111101 -- c整型提升后的反码// 10000000 00000000 00000000 01111110 -- c整型提升后的原码 -- -126return 0;
}
整型提升的意义:表达式的整型运算要在 CPU 的相应运算器内执行,CPU 内整型运算器(ALU)的操作数的字节长度一般就是 int 的字节长度,同时也是 CPU 的通用寄存器的长度。因此,即使两个 char 类型的相加,在 CPU 执行时实际上也要先转换为 CPU 内整型操作数的标准长度。
9.2 算术转换
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数转换为另一个操作数的类型,否则操作无法进行。下面的层次体系称为寻常算术转换。
long double
double
float
unsigned long int
long int
unsigned int
int
在上述表中,如果一个操作数的类型排在另外一个操作数类型的后面,则该操作数要先转换为另外一个操作数的类型再进行计算。比如 int 类型和 double 类型进行计算,则 int 类型要先转换成 double 类型在进行计算。