学习操作符之前我们先对操作符分类,还和之前的初始c语言中一样
操作符分类:
算术操作符 移位操作符 位操作符 赋值操作符 单目操作符 关系操作符 逻辑操作符 条件操作符 逗号表达式 下标引用、函数调用和结构成员
1.算数操作符
+ - * / %
算数操作符本身很简单,不过有几个注意事项
除了%
操作符之外,其他的几个操作符可以作用于整数和浮点数。 对于/
操作符如果两个操作数都为整数,执行整数除法。而只要有浮点数执行的就是浮点数除法。 %
操作符的两个操作数必须为整数。返回的是整除之后的余数。10.0%3
int ret= 10 % 3 ;
int ret = 10 / 3 ;
double ret2 = 10 / 3.0 ;
2.移位操作符
2.1 二进制位
移位操作符这里就涉及了把一个整数化为二进制位,每一位的权重相当于2的1次方,相当于假如有四个1,则1 1 1 1每个1都表示不同的权重
2.2 整数的二进制表达形式
此外我们还得搞清楚整数的三种二进制表达形式
对于整数的3种形式我们有这样一个结论:
下面对一个整数5来举例
再对-5来举例
从这个例子我们可以看到
原码 在第32位中的0和1用来表示符号位,0表示正数,1表示负数,反码同样
反码 相当于原码的符号位 不变,其他位按位取反 得到的就是反码
补码 就是反码最低位+1
用VS调试看内存我们就可以直观看到
这里的ffff
就是16进制的-1也就是二进制下32个1,因为16进制下一个f
相当于15,因此说明内存存储的方式是利用补码
2.3 使用方式
总结一下:
一个整数在被存入内存时,存储方式是利用的补码 打印或者使用的时候,利用的是原码
为什么这么规定呢?这涉及到加法器和减法器的原理,后面再展开
2.4 左移操作符
移位规则:
2.4.1 左移操作符的效果
int main ( )
{
int a = 5 ;
int b = a << 2 ;
printf ( "%d\n" , a) ;
printf ( "%d\n" , b) ;
return 0 ;
}
2.4.2 分析二进制位
注:当然a是不会被改变的
再举个负数的例子:
int main ( )
{
int a = - 5 ;
int b = a << 2 ;
printf ( "%d\n" , a) ;
printf ( "%d\n" , b) ;
return 0 ;
}
负数左移之后,打印出来是多少?
过程是先把-5补码 左移2位,然后按照负数的规律转换成原码,打印的时候是打印原码 结果
2.5 右移操作符
移位规则:
有点不同,稍微复杂
首先我们思考一下右移运算 本身分两种:
逻辑移位:
左边用0填充,右边丢弃
算术移位:
左边用原该值的符号位填充,右边丢弃
到底如何移动,取决于编译器 的不同,我们常用的编译器是算术右移
比如说VS2019
2.5.1 右移操作符的效果
注:这里的前提是在VS2019中,也即算术移位的前提下
int main ( )
{
int a = - 5 ;
int b = a >> 1 ;
printf ( "%d\n" , a) ;
printf ( "%d\n" , b) ;
return 0 ;
}
具体过程就不细给了,按照规则,和左移稍微有一点区别
2.6 移位操作符注意事项
注:
对于移位运算符,不要移动负数位,这个是标准未定义的。
int num = 10 ;
num>> - 1 ;
3. 位操作符
已知位操作符有:
&
|
^
注:他们的操作数必须 是整数。
问:位操作符用哪种二进制位储存形式进行运算?
答:因为都是内存运算,所以都是用的补码
位操作符简单来说就是两个整数的每一位之间互相比较
3.1 位与
看示例
int main ( )
{
int a = 3 ;
int b = - 5 ;
int c = a & b;
printf ( "%d" , c) ;
return 0 ;
}
3.2 位或
int main ( )
{
int a = 3 ;
int b = - 5 ;
int c = a | b;
printf ( "%d" , c) ;
return 0 ;
}
3.3 异或
两个整数的二进制位互相异或,其中二进制位
相同为0
相异为1
int main ( )
{
int a = 3 ;
int b = - 5 ;
int c = a ^ b;
printf ( "%d" , c) ;
return 0 ;
}
3.4 练习位操作符
小栗子1:
int main ( )
{
int num1 = 1 ;
int num2 = 2 ;
printf ( "%d\n" , num1 & num2) ;
printf ( "%d\n" , num1 | num2) ;
printf ( "%d\n" , num1 ^ num2) ;
return 0 ;
}
小栗子2:
不能创建临时变量(第三个变量),实现两个数的交换。
3个解法:
int main ( )
{
int a = 3 ;
int b = 5 ;
int c = 0 ;
printf ( "交换前: a=%d b=%d\n" , a, b) ;
c = a;
a = b;
b = c;
a = a + b;
b = a - b;
a = a - b;
a = a ^ b;
b = a ^ b;
a = a ^ b;
printf ( "交换后: a=%d b=%d\n" , a, b) ;
return 0 ;
}
可以发现第一个解法不符合要求,因为要创建临时变量,剩下两个方法可以巧妙解决,其中第三个方法最为巧妙,巧妙使用异或实现转换,可以尝试化成二进制举例验证一下
小栗子3
编写代码实现:求一个整数存储在内存中的二进制中1的个数。
想法:
我们说如果由一个数a&
1,那么就能得出这个数字二进制最低位是0还是1
那么如果我循环中不断<<1再&1就可以计算出一个整数有几个1
# include <stdio.h>
int main ( )
{
int num = 10 ;
int count = 0 ;
while ( num)
{
if ( num % 2 == 1 )
count++ ;
num = num / 2 ;
}
printf ( "二进制中1的个数 = %d\n" , count) ;
return 0 ;
}
# include <stdio.h>
int main ( )
{
int num = - 1 ;
int i = 0 ;
int count = 0 ;
for ( i = 0 ; i < 32 ; i++ )
{
if ( num & ( 1 << i) )
count++ ;
}
printf ( "二进制中1的个数 = %d\n" , count) ;
return 0 ;
}
# include <stdio.h>
int main ( )
{
int num = 0 ;
int i = 0 ;
int count = 0 ;
scanf ( "%d" , & num) ;
while ( num)
{
count++ ;
num = num & ( num - 1 ) ;
}
printf ( "二进制中1的个数 = %d\n" , count) ;
return 0 ;
}
4.赋值操作符
赋值操作符可以改变之前的赋值
int weight = 120 ;
weight = 99 ;
double salary = 10000.0 ;
salary = 20000.0 ;
当然,赋值操作符可以连续赋值,但不是很建议这么写
a=x=y=100;
当然:常量不能赋值
##4.1 复合赋值符
+=
-=
*=
/=
%=
>>=
<<=
&=
|=
^=
这些运算符都可以写成复合的效果
int x = 0 ;
x = x+ 10 ;
x += 10 ;
5.单目操作符
已知单目操作符有:
!
-
+
&
sizeof
~
--
++
*
( type)
很多在之前的初识c语言 中都已经讲过,这里挑几个记一下关键点
5.1 sizeof
关于sizeof其实我们之前也已经见过了,可以求变量(类型)所占空间的大小。
常见的使用
有求数组的长度:
int sz= sizeof ( arr) / sizeof ( arr[ 0 ] ) ;
有求数组的大小
int arr[ 10 ] = { 1 , 2 , 3 , 4 , 5 , 6 } ;
printf ( "%d\n" , sizeof ( arr) ) ;
有求int类型数组或一个int的大小
printf ( "%d\n" , sizeof ( int [ 10 ] ) ) ;
printf ( "%d\n" , sizeof ( int ) ) ;
void test1 ( int arr[ ] )
{
printf ( "%d\n" , sizeof ( arr) ) ;
}
void test2 ( char ch[ ] )
{
printf ( "%d\n" , sizeof ( ch) ) ;
}
int main ( )
{
int arr[ 10 ] = { 0 } ;
char ch[ 10 ] = { 0 } ;
printf ( "%d\n" , sizeof ( arr) ) ;
printf ( "%d\n" , sizeof ( ch) ) ;
test1 ( arr) ;
test2 ( ch) ;
return 0 ;
}
注意从函数中传过来的是指针,所以sizeof
测的是 指针变量的大小,而主函数里面测的是整个数组中的大小
5.2 ~
按位取反
int main ( )
{
int a = 0 ;
printf ( "%d\n" , ~ a) ;
return 0 ;
}
有多组输入的时候
while ( ~ scanf ( "%d%d" , & n, & m) ;
5.3 前置后置++ --
int main ( )
{
int a = 10 ;
int x = ++ a;
int y = -- a;
return 0 ;
}
int main ( )
{
int a = 10 ;
int x = a++ ;
int y = a-- ;
return 0 ;
}
注:注意值的变化
6.关系操作符
关系操作符:
>
>=
<
<=
!=
==
小结
还是不要搞错=
和==
7.逻辑操作符
逻辑操作符有哪些:
&& 逻辑与
|| 逻辑或
区分逻辑与 和按位与 区分逻辑或 和按位或
1 & 2 -- -- -> 0
1 && 2 -- -- > 1
1 | 2 -- -- -> 3
1 || 2 -- -- > 1
举个栗子:
int main ( )
{
int i = 0 , a = 0 , b = 2 , c = 3 , d = 4 ;
i = a++ && ++ b && d++ ;
i = a++ || ++ b || d++ ;
printf ( "a = %d\n b = %d\n c = %d\nd = %d\n" , a, b, c, d) ;
return 0 ;
}
因为&&只要前面算出有0就不算后面的执行了,所以a后置加加->a为1,而b和d不执行
int main ( )
{
int i = 0 , a = 1 , b = 2 , c = 3 , d = 4 ;
i = a++ && ++ b && d++ ;
printf ( "a = %d\nb = %d\nc = %d\nd = %d\n" , a, b, c, d) ;
return 0 ;
}
倘若这样一改就是2 3 3 5了
因为算到++b的时候已经是真了,所以d++就不算下去了,于是自增的只有a和b
小结:逻辑操作符只关注真与假
&&在遇到0(假)之后就不算后面的执行了
||肯定是遇到1(真)之后
8. 条件操作符
exp1 ? exp2 : exp3
之前就写过两数之间的最大值
max = (a > b ? a : b);
9.逗号表达式
逗号表达式,就是用逗号隔开的多个表达式。 逗号表达式,从左向右依次执行。整个表达式的结果是最后一个表达式的结果。
小栗子
int a = 1 ;
int b = 2 ;
int c = ( a> b, a= b+ 10 , a, b= a+ 1 ) ;
应该从左到右依次执行,因为逗号表达式不只是算最后一个逗号里面的语句最后c应该是13
10.下标引用、函数调用和结构成员
下标引用操作符 操作数:一个数组名 + 一个索引值
int arr[ 10 ] ;
arr[ 9 ] = 10 ;
比如说我想要打印数组中下标为8的数
int main ( )
{
int arr[ 10 ] = { 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 } ;
printf ( "%d" , arr[ 7 ] ) ;
printf ( "%d" , 7 [ arr] ) ;
return 0 ;
}
计算机在计算的时候是arr[7]->*(arr+7)->7[arr]
所以7[arr]也能符合要求
( ) 函数调用操作符 接受一个或者多个操作数:第一个操作数是函数名,剩余的操作数就是传递给函数的参数。
void test1 ( )
{
printf ( "函数调用test1()\n" ) ;
}
void test2 ( const char * str)
{
printf ( "%s\n" , str) ;
}
int main ( )
{
test1 ( ) ;
test2 ( "Strength in Numbers" ) ;
return 0 ;
}
访问一个结构的成员
.
结构体.成员名 ->
结构体指针->成员名
还是熟悉的栗子:
在之前的博客请回答c语言-初识c语言(下)【入门】 的17.结构体出现过的栗子
struct Pokemon
{
char name[ 20 ] ;
int id;
float height;
float weight;
char fighting_type[ 20 ] ;
char species[ 15 ] ;
} ;
int main ( )
{
struct Pokemon pikachu = { "Pikachu" , 25 , 0.4 , 6.0 , "electric" , "mouse pokemon" } ;
printf ( "name = %s id = %d height = %.1f weight = %.1f\n" , pikachu. name, pikachu. id, pikachu. height, pikachu. weight) ;
struct Pokemon * ps = & pikachu;
printf ( "name = %s id = %d height = %.1f weight = %.1f\n" , ps-> name, ps-> id, ps-> height, ps-> weight) ;
printf ( "name = %s id = %d height = %.1f weight = %.1f\n" , ( * ps) . name, ( * ps) . id, ( * ps) . height, ( * ps) . weight) ;
return 0 ;
}
11. 表达式求值
表达式求值的顺序一部分是由操作符的优先级和结合性决定。 同样,有些表达式的操作数 在求值的过程中可能需要转换为其他类型。
这些过程往往是看不到的,但是计算机却在做着这些工作
11.1 隐式类型转换
###11.1.1 要有一个整型提升的概念
C的整型算术运算总是至少以缺省整型类型的精度来进行的。 为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型 提升。
###11.1.2 整型提升的意义
表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。 因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。 通用CPU是难以直接实现两个8比特字节直接相加运算(虽然机器指令 中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int,然后才能送入CPU去执行运算。
11.1.3 整形提升后被截断
这个栗子看上去会认为相加是131实际上输出了-125,这说明了整形提升这件事的存在
int main ( )
{
char a = 5 ;
char b = 126 ;
char c = a + b;
printf ( "%d\n" , c) ;
return 0 ;
}
在这样一个运算过程中
b和a的值被提升为普通整型 ,然后再执行加法运算。
加法运算完成之后,结果将被截断 回char类型 的字节,然后再存储于a中。
11.1.4 如何进行整体提升呢?
答:整形提升是按照变量的数据类型的符号位 来提升的
char c1 = - 1 ;
变量c1的二进制位( 补码) 中只有8 个比特位:
1111111
因为 char 为有符号的 char
提升之后的结果是:
11111111111111111111111111111111
char c2 = 1 ;
变量c2的二进制位( 补码) 中只有8 个比特位:
00000001
因为 char 为有符号的 char
提升之后的结果是:
00000000000000000000000000000001
下面来演示一下计算机如何根据这样的规则来实现之前的代码
int main ( )
{
char a = 5 ;
char b = 126 ;
char c = a + b;
printf ( "%d\n" , c) ;
return 0 ;
}
11.1.5 整形提升的栗子:
栗子一:
int main ( )
{
char a = 0xb6 ;
short b = 0xb600 ;
int c = 0xb6000000 ;
if ( a == 0xb6 )
printf ( "a" ) ;
if ( b == 0xb600 )
printf ( "b" ) ;
if ( c == 0xb6000000 )
printf ( "c" ) ;
return 0 ;
}
这段代码最后只输出了的是c说明a和b在存储和判断的时候被整形提升转换了,而c本来就是int类型,所以判断为真直接打印出来了
栗子二:
通过这个来看就很直观了
int main ( )
{
char c = 1 ;
printf ( "%u\n" , sizeof ( c) ) ;
printf ( "%u\n" , sizeof ( + c) ) ;
printf ( "%u\n" , sizeof ( - c) ) ;
return 0 ;
}
11.2 算术转换
黑线下面的类型发生运算的时候,会产生算数转换
11.2.1 寻常算术转换
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。
下面的层次体系称为寻常算术转换 。
long double
double
float
unsigned long int
long int
unsigned int
int
如果某个操作数的类型在上面这个列表中排名较低 ,那么首先要转换为另外一个操作数的类型后 执行运算。
这里的a的int类型要转换为上一级的float才能够参与运算,这个过程就叫做算术转换
int main ( )
{
int a = 3 ;
float f = 5.5 ;
float r = a + f;
return 0 ;
}
11.2.2 算术转换的栗子
int main ( )
{
short s = 20 ;
int a = 5 ;
printf ( "%d\n" , sizeof ( s = a + 4 ) ) ;
printf ( "%d\n" , s) ;
return 0 ;
}
注:sizeof
内部的表达式不是真实参与计算的,所以第二个s打印出来还是20
11.2.3 算术转换要注意的问题
注:算术转换要合理,要不然会有一些潜在的问题。
float f = 3.14 ;
int num = f;
11.3 操作符的属性
11.3.1 复杂表达式的求值有三个影响的因素
操作符的优先级 操作符的结合性 是否控制求值顺序。
两个相邻的操作符先执行哪个?取决于他们的优先级 。如果两者的优先级相同,取决于他们的结合性 。
11.3.2 操作符优先级
关于优先级高低的表可以参考这个博客C语言运算符优先级列表(超详细)
int main ( )
{
int a = 10 ;
int b = 20 ;
int c = a + b * 10 ;
int c = a + b + 10 ;
return 0 ;
}
11.3.3 问题表达式
然而虽然已经规定了这些个表达式的优先级,但是有时候有些表达式还是无法表达出同一个确定的结果
下面给出一些存在问题的表达式
代码1
a* b + c* d + e* f
注:代码1在计算的时候,由于*
比+
的优先级高,只能保证*
的计算是比+
早,但是优先级并不能决定第三个*
比第一个+
早执行。
所以表达式的计算顺序就可能是:
a* b
c* d
a* b + c* d
e* f
a* b + c* d + e* f
或者
a* b
c* d
e* f
a* b + c* d
a* b + c* d + e* f
注:如果表达式之间存在互相影响,那么顺序不同就会产生问题
代码2
c + -- c;
注:同操作符的优先级只能决定自减–的运算在+的运算的前面,但是我们并没有办法得知,+操作符的左操作数的获取在右操作数之前还是之后求值,所以结果是不可预测的,是有歧义的。
代码3-非法表达式(该栗子来自《c和指针》)
int main ( )
{
int i = 10 ;
i = i-- - -- i * ( i = - 3 ) * i++ + ++ i;
printf ( "i = %d\n" , i) ;
return 0 ;
}
注:表达式在不同编译器中测试结果不同
代码4
int fun ( )
{
static int count = 1 ;
return ++ count;
}
int main ( )
{
int answer;
answer = fun ( ) - fun ( ) * fun ( ) ;
printf ( "%d\n" , answer) ;
return 0 ;
}
注:我们通过看代码发现每次func被调用之后的返回值都是不一样的 但是上述代码answer = fun() - fun() * fun();
中我们只能通过操作符的优先级得知:先算乘法,再算减法。那编译器到底是从左向右调用还是先调用乘法呢?这就会得出不同的结果 函数的调用先后顺序无法通过操作符的优先级确定。
//代码5
# include <stdio.h>
int main ( )
{
int i = 1 ;
int ret = ( ++ i) + ( ++ i) + ( ++ i) ;
printf ( "%d\n" , ret) ;
printf ( "%d\n" , i) ;
return 0 ;
}
对于这样一个表达式,在VS2019和linux的gcc编译器下得出的结果是不同的,我们可以通过反汇编来看一下VS
linux底下就是这样的
所以得出了不同的结果
小结:
我们在写代码的时候应该写出那种一眼就能明白的代码,而不是那种有歧义的问题代码,给自己也给他人添麻烦,所以说能加括号就加括号,能分步就不用混在一起,写出高质量的代码
总结:
花了很久时间总结了和学习了这篇笔记,老铁们有收获的话一定要给个赞,多多评论哦