C生万物 | 操作符汇总大全【庖丁解牛,精细讲解】 —— 下

举报
烽起黎明 发表于 2023/02/27 18:30:47 2023/02/27
【摘要】 ✒C语言操作符汇总大全,全程精析讲解,拨开云雾见天明☀

接上

六、关系操作符

  • 关系操作符主要有下面这些👇
    【>】、【>=】、【<】、【<=】、【! =】、【==】
  • 对于这些操作符来说大家平常在使用的时候也都会碰到过,这里主要强调的一块就是==,因为它经常会和赋值操作符=混淆,其实也不能说是混淆,应该说是【遗漏】,包括是很多资深的程序员在写代码判断一个数是否等于某个值的时候都会犯这样的错误
    • 例如将a == 6写成a = 6 ,若是将这个语句写在if条件判断里的话那么无论a的值为多少都会进入这个条件分支,因为赋值永远都是成立的,也就是为真
  • 所以我们平常在判断的时候一般不写a == 10,一般都写成10 == a
    • 因为前者若是稍加了一个【=】的话编译器是不会报出错误的,那后面再调试的时候就会很麻烦
    • 可是若后者少加了一个【=】的话编译器就会报出错误,因为一个变量是不可以赋值给到一个常量的,这就==属于语法方面的错误了==

对于==操作符还想再说几句的是我们有时候不仅仅会去比较某个数值,而是去比较两个字符串的内容是否相同或者是比较一些结构体成员的变量,可是呢对于【==】操作符来说是不具备那么强大的功能的,所以要用其他手段去实现

  • [x] 对于两个字符串的内容,我后面在将字符串内容的时候会说到一个库函数叫做strcmp,它是专门用来比较字符串内容的,若是直接用==去比较的话不是比较的两个字符串的内容,而是比较的两个字符串的首元素地址罢了
  • [x] 对于结构体成员的比较,也是有独特的方法,不过对于普通的结构体成员,整数、浮点数就直接用【==】比较也是可以的,字符串的话用上面的strcmp,而对于定义出来的整个结构体成员对象的内容就不好比较了,之后我们在学习了C++之后就知道有个东西叫做[仿函数],可以比较自定义的数据类型

七、逻辑操作符

逻辑操作符很好记,就两个,和我们前面学过的位操作符中的按位与&按位或|很像

在这里插入图片描述

  • [x] 要区分逻辑与按位与
  • [x] 要区分逻辑或按位或

在这里插入图片描述

  • 可以看到对于逻辑与逻辑或来说它们最终的结果只会有两种,那就是【1】和【0】;但是对于位操作符来说是千变万化的,因为两个数进行位运算取决的是32个二进制位上的0和1

逻辑与和或的特点:
✔【逻辑与&】:表达式两边均为真才是真,若第一个为假,那么整个表达式为假,第二个表达式不参与运算
✔【逻辑或 |】:表达式两边有一边为真即为真,若第一个为真,那么整个表达式为真,第二个表达式不参与运算

一道【奇虎360】笔试题✍

下面是一道【奇虎360】公司的校招笔试题,请问程序输出的结果是什么?

#include <stdio.h>
int main()
{
    int i = 0, a = 0, b = 2, c = 3, d = 4;
    i = a++ && ++b && d++;
    
    printf("a = %d\nb = %d\nc = %d\nd = %d\n", a, b, c, d);
    printf("------\n");
    printf("i = %d\n", i);
    return 0;
}
  • 这道题考察的就是你对于[逻辑操作符][单目操作符]的综合运用能力
  • 下面是最终的结果,你算对了吗❓

在这里插入图片描述

  • 来分析一下其实就可以看出 ,因为a一开始为0,所以前两个逻辑与之后的结果一定为0,那么除了第一个a++表达式需要运算之外后面的表达式都不会参与运算,因此最后的结果为1 2 3 4,【i】的结果即为0
  • 这里要注意的一点就是逻辑与前面一个表达式已为假那么第二个表达式是不会参与运算的
    在这里插入图片描述

现在我将这个题目做几个变形,看看读者是否具有举一反三的能力💪

题目变形①

  • 看到我将a的初始值做了一个修改,变为1,那么请问结果是多少呢?
int i = 0, a = 1, b = 2, c = 3, d = 4;
i = a++ && ++b && d++;

在这里插入图片描述

  • 来分析一下,初始化a为1,b为2,那在a++++b之后与运算的表达式即为1 && 3,运算之后的结果即为1,然后这个1再和d++去进行一个运算便可以得出最后的结果为【1】,那么a b c d 最后的结果即为2 3 3 5

在这里插入图片描述


题目变形②

  • 既然学习了逻辑与,那逻辑或也少不了,接下去来练练逻辑或的用法吧
int i = 0, a = 0, b = 2, c = 3, d = 4;
i = a++ || ++b || d++;

在这里插入图片描述

  • 这里要注意的一点就是逻辑或前面一个表达式已为真那么第二个表达式是不会参与运算的
  • 因此最后的结果即为1 3 3 4,【i】的值为1
    在这里插入图片描述

题目变形③

  • 将这个a改为1再来看看最后输出的结果为多少?
int i = 0, a = 1, b = 2, c = 3, d = 4;
i = a++ || ++b || d++;

在这里插入图片描述

  • 同理,本次的修改也可以看出逻辑或的特性,之后a++参与了运算,因为它不为1,那么后面都不会参与运算了,最后的a b c d结果即为2 2 3 4
    在这里插入图片描述

通过这道奇虎360公司的笔试题以及举一反三的练习,相信你对逻辑操作符一定有了自己的理解👍

八、条件操作符

接下去我们来看看条件操作符,不过一般我们都将其叫做条件表达式(三目操作符)

在这里插入图片描述

  • 来看看下面这段代码,一段很简答的逻辑判断,但是呢你是否可以发现经过if分支的一个判断之后显得非常冗余,那这个时候其实就可以使用到我们本节所要将的条件操作符
int main(void)
{
	int a = 5;
	int b = 0;

	if (5 == a)
		b = 3;
	else
		b = -3;

	printf("a = %d\n", a);
	printf("b = %d\n", b);
	return 0;
}
  • 将整个if的分支判断写成这样的一句代码时,就显得非常简洁,但是可能不了解这个操作符的同学可能会看懵,不够你现在学习了这个操作符之后一定是完全没问题的
  • 也就是当条件成立的时候,执行第一个表达式,当条件不成立的时候,执行第二个表达式。可以看出我写了后面也可以是一个表达式👈
b = (5 == a) ? 3 : -3;

在这里插入图片描述


然后我们使用这个条件操作符来练习一下求解两个数的较大值

int a = 5;
int b = 3;

int ret = (a > b) ? a : b;
printf("ret = %d\n", ret);

九、逗号表达式【生僻,需了解】

下面来说说有关逗号表达式的用法

【格式】:exp1, exp2, exp3, …expN

【运算规则】:从左向右依次计算,整个表达式的结果是最后一个表达式的结果

  • 首先来看一下第一段代码,请你计算一个
//代码1
int a = 1;
int b = 2;
int c = (a > b, a = b + 10, a, b = a + 1);//逗号表达式
printf("c = %d\n", c);
  • 最后的结果是13,你算对了吗?运行结果就不展示了
  • 来分析一下,根据逗号表达式的运算规则可以知道它是从左向右进行计算的,最终结果取的是最后一个表达式的结果,那么根据前面的计算可以得知b = 12,那么最后再计算便是13

  • 再来看一句代码,可以看到这不是一个结果的运算,而是将逗号表达式放在一个if分支判断中,可以看到最后一个逗号后面的表达式为d > 0,那此时我们就要去看看前面一些表达式的运算会不会使得这个d变化,若不会那么这个if判断其实就等价于if(d > 0)
//代码2
if (a = b + 1, c = a / 2, d > 0)

  • 最后再来看看下面这段代码,就是不断地在计算一个a的值然后求和,若是a什么时候到0了,便跳出这个while()循环,但是可以看到a = get_val()count_val(a)这两个表达式在while()循环上面调用了一次,然后再while()循环中在调用,显得就有些冗余了,那此时我们就可以使用【逗号表达式】去进行一个优化
//代码3
a = get_val();
count_val(a);
while (a > 0)
{
 	//业务处理
    a = get_val();
    count_val(a);
}
  • 可以看到,通过逗号表达式的一个优化,代码看起来就显得很简洁,当while()循环一进来,就会执行a = get_val(), count_val(a)这两个表示,但是呢最后起作用的还是a > 0,前面两个表达式只是有可能会使a的值发生一个变化罢了
while (a = get_val(), count_val(a), a > 0)
{
      //业务处理
}

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

1、下标引用操作符 [ ]

【操作数】:一个数组名 + 一个索引值

  • 这个操作符我们在讲数组的时候也有用到过,可能我们大家在使用的时候都是arr[1],不过既然它一个操作符,那么对于操作数来说其实没有位置的一个限制,其实是可以写成1[arr],这个语法也是支持的,访问的都是arr这个数组中的第一个元素
  • 我们可以到VS中来演示看看

在这里插入图片描述

  • 可以看出两种语法都是可行的,这一点你了解了吗😀

2、函数调用操作符 ( )

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

  • 对于函数来说相信都不陌生,这里主要给读者讲讲有关函数的操作符和操作数之间的关系,可以看到下面这段代码,对于test1()来说它的操作符为(),只有一个操作数就是函数名test1
  • 再看到test2("hello bit."),对于它来说操作符也为(),操作数的话有两个,一个为函数名test1,另一个则为函数参数"hello bit."
void test1()
{
	printf("hehe\n");
}

void test2(const char* str)
{
	printf("%s\n", str);
}

int main(void)
{
	test1();				//实用()作为函数调用操作符。
	test2("hello bit.");	//实用()作为函数调用操作符。
	return 0;
}

3、结构成员调用操作符 . ->

最后再来说说这个结构成员调用操作符【.】和【->】

  • 首先看到下面声明了一个结构体,是一本书,结构成员有作家价格。然后我声明了一个结构体成员,初始化了它的成员变量
typedef struct book {
	char writer[20];
	double price;
}st;

st s1 = { "罗曼·罗兰", 50 };
  • 首先我使用.操作符先进行访问,可以看到获取了这个成员所有的成员变量
int main(void)
{
	st s1 = { "罗曼·罗兰", 50 };
	printf("name = %s\n", s1.writer);
	printf("price = %f\n",s1.price);  //结构体变量.结构体成员名
	return 0;
}

在这里插入图片描述


  • 接下去我们再来尝试一下->操作符进行一个访问,那么对于这个操作符在上面讲到过,那既然这样的话我们就需要去定义一个指针去接收这个结构体成员的地址,那么这个指针就叫做[结构体指针]
  • 那我们使用这个指针变量解引用是不是取到了这个结构体的值,此时就可以去访问这些结构体成员了,如下所示👇
st* ps = &s1;

printf("name = %s\n", (*ps).writer);
printf("price = %f\n", (*ps).price);
  • 那我们就用->操作符来试试吧
printf("name = %s\n", ps->writer);	//结构体指针->结构体成员名
printf("price = %f\n", ps->price);
  • 可以看到对于这三种形式都是可以访问到这个结构体变量的成员

在这里插入图片描述

十一、表达式求值

1、隐式类型转换【⭐整型提升⭐】

接下去要讲的这一个隐式类型转换,可以很好地解开你对很多类型转换的一些困惑

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

① 整型提升的意义

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

通过上面的陈述,相信你对整型提升有了一个初步的概念,接下去我们来看看如何去进行一个整型提升👇

② 如何进行整型提升

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

对于整型提升来说,整数和负数是不一样的,首先我们来看看正数的整型提升

//正数的整形提升
char c1 = 1;
  • 首先先来写出1的32个二进制位00000000000000000000000000000001,然后将其转换为原码的形式,但是通过上面的学习我们可以知道对正数的原、反、补码是相同的,因此这就是它在内存中的形式
  • 但是可以看到现在要将这个整数1给到一个char类型的变量c2,此时,此时就会发生一个【截断】的现象,因为一个字符型的数据在内存中只占1个字节,也就是8个比特位,所以在截断之后就只剩下00000001
  • 接下去若是要去使用这个c1的话就会进行一个整型提升,此时在八个比特位的首部填充24个符号位,以达到整型4个字节32个比特位在内存中的要求,那么此时提升之后的结果便是00000000000000000000000000000001可以看出又回到了原来的1,不过若是你不去使用这个c1的话是不需要进行整型提升的

接下去再来看看负数的整型提升

//负数的整形提升
char c2 = -1;
  • 那对于负数其实也是一样,-1 在内存中的32位二进制补码为11111111111111111111111111111111。同理,将其给到一个整型的变量之后就会发生截断即为11111111
  • 那这个时候再进行一个整型提升就和正数不一样了,因为负数的符号位为1,而整型提升在高位是要补充符号位,所以会在前头加上24个1,那其实也就变回了和之前-1的补码一般的样子,为32个1

好,说完了该如何去进行整型提升之后我们就可以去代码段中看看到底是如何进行的

③ 实战演练

首先来看第一个,我们要去计算两个整数的和,但是呢却要放到char类型的变量中去,那会发生什么化学反应呢:crystal_ball:

int main(void)
{
	char a = 5;
	char b = 126;
	char c = a + b;
}
  • 根据上一小节的讲解,相信你已经知道编译器第一步会做什么了,首先的话就是分别写出这两个整数的32个比特位,接着转换为补码的形式,正数三者均一致。然后因为要给到一个char类型的变量,所以会进行一个【截断】
00000000000000000000000000000101 - 5
——> 00000101 - 5【截断】
00000000000000000000000001111110 - 126
——> 01111110 - 126【截断】
  • 接下去我们要开始去使用到这两个字符型的变量了,使用它们进行一个加法运算,那么此时就会发生一个[整型提升],在高位补充24个符号位之后就变成了下面这样,然后便可以对它们去进行一个运算了
//到了内存中开始计算 —— 整型提升(正数)
00000000000000000000000000000101 - 5
00000000000000000000000001111110 - 126
  • 那在运算出来之后呢在计算机中是一个补码的形式,输出来便得是一个原码的形式,由于正数三者是一致,所以不发生改变(==一强调这点是因为想让读者搞懂每一步==)其实这时可以看到运算出来的数字是正确的,5 + 126 = 131
  • 可是呢可以看到左边又是拿了一个char类型定义的变量在接受这个运算后的结果,因此便又会发生一个【截断】,就只剩下1个字节8个比特位
  • 那其实在这个地方如果我用一个整型变量去接收一下然后再用%d做一个打印,那么此时就会输出正确的内容131
00000000000000000000000010000011 - 131
10000011 - 131【截断】

可是呢,我就是不用整形去接收,就是玩:satisfied:用字符型去接受,然后再用%d去打印(“主要还是为了加深知识点的灵活运用”)

printf("c的整数打印形式为:%d\n", c);
  • 那在若是在这个时候又要去进行打印的话,又要放到内存里面去运算了,调用这个printf()库函数其实也算是一个运算,也要放到内存里面去,然后这个变量c又不是整型,所以此时呢就又会发生一个[整型提升]
  • 此时就需要去补充10000011前面的24个符号位了
//整型提升(负数)
11111111111111111111111110000011 - 补码
11111111111111111111111110000010 - 反码
10000000000000000000000001111101 - 原码
  • 然后变要将这个32个二进制位以十进制的形式打印出来,可是计算机中的运算是采取补码的形式,打印输出的话就要采取补码的形式了,所以此时就需要将这个补码转化为原码了,可以看到这是一个负数的补码,所以转化为原码的时候要小心了,需要将补码-1然后再除符号位外均做取反
  • 此时再去转化为十进制的形式输出便是-125,我们来看看结果【全体起立:cop:】

在这里插入图片描述


接下去再来看看第二个栗子🌰

  • 上面呢我们只说到了字符类型的整型提升,下面呢我们再来看看短整型,它们都是属于整型数据类型的一种
  • 可以看到定义了三个变量,分别是字符型、短整型、整型,然后初始化了一个十六进制的数据,那我们可以将其转换为二进制的形式便为10110110,那其实到这里我就已经可以看出答案是多少了,只有最后一个if语句会进去,其余的都不成立
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;
}

在这里插入图片描述

  • 好,来解释一下为什么我一眼就可以看出最后的结果是多少,并不是因为我知道结果,而是我看到了这个十六进制的b,因为它的二进制为1011,可以看到首尾是为0,那么当这个变量a参与运算的时候就会发生一个[整型提升],在上面我说到过对于负数的整型提升和正数不一样,填充符号位后均为1,那么再转化为原码从计算机输出之后就一定不会是原来的值了,会发生一个改变👈
  • 对于char ashort b它们均不是一个整型int类型的数据,所以都会发生一个[整型提升],不过int c它就是一个整型的变量,所以是不会发生变化的

通过这个例子相信你对整型提升一定有了更加深刻的理解


最后一个小案例我们和sizeof做一个结合,顺便再回顾一下前面的知识点:scroll:

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

	return 0;
}

在这里插入图片描述

  • 通过运行结果可以看到,有三个结果发生了整型提升,首先对于sizeof(c)很明确,计算的就是char这个数据类型的字节长度,也就是1,可以对于下三个结果为什么会发生整型提升呢?我们来分析一下:mag:
  • [x] 对于c + 1来说它是一个表达式,上面说到过若是一个char类型或者是short类型定义的变量参与了运算,那在内存中就会发生一个整型提升,那如果这是一个表达式的话也就相当于是参与了运算,整型提升后变为4个字节,具体细节不做展开
  • [x] 那对于+c-c来说就是我们前面说到过的单目操作符中的+-操作符,和一个操作数结合也可以说它是一个表达式,那么同理也会进行一个整型提升

但是我再将它们变个形却又不会发生【整型提升】了,一起来看看👇

char c = 1;
printf("%u\n", sizeof(c + 1));
printf("%u\n", sizeof(+c));

printf("-------------------\n");
printf("%u\n", sizeof(c = c + 1));
printf("%u\n", sizeof(++c));

在这里插入图片描述

  • 可以看到,若是将c + 1改换成了c = c + 1,就不会发生整型提升了,这是为什么呢?因为对于c + 1这个表达式来说确实会发生整型提升,但是呢我又将这个表达式计算后的结果放到c里面去,还记得我在讲述【sizeof()】的时候说到它里面的表达式是不会运算的吗,所以整个表达式的结果其实就是sizeof(c)的结果,和上面所列出的第一个是一样的
  • 再来看看这个++c,那又有同学会产生疑惑,为何+c会发生整型提升,但是++c却不会呢,其实对于++c来说就等价于c = c + 1,那其没有发生整型提升的原因相信你已经清楚了

以上就是有关【整型提升】要介绍的所有内容,看完这些相信你对计算机内部隐式类型转换一定有了一个深刻的了解😀

2、算术转换

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

在这里插入图片描述

  • 其实很好理解。如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运算。【排名从上面最高,下面最低】
  • [x] 例如intunsigned int一起进行算术运算的时候这个前者就要转换为后者的类型
  • [x] 例如long intlong double一起进行算术运算的时候这个前者就要转换为后者的类型
  • 那其实可以看出,在char和short面前称霸的int如今沦落为了小弟:monkey:

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

int main(void)
{
	float f = 3.14;
	int num = f;//隐式转换,会有精度丢失

	printf("%d\n", num);
	return 0;
}
  • 可以看到,在编译的阶段就出现了一个==精度丢失==的Warning⚠

在这里插入图片描述

3、操作符的属性【附优先级列表】

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

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

下面有一张关于操作符优先级的列表,可以保存一份日常的参考

在这里插入图片描述

👴聊聊问题表达式

表达式的求值部分由操作符的优先级决定,优先级只能决定先算谁,但是哪个表达式先调用要取决于编译器

  • 对于有些表达式而言,其实在不同的编译器上所呈现的结果是不同的,我们将其称作为【问题表达式】

① 问题表达式1

a*b + c*d + e*f
  • 来看上面这段代码,通过上面的优先级列表可以看出[*]的优先级一定是比[+]要来得高,因此可以保证[*]两端的数字先进行运算,但是却==不能保证第三个*比第一个+早执行==

在这里插入图片描述
在这里插入图片描述

  • 可以看到,即使是存在操作符的优先级,但是这个表达式的计算还是存在一个歧义

② 问题表达式2

  • 继续来看下一个问题表达式
//表达式2
c + --c;
  • 虽然对于这个[--]操作符来说比[+]操作符的优先级来得高,但是呢我们却不知道在编译器运算的时候这个【c】是什么时候准备好
  • 这样说你可能不是很理解,可以看看我的这篇文章 ——> 反汇编深挖【函数栈帧】的创建和销毁。通过这篇文章你一定可以看出对于遍历的创建时机其实在编译器内部是存在一个时间的先后顺序的,你并不知道它何时会压栈
    在这里插入图片描述
  • 通过画图分析我们也可以看出若是前面的这个c先入栈了,先准备了,那么后的--c就是根据这个【2】来运算;可若是这个--c先执行的话,后面再去加上这个c结果就不一样了。因此也将其成为问题表达式

③ 问题表达式3

  • 同样,对于下面这段代码,也是存在很大的争议,特别是对于++--混搭的这种表达式尤其严重,你可以去不同的编译器上运行看看,结果都是不一样的【这种代码不要写,练练思维就行】
//代码3-非法表达式
int main()
{
	int i = 10;
	i = i-- - --i * ( i = -3 ) * i++ + ++i;
	printf("i = %d\n", i);
	return 0;
}
  • 下面是我在不同的编译器上运行出来的结果,可见这个表达式问题有多大!
编译器
- 128 Tandy 6000 Xenix 3.2
- 95 Think C 5.02(Macintosh)
- 86 IBM PowerPC AIX 3.2.5
- 85 Sun Sparc cc(K&C编译器)
- 63 gcc,HP_UX 9.0,Power C 2.0.0
4 Sun Sparc acc(K&C编译器)
21 Turbo C/C++ 4.5
22 FreeBSD 2.1 R
30 Dec Alpha OSF1 2.0
36 TDec VAX/VMS
42 Microsoft C 5.1

④ 问题表达式4

  • 继续来看下面是一个有关函数调用的问题表达式,函数内部声明了一个静态的整型变量count,我们知道对于静态变量是存放在内存中的【静态区】,每一次运算都是在上一次的运算的结果后进行一个累加
  • 看到main函数中的函数调用表达式answer = fun() - fun() * fun();其实也是存在一个歧义的,因为你完全不知道编译器先调用的是哪个fun()
//代码4
int fun()
{
     static int count = 1;
     return ++count;
}
int main()
{
     int answer;
     answer = fun() - fun() * fun();
     printf( "%d\n", answer);//输出多少?
     return 0;
}
  • 可以看到,若是前面的fun()先执行的话,最后的结果就是-10,若是后面的fun()先执行的话,最后的结果就是-2

在这里插入图片描述

  • 正常来说大家应该都认为是第二个表达式符合我们的运算规则,因为先乘除后加减,可是呢我们最常用的VS出来的结果都不是我们想要的

我们可以到不同编译器上面去观察一下

在这里插入图片描述

  • 可以看到,虽然在【VS】和【Linux】在执行的结果是-10,而且在大多数的编译器下都是这个,但是呢对于==函数的调用先后顺序无法通过操作符的优先级确定==,因此这也是一个问题表达式

在这里插入图片描述


⑤ 问题表达式5【VS下反汇编调试观察】

  • 好,我们再来看最后一个,有关+++结合的问题表达式,这个我在之前的文章中也有提到过,运算出来的结果其实是存在歧义的
//代码5
#include <stdio.h>
int main()
{
	int i = 1;
	int ret = (++i) + (++i) + (++i);
	printf("%d\n", ret);
	printf("%d\n", i);
	return 0;
}

一样,我们可以到不同的编译器下去做一个测试

在这里插入图片描述

  • 可以看到,这里就出现了两个不同的结果,在VS里运行出来是【12】,但是在Xshell里面运行出来却是【10】

在这里插入图片描述

下面我在VS下通过调用反汇编的指令带大家来看一下在底层编译器到底是如何执行的,如果不懂可先看看我的这篇文章——> 反汇编深挖【函数栈帧】的创建和销毁

  • 将调试指针移动到main函数的栈帧中代码所要执行的位置,便可以观察到一些所对应的汇编代码

在这里插入图片描述
==第一条指令==

00631865 C7 45 F8 01 00 00 00    mov     dword ptr [ebp-8],1
  • 首先我们来看int i = 1,汇编指令为【mov】,意思是将1这个值放到main函数栈帧中ebp - 8这个位置,也就相当于是在这块位置存放了变量i的地址,然后令它的值为1,那此时其实可以想到ebp - 8&i的地址是一致的,我们可以通过【监视窗口】来观察一下

在这里插入图片描述


==第二条指令==

0063186C 8B 45 F8        mov      eax,dword ptr [ebp-8]
  • 接下去我们来看第二条指令,现在已经进入(++i) + (++i) + (++i)这个表达式。可以看到汇编指令为【mov】,通过后面的命令可以看出是将ebp - 8这块地址的值放到寄存器eax中去,那么执行完后eax = 1

在这里插入图片描述


==第三条指令==

0063186F 83 C0 01       add       eax,1
  • 接下去第三条汇编指令为【add】。很清楚,就是给eax寄存器中的值加1

在这里插入图片描述


==第四条指令==

00631872 89 45 F8        mov       dword ptr [ebp-8],eax
  • 第四条汇编指令为【mov】,意思是将eax所存放的值再放回ebp - 8这块空间上去。通过上面一条指令我们知道此时eax里面存的值为2,并且ebp - 8这块地址和变量i的地址是一样的,所以==二、三、四条指令==也就等价于++i,只不过是利用寄存器eax做一个转移

在这里插入图片描述


==第五条指令==

00631875 8B 4D F8        mov        ecx,dword ptr [ebp-8]
  • 第五条指令是【mov】,作用其实和第二条是一个意思,把ebp - 8这块地址的值放到寄存器ecx中去,那么执行完后ecx = 2

在这里插入图片描述


==第七条指令==

00631878 83 C1 01        add         ecx,1
  • 接下去第三条汇编指令为【add】。和第三条是一样的意思,就是给ecx寄存器中的值加1

在这里插入图片描述


==第八条指令==

0063187B 89 4D F8        mov        dword ptr [ebp-8],ecx
  • 第把条汇编指令为【mov】,和第四条是一样的意思,将寄存器ecx中存放的值再放回ebp - 8这块地址中去,也就相当于 ++i

在这里插入图片描述


==第九、十、十一条指令==

0063187E 8B 55 F8        mov         edx,dword ptr [ebp-8]  
00631881 83 C2 01        add         edx,1  
00631884 89 55 F8        mov         dword ptr [ebp-8],edx
  • 接下去的第九、十、十一条指令和上面是一样的,便不再赘述,给出最终结果

在这里插入图片描述


==第十二、十三、十四、十五条指令==

00631887 8B 45 F8        mov         eax,dword ptr [ebp-8]  
0063188A 03 45 F8        add         eax,dword ptr [ebp-8]  
0063188D 03 45 F8        add         eax,dword ptr [ebp-8]  
00631890 89 45 EC        mov         dword ptr [ebp-14h],eax 

上面这五条指令一起说,因为和上面三条一样是行云流水式的

  • [x] 首先将ebp -8里面的值存放到寄存器【eax】里面去

在这里插入图片描述

  • [x] 然后给【eax】的值加上一个ebp - 8里面存放的值,那也就是加上一个i的值,等价于(++i) + (++i)

在这里插入图片描述

  • [x] 然后再给【eax】的值加上一个ebp - 8里面存放的值,等价于(++i) + (++i) + (++i)

在这里插入图片描述

  • [x] 最后将上面计算出来eax里面的值存放到ebp - 14这块地址中去,通过调试可以看到这块地址和&ret是一致的,也就是说它们是同一块空间,那也就是将最后的值存放到ret里面去,那么最后打印出来的ret也就是12

在这里插入图片描述

通过反汇编进行观察调试,这回应该清楚了为什么最后的结果为12了吧

后续会更新Linux下的反汇编调试,单独出文章,敬请期待。。。

十二、总结与提炼【最后的舞台】

好,来总结一下本文所学习的内容✒

  • 本文我总共讲到了46种操作符,可以说是很全了,请读者观赏👇
  • [x] 算术操作符:【+ 】、【- 】、【* 】、【/ 】、【% 取余

  • [x] 位操作符:【& 按位与】、【| 按位或】、【^ 按位异或】、【~ 按位取反】、【<< 按位左移】、【>> 按位右移

  • [x] 赋值操作符:【= 赋值】、【+= 复合加】、【-= 复合减】、【*= 复合乘】、【/= 复合除】、【%= 复合取余】、【<<= 复合左移】、【>>= 复合右移】、【&= 复合按位与】、【|= 复合按位或】、【^= 复合按位异或】、【~= 复合按位取反

  • [x] 单目操作符:【! 逻辑反】、【- 负值】、【+ 正值】、【& 取地址】、【sizeof 操作数的类型长度】、【- - 前置、后置--】、【++ 前置、后置++】、【* 间接访问】、【() 强制类型转换

  • [x] 关系操作符:【> 大于】、【>= 大于等于】、【< 小于】、【<= 小于等于】、【!= 不等于】、【== 等于

  • [x] 逻辑操作符:【&& 逻辑与】、【| | 逻辑或

  • [x] 条件操作符:【? 三目运算符

  • [x] 逗号表达式:【exp1, exp2, exp3, …expN】整个表达式的结果为最后一个逗号后面的表达式

  • [x] 下标引用操作符:【[ ]】

  • [x] 函数调用操作符:【( )】

  • [x] 结构成员调用操作符:【.】、【->

  • 最后的话是讲到了有关表达式的求值相关的概念。为读者介绍了隐式类型转换中的【整型提升】,知道了原来短整型和字符型的数据在内存中是这样变化的;然后说到【算术转换】,清楚了再两个不同等级的数据类型一起操作的时候等级低的会转化为等级高的;最后说到了各种各样的【问题表达式】,也带大家通过反汇编观察了编译器的执行逻辑

以上就是本文要介绍的所有内容,感谢您的观看。记得给个三连哦:heart::heart::heart:

在这里插入图片描述

【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。