一、操作符的分类
1、操作符的分类
(1)算数操作符:+、-、、/、%
(2)移位操作符:<< 、>>
(3)位操作符:& 、|、^
(4)赋值操作符:=、+=、-=、=、/=、%=、<<=、>>=、&=、|=、^=
(5)单目操作符:!、++、–、&、*、+、-、~、sizeof、(类型)
(6)关系操作符:>、>=、<、<=、==、!=
(7)逻辑操作符:&&、||
(8)条件操作符:? :
(9)逗号操作符:,
(10)小标操作符:[]
(11)函数调用:()
(12)结构成员访问:. 、->
二、二进制和进制转换
在我们生活中经常都能听到有二进制、八进制、十进制和十六进制这样的说法。我们经常会产生很多疑问,这些进制都是什么东西,或者说都有什么作用?其实二进制、八进制、十进制、十六进制是数值的不同表现形式而已,本质其实就是一个数。我们可以用15这个数来举一个例子:
15的二进制:1111
15的八进制:17
15的十进制:15
15的十六进制:F
//八进制前要写:0
//十六进制前要写:0x
看似有很多什么进制啊其实本质上都是一个数,只是它的表达方式不一样。
虽说有那么多个进制,我们首要讲的还是二进制:我们在之前学校加法呀我们都知道的一件事是满十要进一位,换成二进制也是一个道理(满2进一位)由此我们可以知道像:1101这一类就是二进制的数字了,是不是非常简单理解。
二进制转十进制
二进制和十进制之间是可以相互转转化的,其实进制与进制间都是可以相互转换的。下面我们举个例子来说明二进制和十进制之间的相互转换,我们用123这个数来讲解吧!我们知道123的十进制值是一百二十三,那为什么是这个值呢?其实十进制每一位都是有权重的,十进制的数字从右向左是个位、十位、百位…,分别每位的权重是十的零次方、十的一次方和十的二次方。
二进制和十进制是类似的,只不过二进制的每一位权重,从右到左十二的零次方、二的一次方…
十进制转换二进制数字
十进制的125转换的二进制:1111101
二进制转换成八进制
八进制的数字每一位都是0~7之间的数,我们举一个例子就可以直接理解了。例子如下:二进制的01101011,换成八进制就是:0153,以0开头的数字,就会被当作八进制。
二进制转十六进制
十六进制的数字每一位是0 ~ 9、a ~ f的,其实你可以这样看a表示10,b表示11以此类推f表示16.如二进制的01101011,转换成十六进制就是:0x6b,十六进制表示的时候前面要加上0x。
三、原码、反码、补码
整数的二进制有三种表达方式,即原码、反码和补码。
有符号整型的三种表达方法均有符号位和数值位两个部分组成,在二进制中最高位的1被当作符号位,而1后面的位就是数值位(剩余的全部)符号位:0表示正,1表示负。
正整数的原码、反码、补码都是相同的。
负整数的三种表达方式各不相同。
原码:直接将数值转换成二进制的数值。
反码:符号位保持不变,数值位每一位按位取反。
补码:反码 + 1
*补码想得到原码可以取反后加一
对于整型来说:数据存放内存中其实存放的是补码。
四、移位操作符
左移操作符:<<
右移操作符:>>
*移位操作符的操作数只能是整数
1、左移操作符
左移操作符的规则是:左边抛弃,右边补0。
#include <stdio.h>
int main()
{
int num1 = 10;
int num2 = num1<<1;
printf("num2= %d\n",num2);
printf("num1= %d\n",num1);
return 0;
}
我们会很奇怪为啥左移一位,得到的数是20
我们要转换成二进制的形式来理解,如下:
2、右操作符
右操作符规则:有两种
(1)逻辑右移:左边用0填充,右边丢弃
(2)算术右移:左边用原该值的符号位填充,右边丢弃
#include <stdio.h>
int main()
{
int num1 = 10;
int num2 = num1>>1;
printf("num2= %d\n",num2);
printf("num1= %d\n",num1);
return 0;
}
以上跟上面左移是一样的,要遵循右移的规则
若num1是负数的时候,方法如下:
- 对于移位运算符,不要移动负数位,这个是标准未定义的。
五、位操作符
位操作符有:
1、& //按位与
2、| //按位或
3、^ //按位异或
4、~ //按位取反
1、按位与 &
&就是将两个二进制的数,有0为0,两个都是1才为1。
来举个例子:
2&3
2 -> 00000000 00000000 00000000 00000010
3 -> 00000000 00000000 00000000 00000011
2&3 -> 00000000 00000000 00000000 00000010
//转换成十进制结果就为2
2、按位或 |
|就是两个二进制的数,有1为1,连个数都是0才为0。
来举个例子:
2|3
2 -> 00000000 00000000 00000000 00000010
3 -> 00000000 00000000 00000000 00000011
2|3 -> 00000000 00000000 00000000 00000011
//转换成十进制结果为3
3、按位异或 ^
^就是两个二进制的数,相同为0,不同为1。
举个例子:
2^3
2-> 00000000 00000000 00000000 00000010
3-> 00000000 00000000 00000000 00000011
2^3-> 00000000 00000000 00000000 00000001
//转换成十进制结果为1
4、按位取反 ~
~是指将二进制形式的数按位置取反
//例如0
0-> 00000000 00000000 00000000 00000000
~0 -> 11111111 11111111 11111111 1111111
六、单目操作符
单目操作符有:!、++、–、&、*、+、-、~、sizeof、(类型)
这里应该就不用过多赘述了这些操作符应该都了解了。
七、逗号操作符
逗号表达式,就是用逗号隔开的多个表达式。
逗号表达式,从左向右依次执行。整个表达式的结果是最后一个表达式的结果。
例如:
//代码1
int a = 1;
int b = 2;
int c = (a>b,a=b+10,a,b=a+1);
//代码2
if(a = b + 1,c=a / 2,d > 0)
八、下标访问[] ,函数调用()
1、[] 下标引用操作符
操作数:一个数组名 + 一个索引值(下标)
1、 int arr[10];
2、 arr[9] = 10;
//[]两个操作数是arr和9
2、函数调用操作符
接受一个或者多个操作数:第一个操作数是函数名,剩余的操作数就是传递给函数的参数。
#include <stdio.h>
voif test1()
{
printf("haha\n");
}
void test2(const char *str)
{
printf("%s\n",str);
}
int main()
{
test1();
test2("hello world");
return 0;
}
这里test1中的()就是作为函数调用操作符
test2中的()就是函数调用操作符
九、结构成员访问操作符
1、结构体
在C语言中已经提供了很多内置类型:如int、char、float、double等。但只有这些内置的类型还是不够的,比如我要描述一个学生的姓名、身高和体重,又或者是一本书的出版社、作者和定价。这个我们仅仅是用内置类型是不行的。所以C语言为了解决这个问题,增加了结构体这种自定义的数据类型,让程序员可以自己创造合适的类型。
结构是一些值的集合,这些值称为成员变量。结构体中的每一个成员是不同的变量。如标量、数组、指针、甚至是其他的结构体。
(1)、结构体的声明
描述一个学生:
struct student
{
char nume[20]; //学生的名字
int age; //学生的年龄
char sex[2]; //学生的性别
}; //注意这里有个分号
(2)、结构体变量的定义和初始化
//代码1:变量的定义
struct one
{
int x;
int y;
}p1; //声明类型的同时定义变量p1
struct one p2; //定义结构体变量p2
//代码2:初始化
struct two p2 = {10,20};
struct two //类型声明
{
char name[20];
int age;
};
struct two s1 = {"zhangsan",20); //初始化
struct two s2 = {.age = 20, .name = "lisi"}; //指定顺序初始化
//代码3
struct three
{
int data;
struct point p;
struct three next;
}n1 = {10,{4,5},NULL}; //结构体嵌套初始化
struct three p2 = {20,{4,5},NULL};
2、结构体成员访问操作符
(1)、结构体成员的直接访问
结构体成员的直接访问是通过点操作符(.)访问的。上面代码2我们也能看到。点操作符接受两个操作数。如下列代码所示:
#include <stdio.h>
struct Point
{
int x;
int y;
}p = {1,2};
int main()
{
printf("x:%d y:%d\n",p.x,p.y);
return 0;
}
综上所述使用方法是:结构体变量.成员名
(2)、结构体成员的间接访问
有时候我们得到的不是一个结构体变量,而是得到了一个指向结构体的指针。如下所示:
#include <stdio.h>
struct Point
{
int x;
int y;
};
int main()
{
struct Point p = {3,4};
struct Point *prt = &p;
prt -> x = 10;
prt -> y = 20;
printf("x = %d y = %d\n",prt->x,prt->y);
reurn 0;
}
综上所述使用方法为:结构体指针->成员名
十、操作符的属性:优先级、结合性
C语言的操作符有两个重要的属性:优先级、结合性,这两个属性决定了表达式求值的计算顺序。
1、优先级
优先级指的是,一个表达式包含多个运算符,那个运算符先执行。每个运算符的优先级是不一样的。
13 + 4 * 5
如上我们可以看到表达式中既有加法又有乘法,这个小学都应该知道先算乘法再算加法,所以说乘法的优先级高于加法所以就先算乘法后算加法。
2、结合性
如果两个运算符的优先级相同,优先级没办法确定先要计算哪个的时候,这个时候就看结合性了,则根据运算符的左结合,还是右结合,决定执行顺序。大部分运算符是左结合(从左到右执行),少部分运算符是右结合(从右到左执行),比如赋值运算符。
参考:https://zh.cppreference.com/w/c/language/operator_precedence
十一、表达式求值
1、整型提升
C语言中整型算术运算中总是至少以缺省(默认)整数类型的精度来进行的。
为了获取这个精度,表达式中的字符和短整形操作数在使用前被转换为普通整型,这种转换称为整型提升。
整型提升的意义:
表达式的整型运算要在CPU的相应运算器件内执⾏,CPU内整型运算器(ALU)的操作数的字节⻓度⼀般就是int的字节⻓度,同时也是CPU的通⽤寄存器的⻓度。因此,即使两个char类型的相加,在CPU执⾏时实际上也要先转换为CPU内整型操作数的标准⻓度。通⽤CPU(general-purpose CPU)是难以直接实现两个8⽐特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以,表达式中各种⻓度可能⼩于int⻓度的整型值,都必须先转换为int或unsigned int,然后才能送⼊CPU去执⾏运算。
//例如
char a,b,c;
...
a = b + c;
b和c的值被提升为普通整型后,再进行加法运算。加法运算完成后,结果将被截断,然后再存储于a中。
如何进行整体提升呢?
1、有符号整数提升是按照变量的数据类型的符号位来提升的。
2、无符号整数提升,高位补0。
//负数的整型提升
char a1 = -1;
//a1的二进制位中只有8个比特位
//11111111
//因为char为有符号的char,所以整型提升的时候,高位补充符号位,即1
//提升后为:11111111111111111111111111111111
//正数的整型提升
char a2 = 1;
//变量a2的二进制位中只有8个比特位:
//00000001
//因为char为有符号的char,所以整型提升的时候,高位补充符号位,即0
//提升后为:00000000000000000000000000000001
//无符号整型提升,高位补0
2、算术转换
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系为寻常算术转换
1、long
2、double
3、float
4、unsigned long int
5、long int
6、unsigned int
7、int
如果某个操作数的类型在上面这个列表中排名靠后,那么首先要转换为另一个操作数的类型后执行运算。