1.操作符分类
操作符分为:算术操作符,移位操作符,位操作符,赋值操作符,单目操作符,关系操作符,逻辑操作符,条件操作符,逗号操作符,下标引用、函数调用和结构成员。
下面让我来为大家一一详解
2.算术操作符
+ - * / %
这五个操作符想必大家都已经用的得心应手了,不过我还是要来提一下 / 和 % 需要注意的一些小细节,我们直接来看代码
对于 / 操作符如果两个基数都为整数,执行整数除法,而只要有浮点数,执行的就是浮点数除法
以上这种写法为什么会报错呢?
这是因为 % 操作符的两个操作数必须为整数,返回的是整除后的余数。除了 % 操作符之外,其他的几个操作符都可以用于整数和浮点数。
3.移位操作符
<< 左移操作符
>> 右移操作符
注:移位操作符的操作数只能是整数
整数在内存中存储的是二进制位的补码,移位操作符移动的是存储在内存中的补码,要学习它我们首先要了解什么是原码,反码,补码。
3.1原码,反码,补码
整型的二进制可以用原码,反码和补码来表示,而计算机中是不能识别负号的,所以计算机中用补码的形式来存储正数和负数,规定最高符号位是0,表示正数,最高符号位是1,表示负数,正数的原码,反码,补码相同,而负数需要进行计算,其中反码 = 原码的最高符号位不变,其他位按位取反;补码 =反码 + 1
下面我们拿 12 和 -12来为大家举个栗子:
12 ---整数,在C语言可以存放到 int 类型的变量中,int 类型是4个字节,32个比特位
00000000000000000000000000001100 ---原码
00000000000000000000000000001100 ---反码
00000000000000000000000000001100 ---补码
正数的原码,反码,补码相同
-12
10000000000000000000000000001100 ----原码
111111111111111111111111111111110011 ----反码(原码的符号位不变,其他位按位取反)
111111111111111111111111111111110100 ----补码(反码 + 1)
负数的反码 = 原码的最高符号位不变,其他位按位取反 补码 =反码 + 1
3.2左移操作符
移位规则:左边抛弃,右边补零
我们来看个栗子
#include<stdio.h> int main() { int m = 12; int n = m << 1; printf("n=%d", n); return 0; }
我们一起来用上面的方法计算一下会输出多少
12,正数补码与原码相同
00000000000000000000000000001100 ---补码
向左移动一个一个比特位,左边抛弃,右边补零
00000000000000000000000000011000 = 24(补码原码相同)
下面看个图先
通过多组计算我们得到了一个有趣的现象:用左移操作符,每左移一位都可以对原数乘以2
3.3右移操作符
移位规则:首先右移运算分为两种:
1.逻辑移位:左边用0填充,右边丢弃
2.算数移位:左边用原该值的符号位填充,右边丢弃
其中到底用哪一种,主要是取决编译器的,大多数用的都是算数移位。今天我们要讲解的也是这个
我们还是来举个栗子
#include<stdio.h> int main() { int m = 12; int n = m >> 1; printf("n=%d", n); return 0; }
我们运用上面的规则来计算一下会输出多少
12,正数补码与原码相同
00000000000000000000000000001100 ---补码
向右移动一个比特位,左边用原该值的符号位填充,右边丢弃
00000000000000000000000000000110 = 6(原码补码相同)
右移操作符也有一个有趣的现象,我们来看图
通过计算我们发现:用右移操作符,每右移一位都可以对原数除以2
警告 :对于位移运算符,不要移动负数位,这个是标准未定义的
例如:
4.位操作符
& 按位与
| 按位或
^ 按位异或
注:他们的操作数必须是整数
4.1按位与 &
使用规则:补码二进制位同为1时才为1,不同为 0
下面来看实例:
#include<stdio.h> int main() { int m = 12; //000000000000000000000000000001100(原码补码相同) int n = -6; //100000000000000000000000000000110(原码) //111111111111111111111111111111001(反码) //111111111111111111111111111111010(补码) int i = m & n; //000000000000000000000000000001100(m补码) //111111111111111111111111111111010(n补码) //m&n //000000000000000000000000000001000 = 8(i的补码,正数等于原码) printf("i=%d", i); return 0; }
4.2按位或 |
使用规则:二进制位同为0时才为0,不同时为1
下面来看实例:
#include<stdio.h> int main() { int m = 12; //000000000000000000000000000001100(原码补码相同) int n = -6; //100000000000000000000000000000110(原码) //111111111111111111111111111111001(反码) //111111111111111111111111111111010(补码) int i = m | n; //000000000000000000000000000001100(m补码) //111111111111111111111111111111010(n补码) //m|n //111111111111111111111111111111110(i补码,负数) //111111111111111111111111111111101(i反码) //100000000000000000000000000000010 = -2(i原码) printf("i=%d", i); return 0; }
4.3按位异或 ^
使用规则:二进制位相同为0,不同为1
下面来看实例:
#include<stdio.h> int main() { int m = 12; //000000000000000000000000000001100(原码补码相同) int n = -6; //100000000000000000000000000000110(原码) //111111111111111111111111111111001(反码) //111111111111111111111111111111010(补码) int i = m ^ n; //000000000000000000000000000001100(m补码) //111111111111111111111111111111010(n补码) //m^n //111111111111111111111111111110110(i补码,负数) //111111111111111111111111111110101(i反码) //100000000000000000000000000001010 = -10(i原码) printf("i=%d", i); return 0; }
想必看到这儿大家对这三个位操作符的使用就很清楚了吧,那么下面我们来看一道奇怪的题目
4.3 奇怪的题目:不能创建临时变量(第三个变量),实现两个数的交换
我猜你跟我当时一样,一时半会有点儿想不出来,下面我用两个方法来求解:
方法一:加减法
#include<stdio.h> int main() { int a = 3; int b = 6; a = a + b; b = a - b; a = a - b; printf("a=%d b=%d", a, b);//a=6,b=3 return 0; }
方法二:按位异或
#include<stdio.h> int main() { int a = 3; //00000000000000000000000000000011(补码) int b = 6; //00000000000000000000000000000110(补码) a = a^b; //00000000000000000000000000000101(补码) b = a^b; //00000000000000000000000000000011 = 3(补码=原码) a = a^b; //00000000000000000000000000000110 = 6(补码=原码) printf("a=%d b=%d", a, b);//a=6,b=3 return 0; }
5.赋值操作符
赋值操作符可以让你给自己重新赋值,得到一个自己满意的值
比如:
#include<stdio.h> int main() { int height = 165;//身高 height = 180;//赋值 int weight = 150;//体重 weight = 120;//赋值 return 0; }
在这里我们要注意赋值和初始化的区别:
初始化是创建变量的同时给变量定义一个值,会导致系统分配一个新的存储空间
int a = 0; //初始化语句:依次给出变量类型,名称和初值 int b = 8;//同上 int c; //定义语句:依次给出变量类型,名称,但没有给出初值,严谨的说这被称为非初始化的定义
赋值是定义后给变量一个值,不会导致系统分配新的存储空间,而只是将一个已分配的存储空间内部所存储的值复制到另一个已分配的存储空间中
c = 6//赋值语句:不需要(也不能)给出变量的类型,因为所涉及的变量必定已经被定义过了 a = b//同上
顺便提一下,定义分为两种:
1.非初始化的定义(如c),这不是个好习惯,不推荐。
2.初始化(如a,b),这是个好习惯,值得推荐。
5.1连续赋值问题
赋值操作符可以连续使用,比如:
#include<stdio.h> int main() { int a = 6; int b = 8; int c = 5; a = b = c + 3;//连续赋值 }
连续赋值的代码阅读体验感极差 ,是不推荐的,同样的语意我们不妨分开来写
#include<stdio.h> int main() { int a = 6; int b = 8; int c = 5; b = c + 3; a = b; }
这样的写法是不是更加一目了然,且易于尝试,这是推荐的
5.2复合赋值符
+= -= *= /= %= >>= <<= &= |= ^=
这些运算符都可以写成复合的效果,比如
#include<stdio.h> int main() { int a = 12; a += 12; //相当于a=a+12 return 0; }
其他几个的使用都是一个道理,这样写可以是代码更简洁
6.单目操作符
6.1单目操作符介绍
! 逻辑反操作
- 负值
+ 正值
& 取地址
sizeof 操作数的类型长度(以字节为单位)
~ 对一个数的二进制按位取反
-- 前置、后置--
++ 前置、后置++
* 间接访问操作符(解引用操作符)
(类型) 强制类型转换
我们选几个来讲解一下
1.逻辑反操作符 !
#include<stdio.h> int main() { int a = 3; int b = 0; if (a) printf("666\n"); if (!b)//条件为假成立 printf("888\n"); return 0; }//输出666 //888
2.对一个二进制位按位取反 ~
#include <stdio.h> int main() { int a = 12; //00000000000000000000000000001100(补码) int b = ~a; //11111111111111111111111111110011(补码) //11111111111111111111111111110010(反码) //10000000000000000000000000001101 = -13(原码) printf("b=%d", b); return 0; }
3. ++ 和 -- 操作符
#include<stdio.h> int main() { int a = 3; int b = ++a;//前置++,先++,后使用 //a = a + 1, b = a; printf("a=%d b=%d\n", a, b);//4 4 int a = 3; int b = a++;//后置++,先使用,后++ //b = a, a = a + 1; printf("a=%d b=%d\n", a, b);//4 3 int a = 3; int b = --a;//前置--,先--,后使用 //a = a - 1, b = a; printf("a=%d b=%d\n", a, b);//2 2 int a = 3; int b = a--;//后置--,先使用,后-- //b = a, a = a - 1; printf("a=%d b=%d\n", a, b);//2 3 return 0; }
4.强制类型转换操作符 类型)
#include <stdio.h> int main() { float a = 3.14f; int b = (int)a;//将a从float类型强制转化为int类型 printf("b=%d", b);//b=3 return 0; }
6.2sizeof和数组
sizeof()可以求变量(类型)所占空间的大小(以字节为单位)
举个栗子(32位平台下)
我们通过代码来看用sizeof求数组大小的一些细节
注意 sizeof(数组名)是求整个数组的大小
再来举起第二个栗子:
这里函数传参传过去的是数组名,也就是数组首元素的地址,所以在32位平台下的大小是4
7.关系操作符
> >= < <= != ==
!= 用于测试”不相等“,== 用于测试”相等“ 这几个运算符都比较简单想必大家都能理解
注意:在编译过程中 ==(相等) 和 = (赋值)不小心写错导致的错误!
8.逻辑操作符
&& 逻辑与
|| 逻辑或
逻辑操作符用来判断真假很常见,&&是两边都为真为真,|| 是只要有一边为真就为真
我们来两道例题:
#include <stdio.h> int main() { int i = 0,a=0,b=2,c =3,d=4; i = a++ && ++b && d++; printf("a = %d\n b = %d\n c = %d\nd = %d\n", a, b, c, d); return 0; }//输出1 2 3 4
这里为什么会输出1 2 3 4呢?
这是因为逻辑与和逻辑或会发生”短路现象“:逻辑与和逻辑或操作符都是从左往右依次执行,对于&&如果前操作数为假(0),那么后面的式子就不会执行,为真(非0)则继续执行;II 也是一样的道理如果前式为真,那么后面的式子就不会执行,为假则会继续执行。
这里a++是后置++,先使用再++,a=0后,后面的式子就不会判断了,所以a使用后++为1,b,c,d的值不变所以就会输出1 2 3 4
再来看另一个同理的题目:
#include <stdio.h> int main() { int i = 0,a=0,b=2,c =3,d=4; i = a++||++b||d++; printf("a = %d\n b = %d\n c = %d\nd = %d\n", a, b, c, d); return 0; }//输出1 3 3 4
同样的道理,我相信有友们都能理清楚。
9.条件操作符
exp1 ? exp2 : exp3
我们分别用if语句和条件表达式来求两个数中的较大值:
//if语句 if(a>1) { b=6; } else { b=-6; } //条件表达式 b = (a>1)?6:-6
这样看来有时候使用条件操作符会比if语句更加简洁
10.逗号表达式
exp1,exp2,exp3,exp...
执行规则:逗号表达式,从左向右依次执行。整个表达式的结果是最后一个表达式的结果。
我们还是举个栗子,来看看怎样使用:
#include <stdio.h> int main() { int a = 1; int b = 2; int c = (a > b, a = b + 10, a, b = a + 1); // 0 , a=12 ,a=12, b=13 printf("%d\n", c); return 0; }//最终输出最后一个表达式的结果13.
11.下标引用,函数调用和结构成员
11.1下标引用操作符[ ]
操作数:一个数组名 + 一个索引值
int arr[10];//创建数组 arr[9] = 10;//引用数组中第十个元素赋值为10 //[ ]的两个操作数是arr和9
11.2函数调用操作符
接受一个或者多个操作数:第一个操作数是函数名,剩余的操作数就是传递给函数的参数。
#include <stdio.h> void test1() { printf("happy\n"); } int main() { test1(); //()作为函数调用操作符。 return 0; }
11.3访问结构成员
访问一个结构的成员
. 结构体。成员名
-> 结构体指针->成员名
#include<stdio.h> struct S { int data[1000]; char buf[100]; }; void print1(struct S ss) { int i = 0; for (i = 0; i < 10; i++) { printf("%d ", ss.data[i]); } printf("%s\n", ss.buf); } void print2(struct S* ps) { int i = 0; for (i = 0; i < 10; i++) { printf("%d ",ps->data[i]); } printf("%s\n", ps->buf); } int main() { struct S s = { {1,2,3},"haha" }; print1(s); print2(&s); return 0; }
12.表达式求值
表达式求值的顺序一部分是由操作符的优先级和结合性决定。
同样,有些表达式的操作数在求值的过程中可能需要转换为其他类型。
12.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中
整型提升是按照变量的数据类型的符号位来提升的
//负数的整形提升 char c1 = -1; 变量c1的二进制位(补码)中只有8个比特位: 1111111 因为 char 为有符号的 char 所以整形提升的时候,高位补充符号位,即为1 提升之后的结果是: 11111111111111111111111111111111 //正数的整形提升 char c2 = 1; 变量c2的二进制位(补码)中只有8个比特位: 00000001 因为 char 为有符号的 char 所以整形提升的时候,高位补充符号位,即为0 提升之后的结果是: 00000000000000000000000000000001 //无符号整形提升,高位补0
下面来看一个栗子
#include <stdio.h> int main() { char a = 0xb6; //10110110=a //整型提升后 //11111111111111111111111110110110(a最高符号位是1,所以补1) short b = 0xb600; //1011011000000000=b //整型提升后 //11111111111111111011011000000000 int c = 0xb6000000; if (a == 0xb6)//会发生整型提升不会再等于0xb6 printf("a"); if (b == 0xb600)//会发生整型提升 printf("b"); if (c == 0xb6000000)//int类型不会发生整型提升 printf("c"); //最终输出c return 0; }
这里a,b要进行整形提升,但是c不需要整形提升 a,b整形提升之后,变成了负数,所以表达式 a==0xb6 , b==0xb600 的结果是假,但是c不发生整形提升,则表达式 c==0xb6000000 的结果是真.
栗子X2:
int main() { char c = 1; printf("%u\n", sizeof(c)); printf("%u\n", sizeof(+c)); printf("%u\n", sizeof(-c)); return 0; } //只要short和char类型的运算,就会发生整型提升。
这里c只要参与表达式运算,就会发生整形提升,表达式 +c ,就会发生提升,所以 sizeof(+c) 是4个字节.表达式 - c 也会发生整形提升 , 所以 sizeof( - c) 是4个字节, 但是 sizeof(c) , 就是 1 个字节.
12.2算数转换
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算数转换
1.long double 2.double 3.float 4.unsigned long int 5.long int 6.unsigned int 7.int
从下往上是由低精度到高精度,如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运算。
注意:算数转换要合理,不然会有一些潜在的问题,比如
float f = 3.14;//double类型转换为float类型 int num = f;//float类型转换为int类型,这是隐式转换,会有精度丢失
12.3操作符的属性
复杂表达式的求值有三个影响的因素。
1. 操作符的优先级
2. 操作符的结合性
3. 是否控制求值顺序。
两个相邻的操作符先执行哪个,取决于他们的优先级。如果两者的优先级相同,取决于他们的结合性。 至于操作优先级有友们可以在CSDN里搜索一下某个大佬的表格,每次要用的时候就看看,时间长了那些常用的就记住了。
好了以上就是今天的全部内容了,对友友们有帮助的话不妨三连加关注走一波,后期会持续更新C语言干货。