在上一篇文章中,讲述了简易版扫雷游戏。今天就讲解新的知识---操作符。其实在之前我们就已经使用了一部分操作符,例如:+、-、*、/、&&、||等等。今天对于操作符进行更加充分的介绍。
1. 操作符的分类
算术操作符:+、-、*、/、%
移位操作符:<<、>>
位操作符:&、|、^
赋值操作符:=、+=、-=、*=、/=、%=、<<=、>>=、&=、|=、^=
关系操作符:<、>、=、<=、>=、!=
单目操作符:++、--、&、+、-、*、!、~、sizeof(类型)
逻辑操作符:&&、||
条件操作符:?:
逗号表达式:,
下标引用操作符:[ ]
函数调用操作符:( )
结构体成员访问:. 、->、、
在前面的文章中,算数操作符、赋值操作符、关系操作符等等大部分都是讲解了。今天就对剩下的一些操作符进行讲解,余下的操作符大多都与二进制有关。所以我们先来了解一下二进制!
2. 二进制和进制转换
我们日常生活中,频繁使用的是10进制,例如153就是十进制数字。即十进制:满10进1。
那么相同的道理:
二进制:满2进1(即组成的数字只有0~1);
八进制:满8进1(组成的数字为0~9);
十六进制:满16进1(组成的数字为0~15)。
为了便于理解,拿一个数举例:15
2进制 | 1111 |
8进制 | 17 |
10进制 | 15 |
16进制 | F |
注意:在使用8进制和16进制时,8进制数值前要加:0,16进制数值前要加:0x
15---017(8进制)---0xF(16进制)
2.1 二进制转10进制
在上述不同进制,同一个值得结果不一样,这是因为不同进制分配的权重不一样。
对于二进制而言:2进制的每一位的权重,从右向左是: 2^0、2^1、2^2、2^3……
同理对于10进制:
2.2 10进制转2进制
2.3 2进制转8进制和16进制
2.3.1 2进制转8进制
由于8进制是由0~7的数字组成,由于2^3=8,那么3位二进制就能表示8进制了。
2.3.2 2进制转16进制
由于16进制是由0~15的数字组成,又2^4=16,即用4位二进制即可表示16进制。
3. 原码、反码和补码
整数的2进制表达方式有三种,原码、反码和补码
对于有符号整数来说,原码、反码和补码是由符号位和数值位所构成,其中符号位是最高位。
在C语言中的符号位:0---表示正(+);1---表示负(-)
整形是占4个字节==32位bit位,即原码、反码和补码都是32位,最高1位是符号位
无符号整数:原码、反码和补码是一样的
有符号整数:原码、反码和补码的表达结果不一样
有符号整数:
原码:将数值按照正负形式转化为2进制序列(32位bit位)。
反码:在原码的基础上,符号位不便,其他位按位取反。
补码:反码+1得到补码。
这样说可能有点难以理解,举个简单例子:
#include <stdio.h>
int main()
{
int num = -5;//0--表示正,1--表示负
//101--这是5,其他位补0
//10000000000000000000000000000101---最高位为符号位1--表示是负数
//10000000000000000000000000000101 -- (-5)的原码
//11111111111111111111111111111010 -- (-5)的反码(符号位不变,其他位按位取反)
//11111111111111111111111111111011 -- (-5)的补码(补码+1)
return 0;
}
注:原码得到补码:取反,+1;
补码得到原码也可以:取反,+1。
同时这里需要补充一点:在C语言中,内存中存储的是2进制,并且在内存中存储的是补码。
在计算机系统中,数值一律采用补码来存储。这样做的原因是:方便在内存中将符号位和数值位进行统一的处理。这样在内存中可以统一处理加减法运算(cpu只有加法器)。同时,原码和补码的转换过程是一样的,不需要额外的硬件电路。
举个简单例子:1+(-1)=0;如果直接使用原码进行运算,
int main()
{
//(-1)+1 = 0;
//-1
//10000000000000000000000000000001 --- (-1)的原码
//11111111111111111111111111111110 --- (-1)的反码
//11111111111111111111111111111111 --- (-1)的补码
//1
//00000000000000000000000000000001 --- 1的原码、反码和补码
//如果用原码进行运算
//10000000000000000000000000000001 --- (-1)的原码
//00000000000000000000000000000001 --- 1的原码
//10000000000000000000000000000010 --- 结果(-2)//err
//如果使用补码进行运算
//11111111111111111111111111111111 --- (-1)的补码
//00000000000000000000000000000001 --- 1的补码
//00000000000000000000000000000000 --- 0//ok
//存储在内存中是补码,它的结果需要将其转换为原码
//补码转原码:取反,+1
//00000000000000000000000000000000 --- (-1)+1得到的补码
//11111111111111111111111111111111 --- 反码
//00000000000000000000000000000000 --- 原码//0--结果正确
return 0;
}
4. 移位操作符
<<左移操作符
>>右移操作符
注意:这里移动的是二进制序列
4.1 左移操作符
用法:左边抛弃,右边补0
举个简单例子:5<<1的结果
int main()
{
int a = 5;
//00000000000000000000000000000101 5的补码
//5<<1
//00000000000000000000000000001010 5<<1得到的补码,也是原码
printf("%d ", 5 << 1);//10
return 0;
}
4.2 右移操作符
1)逻辑右移:右边抛弃,左边补0;
2)算术右移:右边抛弃,左边补该原值的符号位
在C语言中采用什么右移方式是由编译器决定的,但是大多数编译器都采用的是算术右移
举个简单例子:(-1)<< 2
注意:在使用移位操作符时,不能移动负数位,例如1>>-3这种写法是错误的。
5. 位操作符
& ----- 按位与
| ----- 按位或
^ ----- 按位异或
~ ----- 按位取反
对于位操作符,操作数必须是整数!!
5.1 & -- 按位与
用法:对应二进制为同时为1才为1,有0则为0。(二进制位)
举个简单例子:3 & 5,求它的结果
int main()
{
int a = 3;
int b = 5;
//a & b
//00000000000000000000000000000011 3的补码
//00000000000000000000000000000101 5的补码
//
//&---同时为1才为1,有0则为0;
//00000000000000000000000000000001 按位与之后得到的补码
//00000000000000000000000000000001 原码//1
printf("%d\n", a & b);//1
return 0;
}
5.2 | -- 按位或
用法:有1则为1,同时为0才为0。
举个简单例子:-10 | 5,求其结果
int main()
{
int a = -10;
int b = 5;
//a | b
// //-3
//10000000000000000000000000001010 原码
//11111111111111111111111111110101 反码
//11111111111111111111111111110110 补码
//5
//00000000000000000000000000000101 补码
//有1则为1,同时为0才为0
//11111111111111111111111111110110 (-10)补码
//00000000000000000000000000000101 ( 5)补码
//11111111111111111111111111110111 补码
//10000000000000000000000000001000
//10000000000000000000000000001001 原码
printf("%d\n", a | b);//-9
return 0;
}
5.3 ^ -- 按位异或
用法:相同为0,相异为1。
举个简单例子:-2 ^ 5,求其结果。
int main()
{
int a = -2;
int b = 5;
//-2
//10000000000000000000000000000010 原码
//11111111111111111111111111111101 反码
//11111111111111111111111111111110 补码
//5
//00000000000000000000000000000101 补码
//-2 ^ 5
// //相同为0,相异为1
//11111111111111111111111111111110 补码
//00000000000000000000000000000101 补码
//11111111111111111111111111111011 补码
//10000000000000000000000000000100
//10000000000000000000000000000101 补码//-5
printf("%d\n", a ^ b);//-5
return 0;
}
5.4 ~ -- 按位取反
用法:对每一位进行取反
举个例子:~0,求结果。
int main()
{
int a = 0;
//00000000000000000000000000000000
// ~a
//11111111111111111111111111111111 补码
//10000000000000000000000000000000
//10000000000000000000000000000001 原码
printf("%d\n", ~a);//-1
}
练习题:
1、不能创建临时变量(第三个变量),实现两个整数的交换。
//^ 相同为0,相异为1
//3^3 = 0;
//011
//011
//000---0
//0^3 = 3;
//000
//011
//011---3
//3^3^5=5;
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("交换前:a = %d b = %d\n", a, b);
a = a ^ b;
b = a ^ b;//a^b^b = a
a = a ^ b;//a^a^b = b;
printf("交换后:a = %d b = %d\n", a, b);
return 0;
}
分析:根据按位异或的用法,相同为0,相异为1;可以知道a ^ a = 0,0 ^ b = b,即a ^ a ^ b = b
2.编写代码实现:求⼀个整数存储在内存中的⼆进制中1的个数
int main()
{
int a = 0;
scanf("%d", &a);
int i = 0;
int count = 0;
while (i < 32)
{
//a&(1<<i)//统计第i位是否是1
if (a & (1 << i))//如果没有1了,这个按位与的结果则为0
{
count++;
}
i++;
}
printf("%d\n", count);
return 0;
}
这里,我们可以看另外一种方法:
int main()
{
int n = 0;
scanf("%d", &n);
int count = 0;
while (n)
{
n = n & (n - 1);//每次减少1个1
count++;
}
printf("%d ", count);
return 0;
}
6. 单目操作符
在上述分类中,我们已经了解了大部分操作符,在单目操作符只有 &和 *没有做详细的解释,这个部分我们将会和指针练习起来,所以在介绍指针的时候再介绍。
7. 逗号表达式
其实这个逗号就是我们日常使用的逗号,它的作用主要是分隔代码。
基本格式:
exp 1, exp 2, exp 3, .....
逗号表达式从左向右执行,逗号表达式的结果取决于最后一个表达式的结果
举个简单例子:
int main()
{
int a = 5;
int b = 3;
if (a > b, b = a + 1, a++, a > 0)//这里的结果a>0,只要满足就可进行下一语句
//这里逗号将表达式隔开,但最终的计算结果取决于最后一个表达式
{
a = a + b;
}
printf("%d\n", a);//那么a>0满足,这里的a=12
return 0;
}
这样是不是更加容易理解了呢?相信聪明的你们肯定理解了!
8. 下标访问[ ]和函数调用( )
8.1 下标访问操作符[ ]
在数组中,我们经常通过使用数组名+[ ]来访问数组中的每一个元素
所以[ ]的操作数为:数组名和元素下标
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
int num = arr[3];//[]就是下标引用操作符,访问数组中下表为3的元素
printf("%d\n", num);
return 0;
}
8.2 函数调用操作符
我们在函数篇章中,通常使用函数名+( )+实参,去调用函数
函数的操作数可以是一个也可以是多个:第一个操作数是函数名,第二个操作数是实参变量1 等等
int Add(int x, int y)
{
return x + y;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
int ret = Add(a, b);//这里( )就是函数调用操作符,调用函数
//三个操作数:函数名Add 实参变量a 实参变量b
printf("%d\n", ret);
return 0;
}
9. 结构体成员访问操作符
9.1 结构体
对于结构体,简单来说就是元素的集合体。
举个通俗点的例子:把结构体看作学生,那么其中的元素可以是学生的年龄、身高、体重、成绩等
同理:把结构体看作商品,那么其中的元素可以是商品的价格,商品的质量等
这就是结构体,相当于描述一个物理具体的细节。
9.1.1 结构体的基本结构
struct s
{
menber-list;
}variable-list;
这既是结构体的基本结构,也是结构体的声明
描述一个学生:
struct student
{
char name[];//姓名
int age;//年龄
double high;//身高
int arr[20];//学号
};//分号不能丢
9.1.2 结构体的定义和初始化
//结构体声明
struct s
{
int x;
int y;
}p1;//p1为结构体变量,这里的定义p1是全局变量
//定义结构体变量
struct s p2;
//结构体初始化
struct s p3 = { 10,20 };
struct stu //声明类型---结构体
{
char name[20];//姓名
int age;//年龄
double high;//身高
};
struct p
{
struct s s5;//结构体的嵌套
struct stu stu5;
};
int main()
{
//结构体创建学生s1(结构体变量)
struct stu s1 = { "lisi",16,1.78 };//初始化
//不按顺序初始化 即 . + 元素名称
struct stu s2 = { .high = 1.75,.age = 25,.name = "zhangsan" };
//结构体嵌套初始化
struct p p1 = { {10,20},{"wangwu",16,1.89} };//按顺序初始化
struct p p2 = {.s5.y=10,.stu5.age=22,.stu5.name ="lisi"};//不按顺序初始化};
return 0;
}
9.2 结构体成员访问操作符
9.2.1 结构体直接访问
结构体成员的访问是由 (.)操作符进行访问的
struct s
{
int x;
int y;
}s2 = {10,20};
int main()
{
struct s s1 = { .x = 15,.y = 20 };//结构体直接访问
return 0;
}
9.2.2 结构体间接访问
间接访问在后面的篇章中讲解。
10.操作符的优先级和结合性
10.1 优先级
我们在学小学数学的时候,在加减乘除运算中,老师会告诉我们先算乘除再算加减。例如:3+4*5我们首先会算4*5在计算加法,这就是优先级的体现。在C语言中,也是存在优先级的。
这里我们可以通过连接去查看:
C 运算符优先级 - cppreference.comhttps://zh.cppreference.com/w/c/language/operator_precedence
10.2 结合性
举个简单例子:3+4*5先算的时4*5,如果我把这个表达式变一变(3+4)*5,这个时候就先算3+4,这就体现的结合性。结合性决定了执行的顺序,左结合(左边先算),右结合(右边先算)。大部分运算符是左结合(从左到右执行),少数运算符是右结合(从右到左执行),比如赋值运算符( = )。下面就说一下经常使用的操作符的结合性高低吧~
1)圆括号 ()
2)自增(++)自减(--)
3)单目运算符 (+、-)
4)乘法(*) 除法(/)
5)加法(+) 减法(-)
6)关系运算符(<、>等)
7)赋值运算符(=)
11. 表达式求值
11.1 整型提升
在C语言中,整形算术运算总是至少以默认的整型类型的精度来进行计算。
为了获得这个精度,在进行运算时,字符型和短整型的操作数在使用之前会被转化为普通整形,这就是整型提升。
int main()
{
char a = 67;
char b = 46;
char c = a + b;
//这里a和b会先发生整形提升
//由于接收的类型为字符型
//a和b就会发生截断再存储
return 0;
}
如何进行整体提升呢?
1)有符号整数:按照变量的类型的符号位进行提升
2)无符号整数:高位补0
举个例子:
int main()
{
char a = 5;
//5 -- 整形
//00000000000000000000000000000101
//00000101 //发生截断,char类型占1个字节==8个bit位
char b = 126;
//126
//00000000000000000000000001111110
//01111110 //发生截断
char c = a + b;
//00000101 -- a
//01111110 -- b
//a+b要发生整形提升再进行运算
//char是有符号,那么补位补变量类型的符号位
//a为正数,最高位为0补0
//00000000000000000000000000000101 -- a发生整形提升
//b为正数,最高位补0
//00000000000000000000000001111110 -- b发生整形提升
//a+b
//00000000000000000000000010000011
//发生截断
//10000011
//%d是打印有符号整形,即再次发生整形提升
//11111111111111111111111110000011 -- 补码
//10000000000000000000000001111100
//10000000000000000000000001111101 -- 原码
printf("%d\n", c);//-125
return 0;
}
11.2 算数转换
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换。
如果某个操作数的类型在上面这个列表中排名靠后,那么首先要转换为另外一个操作数的类型后执行运算。
那么操作数部分到这里就差不多了,由于操作符的内容比较多,所以时间晚了点,求谅解一下啦~
下一节就是指针部分了, 内容也是比较多,所以可能要晚一点啦~ 我们下期再见