操作符详解
操作符的分类
操作符分为:
- 算数操作符:+、-、*、/、%
- 赋值操作符:=、+=、-=、/=、*=、%=、<<=、>>=、&=、|=、^=
- 移位操作符:<<、>>
- 位操作符:&、|、^、~
- 逻辑操作符:&&、||
- 关系操作符:>、<、==、!=、<=、>=
- 单目操作符:&、+、-、~、*、++、–、siezeof(类型)、!
- 条件操作符:? :
- 下标引用操作符:[]
- 函数调用操作符:()
- 逗号操作符:,
- 结构成员访问:.、->
算数操作符:
算数操作符不用多说了,就相当于数学里的加减乘除,但有一个需要注意的:%,取模(取余)操作符,它只能使用与整数或者整数的运算。
赋值操作符
赋值操作符里,需要注意的是类似于:+=、-=这类操作符,它实际上是:
a+=b;
//实际为
a=a+b;
**********
a-=b;
a=a-b;
移位操作符
位移操作符分为:左移
和右移
。
左移操作符(<<):将值所对应的二进制数,进行左移。
操作对象<<移的位数
int a=2;
a=a<<2;//这里a是操作对象,2是移的位数
这段代码是将a的值左移两位,也就是将0010
左移两位变成1000
。它输出的值则为:8
有的人看到这可能会疑惑,它左移后,右边的0是怎么来的呢?是这样的:
左移操作符,将值的二进制数左移相应的位数,移动多少位,右边就补上多少的0。再比如:
int a=12;
//0000 1100,这是12的二进制数
a=a<<3;
这里将12左移三位,那么得到的值是:96,而96对应的二进制数位: 0110 0000
,与12的二进制数相比,1左移了三位,右边也补上了3个0。
右移操作符(>>):将值所对应的二进制位进行右移。
右移操作符和左移操作符是存在区别的,它分为:
- 逻辑右移:将二进制的值右移,左边补0 。
- 算术右移:将二进制的值右移,左边根据原二进制的值的符号位进行补充。
我想逻辑右移很好理解,与左移是一样的,但是算术右移是怎么一回事呢?
看下边:
int a=4;
//00000000000000000000000000000100 补码
a=a>>2;
将a的值右移两位,得到:1(00000000000000000000000000000001,补码),这里因为4
的二进制位00000000000000000000000000000100
的符号位是0(32个bit位的最前面的数字,1为负,0为正),所以左边补0 。
接下来我们再来个例子,观察一下。
int a=-4;
//10000000000000000000000000000100 原码
//11111111111111111111111111111100 补码
a=a>>2;
此时输出的结果为:-1(11111111111111111111111111111111,补码)。
原码、反码、补码
这里提一下原码、反码、补码。
原码:最高位作为符号位,其余位表示值。
比如:1
的原码00000000000000000000000000000001
而-1
的原码10000000000000000000000000000001
。这里的最高位的数字就是最左边的数字,它表示符号位,0位正,1为负。因为1和-1都是整型,所以它的二进制数一共有八位,如果是char类型,它只占1字节,它的二进制数的位数则只有8位。
反码:如果是正数,则它的反码与原码相同,如果是负数,则它的反码是原码的符号位不变,其余位按位取反。
比如:1
的原码是00000000000000000000000000000001
,它的反码也是00000000000000000000000000000001
。-1
的原码为10000000000000000000000000000001
,则它的反码为:11111111111111111111111111111110
补码:如果是正数,则它的补码与原码相同,如果是负数,则它的补码是反码加一。
比如:-1
的反码是11111111111111111111111111111110
则它的补码是11111111111111111111111111111111
。不管是左移运算还是右移运算,移动的二进制码都是补码。
位操作符
位操作符有三个:
- 按位与(&):都为1则为1,否则为0
- 按位或(|):有1则为1,否则为0
- 按位异或(^):不同则为1,相同则为0
- 按位取反(~):0变1,1变0(它也是单目运算符)
我们先来看个例子,按位与:
int a=2;//0010
int b=1;//0001
int c=a&b;//0000
此时,c为0。
再来看按位或的例子:
int a=2;//0010
int b=1;//0001
int c=a|b;//0011
此时c的值为3。
再来看按位异或的例子:
int a=12;//1100
int b=2;//0010
int c=a^b;//1110
此时c的值为:
再来看按位取反的例子:
int a=12;//00000000000000000000000000001110
a=~a;//取反后为11111111111111111111111111110011
此时a的值为
看到这里,有的小伙伴可能会有点卡壳了,先不急,我一一道来。
int a=12;//00000000000000000000000000001110
a=~a;//取反后为11111111111111111111111111110011
因为在计算机中,数值的二进制码都是以补码的形式存储的,所以对值进行位运算时都是对值的补码进行操作的。
这里a最开始等于12,因为他是正数,所以它的原反补都是一样的00000000000000000000000000001110
,当对它进行取反运算后,我们得到的二进制码是11111111111111111111111111110011
通过观察,我们会发现,这是一个负数的补码,由于负数的补码是原码取反加一,所以我们要把他转换位原码的形式10000000000000000000000000001101
此时计算一下就能得出-13。
逻辑操作符
逻辑操作符分为:
- 逻辑与(&&):都为1(真)则为1(真),否则为0(假)
- 逻辑或(||):有1(真)则为1(真),否则为0(假)
这里就不用二进制的数进行举例了,我们上代码会更好理解:
#include<stdio.h>
int main()
{
int a=1;
int b=3;
if(a<2&&b>2)//如果a小于2与b大于2同时成立,则为真,进入if语句,记住,时同时成立。
{
printf("yes");
}
return 0;
}
#include<stdio.h>
int main()
{
int a=5;
int b=3;
if(a>3||b>3)只要a大于3和b大于3有一个成立的,就进入if语句
{
printf("yes");
}
return 0;
}
关系操作符
这部分我觉得不用过多的阐述,相信各位小伙伴们看一眼大概就知道这些符号是什么意思了。
- 大于 >
- 小于 <
- 小于等于 <=
- 大于等于 >=
- 等于==
- 不等于 !=
单目操作符
单目操作符:只操作一个对象的操作符
&、+、-、~、*、++、–、siezeof(类型)、!
- 取地址符(&)
- 正号(+)
- 负号(-)
- 按位取反(~)
- 间接引用操作符(*),指针里会提到
- 自增(++)
- 自减(–)
- 计算操作数的大小(sizeof),或者说计算操作数所占的字节大小。
- 逻辑取反操作符(!)
取地址操作符我想小伙伴们应该不陌生吧,在使用scanf函数的时候是不是经常漏掉?
正负号操作符、按位取反操作符我想也不需要我赘述了,但是这里需要注意一下,按位取反(~)
和逻辑取反(!)
,前者是在二进制数中,0变1,1变0的,后者是逻辑取反,这里稍微解释下逻辑取反:
#include<stdio.h>
int main()
{
int a=1;
while(a)
{
printf("yes");
}
return 0;
}
*******************
#include<stdio.h>
int main()
{
int a=1;
while(!a)
{
printf("yes");
}
return 0;
}
在第一段代码中,当a为真(true)时,进入循环,输出yes,并且在循环中没有代码可以改变a的值,导致a永远为真,所以这是一个死循环,第二段代码中,加了一个!,意思为当a为假时,进入循环,但是a的值为1,所以不会进入循环。
siezeof操作符,用于获取特定类型或对象所占用的内存空间大小的运算符
#include<stdio.h>
int main()
{
int a=10;
printf("%zd",sizeof(a));
return 0;
}
这里计算得出:
sizeof计算的是该操作对象的类型所占的内存大小,与操作对象的值无关。
再来个例子:
#include<stdio.h>
int main()
{
int a[10]={1,2,3,4,5,6,7,8,9,10};
printf("%zd",sizeof(a));
return 0;
}
这里计算出来的结果为:
因为数组可以理解为同一类型变量的集合,所在该代码中共有10个int类型的变量,一个int类型的变量占的内存大小为4字节,10个就为40字节。
条件操作符
条件操作符也可以叫“三目操作符”。
直接上例子来讲解:
#include<stdio.h>
int main()
{
int a=2;
int b=1;
printf("%d",a>b?1:0;);
return 0;
}
这段代码的意思为:如果a>b为真,则输出1否则输出0。
下标引用操作符、函数调用操作符
下标引用操作符:当我们创建完一个数组后,想要访问数组中某一位置的值,就需要用到下标引用操作符。
int a[10]={1,2,3,4,5,6,7,8,9,10};
//这里我们要输出下标位3的元素的值。在这之前我得先访问该位置
printf("%d",a[3]);//通过下标引用符,访问下标为3的元素的值,并输出
函数调用操作符:在调用涵数时使用。
#include<stdio.h>
int add(int a)
{
a++;
return a;
}
int main()
{
int a =1;
int ret=add(a);
printf("%d",ret);
return 0;
}
逗号操作符
逗号表达式:exp1, exp2, exp3, …expN
逗号表达式,就是⽤逗号隔开的多个表达式。
逗号表达式,从左向右依次执⾏。整个表达式的结果是最后⼀个表达式的结果
int a = 1;
int b = 2;
int c = (a>b, a=b+10, a, b=a+1);//逗号表达式
c是多少?,这里c的值就为b=a+1的值:13
a = get_val();
count_val(a);
while (a > 0)
{
//业务处理
//...
a = get_val();
count_val(a);
}
如果使⽤逗号表达式,改写:
while (a = get_val(), count_val(a), a>0)
{
//业务处理
}
结构成员访问
c语言提供了char、int、double、float等内置类型,但是只有这些内置类型变量还是不够的,假设我想描述学⽣,描述⼀本书,这时单⼀的内置类型是不⾏的。
描述⼀个学⽣需要名字、年龄、学号、⾝⾼、体重等;
描述⼀本书需要作者、出版社、定价等。C语⾔为了解决这个问题,增加了结构体这种⾃定义的数据类型,让程序员可以⾃⼰创造适合的类型。
结构体的声明:
struct 结构体标签
{
成员变量1
成员变量2
.
.
.
}结构体变量;
描述一个学生:
struct student
{
char name[20];
int age;
int height;
double weight;
};
结构体变量的定义和初始化
struct Point
{
int x;
int y;
}p1;//声明类型的同时,定义变量p1
struct Point p2;定义结构体变量p2
struct Point p3={10,20};//结构体变量初始化
**********************************
struct Stu
{
char name[20];
int age;
};
struct Stu s1={"lisi",10};//初始化
struct Stu s2={.age=10,.name="lisi"};//指定顺序初始化
***********************************
struct Point
{
int x;
int y;
};
struct Node
{
int data;
struct Point p;
struct Node*next;
}n1={1,{10,20},NULL};//结构体嵌套初始化
struct Node n2={2,{10,20},NULL};//结构体嵌套初始化
结构体成员访问
结构体成员的直接访问:我们通过(.)点操作符进行访问。
#include<stdio.h>
struct Point
{
int x;
int y;
}n={10,20};
int main()
{
printf("%d %d",n.x,n.y);//输出x,y的值
return 0;
}
结构体成员的间接访问:有时候我们得到的不是⼀个结构体变量,⽽是得到了⼀个指向结构体的指针。如下所示。
#include<stdio.h>
struct Point
{
int x;
int y;
};
int main()
{
struct Point p={10,20};
struct Point *ptr=&p;
ptr->x=1;//修改x的值
ptr->y=2;//修改y的值
printf("%d %d",ptr->x,ptr->y);//输出x,y的值
return 0;
}