目录
一、操作符分类
算数操作符:【+、-、*、/、%】
移位操作符:<<、>>
位操作符:&、|、^、~
赋值操作符:【=、+=、-=、*=、/=、%=】、<<=、>>=、&=、|=、^=
单目操作符:【!、+、-、sizeof、(类型)、++、--】、~、&、*
关系操作符:【>、>=、<、<=、==、!=】
逻辑操作符:【&&、||】
条件操作符:【? :】
逗号表达式:,
下标引用:【[]】
函数调用:【()】
【】里面的操作符在之前已经学习过,今天学习的是没有被【】括起来的操作符。
【+、-、*、/、%】、【=、+=、-=、*=、/=、%=】、【!、+、-、sizeof、(类型)、++、--】请看文章:http://t.csdnimg.cn/msdld 操作符部分。
【>、>=、<、<=、==、!=】、【&&、||】、【? :】请看文章:http://t.csdnimg.cn/drUIS 操作符部分。
【[]】请看文章:http://t.csdnimg.cn/OF3yE 访问一维数组元素的部分。
【()】请看文章:http://t.csdnimg.cn/3CtAH 函数调用操作符部分。
下面涉及原码、反码、补码的知识,请看文章:http://t.csdnimg.cn/sn9lE。
二、移位操作符
注意:
- 移位操作符的操作数只能是整数;它右边的操作数(移动几位)只能是非负整数。
- 移动的位数范围是0~31(int类型4个字节为32 bit)。超过这个范围是不符合标准的,会给出警告。
(1)左移位操作符
规则:左抛弃,右补0。
例子:-12<<2
-12
= [1000 0000 0000 0000 0000 0000 0000 1100]原
= [1111 1111 1111 1111 1111 1111 1111 0011]反
= [1111 1111 1111 1111 1111 1111 1111 0100]补
左移两位:
[1111 1111 1111 1111 1111 1111 1101 0000]补
= [1111 1111 1111 1111 1111 1111 1100 1111]反
= [1000 0000 0000 0000 0000 0000 0011 0000]原
= -48
(2)右移位操作符
规则:
逻辑右移:右抛弃,左补0。
算术右移:右抛弃,左补原数的符号。
例子:-12>>2
-12
= [1111 1111 1111 1111 1111 1111 1111 0100]补
① 逻辑右移两位:
[0011 1111 1111 1111 1111 1111 1111 1101]补
= 1073741821
② 算术右移两位:
[1111 1111 1111 1111 1111 1111 1111 1101]补
= [1111 1111 1111 1111 1111 1111 1111 1100]反
= [1000 0000 0000 0000 0000 0000 0000 0011]原
= -3
执行逻辑右移还是算术右移取决于编译器,编译器通常是算术右移。
代码验证:
观察上面的例子,可以发现左移操作符有乘以2的幂次方的效果,算术右移操作符有除以2的幂次方的效果,移动几位,幂指数就是几。
三、位操作符
下面按位的意思是按照二进制位(bit)计算,且它们的操作数必须为整数。
- 按位与(&):两者都为1,取1;其它取0。
- 按位或(|):两者都为0,取0;其它取1。
- 按位异或(^):两者相同取0;相反取1。
- 按位取反(~):每一位都取反。
举例:
5
= [0000 0000 0000 0000 0000 0000 0000 0101]补
-3
= [1000 0000 0000 0000 0000 0000 0000 0011]原
= [1111 1111 1111 1111 1111 1111 1111 1100]反
= [1111 1111 1111 1111 1111 1111 1111 1101]补
5 & -3
= [0000 0000 0000 0000 0000 0000 0000 0101]补
= 5
5 | -3
= [1111 1111 1111 1111 1111 1111 1111 1101]补
= [1000 0000 0000 0000 0000 0000 0000 0010]取反
= [1000 0000 0000 0000 0000 0000 0000 0011]原
= -3
5 ^ -3
= [1111 1111 1111 1111 1111 1111 1111 1000]补
= [1000 0000 0000 0000 0000 0000 0000 0111]取反
= [1000 0000 0000 0000 0000 0000 0000 1000]原
= -8
0
= [0000 0000 0000 0000 0000 0000 0000 0000]补
~0
= [1111 1111 1111 1111 1111 1111 1111 1111]补
= [1000 0000 0000 0000 0000 0000 0000 0000]取反
= [1000 0000 0000 0000 0000 0000 0000 0001]原
= -1
代码验证:
四、移位、位操作符的综合练习
(1)不用临时变量,交换两个整数
题目:不能创建临时变量(第三个变量),实现两个整数的交换。
若不考虑“不能创建临时变量”这个条件的情况下,我们通常使用的方法是:
但这里不能使用临时变量,需要想想其它方法。
方法1:(加减法实现)
首先,我们要明白,第三个变量是用来保留数据,避免数据在交换过程中被覆盖掉的。现在,我们不能使用第三个变量,那么就需要换一种方式把a、b的数据都保留下来。
第37行a被赋值为 原a + 原b,这样既保留了 原a 的数据,又保留了 原b 的数据。然后,第38行,b被赋值为 a - 原b 等于 原a + 原b - 原b 等于 原a,此时a还保留着 原a+原b,不用担心原b的数据被覆盖掉了,在这一过程中,完成了 原a 向 原b 赋值。第39行a被赋值为 a - b 等于 原a + 原b - 原a 等于 原 b ,在这一过程中,完成了 原b 向 原a 赋值。
但是这种方法有很大的局限性,就是当 a 和 b 的值过大时,因为第一个式子有加法运算,所以会发生溢出,得到不正确的值。
因此,我们有需要寻找其它更优的办法。
方法2:(位异或操作符^实现)
因为 ^ 就是两数相同则为0,两数不同则为1,所以自身位异或自身等于0,如a^a得0,b^b得0。因为0^0得0,1^0得1,所以一个数位异或0等于自身,如a^0得a,b^0得b。
故第52行的b被赋值为:a ^ b 等于 原a ^ 原b ^ 原b 等于 原a ^ 0 等于 原a,这一过程完成了原a 向 原b 赋值;第53行a被赋值为:a^b 等于 原 a ^ 原b ^ 原a 等于 原b ^ 0 等于 原b,这一过程完成了 原b 向 原a 赋值。
(2)求内存中整数的二进制中1的个数
题目:求一个整数存储在内存中的二进制中1的个数。
如果想计数1的个数,我们会想到取出该整数的每一位数字。取出每一位数字有一个常用的方法,就是:
那么对于二进制整数,我们可以换成 %2 和 /2 :
1234
= [0000 0000 0000 0000 0000 0100 1101 0010]补
当二进制整数为整数时,计算结果正确。
-1234
= [1000 0000 0000 0000 0000 0100 1101 0010]原
= [1111 1111 1111 1111 1111 1011 0010 1101]反
、 = [1111 1111 1111 1111 1111 1011 0010 1110]补
当二进制整数为负数时,计算结果错误。分析原因:因为a是负数,所以不管是余数还是商都是带有负号的,对于余数来说只有0和-1两情况,所以 一次都没计数。因此,a % 2 == 1对负数并不适用。如果把 a % 2 == 1 改成 a % 2 == -1 呢?结果如下:
对比负数 -1234 的原码会发现只计数了原码的数值位上的1,符号位上的1根本没被计入;并且题目要求的是,计数在内存中整数的二进制中1的个数,内存中是用的补码形式而不是原码形式。因此,这种取余、求商的方法,是不能解决负数的情况的。
方法1:
首先,要清楚位与的规则,全1才为1,存在0就为0。然后,举例说明:
-2
= [1000 0000 0000 0000 0000 0000 0000 0010]原
= [1111 1111 1111 1111 1111 1111 1111 1101]反
= [1111 1111 1111 1111 1111 1111 1111 1110]补
[1111 1111 1111 1111 1111 1111 1111 1110] ^ [0000 0000 0000 0000 0000 0000 0000 0001]
= 0 (结果为0,第一位为0,不计数)
[1111 1111 1111 1111 1111 1111 1111 1110] ^ [0000 0000 0000 0000 0000 0000 0000 0010]
= [0000 0000 0000 0000 0000 0000 0000 0010](结果非0,第二位为1,计数器加1)
……
从上面的例子可以看到,用这种方法可以计数负数补码的每一位1。与-2作位与的数字中1的位置变化可以用左移位操作符实现。
方法2:
首先思考下面的问题:
二进制:若n = 1100,则1100 - 0001 = 1011
二进制:若n = 1000,则1000 - 0001 = 0111
……
观察上面的结果,我们可以得到一个结论,n - 1 会将 n 的最低位1改为0,最低位1后面的0都改为1。原因:减1必向最低位1借一个1,被借1后自然变成了0;而最低位1后面的0,因为自身没有1,都会向前一位借1,来减当前这个1,又因为是二进制,向前一位借到的1其实是2,2-1 = 1,最终最低位1后面的0都变为了1。
因此,原数最低位的1以及之后的0,在n-1后都被更改为相反数,相反数(0和1)作位与的结果会是0;而n的最低位1前面的数在n-1后都未改变,1 & 1 得1,0 & 0 得0,即自身和自身作位与的结果是自身。
综上所述,n & (n - 1) 的结果是,n的最低位1之前的高位数不变,之后的低位数以及最低位1都变为0。得到的效果简而言之就是,会将n最低位的1改为0。
源代码的最终效果:n 的补码中有多少个1,就会进多少次 while 循环,每次进循环都会更改一个1并计数器加1,直到所有的1都被更改为0,n的值变为0结束循环。
对比方法1:方法1必须要对补码的每一位作位与运算,对int类型来说,要循环固定的32次,才能统计出1的个数;而方法2则是有多少个1,就作几次循环,效率比方法1更优。
(3)二进制位置0或置1
题目:编写代码将13的二进制序列的第5位修改为1,然后再改为0。
13 = [0000 0000 0000 0000 0000 0000 0000 1101]补
第5位置1:
[0000 0000 0000 0000 0000 0000 0000 1101] | [0000 0000 0000 0000 0000 0000 0001 0000]
= [0000 0000 0000 0000 0000 0000 0001 1101]补 = 29
第5位置回0:
[0000 0000 0000 0000 0000 0000 0001 1101] & [1111 1111 1111 1111 1111 1111 1110 1111]
= [0000 0000 0000 0000 0000 0000 0000 1101] = 13
代码及运行结果:
五、单目操作符
还剩&和*,在后面学习指针后再讲解。
六、逗号表达式
语法形式,用逗号隔开多个表达式:
表达式1, 表达式2, 表达式3......
逗号表达式从左向右依次执行表达式,整个表达式的结果是最后一个表达式的结果。
应用场景:
a = get_val();
count_val(a);
while(a){
... // 业务处理
a = get_val();
count_val(a);
}
可用逗号表达式改写:
while(a = get_val(), count_val(a), a){
... // 业务处理
}
改写后,可以减少冗余的代码。
七、结构体成员访问操作符
(1)结构体
我们有了内置类型还不够,比如我想描述一个学生,他有姓名、年龄、性别、学号....,这时应该怎么定义这个学生呢?于是,自定义类型结构体解决了这个问题。
结构体是一些变量的集合,这些变量叫做它的成员变量,每个成员可以是不同类型的变量:字符型、整型、浮点型,甚至其它的结构体类型。
(2)结构体的声明
语法形式如下:
struct tag{
成员列表
}变量列表;
举例:学生结构体类型的声明
struct student{
char name[20]; // 姓名
int age; // 年龄
char sex[5]; // 性别
char id[20]; // 学号
};
(3)结构体变量的定义和初始化
// 结构体类型变量的定义:
// 班级结构体类型声明
struct Class{
int grade[20]; // 年级
char college[20]; // 学院
char major[20]; // 专业
char class[20]; // 班级
};
struct Class p1; // 定义班级结构体类型的变量p1(第一种方式的定义)
// 学生结构体类型的声明
struct Student{
char name[20]; // 姓名
int age; // 年龄
char sex[5]; // 性别
char id[20]; // 学号
struct Class class; // 班级
} stu1, stu2; // 声明结构体类型的同时,定义变量(第二种方式的定义)
// 结构体类型变量的初始化:
struct Student stu3 = {'zhangsan', 20, 'male', '12003990101', NULL}; // 默认顺序初始化
struct Student stu4 = {.age = 21, .name = 'xiaoming', .sex = 'famale', .id = '12003990102', .class = NULL}; // 指定顺序初始化
struct Point{
int data;
struct Point p;
} n1 = {1, {2, NULL}}; // 结构体嵌套初始化
struct Point p1 = {.p = {.p.p = NULL, .p.data = 2}, .data = 2}; // 结构体嵌套初始化
(4)结构体成员访问操作符
① 结构体成员的直接访问
直接访问用操作符(.),如下例子:
#include<stdio.h>
struct Point{
int x;
int y;
} p = {1, 2};
int main(){
printf("%d, %d", p.x, p.y); // 结构体变量.成员名
return 0;
}
② 结构体成员的间接访问
暂留,学习完指针后补充
八、操作符的属性:优先级和结合性
操作符的优先级和结合性两个属性,决定了表达式的计算顺序。
(1)优先级
对于相邻的两个操作符,优先级高的操作符先执行计算,优先级低的操作符后执行计算。如下例子:
5 + 3 * 4
乘号 * 比加号 + 的优先级高,所以先执行3 * 4 得 12,再执行5 + 12。
(2)结合性
对于相邻的两个操作符的优先级相同时,无法再用操作符的优先级判断它们执行顺序。这时,就根据操作符的结合性决定执行顺序。结合性分为左结合(从左向右执行)和右结合(从右向左执行)。如下例子:
2 * 6 / 4
因为乘号 * 和除号 / 的优先级相同,所以根据结合性决定执行顺序。* 和 / 都为左结合,故先执行2 * 6 得 12,再执行 12 / 4。
(3)优先级和结合性表
参考: C 运算符优先级 - cppreference.com
这些也不用全记住,需要的时查表就行,况且还有万能的()。不过我们记住一些常用的更好:
- 圆括号(()):最高级,从左到右。
- 后缀自增与自减(++、--):1级,从左到右。
- 前缀自增与自减(++、--):2级,从右到左。
- 单目运算符(+、-):2级,从右到左。
- 乘除法(*、/):3级,从左到右。
- 加减法(+、-):3级,从左到右。
- 关系运算符(<、>、<=、>=):6级,从左到右。
- 各种赋值运算符:倒数第2级,从右到左。
九、表达式求值
(1)整型提升
整型提升就是,对于字符型和短整型的操作数(长度小于 int 类型),当他们在表达式中发生运算时,会先提升为 int 类型,再进行计算。
为什么要进行整型提升:整型运算要在CPU中的器件 ALU 里执行,里面的操作数字节长度一般是 int 类型字节长度,同时CPU的通用寄存器里的操作数也是这个长度。而通用CPU是难以实现8 bit 长度的直接相加运算(虽然机器指令中可能有 8 bit 长度的相加指令),因此,对于长度小于 int 类型的整型会先转换为标准的 int 或者 unsigned int,再送进CPU执行运算。
整型提升规则:
- 有符号整数:高位补符号位上的值。
- 无符号整数:高位补0。
在整型提升后,送入CPU执行运算完毕,会将 int 类型长度的数据截断为原本定义的数据类型长度,最后存储到变量中。举例:
(2)算术转换
若一个操作符的操作数的类型不同时,要先将它们的类型转换成同一类型,才能进行运算。下面的层次体系被成为寻常算术转换:
例如:
a 为 double 类型8字节,b 为 int 类型 4字节,根据转换层次体系,b转换为 double 类型,得到计算结果也为 double类型 8字节。
十、问题表达式的解析
(1)表达式1
a * b + c * d + e * f
对于这个表达式,我们可以确定的是,相邻的 * 比 + 的优先级高,故先执行 * 再执行 + 。但是我们确定的只是相邻的 * 和 + 的执行顺序,不相邻的 * 和 + 的执行顺序是确定不了的。因此,第一个 + 和第三个 * (不相邻)到底是哪个先执行,是存在歧义的:
执行顺序可以是这样:
按照优先级,1和2比4先执行;2和3比5先执行。按照 + 的结合性(左结合),4比5先执行。在这个执行路线中,第一个 + 比第三个 * 后执行。
执行顺序也可以是这样:
按照优先级,1和2比3先执行;2和4比5先执行。按照 + 的结合性(左结合),3比5先执行。在这个执行路线中,第一个 + 比第三个 * 先执行。
(2)表达式2
c + --c
对于这个表达式,我们可以确定的是: -- 比 + 的优先级高,故先执行 -- 再执行 + 。但是,到底是执行表达式前就把值放入了第一个c里,还是执行了--c后,才把值放入第一个c里,是存在歧义的。
假如c的值初始化为1。若在执行表达式前就把值放入了第一个c里,那么 1 + 0 得 1;若执行了--c后,才把值放入第一个c里,那么 0 + 0 得 0。
(3)表达式3
int main()
{
int i = 10;
i = i-- - --i * ( i = -3 ) * i++ + ++i;
printf("i = %d\n", i);
return 0;
}
此代码来自于《C和指针》这本书,作者将该代码在不同的编译器中执行,得到了截然不同的结果:
可以得出,由于这些歧义,以及不同编译器的实现不同,而导致执行结果不同。这种有歧义的代码,可移植性是很差的。
(4)表达式4
#include <stdio.h>
int fun()
{
static int count = 1;
return ++count;
}
int main()
{
int answer;
answer = fun() - fun() * fun();
printf( "%d\n", answer);
return 0;
}
在这个表达式中,我们可以知道相邻的 * 比 - 优先级高,先执行 * 再执行 - 。但是先调用哪个fun 函数是无从而知的,这也造成了歧义。
如果执行顺序是:
结果为:2 - 3 * 4 得 2 - 12 等于-10。
如果执行顺序是:
结果为:3 - 2 * 4 得 3 - 8 等于 -5。
当然还有其它的执行顺序,就不一一例举。
(5)表达式5
#include <stdio.h>
int main()
{
int i = 1;
int ret = (++i) + (++i) + (++i);
printf("%d\n", ret);
printf("%d\n", i);
return 0;
}
这个代码在gcc编译器上运行的结果:
在VS2019上运行的结果:
为什么运行得到的结果不同呢?其实还是跟表达式1类似的问题:我们只知道相邻 ++ 比 + 的优先级高,+ 为左结合,但是不相邻的第一个 + 和第三个 ++ 哪个先执行是不知道的。
对于VS2019的编译器,是先执行第三个 ++,后执行第一个 +:执行第一个 ++i,i为2 >> 执行第二个 ++i,i为3 >> 执行第三个 ++i,i为4 >> 执行第一个 + ,4 + 4 为 8 >> 执行第二个 + ,8 + 4 为12。
对于gcc编译器,是先执行第一个 +,后执行第三个 ++:执行第一个 ++i,i为2 >> 执行第二个 ++i,i为3 >> 执行第一个 + ,3 + 3 为 6 >> 执行第三个 ++i,i为4 >> 执行第二个 + ,6 + 4 为10。
以上的过程,都可以通过调试中的反汇编窗口验证(以下是VS2019的):
(6)总结
就算有了优先级和结合性,表达式仍然可能不能通过操作符的属性来确定唯一的路径。为了降低表达式存在的风险,我们写表达式时,建议将一个复杂表达式分解为简单的表达式。
标签:0000,int,C语言,1111,详解,操作符,执行,表达式 From: https://blog.csdn.net/2401_86272648/article/details/140774954