请回答c语言-操作符【入门】

学习操作符之前我们先对操作符分类,还和之前的初始c语言中一样

操作符分类:

  1. 算术操作符
  2. 移位操作符
  3. 位操作符
  4. 赋值操作符
  5. 单目操作符
  6. 关系操作符
  7. 逻辑操作符
  8. 条件操作符
  9. 逗号表达式
  10. 下标引用、函数调用和结构成员

1.算数操作符

+ - * / %

算数操作符本身很简单,不过有几个注意事项

  1. 除了% 操作符之外,其他的几个操作符可以作用于整数和浮点数。
  2. 对于/ 操作符如果两个操作数都为整数,执行整数除法。而只要有浮点数执行的就是浮点数除法。
  3. % 操作符的两个操作数必须为整数。返回的是整除之后的余数。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都表示不同的权重

第4位 第3位 第2位 第1位
8 4 2 1

2.2 整数的二进制表达形式

此外我们还得搞清楚整数的三种二进制表达形式

  • 原码

  • 反码

  • 补码

对于整数的3种形式我们有这样一个结论:

image-20211111195950022

下面对一个整数5来举例

image-20211111200054762

再对-5来举例

image-20211111200311163

从这个例子我们可以看到

原码在第32位中的0和1用来表示符号位,0表示正数,1表示负数,反码同样

反码相当于原码的符号位不变,其他位按位取反得到的就是反码

补码就是反码最低位+1

用VS调试看内存我们就可以直观看到

image-20211111200939196

这里的ffff就是16进制的-1也就是二进制下32个1,因为16进制下一个f相当于15,因此说明内存存储的方式是利用补码

2.3 使用方式

总结一下:

  • 一个整数在被存入内存时,存储方式是利用的补码
  • 打印或者使用的时候,利用的是原码

为什么这么规定呢?这涉及到加法器和减法器的原理,后面再展开

2.4 左移操作符

移位规则:

  • 左边抛弃、右边补0

2.4.1 左移操作符的效果

int main()
{
	int a = 5;
	int b = a << 2;
	printf("%dn",a);
	printf("%dn", b);

	return 0;
}

image-20211111201219590

2.4.2 分析二进制位

image-20211111201524713

注:当然a是不会被改变的

再举个负数的例子:

int main()
{
	int a = -5;
	int b = a << 2;
	printf("%dn",a);
	printf("%dn", b);
	return 0;
}

负数左移之后,打印出来是多少?

image-20211111223130139

过程是先把-5补码左移2位,然后按照负数的规律转换成原码,打印的时候是打印原码结果

image-20211111223432324

image-20211111224005042

2.5 右移操作符

移位规则:

有点不同,稍微复杂

首先我们思考一下右移运算本身分两种:

  1. 逻辑移位:

左边用0填充,右边丢弃

  1. 算术移位:

左边用原该值的符号位填充,右边丢弃

到底如何移动,取决于编译器的不同,我们常用的编译器是算术右移

比如说VS2019

2.5.1 右移操作符的效果

注:这里的前提是在VS2019中,也即算术移位的前提下

image-20211111203141623

int main()
{
	int a = -5;
	int b = a >> 1;
	printf("%dn",a);
	printf("%dn", b);
	return 0;
}

image-20211111202649398

具体过程就不细给了,按照规则,和左移稍微有一点区别

2.6 移位操作符注意事项

注:

对于移位运算符,不要移动负数位,这个是标准未定义的。

int num = 10;
num>>-1;//error

image-20211111203503858

3. 位操作符

已知位操作符有:

& //按位与
| //按位或
^ //按位异或

注:他们的操作数必须是整数。

问:位操作符用哪种二进制位储存形式进行运算?

答:因为都是内存运算,所以都是用的补码

位操作符简单来说就是两个整数的每一位之间互相比较

3.1 位与

输入a 输入b 结果
0 0 0
1 0 0
0 1 0
1 1 1

看示例

int main()
{
	int a = 3;
	int b = -5;
	int c = a & b;
	printf("%d", c);
	return 0;
}

image-20211111225140813

3.2 位或

输入a 输入b 结果
0 0 0
1 0 1
0 1 1
1 1 1
int main()
{
	int a = 3;
	int b = -5;
	int c = a | b;
	printf("%d", c);
	return 0;
}

image-20211111225936974

3.3 异或

两个整数的二进制位互相异或,其中二进制位

相同为0

相异为1

输入a 输入b 结果
0 0 0
1 0 1
0 1 1
1 1 0
int main()
{
	int a = 3;
	int b = -5;
	int c = a ^ b;
	printf("%d", c);
	return 0;
}

image-20211111230139768

3.4 练习位操作符

小栗子1:

int main()
{
	int num1 = 1;
	int num2 = 2;
	printf("%dn",num1 & num2);
	printf("%dn", num1 | num2);
	printf("%dn", num1 ^ num2);
	return 0;
}

image-20211111230424208

小栗子2:

不能创建临时变量(第三个变量),实现两个数的交换。

3个解法:

int main()
{
	int a = 3;
	int b = 5;
	//1
	int c = 0;//临时变量
	printf("交换前: a=%d b=%dn", a, b);
	c = a;
	a = b;
	b = c;

	//2
	a = a + b;
	b = a - b;
	a = a - b;

	//3 - 异或
	a = a ^ b;
	b = a ^ b;//a ^ b ^ b
	a = a ^ b;//a ^ b ^ a

	printf("交换后: a=%d b=%dn", a, b);

	return 0;
}

可以发现第一个解法不符合要求,因为要创建临时变量,剩下两个方法可以巧妙解决,其中第三个方法最为巧妙,巧妙使用异或实现转换,可以尝试化成二进制举例验证一下

小栗子3

编写代码实现:求一个整数存储在内存中的二进制中1的个数。

想法:

我们说如果由一个数a&1,那么就能得出这个数字二进制最低位是0还是1

那么如果我循环中不断<<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的个数 = %dn", count);
	return 0;
}

//方法2:
#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的个数 = %dn", count);
	return 0;
}
//这个方法还能更加优化
//方法3:
#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的个数 = %dn", 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("%dn",sizeof(arr));//40字节---4*10

有求int类型数组或一个int的大小

printf("%dn",sizeof(int [10]));//40字节---4*10
printf("%dn",sizeof(int));//4
  • 小栗子:

    • 以下四个输出的是什么
void test1(int arr[])
{
	printf("%dn", sizeof(arr));//(3)
}
void test2(char ch[])
{
	printf("%dn", sizeof(ch));//(4)
}
int main()
{
	int arr[10] = { 0 };
	char ch[10] = { 0 };
	printf("%dn", sizeof(arr));//(1)
	printf("%dn", sizeof(ch));//(2)
	test1(arr);
	test2(ch);
	return 0;
}

image-20211114093751879

注意从函数中传过来的是指针,所以sizeof测的是 指针变量的大小,而主函数里面测的是整个数组中的大小

5.2 ~按位取反

int main()
{
	int a = 0;
	//~ 按(内存中补码的2进制)位取反
	//00000000000000000000000000000000
	//11111111111111111111111111111111 - 补码
	//11111111111111111111111111111110 - 反码
	//10000000000000000000000000000001 - 原码 --> -1
	printf("%dn", ~a);
	return 0;
}

有多组输入的时候

 while(~scanf("%d%d",&n,&m);

5.3 前置后置++ --

//++和--运算符
//前置++和--
int main()
{
	int a = 10;
	int x = ++a;
		//先对a进行自增,然后对使用a,也就是表达式的值是a自增之后的值。x为11。
		int y = --a;
	//先对a进行自减,然后对使用a,也就是表达式的值是a自减之后的值。y为10;
	return 0;
}
//后置++和--
int main()
{
	int a = 10;
	int x = a++;
	//先对a先使用,再增加,这样x的值是10;之后a变成11;
	int y = a--;
	//先对a先使用,再自减,这样y的值是11;之后a变成10;
	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 = %dn b = %dn c = %dnd = %dn", a, b, c, d);
	return 0;
}
  • 第一次的结果是:1 2 3 4

因为&&只要前面算出有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 = %dnb = %dnc = %dnd = %dn", a, b, c, d);
	return 0;
}

倘若这样一改就是2 3 3 5了

  • 第二次的结果是:1 3 3 4

因为算到++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.下标引用、函数调用和结构成员

  1. 下标引用操作符
    操作数:一个数组名 + 一个索引值
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]也能符合要求

  1. ( ) 函数调用操作符
    接受一个或者多个操作数:第一个操作数是函数名,剩余的操作数就是传递给函数的参数。
void test1()
{
	printf("函数调用test1()n");
}
void test2(const char* str)
{
	printf("%sn", str);
}
int main()
{
	test1(); //用()作为函数调用操作符。
	test2("Strength in Numbers");//用()作为函数调用操作符。
	return 0;
}
  1. 访问一个结构的成员

. 结构体.成员名
-> 结构体指针->成员名

还是熟悉的栗子:

在之前的博客请回答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 = %.1fn", pikachu.name, pikachu.id, pikachu.height, pikachu.weight);
	//->操作符
	struct Pokemon* ps = &pikachu;//结构体指针
	printf("name = %s id = %d height = %.1f weight = %.1fn", ps->name, ps->id, ps->height, ps->weight);
	printf("name = %s id = %d height = %.1f weight = %.1fn", (*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("%dn", c);//-125
	return 0;
}

在这样一个运算过程中

b和a的值被提升为普通整型,然后再执行加法运算。

加法运算完成之后,结果将被截断char类型的字节,然后再存储于a中。

11.1.4 如何进行整体提升呢?

答:整形提升是按照变量的数据类型的符号位来提升的

//负数的整形提升
char c1 = -1;
变量c1的二进制位(补码)中只有8个比特位:
1111111
因为 char 为有符号的 char
//所以整形提升的时候,高位补充符号位,即为1
提升之后的结果是:
11111111111111111111111111111111
//正数的整形提升
char c2 = 1;
变量c2的二进制位(补码)中只有8个比特位:
00000001
因为 char 为有符号的 char
//所以整形提升的时候,高位补充符号位,即为0
提升之后的结果是:
00000000000000000000000000000001
//无符号整形提升,高位补0

下面来演示一下计算机如何根据这样的规则来实现之前的代码

int main()
{
	char a = 5;
	//截断
	//00000000000000000000000000000101
	//00000101

	char b = 126;
	//00000000000000000000000001111110
	//01111110
    
	//00000000000000000000000000000101 - a
	//00000000000000000000000001111110 - b
	//00000000000000000000000010000011
	//10000011 - c
	//当a和b相加的时候,a和b都是char类型
	//表达式计算的是就要发生整形提升
	char c = a + b;
	//10000011 - c  //c的符号位是1
	//11111111111111111111111110000011 - 补码
	//11111111111111111111111110000010 - 反码
	//10000000000000000000000001111101 -> -125
	printf("%dn", c);//-125打印用原码
	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("%un", sizeof(c));
	printf("%un", sizeof(+c));
	printf("%un", sizeof(-c));
	return 0;
}

image-20211114113036783

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("%dn", sizeof(s = a + 4));//-->2
	printf("%dn", s);//-->20
	return 0;
}

注:sizeof内部的表达式不是真实参与计算的,所以第二个s打印出来还是20

11.2.3 算术转换要注意的问题

注:算术转换要合理,要不然会有一些潜在的问题。

//隐式转换导致丢失精度
float f = 3.14;
int num = f;

11.3 操作符的属性

11.3.1 复杂表达式的求值有三个影响的因素

  1. 操作符的优先级
  2. 操作符的结合性
  3. 是否控制求值顺序。

两个相邻的操作符先执行哪个?取决于他们的优先级。如果两者的优先级相同,取决于他们的结合性

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;
//5 + 4 = 9 ?
//4 + 4 = 8 ?

注:同操作符的优先级只能决定自减–的运算在+的运算的前面,但是我们并没有办法得知,+操作符的左操作数的获取在右操作数之前还是之后求值,所以结果是不可预测的,是有歧义的。

代码3-非法表达式(该栗子来自《c和指针》)

int main()
{
	int i = 10;
	i = i-- - --i * (i = -3) * i++ + ++i;
	printf("i = %dn", i);
	return 0;
}

注:表达式在不同编译器中测试结果不同

代码4

int fun()
{
	static int count = 1;
	return ++count;
}
int main()
{
	int answer;
	answer = fun() - fun() * fun();
	printf("%dn", answer);//输出多少?
	return 0;
}

注:我们通过看代码发现每次func被调用之后的返回值都是不一样的
但是上述代码answer = fun() - fun() * fun(); 中我们只能通过操作符的优先级得知:先算乘法,再算减法。那编译器到底是从左向右调用还是先调用乘法呢?这就会得出不同的结果
函数的调用先后顺序无法通过操作符的优先级确定。

//代码5

#include <stdio.h>

int main()
{
	int i = 1;
	int ret = (++i) + (++i) + (++i);
	printf("%dn", ret);
	printf("%dn", i);
	return 0;
}

对于这样一个表达式,在VS2019和linux的gcc编译器下得出的结果是不同的,我们可以通过反汇编来看一下VS

image-20211114162238450

image-20211114162907853

linux底下就是这样的

image-20211114162951102

所以得出了不同的结果

小结:

我们在写代码的时候应该写出那种一眼就能明白的代码,而不是那种有歧义的问题代码,给自己也给他人添麻烦,所以说能加括号就加括号,能分步就不用混在一起,写出高质量的代码

总结:

花了很久时间总结了和学习了这篇笔记,老铁们有收获的话一定要给个赞,多多评论哦

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>