超详细的操作符解析

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

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

目录

一.操作符分类

二.算术操作符

三.移位操作符 

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

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

 如果要打印

 int num2=num<<1;
 printf("%d",num2);

这里的打印用的是num2的原码的值

需要注意的是:

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

(2).右移操作符 

移位规则:

首先右移运算分两种:

  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.( ) 函数调用操作符 接受一个或者多个操作数:第一个操作数是函数名,剩余的操作数就是传递给函数的参数。 

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

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

#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长度的整型值,都必须先转 换为int或unsigned 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 *

注释:代码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环境的结果:

 

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

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

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

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

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

   

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值