操作符详解与表达式求值

news2024/12/21 20:29:10

目录

操作符分类

 1.算数操作符

2.移位操作符(只适用于整数范围)

(1)引入

(2)左移操作符<<

(2)右移操作符>>

 3.位操作符

 4.赋值操作符

 复合赋值符

5.单目操作符

5.1单目操作符介绍

5.2单目操作符的一些常见场景

5.3sizeof和数组练习题

 6.关系操作符

 7.逻辑操作符

8.条件操作符

 9.逗号表达式

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

10.1下标引用操作符[ ],操作数:一个数组名+一个索引值

10.2函数调用操作符()

10.3成员访问操作符 

​编辑 11.表达式求值

11.1隐式类型转换

 11.2算术转换

 11.3操作符属性


操作符分类

 1.算数操作符

        算术操作符主要有:+(加)、-(减)、*(乘)、/(除)、%(取余)。

        值得注意的几点有:(1)+、-、*、/都可用于整数与浮点数的运算,%又称整数取余,只适用于整数的运算,且返回的是整除之后的余数(求余数的操作符)。

                                        (2)当 +、-、*、/这几个操作数在处理整数与浮点数的混合运算时,运算结果为浮点类型。

        7 / 3的本质是整数/整数,发生的是整数除法,所以会先产生一个整数2,然后才会将2赋值给m,n。 

        7 / 3.0的本质是整数/浮点数,它会先产生一个浮点数2.333333,然后赋值给整型变量m的时候把小数部分截断了,但是浮点数变量n是能直接接收的。 

2.移位操作符(只适用于整数范围)

(1)引入

        整数的二进制表示形式有:原码、反码、补码。对于一个整型变量是4字节 = 32bit位。

        原码:按照数值的正负,直接写出的二进制序列(整型变量就是32个bit位)就是原码。

        对于有符号整数来说,第一位(最高位)是符号位,符号位为1表示是负数,符号位为0表示正数。 

        对于无符号整数来说,没有符号位,所有位都是有效位。

eg:10

原码->00000000 00000000 00000000 00001010从最低位到最高位(从右到左),第n位的位权为2^(n-1),但不包括符号位。

eg:-10

原码->10000000 00000000 00000000 00001010

        对于正的整数来说,原码反码补码三者一致,而对于负的整数来说,反码是原码的符号位不变,其他位按位取反;补码是反码+1。在内存中存一个整数的时候,存的是它的补码二进制序列,同时,在计算的时候,也是用补码来计算。

(2)左移操作符<<

        7的二进制序列是00000000 00000000 00000000 00000111,移位之后变成00000000 00000000 00000000 00001110,最高位要丢弃,最低位后面补0,也就是14,然后赋值给m。注意:n是不变的,还是7,它只是参与运算了。向左移动一位的时候和乘以2的效果是一样的。

        n = -7->

原码:10000000 00000000 00000000 00000111

反码:11111111 11111111 11111111 11111000

补码:11111111 11111111 11111111 11111001(计算机拿来计算)

左移:11111111 11111111 11111111 11110010(还是补码)

补码求反码  - 1:11111111 11111111 11111111 11110001

反码求原码:10000000 00000000 00000000 00001110(展示)

        左移的特点就是左边丢弃,右边补0。

(2)右移操作符>>

        右移操作符分为两种:一种右移叫算术右移,另一种叫逻辑右移。

        绝大多数的编译器采用的都是算术右移,但具体得根据编译器来判断。算术右移有除2的效果。

        对于移位操作符,不要移动负数位,这个是标准未定义的。eg:10 >> -1//error

 3.位操作符

         位操作符有:

&        按位与

|        按位或

^        按位异或

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

eg:

int a = 3;

int b = -5

int c = a & b;

a的补码:00000000 00000000 00000000 00000011

b的原码:10000000 00000000 00000000 00000101

b的反码:11111111 11111111 11111111 11111010

b的补码:11111111 11111111 11111111 11111011

计算机计算过程拿补码来进行计算

所以a & b ->

00000000 00000000 00000000 00000011

11111111 11111111 11111111 11111011

=>00000000 00000000 00000000 00000011

因为符号位为0->正数,所以原码也是这个即3

        按位或和按位异或都是这样计算的。就不举例计算了,需要知道在位操作上,1或上任何数=1,0或上任何数=任何数 异或就是相同为0,不同为1。异或有两条常用的数学性质:a ^ a = 0;0 ^ x = x。

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

        这是创建临时变量的做法。 可以用加减来实现不创建临时变量的做法->

        这种方法会有溢出的风险,因为a + b可能超出int的范围 。

        这种用位运算(异或操作)来实现两个数的交换,且不会有溢出风险。 注:异或操作是支持交换律的。但是这种方法只适用于整型,浮点数不支持异或操作。

练习:求一个整数存储在内存中的二进制中1的个数->

        这个题的意思就是找出这个数在内存中的二进制序列中1的个数,可以使用按位与&1操作这个二进制序列上的每一位,如果是1,计数器++->

#include <stdio.h>
int main()
{
	int num = 10;
	int count = 0;//统计1的个数
	for (int i = 0; i < 32; i++)
	{
		num = num >> i;
		if ((num & 1) == 1)
			count++;
	}
	printf("%d\n", count);

	return 0;
}

        当然也可以让1去左移31位去&num,判断num的二进制序列上的每一位是不是1,其实还有一种思路,就是在上述基础上判断那里可以改成num是否是奇数,因为最低位是1,表示它是个奇数,其他位的位权全是2的倍数。->

#include <stdio.h>
int main()
{
	int num = 10;
	int count = 0;//统计1的个数
	while (num)
	{
		if (num % 2 == 1)
			count++;
		num = num / 2;
	}
	printf("%d\n", count);

	return 0;
}

        注:一定是/2,因为当num在内存中的二进制序列表示负数的时候,符号位也要算上去,如果是>>此时,编译器默认为是算术右移,会一直将符号位置1,导致陷入死循环->

        总之第二种方法的在适用范围很小,很容易出错,慎用。 

 4.赋值操作符

#include <stdio.h>

int main()
{
	int a = 20;//不是赋值,初始化
	a = 30;//赋值

	return 0;
}

        赋值是变量存在的情况下,修改变量的值,赋值是能够连续赋值的->

int a = 30;
int x = 20;
int y = 10;
a = x = y * 5;

        在连续赋值那块,是从右向左依次赋值,等效于下面->

x = y * 5;
a = x;

        下面这种操作,逻辑较清晰,且易于调试。

 复合赋值符

+=-=*=/=%=
>>=<<=&=|=^=

         这些运算符都可以拆分为 x = x () y;就是x和y运算后的结果存入x。

5.单目操作符

5.1单目操作符介绍

!                                逻辑反操作

-                                负值

+                               正值

&                               取地址

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

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

--                               前置--,后置--

++                             前置++,后置++

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

(类型)                        强制类型转换

        单目操作符的意思就是只有一个操作数。 

5.2单目操作符的一些常见场景

#include <stdio.h>

int main()
{
	int flag = 0;
	int x = !flag;
	if (!flag)
	{
		printf("%d\n", x);
		printf("%d\n", !100);
	}

	return 0;
}

         !就是将逻辑真(非0),变为逻辑假(0),逻辑假变为逻辑真。

#include <stdio.h>

int main()
{
	int a = 50;
	int b = +a;
	printf("%d\n", b);
	int c = -a;
	printf("%d\n", c);

	return 0;
}

         正值+操作符本质上是没什么意义的,并不会改变操作数的值。

#include <stdio.h>

int main()
{
	int a = 20;
	int* p = &a;
	int arr[20];
	int (*pa)[10] = &arr;

	return 0;
}

        注:&arr是数组的地址,我们需要用数组指针来接收它的地址,比如整型变量a的地址用整型指针来接收a的地址,这里关于指针、地址后面会细讲,这里不必太关心细节。 

#include <stdio.h>

int main()
{
	int a = 10;
	int* p = &a;
	printf("%d\n", *p);

	return 0;
}

        *p的意思就是对p进行解引用,也就是通过p存放的地址,找到p指向的对象->例如变量a在内存中的地址是0x3f3f3f3f,这个地址存的数据就是a的值10,然后用指针变量p(p也是个变量,也有自己的地址)来存储变量a的地址0x3f3f3f3f,那我*p就是找到这块地址对应的值,也就是a。*p和a是同一块空间,p是另一块空间,这块空间存的是a那块空间的地址。 

        但其实在新版的编译器下,sizeof的返回值类型是size_t(无符号整型),对此类型的数据进行打印,可以使用%zd->

        老版本的编译器没有size_t类型,所以没有%zd的打印格式,可以使用%d或者%u进行打印。sizeof a(这一点也可以说明sizeof是操作符而不是函数,因为函数调用需要函数调用操作符())和sizeof(a)都是可以的,但对于数据类型应该是sizeof(数据类型)。 

        sizeof 数组名是计算整个数组的大小,因为数组元素是int类型的,一个元素占4字节,那十个元素就是占四十字节。sizeof是在计算类型创建变量或者变量的大小,单位是字节。

 

#include <stdio.h>

int main()
{
	int a = 0;
	printf("%d\n", ~a);

	return 0;
}

        记得转换为二进制序列再操作,这是计算机计算的规则,换算回来就是-1。 

        那我们如何再换算回来呢?->

         ++自增操作符和--自减操作符->

        这种++在a的前面称为前置++,在后面称为后置++;在前面还是在后面对操作数a是没有影响的,都是使其+1,但对于“别人”是有影响的->

  

        前置++的计算口诀是先让操作数a+1,再使用操作数a;后置++的计算口诀是先使用操作数a,再让操作数a+1。++推广到--是类似的。

#include <stdio.h>

int main()
{
	int a = 1;
	int b = (++a) + (++a) + (++a);
	printf("%d\n", b);

	return 0;
}

        这种代码是无意义的,在不同编译器、不同平台、不同环境,算出来的结果是不一样的。工程中应当尽量减少这种未定义行为!在vs2022下运行的结果是12,但是我换一个gcc环境->

        这里的输出答案是10。同一段代码,在gcc环境和vs下的输出结果都不一样,编译器的输出都不一样,那人还怎么算?

        我们知道这样初始化,编译器会将3.14识别为double类型,但a是个整型类型,所以编译器会弹出警告,要消除这个警告,可以用double的类型变量来接受这个3.14,或者是将3.14进行强制类型转换->

        强制类型转换是到万不得已的情况才使用,毕竟“强扭的瓜不甜”。

5.3sizeof和数组练习题

#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;
}
问:
(1)、(2)两个地方分别输出多少?
(3)、(4)两个地方分别输出多少?

        (1)、(3) 很明显是40和10,那(2)和(4)呢?

         我这边是x64环境,输出的是8和8,因为数组传参的时候,传的是数组首元素的地址,是个指针类型的变量,即使形参写成那个样子,但那并不是本质,只是语法允许这样写,所以传过去的是个指针,当然在x64环境下就是8字节,在x86环境下就是4字节(这里就不验证了)。

        只是语法上形参允许这样写,但底层是指针做形参,并不是用数组做形参,因为当数组很大的时候传参,形参是实参的一份拷贝,那这个效率就太低了,C语言是一门追求效率的语言,传地址过去,一样能找到那块空间。 

 6.关系操作符

        关系操作符唯一需要注意的就是不要把赋值操作符=和关系操作符==搞混了。

>

>=

<

<=

!=                 测试不等于

==                测试相等

 7.逻辑操作符

&&        逻辑与        并且

||           逻辑或        或者

 

 

#include <stdio.h>

int main()
{
	int month = 0;
	scanf("%d", &month);
	if (month >= 3 && month <= 5)
		printf("春季\n");
	else if (month == 12 || month == 1 || month == 2)
		printf("冬季\n");

	return 0;
}

 360笔试题:

#include <stdio.h>
int main()
{
    int i = 0,a=0,b=2,c =3,d=4;
    i = a++ && ++b && d++;
    //i = a++ || ++b || d++;
    printf("a = %d\n b = %d\n c = %d\nd = %d\n", a, b, c, d);
    return 0;
}
//程序输出的结果是什么?

         i = a++ && ...我们知道a++表达式的值是0,然后a += 1,->i = 0 && ...我们知道0 && ...等于0,后面的表达式的值已经不重要了,因此这个逻辑表达式&&会发生逻辑短路,后面表达式的值不会再算下去,直接输出i = 0。这种逻辑运算符的这种特性叫做逻辑短路

        我们现在换成||逻辑运算符->

        i = a++ || ++b || d++,a++表达式的值是0,然后a += 1;->i = 0 || ++b || ...,因为0 || ...的结果还可能是真,所以后面会继续计算,++b表达式的值是3,b也已经变成了3,0 || 3 -> 1所以变成1 || ...,此时不管后面的表达式是真还是假,整体一定是真,||左边遇1则整体为1(逻辑真),所以后面的表达式已经没有必要算下去了,即发生了逻辑短路。

        所以,&&左边遇0(假)即发生逻辑短路,||左边遇真(非0)也会发生逻辑短路。或许我们可以利用这个特性,将优先级高的放左边,优先级低的放右边,然后就会发生一些不可思议的现象。

8.条件操作符

        exp1 ? exp2 : exp3   其中expi是表达式,条件操作符是唯一的一个三目操作符。如果表达式1exp1的结果为真,那么就计算表达式2,不计算表达式3,表达式2的结果就是整个表达式的结果;如果表达式1exp1的结果为假,那就计算表达式3,不计算表达式2,表达式3的结果就是整个表达式的结果。

         eg:计算a和b的较大值->

        转换成条件表达式-> 

        这两种写法是等效的。 

 9.逗号表达式

        exp1,exp2,exp3,...expn。逗号表达式就是用逗号隔开的表达式。逗号表达式从,从左至右依次进行。整个表达式的结果是最后一个表达式的结果。

int a = 1;
int b = 2;
int c = (a>b, a=b+10, a, b=a+1);
c是多少?

        从左至右依次计算过去,最后一个表达式的结果为整个表达式的结果。

a = get_val();
count_val(a);
while (a > 0)
{
         //业务处理
        a = get_val();
        count_val(a);
}

         因为上面和循环内部是重复的,所以可以利用逗号表达式的特点写成->

while (a = get_val(), count_val(a), a>0)
{
         //业务处理
}

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

10.1下标引用操作符[ ],操作数:一个数组名+一个索引值

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

        数组的下标也叫数组元素的索引。

10.2函数调用操作符()

 #include <stdio.h>
 void test1()
 {
     printf("hehe\n");
 }
 void test2(const char *str)
 {
     printf("%s\n", str);
 }
 int main()
 {
     test1();            //实用()作为函数调用操作符。
     test2("hello bit.");//实用()作为函数调用操作符。
     return 0;
 }

        对于函数调用操作符来说,有两个操作数,一个是函数名,另一个是参数。对于一个函数调用操作符来说,它至少要有一个操作数——函数名。

10.3成员访问操作符 

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

#include <stdio.h>

struct Book
{
	char name[20];
	int price;
};

int main()
{
	struct Book book1 = { "C语言学习", 100 };
	printf("%s %d\n", book1.name, book1.price);

	return 0;
}

         *pb就是book1这块空间,再.访问变量的成员,C语言语法支持直接pb->成员。二者是完全等价的。

 11.表达式求值

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

11.1隐式类型转换

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

1.整形提升

2.算术转换

        整型提升的意义:

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

int main()
{
	char a = 5;
	//5是整型:
	//00000000 00000000 00000000 00000101
	//a只能接受8位
	//00000101
	char b = 126;
	//126是整型:
	//00000000 00000000 00000000 01111110
	//b只能接受8位
	//01111110
	char c = a + b;
	//a、b发生整型提升
	//a:00000000 00000000 00000000 00000101
	//b:00000000 00000000 00000000 01111110
	//相加:00000000 00000000 00000000 10000011
	//c:10000011
	
	//打印的时候,又整型提升:11111111 11111111 11111111 10000011
	//补码转换为原码打印:10000000 00000000 00000000 01111101	——-125
	printf("%d\n", c);

	return 0;
}

        b和c的值被提升为普通整型,然后在执行加法运算。加法运算完成后,结果被截断,然后在存储在a中。int——signed int(默认)、char到底是signed char还是unsigned char是不确定的,C语言标准没有明确规定,是取决于编译器的,在当前使用的vs2022下,char默认为signed char。整型提升的时候,如果是有符号类型,记得在高位全部加0/1(原先的符号位是0就加0是1就加1)。CPU就是这样算的,但是我们直接5和126的补码直接开算也是对的。

//实例1
#include <stdio.h>

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==0xb6的时候,a会发生整型提升,由于符号位是1,所以高位全部补1,可以想简单点就是,0xb6赋值给a的时候,a是个负数,0xb6是实打实的正数(整型),同理b也是如此。

//实例2
#include <stdio.h>

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 个字节。

 11.2算术转换

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

long double

double

float

unsigned long int

long int

unsigned int

int

        如果某个操作数的类型在上面这个表格中排名较低,那么首先要转换为另一个操作数的类型后执行运算。注意: 数据类型在整型大小以下,才会发生整型提升,在整型大小及以上才会发生算术转换,且转换要合理,要不然会存在一些潜在问题。

float f = 3.14;
int num = f;//隐式转换,会有精度丢失

 11.3操作符属性

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

1.操作符的优先级

2.操作符的结合性

3.是否控制求值顺序

        两个相邻的操作符先执行哪个?取决于他们的优先级。如果两者的优先级相同,取决于他们的结合性。控制求值顺序比如逻辑操作符的逻辑短路和条件操作符的取其一求值。

操作符优先级:

操作
描述
用法示例
结果类
结合
是否控制求值
顺序
()
聚组
(表达式)
与表达
式同
N/A
()
函数调用
rexp rexp ...,rexp
rexp
L-R
[ ]
下标引用
rexp[rexp]
lexp
L-R
.
访问结构成员
lexp.member_name
lexp
L-R
->
访问结构指针成员
rexp->member_name
lexp
L-R
++
后缀自增
lexp ++
rexp
L-R
--
后缀自减
lexp --
rexp
L-R
!
逻辑反
! rexp
rexp
R-L
~
按位取反
~ rexp
rexp
R-L
+
单目,表示正值
+ rexp
rexp
R-L
-
单目,表示负值
- rexp
rexp
R-L
++
前缀自增
++ lexp
rexp
R-L
--
前缀自减
-- lexp
rexp
R-L
*间接访问
* rexp
lexp
R-L
&
取地址
& lexprexp
R-L
sizeof
取其长度,以字节
表示
sizeof rexp sizeof(类型)
rexp
R-L
(
型)
类型转换
( 类型 ) rexp
rexp
R-L
*
乘法
rexp * rexp
rexp
R-L
/
除法
rexp / rexp
rexp
L-R
%
整数取余
rexp % rexp
rexp
L-R
+
加法
rexp + rexp
rexp
L-R
-
减法
rexp - rexp
rexp
L-R
<<
左移位
rexp << rexp
rexp
L-R
>>
右移位
rexp >> rexp
rexp
L-R
>
大于
rexp > rexp
rexp
L-R
>=
大于等于
rexp >= rexp
rexp
L-R
<
小于
rexp < rexp
rexp
L-R
<=
小于等于
rexp <= rexp
rexp
L-R
==
等于
rexp == rexp
rexp
L-R
!=
不等于
rexp != rexp
rexp
L-R
&
位与
rexp & rexp
rexp
L-R
^
位异或
rexp ^ rexp
rexp
L-R
|
位或
rexp | rexp
rexp
L-R
&&
逻辑与
rexp && rexp
rexp
L-R
||
逻辑或
rexp || rexp
rexp
L-R
? :
条件操作符
rexp ? rexp : rexp
rexp
N/A
=
赋值
lexp = rexp
rexp
R-L
+=
...
lexp += rexp
rexp
R-L
-=
...
lexp -= rexp
rexp
R-L
*=
...
lexp *= rexp
rexp
R-L
/=
...
lexp /= rexp
rexp
R-L
%=
... 取模
lexp %= rexp
rexp
R-L
<<=
... 左移
lexp <<= rexp
rexp
R-L
>>=
... 右移
lexp >>= rexp
rexp
R-L
&=
...
lexp &= rexp
rexp
R-L
^=
... 异或
lexp ^= rexp
rexp
R-L
|=
...
lexp |= rexp
rexp
R-L
逗号
rexp rexp
rexp
L-R
int main()
{
	//优先级:相邻操作符
	int a = 10 + 5 * 2;
	//结合性:优先级相同情况下,结合性才有作用
	int b = 3 + 5 + 7;//先3 + 5还是先5 + 7呢?+的结合性是L-R就是先算3+5,从左到右计算。

	return 0;
}
//表达式的求值部分由操作符的优先级决定。
//表达式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

        如果abcdef是变量不论哪种计算路径答案都是对的,但如果是表达式呢?(可能用到相同的变量)。 

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

         +两边的操作数谁先准备呢?假设c=2,左边先准备,那就是2 + 1,右边先准备就是,--c,c变成1,在准备左边的c(已经变成了1),那就是1 + 1。

//代码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();我们只能通过操作符的优先级得知:先算乘法,再算减法。但函数调用先后顺序无法通过操作符的优先级确定。有两种可能:1. 2 - 3 * 4;2. 4 - 2 * 3。 vs2022下结果如下:

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

         可以试试看一下汇编代码,看看到底发生了啥,就可以分析清楚了->

        记得先切到x86环境下,这个环境下较简单一些。

         F11进入调试。记得调出右边的监视窗口。

        然后对着代码鼠标右键跳转到反汇编->

        记得关闭显示符号名->

         逐步调试下来发现->

        这段代码中的第一个 + 在执行的时候,第三个 ++ 是否执行,这个是不确定的,因为依靠操作符的优先级和结合性是无法决定第一个 + 和第三个前置 ++ 的先后顺序。

        vs2022下它的计算逻辑是先算三个++i再计算两次+即4 + 4 + 4 = 12。 

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2185163.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SQL:函数以及约束

目录 介绍 函数 字符串函数 数值函数 日期函数 流程函数 约束 总结 介绍 说到函数我们都不陌生,在C,C,java等语言中都有库函数,我们在平时也是经常使用,函数就是一段代码,我们既可以自定义实现,又可以使用库里内置的函数;从来更加简洁方便的完成业务;同样的在SQL中也有…

vscode qt 最新开发环境配置, 基于最新插件 Qt All Extensions Pack

qt 之前发布了vscode qt offical ,但是最新更新中将其升级改为了几个不同的插件&#xff0c;功能更强大 1. 前置条件 qt 已安装 2. 插件安装 打开vscode 插件安装&#xff0c;搜索qt 会看到很多qt插件&#xff0c;直接选择Qt All Extensions Pack 安装 会安装qt环境所需的…

国内旅游:现状与未来趋势分析

在当今社会快速发展的背景下&#xff0c;国内旅游更是呈现出蓬勃的发展态势。中国&#xff0c;这片拥有悠久历史、灿烂文化和壮丽山河的广袤土地&#xff0c;为国内旅游的兴起与发展提供了得天独厚的条件。 本报告将借助 DataEase 强大的数据可视化分析能力&#xff0c;深入剖…

Linux:深入理解冯诺依曼结构与操作系统

目录 1. 冯诺依曼体系结构 1.1 结构分析 1.2 存储结构分布图 2. 操作系统 2.1 概念 2.2 如何管理 2.3 什么是系统调用和库函数 1. 冯诺依曼体系结构 1.1 结构分析 不管是何种计算机&#xff0c;如个人笔记本电脑&#xff0c;服务器&#xff0c;都是遵循冯诺依曼结构。…

小论树形dp

文章目录 树形dp 概述树形dp 路径问题 树的最长路径 思路代码树的中心 换根DP思路代码数字转换 思路代码树形dp 有依赖的背包 二叉苹果树 思路代码树形dp 状态机 没有上司的舞会 思路代码战略游戏 思路代码皇宫看守 思路代码总结 概述 树形 DP&#xff0c;即在树上进行的 …

通信工程学习:什么是DQDB分布式队列双总线

DQDB&#xff1a;分布式队列双总线 DQDB&#xff08;Distributed Queue Dual Bus&#xff09;&#xff0c;即分布式队列双总线&#xff0c;是美国电气电子工程师学会(IEEE)802.6标准中定义的一种城域网(MAN)数据链路层通信协议。该协议主要用于城域网的数据、语音和视频传输&am…

Python 中的 os 模块

Python 中的 os 模块 在Python中&#xff0c;os 模块是一个内置的标准库&#xff0c;提供了许多与操作系统交互的功能。它允许你执行一系列操作&#xff0c;如文件和目录操作、环境变量管理等。要在Python脚本中使用os模块&#xff0c;你需要首先导入它。 一些常见的用法&…

如何在 Android 中用 Kotlin 将 dp 转换为 px

我们在开发 Android 应用时&#xff0c;经常需要将 dp&#xff08;密度无关像素&#xff09;转换为 px&#xff08;像素&#xff09;。这是因为不同设备有不同的屏幕密度&#xff0c;使用 dp 可以保持在不同设备上的一致性。&#x1f4f1; 但究竟如何将 dp 转换为 px 呢&#x…

鸿蒙网络管理模块02——Socket

如果你也对鸿蒙开发感兴趣&#xff0c;加入“Harmony自习室”吧&#xff01;扫描下方名片&#xff0c;关注公众号&#xff0c;公众号更新更快&#xff0c;同时也有更多学习资料和技术讨论群。 1、概述 Socket 连接主要是通过 Socket 进行数据传输&#xff0c;支持 TCP/UDP/Mul…

Redis篇(面试题 - 连环16炮)(持续更新迭代)

目录 &#xff08;第一炮&#xff09;一、Redis&#xff1f;常用数据结构&#xff1f; 1. 项目里面到了Redis&#xff0c;为什么选用Redis&#xff1f; 2. Redis 是什么&#xff1f; 3. Redis和关系型数据库的本质区别有哪些&#xff1f; 4. Redis 的线程模型了解吗&#x…

探索未来:掌握python-can库,开启AI通信新纪元

文章目录 **探索未来&#xff1a;掌握python-can库&#xff0c;开启AI通信新纪元**背景介绍**python-can**库简介安装指南函数使用示例应用场景常见问题及解决方案总结 探索未来&#xff1a;掌握python-can库&#xff0c;开启AI通信新纪元 背景介绍 在人工智能和物联网的飞速…

[Go语言快速上手]函数和包

目录 一、Go中的函数 函数声明 多个返回值 可变参数 匿名函数 值传递和地址传递 函数执行顺序&#xff08;init函数&#xff09; 二、Go中的包 基本语法 主要包&#xff08;main package&#xff09; 导入其他包 包的作用域 包的使用 包名别名 小结 一、Go中的函…

重生之我们在ES顶端相遇第 18 章 - Script 使用(进阶)

文章目录 0. 前言1. 基本使用2. 读请求中访问文档字段2.1 遍历 List2.2 判断对象存不存在2.3 判断值是否为空2.4 总结 3. 写请求中访问文档字段3.1 数字相加3.2 字符串相加3.3 将字符串转为数组 0. 前言 在前面部分&#xff0c;我们介绍了 ES 的基本使用和要掌握的基础性读写原…

TypeScript 算法手册【快速排序】

文章目录 1. 快速排序简介1.1 快速排序定义1.2 快速排序特点 2. 快速排序步骤过程拆解2.1 选择基准元素2.2 划分数组2.3 递归排序 3. 快速排序的优化3.1 三数取中法选择基准3.2 插入排序与快速排序结合案例代码和动态图 4. 快速排序的优点5. 快速排序的缺点总结 【 已更新完 Ty…

二分查找算法专题(1)

找往期文章包括但不限于本期文章中不懂的知识点&#xff1a; 个人主页&#xff1a;我要学编程(ಥ_ಥ)-CSDN博客 所属专栏&#xff1a; 优选算法专题 目录 二分查找算法的介绍 704. 二分查找 34. 在排序数组中查找元素的第一个和 最后一个位置 35. 搜索插入位置 69. x的平…

10-指针和多维数组

多维数组&#xff0c;本质上是数组的数组&#xff1a; 一、多维数组&#xff1a; int B[2][3] int(*P)[3] B;Print B //400 Print *B; //400 Print B[0] //400 Print &B[0][0] // 400B[i][j] *(B[i]j) *(*(Bi)j); int C[3][2][2] int(*p)[2][2] C; Print C //800 Prin…

大数据开发--1.1大数据概论

目录 一.大数据的概念 什么是大数据&#xff1f; 二. 大数据的特点 三. 大数据应用场景 四. 大数据分析业务步骤 大数据分析的业务流程&#xff1a; 五.大数据职业规划 职业方向 岗位技术要求 六. 大数据学习路线 一.大数据的概念 什么是大数据&#xff1f; 数据 世界…

【北京迅为】《STM32MP157开发板嵌入式开发指南》- 第十六章 Linux 第一个程序 HelloWorld

iTOP-STM32MP157开发板采用ST推出的双核cortex-A7单核cortex-M4异构处理器&#xff0c;既可用Linux、又可以用于STM32单片机开发。开发板采用核心板底板结构&#xff0c;主频650M、1G内存、8G存储&#xff0c;核心板采用工业级板对板连接器&#xff0c;高可靠&#xff0c;牢固耐…

LC记录二:丑数专题,一文秒解丑数3题

文章目录 263.丑数1264.丑数21201.丑数3 263.丑数1 https://leetcode.cn/problems/ugly-number/description/ 简单题&#xff0c;丑数只包含质因子2、3、5。所以直接使用 n 循环 除 2 3 5最后判断结果是否等于1即可。 代码&#xff1a; class Solution {public boolean isUg…

01_SQLite

文章目录 ** SQLite 存储各类和数据类型 **** SQLite 五种亲缘类型** SQLite 创建数据表删除数据表插入数据信息从数据表中获取数据&#xff0c;以结果表的形式返回数据&#xff08;结果集&#xff09;updatedistinctorder bygroup byhaving触发器删除一个触发器&#xff08;tr…