目录
1、操作符分类
2、二进制转换
2.1二进制转十进制
2.1.1、十进制转二进制
2.2、二进制转八进制和十六进制
2.2.1、二进制转八进制
2.2.2、二进制转十六进制
3、原码、反码、补码
4、移位操作符(移动的是二进制位)
4.1、左移操作符
4.2、右移操作符
5、位操作符:&、|、^、~
6、单目操作符
7、逗号表达式
8、下标访问[]、函数调用()
8.1、[]下标引用操作符
9、结构成员访问操作符
9.1、结构体
9.1.1结构的声明
10、操作符的属性: 优先级、结合性
10.1、优先级
10.2、结合性
11、表达式求值
11.1、整型提升
11.2、算术转换
11.3、问题表达式解析
11.3.1、表达式1
11.3.2、表达式2
11.3.3、表达式3
11.3.4、表达式4
11.3.5、表达式5
11.4、总结
1、操作符分类
(1)算术操作符:+、-、*、/、%、
(2)移位操作符:<<、>>、(移动的是二进制位)
(3)位操作符:&、|、^、(也是使用二进制位进行计算)
(4)赋值操作符:=、+=、-=、*=、/=、%=、<<=、>>=、&=、|=、^=、
(5)单目操作符:!、++、--、&、*、+、-、~、sizeof、(类型)、
(6)关系操作符:>、>=、<、<=、==、!=、
(7)逻辑操作符:&&、||、
(8)条件操作符:?=、
(9)逗号表达式:,、
(10)下标引用:[]、
(11)函数调用:()、
(12)结构成员访问:,、->、
上述操作符,我们已经学过算术操作符、赋值操作符、逻辑操作符、条件操作符和部分的单目操作符,今天继续介绍一部分,操作符中有一些操作符和二进制有关系。
2、二进制转换
我们经常能听到:2进制、8进制、10进制、16进制,其实2进制、8进制、10进制、16进制是数值的不同表示形式,比如数值15的各种进制的表示形式:
①15的2进制:1111
②15的8进制:17
③15的10进制:15
④15的16进制:F
//16进制的数值之前写:0x
//8进制的数值之前写:0
我们重点介绍一下2进制:
首先要讲二进制先从十进制讲起:
十进制
(1)十进制中满10进1
(2)十进制的数字每一位都是0~9的数字组成,其实二进制也差不多
二进制
(1)二进制中满2进1
(2)二进制的数字每一位都是0~1的数字组成
那么1101就是二进制的数字了
十进制123组成:
相加之后得123
二进制1111转十进制:
相加之后得15
八进制17转十进制:
相加之后得15
十六进制转十进制:
2.1二进制转十进制
十进制的123表示的值为123,每一位都是有权重的,每一位权重为10^0、10^1、10^2 ……
二进制同理若有1101(二进制)
2.1.1、十进制转二进制
比如十进制125转二进制:
把余数从下到上拿出来就是125的二进制数1111101
2.2、二进制转八进制和十六进制
2.2.1、二进制转八进制
八进制的数字每一位是0~7的,0~7的数字各自写成二进制,最多有3个二进制位就足够了,比如7的二进制是111,所以在二进制转八进制数的时候,从二进制序列中右边低位开始向左每3个二进制会换算下一个八进制位,剩余不够三个二进制位的直接换算。
若二进制数01101011
2.2.2、二进制转十六进制
3、原码、反码、补码
当我们要把一个数转换成二进制表示时
整数的二进制有3中表示形式(暂时不考虑浮点数)
有符号整数的三种表示方法,均有符号位和数值为两部分,二进制序列中,最高位的1位被当作符号位,剩余的都是数值位
int的长度时四个字节,等于32个bit位
对于unsigned int来说没有符号位,全都为数值位
正整数的原码、反码、补码都相同;
负整数的三种表示方法各不相同。
原码:直接将数值按照正负数的形式翻译成二进制得到的就是原码。
反码:将原码的符号位不变,其他位一次按位取反就可以得到反码。
补码:反码+1得到补码。
补码得到原码也可以使用取反+1的操作:
比如说-10:
整数在内存中存储的是补码的二进制序列,原因在于,使用补码,可以将符号位和数值位统一处理,同时(CPU只有加法器)此外,补码与原码相互转换,其运算过程相同,不需要额外的硬件电路。
在计算机系统中,数值一律用补码来表示和存储。
4、移位操作符(移动的是二进制位)
<<左移操作符
>>右移操作符
注:移位操作符的操作数只能是整数。
4.1、左移操作符
移位规则:左边抛弃,右边补0
int main()
{
int a = 10;//移动的是存储在内存中的二进制位(补码)
int b = a << 1;//左移一位
printf("b = %d\n",b);
printf("a = %d\n",a);
return 0;
}
int main()
{
int a = -1;
int b = a << 1;
printf("b = %d\n",b);
printf("a = %d\n",a);
return 0;
}
左移一位有乘二的效果。
4.2、右移操作符
移位规则:首先右移运算分两种:
①逻辑右移:左边用0填充,右边丢弃。
②算术右移:左边用原该值的符号位填充,右边丢弃。
int main()
{
int a = -10;
int b = a >> 1;
printf("a = %d\n",a);
printf("b = %d\n",b);
return 0;
}
右移到底采用算术右移还是逻辑右移,是取决于编译器的!
通常采用的都是算术右移
浮点数不能移位
对于移位运算符,不要移动负数位,这个时标准未定义的。
例如:
int a = 10;
a >>= -1;//error
5、位操作符:&、|、^、~
① & //按位与
② | //按位或
③ ^ //按位异或
④ ~ //按位取反
注:他们的操作数必须是整数
& --- 按(二进制)位与 && --- 逻辑与
| ---按(二进制)位或 || ---逻辑或
&(按位与):
int main()
{
int a = 6;
int b = -7;
int c = a & b;//a和b的补码的二进制序列进行运算
printf("c = %d\n",c);
return 0;
}
00000000 00000000 00000000 00000110 -- 6的补码
11111111 11111111 11111111 11111001 -- -7的补码
00000000 00000000 00000000 00000000 -- 6 & -7 = 0
规则:二进制位对应的补码如果有0则为0,同时为1才为1;
记忆技巧:&(按位与)长得像&&(并且)所以都为1才为1,有一个0就为0。
|(按位或):
int main()
{
int a = 6;
int b = -7;
int c = a | b;
printf("c = %d\n",c);
return 0;
}
6的补码 00000000 00000000 00000000 00000110
- 7的补码 11111111 11111111 11111111 111111001
c的补码 11111111 11111111 11111111 11111111
c的原码 1000000 00000000 00000000 00000001
规则:只要有1就为1,两个同时位0才为0。
记忆技巧:|(按位或)长得像||(或者)所以有一个1就为1,有两个0才为0。
^(按位异或)
int main()
{
int a = 6;
int b = -7;
int c = a ^ b;
printf("c = %d\n",c);
return 0;
}
6的补码 00000000 00000000 00000000 00000110
-7的补码 11111111 11111111 11111111 11111001
c的补码 11111111 11111111 11111111 11111111
c的原码 10000000 00000000 00000000 00000001
规则:a和b补码的二进制位进行运算,相同为0,相异为1。
~(按位取反)
int main()
{
int a = 0;
printf("~a = %d\n",~a);
return 0;
}
~按二进制位取反:
0的补码 00000000 00000000 00000000 00000000
不管符号位,全部取反:11111111 11111111 11111111 11111111
小汇总:
&(按位与)-- 有0为0,同1为1
|(按位或)-- 有1为1,同0为0
^(按位异或)-- 相同为0,相异为1
~(按位取反)-- 不管符号位,全部取反
重点:
0 ^ a = a 3 ^ 3 ^ 5 = 5 3 ^ 5 ^ 3 = 5
异或是支持交换律的:
a ^ b ^ a = b b ^ a ^ b = a
不创建新的变量实现a和b的值互换:
方法一:
int main()
{
int a = 3;
int b = 5;
printf("a = %d\n,b = %d\n",a,b);
a = a + b;
b = a - b;
a = a - b;
printf("a = %d\n,b = %d\n",a,b);
return 0;
}
方法二:
int main()
{
int a = 3;
int b = 5;
printf("a = %d\n,b = %d\n",a,b);
a = a ^ b;
b = a ^ b;
a = a ^ b;
printf("a = %d\n,b = %d\n",a,b);
return 0
}
练习1:编写代码实现:求一个整数存储在内存中的二进制中1的个数
int count_bit_one(int n);int main()
{
int input = 0;
scanf("%d",&input);
int ret = count_bit_one(input);
printf("%d",ret);
return 0;
}int count_bit_one(int num)
{
int count = 0;while(num){if((num % 2)== 1)count++;num /= 2;}
return count;
}
这样只能计算正整数,如果给一个负数就会出错。
这里我们可以把输入的值,在传参是转换成无符号整型就可以解决当下的问题。
我们换一种思路,是否可以不考虑正负数的问题,比如 -1:
-1:11111111 11111111 11111111 11111111
1:00000000 00000000 00000000 00000001
-1 & 1 :00000000 00000000 00000000 00000001
若n&1 == 1则其最低位原本就是1
若n&1 == 0则其最低位原本就是0
如果想知道这个数最低位的前一位就可以使用右移符,若想知道全部,可以右移32位
请看如下代码:
int count_bit_one(int num)
{int i = 0;int count = 0;for(i = 0; i < 32; i++){if(((num >> i)& 1)== 1){count++;}}
return count;
}int main()
{
int input = 0;
scanf("%d",&input);
int ret = count_bit_one(input);
printf("%d",ret);
return 0;
}
n & (n - 1)
若 n = 11 n = n&(n - 1)
可以发现每执行一次n = n&(n - 1),n就去掉一个1,这个表达式执行了几次这个n就有几个1.
把n的二进制序列中最右边的1去掉了,下面看代码:
int count_bit_one(int x)int main()
{
int input = 0;
scanf("%d",&input);
int ret = count_bit_one(input)
printf("%d\n",ret);
return 0;
}int count_bit_one(int num)
{
int count = 0;
while(num){n = n & (n - 1);count++;}
return 0;
}
写一个代码判断n是否是2的次方数:
void count_bit_one(int x)int main()
{
int input = 0;
scanf("%d",&input);
count_bit_one(input);
return 0;
}void count_bit_one(int n)
{
if(n &(n - 1)== 0)
printf("yes\n");
}
练习二:二进制位置0或置1:编写代码将13的二进制序列的第5位修改为1,然后再改回0。
int main()
{
int input = 0;
int n = 0;
printf("请输入数字,以及要改为1的位数\n");
scanf("%d%d",&input,&n);
input = input | (1 << (n -1));
printf("%d\n",input);
input = input & (~(1 << (n - 1)));
printf("%d\n",input);
return 0;
}
6、单目操作符
!、++、--、&、*、+、-、~、sizeof、(类型)、
单目操作符的特点是只有一个操作数,在弹幕操作符中只有&和*没有介绍,这两个操作符,我们放在学习指针的时候学习。
& -- 取地址操作符
* -- 解引用操作符
7、逗号表达式
逗号表达式,就是用逗号隔开的多个表达式
逗号表达式,从左向右依次执行。整个表达式的结果是最后一个表达式的结果。
int main()
{
int a = 1;
int b = 2;
int c = (a > b, a = b + 10, a , b = a + 1);
printf("%d\n",c);
return 0;
}
8、下标访问[]、函数调用()
8.1、[]下标引用操作符
接受一个或者多个操作数:第一个操作数是函数名,剩余的操作数就是传递给函数的参数。
int main()
{
printf("hello\n");
return 0;
}
上述代码的括号就是函数调用操作符,操作数为printf、“hello\n”
int Add(int x, int y)
{
return x + y;
}int main()
{
int ret = Add(3,4);
printf("%d\n"ret);
return 0;
}
上述代码中Add函数的操作数是Add、3、4。
函数调用操作符最少一个操作数。
9、结构成员访问操作符
9.1、结构体
C语言已经提供了内置类型,如:char、short、int、long、float、double等,但是只有这些内置类型还是不够的,假设想描述学生,描述一本书,这时单一的内置类型是不行的。
结构是一些值的集合,这些值称为成员变量,结构的每个成员可以是不同的变量,如:标量、数组、指针,甚至是其他结构体。
9.1.1结构的声明
struct tag //tag自定义
{
member - list;//成员列表
}variable - list;//变量列表
struct Student
{
//成员变量
char name[20];
int age;
float score;
} s4,s5,s6;struct Student s3;//全局变量int main()
{
int a = 0;
struct Student s1;//局部变量
struct Student s2 = {"张三",20,97.0};
return 0;
}
//结构体变量.结构体成员名
//->这个是依赖指针的,所以在后面给大家介绍
10、操作符的属性: 优先级、结合性
C语言的操作符有2个重要的属性:优先级、结合性,这两个属性决定了表达式求值的计算顺序。
10.1、优先级
优先级指的是,如果一个表达式包含多个运算符,哪个操作符优先执行。各种运算符的优先级是不一样的。
//相邻的操作符,优先级高的先执行
比如说 int a = 3 + 4 * 5
那么就会限制性4 * 5,而不是 3 + 4,是因为 *的优先级更高。
10.2、结合性
相邻的操作符的优先级相同的情况下,结合性说了算。
比如说 int b = 3 + 4 + 5,优先级相同,加号的结合性从左到右,所以先3 + 4,而不是4 + 5。
11、表达式求值
11.1、整型提升
C语言中整型算术运算重视至少以默认整型类型的精度来进行的。
为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换被称为整型提升。
就比如:
char a = 10;
char b = 20;
char c = a + b;
这样就会发生整型提升。
整型提升的意义:
表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。因此即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。通用CPU是难以直接实现两个8比特字节直接相加运算。所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int ,然后才能进入CPU去执行运算。
如何进行整型提升?
1.有符号整数提升是按照变量的数据类型的符号位来提升的。
2.无符号整数提升,高位补0。
11.2、算术转换
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数转换为另一个操作数的类型,否则操作就无法进行。
整型提升讨论的是:
表达式中char和收人头类型的值。
算术转换讨论的是:
类型大于等于整形类型的类型
比如:
int a; double b;
a + b;
a就会进行算术转换,转换成double类型,如果某个操作数在上表排名靠后,那就首先要转换为另外一个操作数的类型后才执行运算。
11.3、问题表达式解析
11.3.1、表达式1
a * b + c * d + e * f
由于*比+的优先级高,只能保证*的计算比+早,但优先级并不能决定第三个*比第一个+早执行。
所以运算顺序可能是:
11.3.2、表达式2
c + --c;
11.3.3、表达式3
{
int i = 10;
i = i-- - --i *(i = -3)* i++ + ++i;
}
11.3.4、表达式4
int fun()
{
static int count = 1;
return ++count;
}int main()
{
int answer;
answer = fun() - fun() * fun();
printf("%d\n",answer);
return 0;
}
11.3.5、表达式5
int i = 1;
ret = (++i)+(++i)+(++i)
printf("%d\n",ret);
printf("%d\n",i);
我们用稿纸计算结果位9、4,而使用VS2022计算结果位12、4,结果不一致,使用反汇编来看一下计算过程。
int ret = (++i) + (++i) + (++i)
mov eax,dword ptr [i]
把i的值放到eax中(eax是寄存器)
add eax,1
eax自增1
这时用监视器看到eax由1变为2
mov dword ptr [i],eax
上面代码其实就是++i
mov ecx dword ptr [i]
add ecx,i
mov dword ptr [i],ecx
mov edx,dword ptr [i]
有完成一次++i
add edx,i
这时监视器中
eax = 2 i = 3 ecx = 3 edx = 4
mov dword ptr [i],edx
edx值放到i中
mov eax,dword ptr [i]
add eax,dword ptr [i]
add eax,dword ptr [i]
mov dword ptr [ret],eax
三次++i放到eax中去了,监视器中此时eax = 12
ret = 12
其实VS中先完成三次(++i)再把三个i相加
11.4、总结
即使有了操作符的优先级和结合性,写出的表达式依然不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在风险的,建议不要写出特别复杂的表达式。