【C语言详解】操作符

2021-11-18-

摘要

操作符

总结

目录

操作符分类

算术操作符

移位操作符

位操作符

赋值操作符

单目操作符

关系操作符

逻辑操作符

条件操作符

逗号表达式

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

算术操作符

	+	-	*	 /	%
  1. % 为取余也叫模,它的操作数只能是整数,而其他的操作符的操作数既可以为整数也可以为浮点数。
  2. 操作数有浮点数时,则进行算术转换进行浮点数运算。

移位操作符

左移操作符	<<	
右移操作符	>>

移位操作符对应的是在计算机种的存储形式:补码 (因此移位操作符的操作数仅仅只能是整数。)

左移操作符

对相应的二进制数进行操作。

规则:高位舍去,低位补零。

image-20220412094928520

拓展

左移意味着每一个二进制位的权重会增加1

例如:整形char

假设一个char类型的数据为 6

那么它的二进制表示为

	00000110  	//6
左移一位后:
	00001100	//12

显然任何二进制数都可以表示为:

Σ ∣ ∑ i = 0 n Σ|\sum\limits_{i=0}^{n} Σi=0n​ k * 2i

(k为0或者1)

那么左移一位后,在没有高位溢出的情况下,每一位的权重都增加了1

那么做简单变形:

Σ ∣ ∑ i = 1 n + 1 Σ|\sum\limits_{i=1}^{n+1} Σi=1n+1 k * 2i 根据分配律可得: Σ ∣ ∑ i = 0 n Σ|\sum\limits_{i=0}^{n} Σi=0n​ (k * 2i ) * 2

即乘2操作

注意:仅仅是在无溢出的情况下

右移操作符

移位规则:

  • 算数右移

左边用符号位填充,右边舍弃。

  • 逻辑右移

左边用0填充,右边舍弃。

一般来说我们只针对逻辑右移来讨论:

与左移不同的是,右移操作符在移位的过程中会经常低位非零而被舍弃的情况。

例如:将3右移1位

//逻辑右移
00000011
3 >> 1
00000001

假设 char a = n (n为任何数)

我们准备将 a 右移 k 个位置,可以将其分解为 k 次向右移动一个位置(二进制位)

那么每次分为两种情况

  1. 最低位为1
  2. 最低位为0

最低位为1,即先舍去最低位,那么对应到二进制数则可通过 -1操作来完成,然后在移位。

最低位为0,那么则直接移位,不涉及舍弃有效数问题

通过归纳总结:

可以得到 右移操作:

  1. 如果操作数为奇数 则先 -1 ,再右移。

  2. 如果为偶数,则直接右移。

通过左移操作的推到同理可得,右移可以是 除以2的操作,即与除以2的结果取整等效。

则右移与整除相同,都是结果取整。

如果是对负数进行移位操作呢?

在数据未溢出的情况下,结论是相同的。

左移 <==> 乘2

右移 <==> 除以2(整除)

思考:如果对-1,进行右移 n 位那么结果如何?

11111111111111111111111111111111
    //右移1位
11111111111111111111111111111111

可以看到是未变化的,通过上面得出的结论:

实际上 -1 右移可看作如下步骤:

  1. -1为奇数,因此先-1得到-2;
  2. 进行除以2的操作得到 -1

重复上述过程。

**注意:**对于移位运算符,移动负数位是标准为定义的。

位操作符

	&	按位与
	|	按位或
	^	按位异或	
	
注意:操作数必须为整数

位操作符可以做很多有趣的事情。

对于位操作,我们通常只用进行某一位上对应的二进制值,来获得规律;

异或记住口诀:

相同为0, 相异为1.

可以试想两个个极端情况:

  1. 一个数二进制全为0
  2. 两个数二进制相同

其中一个数全为0的情况:

0 ^ 1 -> 1

0 ^ 0 -> 0

非零数的每一个二进制都没变

那么可以轻松得出异或的值还是等于另一个非零的值

两个数完全一样的情况:

0 ^ 0 -> 0

1 ^ 1 -> 0

无论如何,最终二进制位全为0,

即两个相同值异或会得到 0 。

利用这个特性:

思考:如何不适用第三个变量交换两个变量的值。

通常我们会写出如下代码:

void swap(int* num1, int* num2)
{
	int tmp = *num1;
	*num1 = *num2;
	*num2 = tmp;
}

但是这是不符合题意的,但通过位运算,我们可以做到。

void swap(int* n1, int* n2)
{
	*n1 = *n1 ^ *n2;
	*n2 = *n1 ^ *n2;
	*n1 = *n1 ^ *n2;
}

拿a和b来举例:

设未经过任何修改的a,b的值也等于_a, _b的值

int main()
{
	int a = 10;
	int b = 20;
	a = a ^ b; //(1)
	b = a ^ b; //(2)
	a = a ^ b; //(3)
		printf("a = %d b = %d\n", a, b);
	return 0;
}

经过(1)后,a的值实际上是 _a ^ _b

经过(2)后,b的值实际上是 _a ^ _b ^ _b == _a

经过(3)后,a的值实际上是 _a ^ _b ^ _a == _b

显然目的已经达成。

思考:如何统计一个数它的二进制位的1 的个数

试想,如果有一种操作可以每次消除掉一个二进制的1,消除n次,那么这个n就为1的个数

int numof1(int k)
{
	int cnt = 0;
	while (k)
	{
		k = k & (k - 1);//消除二进制最后一位1
		++cnt;
	}
	return cnt;
}

赋值操作符

+=

-=

*=

/=

%=

>>=

<<=

&=

|=

^=

很简单,=不再是数学种的判断,而是赋值。

而 像这样 += 的复合操作符,可以写成:

例如:

a = a + b <==> c += b;

int main()
{
	int a = 10;
	a += 20;
	printf("%d\n", a);
	return 0;
}

单目操作符

!      逻辑反操作
-      负值
+      正值
&      取地址
sizeof    操作数的类型长度(以字节为单位)
~      对一个数的二进制按位取反
--      前置、后置--
++      前置、后置++
*      间接访问操作符(解引用操作符)
(类型)    强制类型转换
int main()
{
	int a = 10;
	printf("%d", a++);
	a = 10;
	printf("%d", ++a);
	return 0;
}
前置和后置++

++a称作前置加加,a++称作后置加加。

简单记忆:

前置先++再使用,后置先使用再++。

实质上这里的前后置++在C++种是可以通过函数重载来完成的,前置++返回的是加1后的值,而后置++是返回的加一前的值的一份拷贝。

sizeof

sizeof实际也是操作符,类型为size_t,可以是计算类型的大小,也可以是变量。

注意:sizeof(表达式),表达式种的值不会真的去执行,可以看作sizeof是去推导表达式的最终类型,而不会去执行表达式的内容。

int main()
{
	int a = 10;
	a += 1.1;
	printf("%d\n", sizeof(a += 1));
	printf("%d", a);
	return 0;
}

image-20220412094944345

思考:int a = 10; a += 1.1;最终a的值为多少?类型呢?

a += 1.1; <==> a = a + 1.1;

a + 1.1实际上会进行算数转换,a会产生一个类型为double临时拷贝,因此计算结果是 double的11.1,

但是在赋值给a时,进行隐式类型转换为int类型,得到11,赋值给a。

前置 – 和后置 – 同理。

如何验证呢?

int main()
{
	int a = 10;
	printf("%d\n", sizeof(a += 1.1));
	printf("%d", a);
	return 0;
}

image-20220412094952553

int main()
{
	int a = 10;
	printf("字节数:%d\n", sizeof(a + 1.1));
	printf("a:%d", a);
	return 0;
}

image-20220412094958923

可以看到,这里验证了上面所说的情况。

a的值并没有变,并且 a + 1.1的结果时double类型。

数组 和 sizeof

数组名普遍被认为是一个指针,可情况实际如此吗?

int main()
{
	int arr[10] = { 0 };
	printf("%d", sizeof(arr));
	return 0;
}

如果arr是个指针,那么输出应该为 4 或者 8

输出:

image-20220412095010343

输出结果是40,显然不是我们预想的。

实际上,数组名是一个数组类型。

例如:int arr[10] = {0};

那么arr的类型实际上是 int[10];

我们通过typedef来验证一下:

typedef struct A
{
	int a;
}Type[10];

int main()
{
	Type a;
	printf("%d", sizeof(Type));
	return 0;
}

输出为:40

这里的Type就是基于struct A[10]复合类型所定义出的一个新类型。

它的类型和我们struct A arr[10]创建出来的数组是同一个类型,与Type arr等效。

可以看出这里的arr实际上是Type类型,即struct A [10]类型。

所以,虽然通常情况下我们把数组名当作指针来使用,但实际上它的类型并不是指针。

目前我们经常接触到的只有两个场景是将数组名当作一个数组类型而不是指针。

  1. sizeof(arr)
  2. &arr
void test(int arr[10])
{
	printf("传参后:%d\n", sizeof(arr));
}

int main()
{
	int arr[10] = { 0 };
	printf("传参前:%d\n", sizeof(arr));
	test(arr);
	return 0;
}

image-20220412095017792

怎么数组名作为实参传递至函数大小就不同了呢?

虽然我们的形参形式写的是 int arr[10],但是其本质是int*类型,试想如果函数传参拷贝每次都传一整个数组,那太浪费资源了,因此数组名传参,实际上形参是一个指针,指向数组首元素。

这也印证了上面所说的两种情况下,数组名是数组类型外,其他时候都为指针。

关系操作符

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

这些关系运算符比较简单,没什么可讲的,但是我们要注意一些运算符使用时候的陷阱。
注意:
在编程的过程中== 和=不小心写错,导致的错误。

逻辑操作符

&&	逻辑与
||	逻辑或

牢记:

&& 只要有一个为假,结果就为假。

||只要有一个为真,结果就为真。

并且一旦可以推断出结果真假,就不再执行后面的逻辑表达式。

int main()
{
	int a = 0, b = 2, c = 3;
	int i = 0;
	i = a++ && b++ && ++c;1printf("i==%d a==%d c==%d d==%d\n", i, a, b, c);
	a = 0, b = 2, c = 3;
	i = a++ || b++ || ++c;2printf("i==%d a==%d c==%d d==%d\n", i, a, b, c);
	return 0;
}

image-20220412095026213

两组测试c的值并不同,实际上是因为在(1)中,a++的值为假,就不在继续执行后续表达式。

而d的值相同,则是在(2)中b++的值已经为真了,因此不再继续执行后续表达式。

三目操作符

exp1 ? exp2 : exp3

例如:

int main()
{
	int a = 0;
	int ret = a == 0 ? 10 : 100;
	printf("%d", ret);
}

简言之就是如果exp1为真则执行exp2,否则执行exp3.

等效于:

int main()
{
	int a = 0;
	if (a == 0)
		ret = 10;
	else
		ret = 100;
	return 0;
}

我们可以利用三目操作符写出简洁的取两数较大值的代码:

int ret = a < b ? b : a;

逗号表达式

exp1, exp2, exp3, …expN
int main()
{
	int a = 1;
	int b = 2;
	int c = (a > b, a = b + 10, a, b = a + 1);//逗号表达式
	return 0;
}

c的值为13

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

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

  1. []下标引用操作符

其为双目操作符,操作数为一个整形索引值和一个数组名或指针。

int main()
{
	int arr[10] = { 0 };
	arr[9];
	return 0;
}

arr[9] == *(arr+9)== *(9 + arr) == 9[arr];

很奇怪的写法,但确实存在。

  1. ( ) 函数调用操作符
    接受一个或者多个操作数:第一个操作数是函数名,剩余的操作数就是传递给函数的参数。
int Add(int n1, int n2)
{
	return n1 + n2;
}
int main()
{
	int a = 1, b = 2;
	Add(a, b);
	return 0;
}

操作数为Add和n1,n2.

  1. 访问一个结构的成员
    . 结构体.成员名
    -> 结构体指针->成员名
struct Stu
{
	char name[10];
	int age;
	char sex[5];
	double score;
};
void set_age1(struct Stu stu)
{
	stu.age = 18;
}
void set_age2(struct Stu* pStu)
{
	pStu->age = 18;//结构成员访问
}
int main()
{
	struct Stu stu;
	struct Stu* pStu = &stu;//结构成员访问
	stu.age = 20;//结构成员访问
	set_age1(stu);
	pStu->age = 20;//结构成员访问
	set_age2(pStu);
	return 0;
}

表达式求值

表达式求值的顺序一部分是由操作符的优先级和结合性决定。
同样,有些表达式的操作数在求值的过程中可能需要转换为其他类型。

隐式类型转换

C的整型算术运算总是至少以缺省整型类型的精度来进行的。
为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型
提升。

为什么有整形提升

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

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

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

int main()
{
	char a, b = 2, c = 3;
	printf("%d\n", sizeof(b + c));
	a = b + c;
	printf("%d", sizeof(a));
	return 0;
}

输出:

4

1

b + c的大小为4个字节,即int的大小,而a为char。

过程:b和c的值被提升为普通整型,然后再执行加法运算,得到结果后将被截断为char存储到a。

如何进行整型提升

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

  1. 有符号数

    整型提升高位补充符号位

  • 正数 高位补0
  • 负数 高位补1
  1. 无符号数

​ 高位直接补0

//负数的整形提升
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 = 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:1011 0110 整形提升–》1111 1111 1111 1111 1111 1111 1011 0110

b:1011 0110 0000 0000 整型提升–》1111 1111 1111 1111 1011 0110 0000 0000

c:1011 0110 0000 0000 0000 0000 0000 0000 无需整形提升。

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

因此整型提升后:a != 0xb6, b != 0xb600

只要参与运算就会尝试整型提升。

判断赋值等一切操作都被当做运算。

int main()
{
	char c = 1;
	printf("%u\n", sizeof(c));
	printf("%u\n", sizeof(+c));
	printf("%u\n", sizeof(-c));
	return 0;
}

输出:

1

4

4

因为+c和-c也被认为是执行了运算。

c只要参与表达式运算,就会发生整形提升,表达式 +c ,就会发生提升,所以 sizeof(+c) 是4个字
节.
表达式 -c 也会发生整形提升,所以 sizeof(-c) 是4个字节,但是 sizeof© ,就是1个字节.

算术转换

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

long double
double
float
unsigned long int
long int
unsigned int
int

一般来说按照上表从下至上进行转换(向精度更高的类型去转换)

如:1 + 1.1 则这里的1会被转换成double类型。

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

float f = 3.14;//赋值也是一种运算
int num = f;//隐式转换,会有精度丢失

其实像这样的转换是不太合理但又不可避免的,通常会伴随着精度的丢失,相当于强制转换为了精度较低的类型。

int main()
{
	size_t a = 0;
	int b = -1;
	if (b < a)
	{
		printf("<");
	}
	else
	{
		printf(">");
	}
	return 0;
}

输出:

>

-1 > 0,这显然不是这道题的解释。

注意到 a为无符号整形,判断 b < a时 a会算术转换为 size_t

-1的补码为全1,而当其被当作无符号整形时就是一个非常大的数,然后再与0进行比较。

其实在整形家族的类型转换中,无非就是符号位的不同识别方式,以及各种截断,而浮点数类型想要和整形互相转换,那么由于存储的方式的巨大差异,编译器就不仅仅是截断或者改变识别方式那么简单了,而是涉及一些复杂的处理,当然我们不必关心。

操作符的属性

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

  1. 操作符的优先级
  2. 操作符的结合性(优先级相同,决定从左向右运算还是从右向左)
  3. 是否控制求值顺序。(&& , || , ? :,以及逗号表达式)
    两个相邻的操作符先执行哪个?取决于他们的优先级。如果两者的优先级相同,取决于他们的结合性。
    操作符优先级。

优先看优先级,

只有相邻的操作符间才能通过优先级确定计算顺序。

操作符优先级

优先级不建议背,可以使用括号解决优先级顺序问题。

//表达式的求值部分由操作符的优先级决定。
//表达式1
a*b + c*d + e*f

计算上表达式时,由于 * 比+的优先级高,只能保证* 的计算是比+早,但是优先级并不
能决定第三个*比第一个+早执行。

所以表达式的执行顺序可能是:

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-非法表达式
int main()
{
	int i = 10;
	i = i-- - --i * (i = -3) * i++ + ++i;
	printf("i = %d\n", i);
	return 0;
}

表达式3在不同编译器中测试结果:非法表达式程序的结果.

//代码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(); 中我们只能通过操作符的优先级得知:先算乘法,
再算减法。
函数的调用先后顺序无法通过操作符的优先级确定,也就是说操作数的确定顺序和操作符优先级无关。

//代码5
#include <stdio.h>
int main()
{
	int i = 1;
	int ret = (++i) + (++i) + (++i);
	printf("%d\n", ret);
	printf("%d\n", i);
	return 0;
}

看看同样的代码产生了不同的结果,这是为什么?
简单看一下汇编代码.就可以分析清楚.
这段代码中的第一个 + 在执行的时候,第三个++是否执行,这个是不确定的,因为依靠操作符的优先级
和结合性是无法决定第一个 + 和第三个前置 ++ 的先后顺序。

总结:我们写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题
的。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值