Bootstrap

十六、操作符详解

目录

1.操作符分类

2.算数操作符

3.移位操作符

4.位操作符

5.赋值操作符

6.单目操作符

7.关系操作符

8.逻辑操作符

9.条件操作符

10.逗号表达式

11.下标引用、函数调用和结构成员

12.表达式求值

1.隐式类型转换

2.算术转换

3.操作符的属性


1.操作符分类

算术操作符
移位操作符
位操作符
赋值操作符
单目操作符
关系操作符
逻辑操作符
条件操作符
逗号表达式
下标引用、函数调用和结构成员

2.算数操作符

/

整数的除法 1/2 --> 0

浮点数的除法 1.0/2 --> 0.5 1/2.0 1.0/2.0

% 计算的是整除后的余数

取模操作符的两端必须是整数

3.移位操作符

左移操作符:左边丢弃,右边补0

右移操作符:

(1)算数移位:右边丢弃,左边补原符号位(正数补0,负数补1)

(2)逻辑移位:右边丢弃,左边补0

移位操作符,移动的是二进制位

整数的二进制表示有3种

原码 反码 补码

左移×2,右移/2

vs2019编译器采用的是算数右移

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

4.位操作符

& - 按(2进制)位与

| - 按(2进制)位或

^ - 按(2进制)位异或

整数在内存中存的是二进制的补码。

a:00000000000000000000000000000011//3的原码-反码-补码相同

b: 10000000000000000000000000000101//-5的原码

11111111111111111111111111111010//-5的反码

11111111111111111111111111111011//-5的补码

a & b

就是a的补码与b的补码按位与(有0为0,两个都为1才为1)

00000000000000000000000000000011 //3的补码

11111111111111111111111111111011 //-5的补码

00000000000000000000000000000011 //a & b的补码

因为是正数,所以原码,补码,反码相同。

a & b为3

a | b

就是a的补码与b的补码按位或(有1为1,两个都为0才为0)

00000000000000000000000000000011 //3的补码

11111111111111111111111111111011 //-5的补码

11111111111111111111111111111011 //a | b的补码

11111111111111111111111111111010 //a | b的反码

10000000000000000000000000000101 //a | b的原码

a | b为-5

a ^ b

就是a的补码与b的补码按位异或(相同为0,相异为1)

00000000000000000000000000000011 //3的补码

11111111111111111111111111111011 //-5的补码

11111111111111111111111111111000 //a ^ b的补码

11111111111111111111111111110111 //a ^ b的反码

10000000000000000000000000001000 //a ^ b的原码

a ^ b为-8

总结:与0优先,或1优先,异或同0异1

一道变态的面试题:不能创建临时变量(第三个变量),实现两个数的交换。

int main()
{
	int a = 3;
	int b = 5;
	printf("交换前:a=%d b=%d\n", a, b);
	a = a + b;
	b = a - b;
	a = a - b;
	printf("交换后:a=%d b=%d\n", a, b);
	return 0;
}
int main()
{
	int a = 3;
	int b = 5;
	printf("交换前:a=%d b=%d\n", a, b);
	a = a ^ b;
	b = a ^ b;
	a = a ^ b;
	printf("交换后:a=%d b=%d\n", a, b);
	return 0;
}

3^3=0 --->a^a=0

//011

//011
//000

0^5=5 --->0^a=a

//000

//101

//101

//3^3^5=5

//3^5^3=5

//011

//101

=110

//011

=101

异或操作符支持交换率

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

//编写代码实现:求一个整数存储在内存中的二进制中1的个数。
//求补码的二进制中1的个数
int main()
{
	int a = 12;
	int i = 0;
	int count = 0;
	for ( i = 0; i < 32; i++)
	{
		if ((a>>i) & 1 )
		{
			count++;
		}
		
	}
	printf("a二进制中的1有:%d\n", count);
	return 0;
}
int main()
{
	int a = 8;
	int i = 0;
	int count = 0;
	for (i = 0; i < 32; i++)
	{
		
		if (a  & 1)
		{
			count++;
		}
		a = a >> 1;
	}
	printf("a二进制中的1有:%d\n", count);
	return 0;
}

5.赋值操作符

赋值操作符(=)是一个很棒的操作符,他可以让你得到一个你之前不满意的值。也就是你可以给自己重新赋值。
判断相等(==)

a = x = y+1; //连续赋值
x = y+1;

a = x;

这样的写法是不是更加清晰爽朗而且易于调试。

复合赋值符

+= a+=5; 等价 a=a+5;

-=

*=

/=

%=

>>= a>>=1; 等价 a=a>>1;

<<=

&=

|=

^=

这些运算符都可以写成复合的效果。

6.单目操作符

单目操作符,只有一个操作数

双目操作符 (a+b)+操作符有2个操作数

! 逻辑反操作

- 负值

+ 正值

& 取地址

sizeof 操作数的类型长度(以字节为单位)

~ 对一个数的二进制按位取反

-- 前置、后置--

++ 前置、后置++

* 间接访问操作符(解引用操作符)

(类型) 强制类型转换

int main()
{
	//int a = 0;
	~是按二进制位取反
	00000000000000000000000000000000 - 补码
	11111111111111111111111111111111 - ~a的补码
	11111111111111111111111111111110 - ~a的反码
	10000000000000000000000000000001 - ~a的原码
	~a为-1
	//printf("%d\n", ~a);

	int a = 3;
	//~是按二进制位取反
	//00000000000000000000000000000011 - 补码
	//11111111111111111111111111111100 - ~a的补码
	//11111111111111111111111111111011 - ~a的反码
	//10000000000000000000000000000100 - ~a的原码
	//~a为-4
	printf("%d\n", ~a);
	return 0;
}
int a = 13;
	a |= (1 << 1);
	printf("%d\n", a);//a=15

	//0000000000000000000000000001101
	//0000000000000000000000000000001
	//1<<1
	//0000000000000000000000000001111

    int a = 13;
	a |= (1 << 4);
	printf("%d\n", a);//a=29
	//0000000000000000000000000001101
	//0000000000000000000000000000001
	//1<<4
	//0000000000000000000000000011101
	a &= ~(1 << 4);
	printf("%d\n", a);//a=13
	//0000000000000000000000000011101
	//1111111111111111111111111101111
	//0000000000000000000000000001101

++ --

int main()
{
	int a = 3;
	int b = ++a;//前置++,先++,后使用
    //a=a+1,b=a;
	printf("%d\n", a);//4
	printf("%d\n", b);//4
	return 0;
}
int main()
{
	int a = 3;
	int b = a++;//后置++,先使用,后++
    //b=a,a=a+1;
	printf("%d\n", a);//4
	printf("%d\n", b);//3
	return 0;
}
int main()
{
	int a = 3;
	int b = --a;//前置--,先--,后使用
	printf("%d\n", a);//2
	printf("%d\n", b);//2
	return 0;
}
int main()
{
	int a = 3;
	int b = a--;
	printf("%d\n", a);//2
	printf("%d\n", b);//3
	return 0;
}

* 间接访问操作符(解引用操作符)

*p 等价 a

(类型) 强制类型转换

强制为int类型

把time_t(long long)类型强制转化为unsigned int 类型

sizeof a 写法可以

sizeof int 写法不行

函数在调用的时候括号不可以省略,sizeof是操作符可以省略。

strlen是库函数,是用来求字符串长度。

(1)40

(2)4/8 因为是x86环境所以是4

32位机器上的地址:32bit位 - 4byte,所以指针变量的大小是4个字节。

64位机器上的地址:64bit位 - 8byte,所以指针变量的大小是8个字节。

x86 是32位机器;

x64 是64位机器。

(3)10

(4)4/8 因为是x86环境所以是4

ch[ ]是指针,存放的是地址,指针不管是什么类型,大小都是4/8个字节。

7.关系操作符

>

>=

<

<=

!= 用于测试“不相等”
== 用于测试“相等”

在编程的过程中== 和=(赋值)不小心写错,导致的错误。
不是所有的对象都能用==来比较

“abc”==“abcdef” 这样写是在比较2个字符串的首字符的地址

两个字符串比较应该使用库函数strcmp。

8.逻辑操作符

逻辑操作符只关注真假。

&& 逻辑与 - 并且(都为真才真,一个为假就为假)

|| 逻辑或 - 或者(都为假才假,一个为真就为假)

区分逻辑与和按位与

1&2----->0

1&&2---->1

区分逻辑或和按位或
1|2----->3

1||2---->1

程序输出的结果是1 2 3 4

从a++开始算,因为a为0,为假,所以后面不用再进行计算。

如果&& 换成|| 输出的结果应该是2 2 3 4

从a++开始算,因为a为1,为真,所以后面不用再进行计算。

如果a=0 输出的结果应该是1 3 3 4

从a++开始算,因为a为1,为假,再计算++b,为真所以后面不用再进行计算。

&& 左边为假,右边就不计算了

|| 左边为真,右边就不计算了

9.条件操作符

条件操作符(三目操作符)

exp1 ? exp2 : exp3
exp1为真,则执行exp2。

exp1为假,则执行exp3。

10.逗号表达式

exp1, exp2, exp3, …expN
逗号表达式,就是用逗号隔开的多个表达式。

逗号表达式,从左向右依次执行。整个表达式的结果是最后一个表达式的结果。
 

11.下标引用、函数调用和结构成员

1.[ ] 下标引用操作符操作数:arr[7]

一个数组名 + 一个索引值

[ ] 是一个操作符

2. ( ) 函数调用操作符
接受一个或者多个操作数:第一个操作数是函数名,剩余的操作数就是传递给函数的参数。

3. 访问一个结构的成员

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

-> 成员名

#include <string.h>

struct Stu
{
	char name[20];
	int age;
	double score;
};

void set_stu(struct Stu* ps)
{
	//strcpy((*ps).name, "zhangsan");
	//(*ps).age = 20;
	//(*ps).score = 100.0;

	strcpy(ps->name, "zhangsan");
	ps->age = 20;
	ps->score = 100.0;
}

void print_stu(struct Stu* ps)
{
	printf("%s %d %lf\n", ps->name, ps->age, ps->score);
}

int main()
{
	struct Stu s = {0};
	set_stu(&s);
	print_stu(&s);

	return 0;
}

12.表达式求值

1.表达式求值的顺序一部分是由操作符的优先级和结合性决定。

2.同样,有些表达式的操作数在求值的过程中可能需要转换为其他类型

1.隐式类型转换

C的整型算术运算总是至少以缺省整型类型的精度来进行的。 (缺省 - 默认)

为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升

整型提升的意义:表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。

因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。

通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int,然后才能送入CPU去执行运算。

如何进行整体提升呢?

整形提升是按照变量的数据类型的符号位来提升的
//负数的整形提升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 a=5

	char b = 126;
	//00000000000000000000000001111110 - 补码
	//01111110       - char b=126
	//
	char c = a + b;
	 //00000101 - a
	//01111110 - b
	//此时就会发生整形提升
	//00000000000000000000000000000101  - a
    //00000000000000000000000001111110  - b
	//00000000000000000000000010000011  - a+b
	//10000011    -  c

	
	printf("%d\n", c);//-125
	//%d 打印整形,此时发生整形提升
	//11111111111111111111111110000011  -  补码(一直用的是补码,只不过正整数三码同一)
	//11111111111111111111111110000010  -  反码
	//10000000000000000000000001111101  -  原码
	//-125
	//char类型在归类的时候属于整形家族
	//只要是整形家族,在内存中存的都是补码.
	return 0;
}

下列代码中,a,b要进行整形提升,但是c不需要整形提升

a,b整形提升之后,变成了负数,所以表达式 a==0xb6 , b==0xb600 的结果是假,但是c不发生整形提升,则表达式 c==0xb6000000 的结果是真。程序输出c

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;
}

如果a改为 unsigned char 整形提升的时候,高位补的是0。

程序输出ac。


2.算术转换

如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换

long double

double

float

unsigned long int

long int

unsigned int

int
如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运算。例如 int+folat 则int会转换为folat再运算。

警告:但是算术转换要合理,要不然会有一些潜在的问题

3.操作符的属性

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

1. 操作符的优先级

2. 操作符的结合性

3. 是否控制求值顺序。

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

操作符优先级,从上到下,操作符的优先级的由高到低变化。

N/A 无结合性

L-R 从左向右结合

R-L 从右向左结合

是否控制求值顺序:

&& 逻辑与 左边为假,右边就不算了,影响了求值顺序。

&& 逻辑与 左边为真,右边就不算了。

?:条件操作符 表达式1为真,表达式2算;表达式1为假,表达式2不算,表达式3算。

,逗号表达式,从左往右依次计算,但最终起到结果作用的,是最后一个表达式的结果。

一些问题表达式
//表达式的求值部分由操作符的优先级决定。

//表达式1

a*b + c*d + e*f

1 3 2 5 4 按照这个顺序算,可以

1 4 2 5 3 这样也行

没有办法确定唯一计算路径
注释:代码1在计算的时候,由于*比+的优先级高,只能保证,*的计算是比+早,但是优先级并不能决定第三个*比第一个+早执行。
写代码尽量要写出唯一计算路径的代码。可以加()

//表达式2

c + --c;

例如 int c=2;

我们提前把c存到寄存器里面了,结果就是2+1 = 3;

如果没有提前准备好,结果就是1+1 =2;

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

//代码3-非法表达式

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;

}
这个代码有没有实际的问题?

有问题!

虽然在大多数的编译器上求得结果都是相同的。

但是上述代码 answer = fun() - fun() * fun(); 中我们只能通过操作符的优先级得知:先算乘法,再算减法。函数的调用先后顺序无法通过操作符的优先级确定。

;