超详细的操作符解析

148 阅读14分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路

还记得之前对操作符进行了简单的了解,并且学习了一些常见操作符的用法,如今我们又对操作符这部分的知识进行了更深一步的学习,有了一些新的感悟和体会,这篇文章我希望能把关于操作符的方方面面,包括一些细微之处,都说到位。

话不多说,让我们开始吧!

目录

一.操作符分类

二.算术操作符

三.移位操作符

1.有关进制和原码、反码、补码

2.移位操作符

四.位操作符

五.赋值操作符

六.单目操作符

七.关系操作符

八.逻辑操作符

九.条件操作符

十.逗号表达式

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

十二. 表达式求值

1.隐式类型转换

2.算术转换

3.操作符的属性

一.操作符分类 操作符有哪些?可以分为几类: 算术操作符 移位操作符 位操作符 赋值操作符 单目操作符 关系操作符 逻辑操作符 条件操作符 逗号表达式 下标引用、函数调用和结构成员 接下来我们对这些操作符一一进行阐述!

二.算术操作符

  •        -            *            /            % 
    

关于算数操作符,比较简单,这里说一些需要注意的:

  1. 除了 % 操作符之外,其他的几个操作符可以作用于整数和浮点数。

  2. 对于 / 操作符如果两个操作数都为整数,执行整数除法。而只要有浮点数执行的就是浮点数除 法。

  3. % 操作符的两个操作数必须为整数。返回的是整除之后的余数。

三.移位操作符 << 左移操作符

右移操作符

这是我们今天要重点介绍的操作符之一

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

1.有关进制和原码、反码、补码

int a = 5; int b = a << 2; 这里的 a<<2 是把a向左移动2位,实质是:把a在内存中存储的二进制位向左移动两位

这里补充一下进制的知识:

二进制:逢二进一 基数为2,数值部分用两个不同的数字0、1来表示。

十进制:逢十进一 基数为10,数值部分用0、1、2、3、4、5、6、7、8、9来表示.

十六进制:逢十六进一 基数是16,有十六种数字符号,除了在十进制中的0至9外,还另外用6个英文字母A、B、C、 D、E、F来表示十进制数的10至15。

下面用一张图来帮助我们理解进制

下面我们还需要了解一下原码 、反码 、补码 的知识 :

整数有3种二进制表示形式:

原码 反码 补码 (1).原码

原码就是符号位加上真值的绝对值, 即用第一位表示符号, 其余位表示值. 比如如果是8位二进制

[+1]原码 = 0000 0001

[-1] 原码 = 1000 0001

(2).反码

反码的表示方法是:

正数的反码是其本身

负数的反码是在其原码的基础上, 符号位不变,其余各个位取反.

[+1] = [00000001]原码 = [00000001]反码

[- 1] = [10000001]原码 = [111111110]反码

(3).补码

补码的表示方法是:

正数的补码就是其本身

负数的补码是在其原码的基础上, 符号位不变, 其余各位取反, 最后+1. (即在反码的基础上+1)

[+1] = [00000001]原 = [00000001]反码 = [00000001]补码

[-1] = [10000001]原 = [11111110] 反码 = [11111111]补码

经过上面的叙述,我们很容易发现;

正整数:原码、补码、反码相同 负整数:原码、补码、反码不同,需要计算 这里我们再举个例子看看:

而整数在内存中存储的是补码!!!

如图,-1的在内存中存储的是补码

而VS编译器是16进制展示

所以显示的内存是 f f f f f f f f

我们知道16进制的 f 就是10进制的15,所以这里对应的就是-1的补码

2.移位操作符 首先需要说明的是:

移位操作符操作的是补码

而打印或使用的时候,用的是补码

 所以使用移位操作符会某个数的二进制的补码改变后,我们如果需要打印或使用这个数的二进制是,要先通过改变后的补码反推出改变后的原码 

铺垫做的差不多了,下面正式介绍移位操作符 !

(1).左移操作符

移位规则:左边抛弃、右边补0

如图,通过左移操作符把num向左移了一位, 正数10的二进制的补码 左边抛弃、右边补0

变成了 00000000000000000000000000010100

计算结果就为 12^4+12^2=20 了

如果要打印

int num2=num<<1; printf("%d",num2); 这里的打印用的是num2的原码的值

需要注意的是:

这时num的值还是10,没有改变,只是我们计算了一下 num<<1 的结果而已

(2).右移操作符

移位规则:

首先右移运算分两种:

算术移位 : 左边用原该值的符号位填充,右边丢弃 逻辑移位 :左边用0填充,右边丢弃 到底是哪种取决于编译器,我们常见的编译器下都是算数右移

如果是正数,这两种结果一样

我们通过一张图来展示一下右移操作符的作用效果

我们用一个负数 -5 来看一下

经过算术操作符,结果是 -3

经过逻辑操作符,结果是 3

警告⚠ :

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

int b = a >> -2; 这是不行的!

四.位操作符 位操作符有:

& //按位与

| //按位或

^ //按位异或

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

这里的”位“指的都是二进制位

& 按位与 (对应的二进制位有0则为0,全1才为1)

| 按位或 (有1为1,全0为0)

^ 按位异或 (相同为0,相异为1)

(1).& 按位与

我们举个例子

int a = 3 ; int b = -5 ; int c = a & b ; 我们用一张图表示运算的过程:

(2). | 按位或

int a = 3 ; int b = -5 ; int c = a | b ;

结果是 -5

(3). ^ 按位异或

int a = 3 ; int b = -5 ; int c = a ^ b ;

结果是 -8

下面我们看曾经一道变态的面试题

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

首先我们应该明确:

             a ^ a = 0

             a ^ 0 = a 

上代码!

#include <stdio.h> int main() { int a = 10; int b = 20; a = a^b; b = a^b; a = a^b; printf("a = %d b = %d\n", a, b); return 0; } 一个练习:

编写代码实现:求一个整数存储在内存中的二进制中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; } //思考这样的实现方式有没有问题? //方法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的个数 = %d\n",count); return 0; } //思考还能不能更加优化,这里必须循环32次的。 //方法3: #include <stdio.h> int main() { int num = -1; int i = 0; int count = 0;//计数 while(num) { count++; num = num&(num-1); } printf("二进制中1的个数 = %d\n",count); return 0; } //这种方式是不是很好?达到了优化的效果,但是难以想到。

五.赋值操作符 赋值操作符是一个很棒的操作符,他可以让你得到一个你之前不满意的值。也就是你可以给自己重新赋值。

int weight = 120; //体重

weight = 89; //不满意就赋值

double salary = 10000.0;

salary = 20000.0; //使用赋值操作符赋值。

赋值操作符可以连续使用,比如:

int a = 10; int x = 0; int y = 20; a = x = y+1;//连续赋值 a的结果是21,这样写是没错,但不建议这样写

复合操作符

+=

-=

*=

/=

%=

=

<<=

&=

|=

^=

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

int x = 10; x = x+10; x += 10;//复合赋值 //其他运算符一样的道理。这样写更加简洁。 六.单目操作符 ! 逻辑反操作

  •       负值
    
  •       正值
    

& 取地址

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

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

-- 前置、后置--

++ 前置、后置++

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

(类型) 强制类型转换

(1).逻辑反操作符 !

如:

int flag = 0; 则 !flag = 1

int flag = 5; 则 !flag = 0

注:!flag的结果只能为1或0 (真或假)

(2). ++和--运算符

前置++:先++,后使用

后置++:先--,后使用

(3). * 解引用操作符

#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> int main() { int a = 10; int* pa = &a;//pa里存放的是a的地址 *pa = 20;//解引用操作符 printf("%d\n", a); return 0; }

我们把a的地址存到 pa 中

再对它解引用,就能找到a

(4). ( )强制类型转换

int main() { int a = 3.14; return 0; } 这个代码,编译时会有一个警告

如果我们一定要把3.14放到整型a中,就要用到强制类型转换

int main() { int a = (int)3.14; return 0; } 在学习了这些单目操作符后,我们看一个演示代码

#include <stdio.h> int main() { int a = -10; int *p = NULL; printf("%d\n", !2); printf("%d\n", !0); a = -a; p = &a; printf("%d\n", sizeof(a)); printf("%d\n", sizeof(int)); printf("%d\n", sizeof a);//这样写行不行? printf("%d\n", sizeof int);//这样写行不行? return 0; }

首先看逻辑反操作符

接着看这组

sizeof后面跟数据类型(如int)不能不加括号,后边跟变量时可以不加括号

而sizeof计算的是类型或变量所占空间的大小,跟变量是多少无关,只与变量所属的类型有关

如int就是4个字节

下面我们再来看一下sizeof和数组的一个代码演示

#include <stdio.h> void test1(int arr[]) { printf("%d\n", sizeof(arr));//(2) } void test2(char ch[]) { printf("%d\n", sizeof(ch));//(4) } int main() { int arr[10] = {0}; char ch[10] = {0}; printf("%d\n", sizeof(arr));//(1) printf("%d\n", sizeof(ch));//(3) test1(arr); test2(ch); return 0; }

看一下这一步,这个sizeof是在主函数内,没有传参

sizeof()内部单独放一个数组名,表示整个数组,所以这里的sizeof计算的是整个数组的大小

所以第一个计算结果是 4*10=40

第二个是 1*10=10

下面看函数调用,这里就是传参了

数组传参如果传数组名,那么传的是首元素地址,这里的数组名本质上是一个地址,是一个指针

而前面我们介绍过指针的大小,在32位系统下指针的大小是4个字节,在64位是8个字节

这里以 32位系统为例

                            函数 test1 计算的结果是4
                            函数 test2 计算的结果也是4  
                                  它们计算的都是指针的大小,与类型无关

七.关系操作符

=

<

<=

!= 用于测试“不相等”

== 用于测试“相等”

这些关系运算符比较简单,没什么可讲的,但是我们要注意一些运算符使用时候的陷阱。

警告: 在编程的过程中== 和=不小心写错,导致的错误

八.逻辑操作符 && 逻辑与

|| 逻辑或

我们可以把 && 理解为并且,把 || 理解为或者

庸俗的讲,&&是有一个不成立就都不成立,||是有一个成立就都成立

区分逻辑与和按位与

区分逻辑或和按位或

1&2----->0

1&&2---->1

1|2----->3

1||2---->1

逻辑与和或的特点:

360笔试题

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

}

我们来分析一下

这里由于a=0为假,所以整个i=a++&&++b&&d++都判断为假,所以a++后面的++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; }

分析:

这里的a=1所以为真,所以整个

i = a++||++b||d++ 都判断为真,后面的++b和d++没有执行

总结

&& 左操作数为假,右边不计算

| | 左操作数为真,右边不计算

九.条件操作符 exp1 ? exp2 : exp3

表达式1为真,则结果为表达式2的值

表达式1为假,则结果为表达式3的值

十.逗号表达式 exp1, exp2, exp3, …expN

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

这里的c等于3

//代码1 if (a =b + 1, c=a / 2, d > 0) //代码2 a = get_val(); count_val(a); while (a > 0) { //业务处理 a = get_val(); count_val(a); }

//如果使用逗号表达式,改写: while (a = get_val(), count_val(a), a>0) { //业务处理 }

十一.下标引用、函数调用和结构成员 1.[ ] 下标引用操作符 操作数:一个数组名 + 一个索引值

​ int arr[10];//创建数组 arr[9] = 10;//实用下标引用操作符。 [ ]的两个操作数是arr和9。

​ 这里补充一点,

arr[7]--> *(arr+7) --> *(7+arr)--> 7[arr]

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

接受一个或者多个操作数:第一个操作数是函数名,剩余的操作数就是传递给函数的参数。

  1. 访问一个结构的成员 . 结构体.成员名 结构体指针->成员名

#include <stdio.h> 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; }

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

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

整型提升的意义:

      表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度 一般就是int的字节长度,同时也是CPU的通用寄存器的长度。 因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长 度。 通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令 中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转 换为intunsigned int,然后才能送入CPU去执行运算

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

原因: a是char 类型,但5是int类型,

char是1个字节,int是4个字节,放不下,这时就要将5的二进制序列的补码截断

同理,126

此后 要进行计算,计算时要整型提升

如何进行整体提升呢?

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

//负数的整形提升

char c1 = -1;

变量c1的二进制位(补码)中只有8个比特位:

1111111

因为 char 为有符号的 char

所以整形提升的时候,高位补充符号位,即为1

提升之后的结果是: 11111111111111111111111111111111

//正数的整形提升

char c2 = 1;

变量c2的二进制位(补码)中只有8个比特位:

00000001

因为 char 为有符号的 char

所以整形提升的时候,高位补充符号位,即为0

提升之后的结果是: 00000000000000000000000000000001

//无符号整形提升,高位补0

实际操作:

我们要把计算好的结果放到c中,所以又要截断

现在我们要把c打印出来

打印的是%d,是整型,而c是char类型,所以这时候我们又要整型提升

整型提升 提升的是在内存中的补码,而打印用的是原码所以我们还需要反推出原码

这样,我们就可以把c打印出来了,这个原码对应的数是 -125

再看一个整型提升的例子:

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; } 实例1中的a,b要进行整形提升,但是c不需要整形提升

a,b整形提升之后,变成了负数,所以表达式 a==0xb6 , b==0xb600 的结果是假,

但是c不发生整形提升,则表 达式 c==0xb6000000 的结果是真.

所以程序输出的结果是: c

这里那a的整型提升举例

示例2

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

分析:实例2中的,c只要参与表达式运算,就会发生整形提升,表达式 +c ,就会发生提升,所以 sizeof(+c) 是4个字节, 表达式 -c 也会发生整形提升,所以 sizeof(-c) 是4个字节,但是 sizeof(c) ,就是1个字节.

注意:sizeof内部表达式的值是不会去真实计算的,只关心它的类型,计算类型的大小

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

long double

double

float

unsigned long int

long int

unsigned int

int

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

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

float f = 3.14;

int num = f;

//隐式转换,会有精度丢失

3.操作符的属性 复杂表达式的求值有三个影响的因素。

操作符的优先级 操作符的结合性 是否控制求值顺序。 两个相邻的操作符先执行哪个?取决于他们的优先级。如果两者的优先级相同,取决于他们的结合性。

操作符优先级

注:N/A表示无

     L-R表示从左向右

一些问题表达式

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

//表达式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

//表达式2

c + --c;

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

3

//代码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; } //尝试在linux 环境gcc编译器,VS2013环境下都执行,看结果。

VS2017环境的结果:

看看同样的代码产生了不同的结果,这是为什么?

简单看一下汇编代码.就可以分析清楚.

这段代码中的第一个 + 在执行的时候,第三个++是否执行,这个是不确定的,

因为依靠操作符的优先级 和结合性是无法决定第一个 + 和第三个前置 ++ 的先后顺序。

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