初级C语言之【操作符】

news2024/9/22 13:41:34

🦖作者:学写代码的恐龙
🦖博客主页:学写代码的恐龙博客主页
🦖专栏:【初级c语言】
🦖语录:❀未来的你,一定会感谢现在努力奋斗的自己❀
在这里插入图片描述

初级C语言之【操作符详解】

  • 一:操作符分类
  • 二:算数操作符
  • 三:移位操作符
    • 3.1:左移
      • 3.1.1:正数左移
      • 3.1.2:负数左移
    • 3.2:右移
  • 四:位操作符
    • 4.1:按位与
    • 4.2:按位或
    • 4.3:按位异或
    • 4.4:不创建临时变量,实现对两个整数的交换
      • 4.4.1:方法一
      • 4.4.2:方法二
  • 五:赋值操作符
    • 5.1:复合赋值符
  • 六:单目操作符
    • 补充:如果把一个负数放到了无符号类型的变量里会发生什么呢?比如:
    • 6.1:取地址符&
    • 6.2:解引用操作符*
    • 6.3:sizeof操作符
    • 6.4:~按位取反操作符
    • 6.5:综合练习
    • 6.6:++、--操作符
      • 6.6.1:后置
      • 6.6.2:前置
    • 6.7:强制类型转化
    • 6.8:sizeof和数组
  • 七:关系操作符
  • 八:逻辑操作符
    • 8.1:一道例题
  • 九:条件操作符
  • 十:逗号表达式
  • 十一:下标引用、函数调用和结构成员
    • 11.1:下标引用操作符
    • 11.2:函数调用操作符
    • 11.3:结构成员访问操作符
  • 十二:表达式求值
    • 12.1:隐式类型转换
    • 12.2:如何进行整型提升?
    • 12.3:整型提升的例子
    • 12.4:算数转化
    • 12.5:操作符的属性
    • 12.6:一些问题表达式

一:操作符分类

1:算术操作符
2:移位操作符
3:位操作符
4:赋值操作符
5:单目操作符
6:关系操作符
7:逻辑操作符
8:条件操作符
9:逗号表达式
10:下标引用、函数调用和结构成员

二:算数操作符

    +    -   *   /   %

注意:对于除法操作符来说,两边的操作数都是整数,执行的是整数除法,即就是结果只取整数部分。如果想计算出小数,除号的两端至少有一个操作数是浮点数

//除法操作符
int main()
{
	int ret1 = 10 / 3;
	printf("%d\n", ret1);
	double ret2 = 10.0 / 3;
	printf("%lf\n",ret2);
	printf("%.1lf\n", ret2);//得到一位小数
	return 0;
}
//结果:
3
3.333333
3.3
//取模(取余)操作符(得到余数)
int main()
{
	int ret = 10 % 3;
	printf("%d\n", ret);
	return 0;
}
//结果:
1

注意:取模操作符两边的操作数只能是整数(整型类型),除了%操作符,其他几个操作符可以用于整数和浮点数

三:移位操作符

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

注:移位操作符的操作数只能是整数。 这里说的移位是移的二进制位

//移位操作符
int main()
{
	int a = 10;
	int b = a << 2;//a向左移动两位的结果放到b里
	int c = a >> 1;//a向右移动一位的结果放到c里
	printf("%d\n", b);
	printf("%d\n", c);
	return 0;
}
//结果:
40
2

既然上面已经说到移位操作符移动的是二进制位,那就先来给大家介绍一下二进制。整数的二进制表示形式其实有三种原码、反码、补码
例如

    12-数值
    二进制:1100
    八进制:14
    十进制:12
    十六进制:C
   这里的12只是一个数值,我们可以用不同的进制将其表示出来 以上这些都可以用来表示12

对于正数,它的 原码、反码、补码都是相同的
例如: 10的二进制是1010,在计算机中一个整型在计算机中占4个字节(32bit),补全就是:00000000000000000000000000001010,这是正10,在计算机中-10如何表示呢?在计算机中用二进制的最高位区分正负最高位如果是0,那就表示正数,最高位如果是1,那就表示负数,因此:二进制的最高位在计算机中又被称作符号位,所以-10在计算机中用10000000000000000000000000001010表示。
原码:按照一个数的正负,直接写出它的二进制表示形式得到的就是原码

例如:10的原码:00000000000000000000000000001010
     -10的原码:10000000000000000000000000001010

正数的原码、反码、补码都是相同的,负数的反码和补码要经过计算

反码:原码的符号位不变,其他按位取反得到的就是反码

例如:10的反码:00000000000000000000000000001010(正数的原码、反码、补码都是相同)
     -10的反码:11111111111111111111111111110101

补码:反码加1得到的就是补码

例如:10的补码:00000000000000000000000000001010(正数的原码、反码、补码都是相同)
     -10的补码:11111111111111111111111111110110

内存中存的是二进制的补码形式,所以在参与移位的时候,移的是补码

原码、反码、补码三者之间的转换
在这里插入图片描述

可见:不管是原码到补码,还是补码到原码,都可以通过“符号位不变,其他位按位取反后再+1”得到,这便是计算机的奇妙之处

接着介绍左移操作:

3.1:左移

3.1.1:正数左移

以10为例,要对其进行移位操作,首先要得到10的补码,10的补码是00000000000000000000000000001010,向左移动一位形象来说就是把10的二进制的补码向左移动一位,这时右边就空出来一位,这时就需要进行补0,最终得到00000000000000000000000000010100,对应十进制就是20.

在这里插入图片描述

int main()
{
	int a = 10;
	int b = a << 1;//把a向左移动一位的结果放到b里
	printf("%d\n", a);
	printf("%d\n", b);
	return 0;
}
//结果:
10
20
//注意:a的值并不变

3.1.2:负数左移

以-10为例,要对其进行移位操作首先要得到-10的补码,根据上面的分析我们直到-10的补码是:11111111111111111111111111110110,和正10一样,如果要对其进行左移一位的操作,形象来说就是把-10的二进制的补码向左移动一位,这时右边就空出来一位,这时就需要进行补0,最终得到11111111111111111111111111101100,需要注意的是:我们得到的是补码,而在屏幕上打印出来的是由二进制的原码形式转化出来的数字,因此我们需要把的得到的补码转化为原码的形式,通过上面介绍的方式我们并不难得出该补码的原码是:10000000000000000000000000010100,该原码对应的十进制就是-20,最终-10左移1位得到的就是-20.

//-10左移一位
int main()
{
	int a = -10;
	int b = a << 1;
	printf("%d\n", a);
	printf("%d\n", b);
	return 0;
}
//结果:
-10
-20

可见对一个十进制数字左移一位有×2的效果。

3.2:右移

首先右移运算分两种:

    1. 逻辑移位
    左边用0填充,右边丢弃
    2. 算术移位(平时比较常见)
    左边用原该值的符号位填充,右边丢弃

具体采用何种右移主要取决于编译器,大多数编译器都采用算数右移

//算数右移:将-1向右移动一位
int main()
{
	int a = -1;
	int b = a >> 1;
	printf("%d\n", a);
	printf("%d\n", b);
	return 0;
}
//结果:
-1
-1

注意:无论是向左移还是向右移都不能移动负数位!

四:位操作符

 &            //按位与
 |            //按位或
 ^            //按位异或

注意:以上三个操作符的操作数必须是整数,同时也是针对二进制位(内存中的补码)进行操作的

4.1:按位与

//按位与
int main()
{
	int a = 3;
	//00000000000000000000000000000011(3的原、反、补码)
	int b = -5;
	//10000000000000000000000000000101(-5的原码)
	//11111111111111111111111111111010(-5的反码)
	//11111111111111111111111111111011(-5的补码)
	int c = a & b;
	//00000000000000000000000000000011(3的补码)
	//11111111111111111111111111111011(-5的补码)
	//按位与的操作是:对应的二进制位上有0,与的结果就是0,只有当两个都是1,结果才是1。
	//00000000000000000000000000000011(按位与得到的结果,这还是内存中的补码形式,打印出来的是二进制位对应的原码形式,但是这里的符号位是0,说明是正数,那么它的原码、反码、补码是一样的)
	//此时打印出来的结果就是3
	printf("%d\n", c);
	return 0;
}
//结果:
3

4.2:按位或

//按位或
int main()
{
		int a = 3;
		//00000000000000000000000000000011(3的原、反、补码)
		int b = -5;
		//10000000000000000000000000000101(-5的原码)
		//11111111111111111111111111111010(-5的反码)
		//11111111111111111111111111111011(-5的补码)
		int c = a | b;
		//00000000000000000000000000000011(3的补码)
		//11111111111111111111111111111011(-5的补码)
		//按位或的操作是:对应的二进制位上有1,或的结果就是1,只有当两个都是0,结果才是0。
		//11111111111111111111111111111011(按位与得到的结果,这还是内存中的补码形式,打印出来的是二进制位对应的原码形式,这里的符号位是1,说明是负76数,此时我们就需要求出它的原码)
		//10000000000000000000000000000100
		//10000000000000000000000000000101(打印时需要的原码)
		//此时打印出来的就是-5
		printf("%d\n", c);
		return 0;
}
//结果:
-5

4.3:按位异或

//按位异或
int main()
{
	int a = 3;
	//00000000000000000000000000000011(3的原、反、补码)
	int b = -5;
	//10000000000000000000000000000101(-5的原码)
	//11111111111111111111111111111010(-5的反码)
	//11111111111111111111111111111011(-5的补码)
	int c = a ^ b;
	//00000000000000000000000000000011(3的补码)
	//11111111111111111111111111111011(-5的补码)
	//按位异或的操作是:对应的二进制位相同为0,不同为1。
	//11111111111111111111111111111000(按位与得到的结果,这还是内存中的补码形式,打印出来的是二进制位对应的原码形式,这里的符号位是1,说明是负数数,此时我们就需要求出它的原码)
	//10000000000000000000000000000111
	//10000000000000000000000000001000(打印时需要的原码)
	//此时打印出来的就是-8
	printf("%d\n", c);
	return 0;
}
//结果:
-8

4.4:不创建临时变量,实现对两个整数的交换

4.4.1:方法一

//不创建临时变量,实现对两个整数的交换
int main()
{
	int a = 3;
	int b = 5;
	printf("%d %d\n", a, b);

	a = a + b;//把a与b的和8放到a里面
	b = a - b;//此时a里面是a与b的和8,再减去b(5)得到的就是a的值(3),把这个值放到b里面去,此时的b就是之前a的值了,也就是3
	a = a - b;//因为此时的a里面存的还是a与b的和(8),b的值是之前a的值了,也就是3,此时a-b就是就是之前b的值5,把5放到a里面
	//经过上面3个表达式就实现了不创建临时变量,实现对两个整数的交换

	printf("%d %d\n", a, b);
	return 0;
}
//结果:
3 5
5 3

注意:此方法存在一个问题就是:当a和b的数值比较大的时候,它们各自不溢出,但当加到一起时就有可能出现溢出的情况。

4.4.2:方法二

在介绍方法二之前大家需要直到异或的三个性质:

  1:a^a=0
  2:0^a=a
  3:异或满足交换律和结合律,即:a^b^c=a^(b^c),a^b=b^a
int main()
{
	int a = 3;
	int b = 5;
	printf("%d %d\n", a, b);

	a = a ^ b;
	b = a ^ b;//也就是:(a^b)^b=a^(b^b)=a^0=a,此时就把a的值放到b里面去了,b里面存的就是a
	a = a ^ b;//也就是:(a^b)^a=a^a^b=0^b=b,此时就把b的值存到a里面去了,a里面存的就是b
	

	printf("%d %d\n", a, b);
	return 0;
}
//结果:
3 5
5 3

五:赋值操作符

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

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;//连续赋值,先把y+1的值赋值给x,再把x的值赋给a,从右往左进行赋值
//不建议这样写,可读性差,应该写成下面这样:
x=y+1;
a=x;

5.1:复合赋值符

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

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

int x = 10;
x = x+10;
x += 10;//复合赋值
//其他运算符一样的道理。这样写更加简洁。

六:单目操作符

单目操作符就只有一个操作数

 !           逻辑反操作
 -           负值
 +           正值
 &           取地址
 sizeof      操作数的类型长度(以字节为单位)
 ~           对一个数的二进制按位取反
 --          前置、后置--
 ++          前置、后置++
 *           间接访问操作符(解引用操作符)
 (类型)       强制类型转换

补充:如果把一个负数放到了无符号类型的变量里会发生什么呢?比如:

int main()
{
	unsigned int a = -10;//把-10放到无符号类型的变量a里面去
	//因为内存中存的是补码,这里是-10为负数,要先求出-10的补码
	//10000000000000000000000000001010(-10的原码)
	//11111111111111111111111111110101(-10的反码)
	//11111111111111111111111111110110(-10的补码)
	//此时a里面存的就是:11111111111111111111111111110110(-10的补码)
	//由于此时的a是无符号整型,所以对于a来说他就没有符号位这个概念,存在a里面的所有数都是正整数,原、反、补都一样,所以在a看来11111111111111111111111111110110就是一个原码,会直接把它转换成对应的数字
	printf("%u\n", a);//无符号整型用格式符u打印
	return 0;
}
//结果:
4294967286

6.1:取地址符&

//取地址符&
int main()
{
	int a = 10;
	printf("%p\n", &a);
	int* pa = &a;//取出变量a的地址放在整型指针变量pa里面
	printf("%p\n", pa);

	char ch = 'w';
	printf("%p\n", &ch);
	char* pc = &ch;//取出变量ch的地址放在字符型指针变量pc里面
	printf("%p\n", pc);


	char* p = "abcdef";//字符串常量的值其实就是首字符的地址,所以这里我们用一个字符类型的指针变量来接收
	printf("%p\n", p);
	printf("%c\n", *p);
	return 0;
}
//结果:
012FF6F8
012FF6F8
012FF6E3
012FF6E3
00F77BD0
a

6.2:解引用操作符*

//解引用操作
int main()
{
	int a = 10;
	int* pa = &a;//把a的地址放在pa变量里
	*pa=20;//通过解引用操作就可以取到a的值,并对a的值进行修改(注意:是对地址进行解引用)
	printf("%d\n", a);
	return 0;
}
int main()
{
	*(int*)0x0012ff40=10;//随便编写了一个地址对其进行解引用是非法的
	return 0;
}

6.3:sizeof操作符

sizeof是关键字也是操作符,是计算变量或者类型的大小单位是字节

int main()
{
	int a = 10;
	printf("%d\n", sizeof(a));//计算一个变量的大小
	printf("%d\n", sizeof a);//这里可以不带括号,说明sizeof不是函数,因为函数即使没有参数也要带括号
	printf("%d\n", sizeof(int));//计算一个类型的大小,这里sizeof后面的括号不能省略

	int arr[10] = { 0 };
	printf("%d\n", sizeof arr);//计算一个数组的大小
	printf("%d\n", sizeof(int[10]));//其中int [10]是数组arr的类型
	return 0;
}

注意:sizeof内部的表达式是不参与计算的。为什么呢?
因为我们写的是一个.c的代码,最终运行的是一个.exe可执行程序,在这之间会经过编译、链接,而对sizeof的操作是在编译阶段就进行了,而表达式是在最终的可执行程序里运行的,到最终运行的时候就不再有sizeof和它后面的表达式了,因此sizeof后面的表达式根本就没有执行。

int main()
{
	int a = 10;
	short s = 5;
	printf("%d\n", sizeof(s = a + 3));//a+3=13赋给s的这个动作是不会发生的,如果一个整形的数据非要放到一个短整型里会发生截断,所以最终的大小还是s说了算
	printf("%d\n", s);
	return 0;
}
//结果:
2
5

6.4:~按位取反操作符

//~按位取反
int main()
{
	int a = 0;
	//00000000000000000000000000000000
	//11111111111111111111111111111111(按位取反,得到的是一个补码)
	//10000000000000000000000000000000
	//10000000000000000000000000000001(打印时需要的原码)
	printf("%d\n", ~a);//结果就是-1
	return 0;
}

6.5:综合练习

//综合练习

int main()
{
	int a = 9;
	//00000000000000000000000000001001
	//把9的二进制位第五位上的0变成1
	//首先想到按位或操作
	//00000000000000000000000000010000  其实就是:1<<4
	//00000000000000000000000000011001(按位或的结果就是我们想要的),其实就是25
	a |= (1 << 4);
	printf("%d\n", a);//打印出来的就是25

	//把a的二进制中的第五位改回来,变0
	//00000000000000000000000000011001
	// 可以用异或操作
	//00000000000000000000000000010000    其实就是:1<<4
	//00000000000000000000000000001001(按位异或的结果就是我们想要的)
	a ^= (1 << 4);
	printf("%d\n", a);

	//还可以利用按位与操作
	//00000000000000000000000000011001
	//11111111111111111111111111101111   其实就是:~(1<<4)
	//00000000000000000000000000001001(按位与的结果就是我们想要的)
	a &= ~(1 << 4);
	printf("%d\n", a);
	return 0;
}
//结果:
25
9
9

6.6:++、–操作符

6.6.1:后置

先使用,再进行++或者–操作

int main()
{
	int a = 10;
	int b = a++;//后置++,先使用,再++
	//相当于:int b=a;a=a+1;
	printf("a=%d\n", a);
	printf("b=%d\n", b);
	return 0;
}
//结果:
a=11
b=10

6.6.2:前置

先进行++或者–操作,再使用

int main()
{
	int a = 10;
	int b = ++a;//先进行++操作,再把a加之后的值赋值给b
	printf("a=%d\n", a);
	printf("b=%d\n", b);
	return 0;
}
//结果:
a=11
b=11

++和- -有副作用,会让自身的值也发生变化

int main()
{
	int a = 10;
	int b = ++a;//b=11,a=11


	int a = 10;
	int b = a + 1;//b=11,a=10
	return 0;
}

6.7:强制类型转化

强制类型转换是在迫不得已的时候才用,不要故意去强制类型转换,强扭的瓜不甜!

int main()
{
	int a = (int)3.14;//把一个浮点型强制转化成整型,直接取整数部分,不会进行四舍五入
	printf("%d\n", a);
	return 0;
}

6.8:sizeof和数组

void test1(int arr[])//数组传参,形参可以写成数组,也可以写成指针
{
	printf("%zd\n", sizeof(arr));//(2)这里其实是计算整型指针的大小,在×86(32位机)的环境下指针的大小是4个字节
}
void test2(char ch[])
{
	printf("%zd\n", sizeof(ch));//(4)这里的ch本质上也是指针,所以在×86(32位机)的环境下大小还是4个字节
}
int main()
{
	int arr[10] = { 0 };
	char ch[10] = { 0 };
	printf("%zd\n", sizeof(arr));//(1)计算的是数组的大小,单位是字节10*4=40
	printf("%zd\n", sizeof(ch));//(3)计算的是数组的大小,单位是字节10*1=10
	test1(arr);//数组名是首元素地址
	test2(ch);//数组名是首元素地址
	return 0;
}
//结果:
40
10
4
4

补充:

void test1(int arr[], int sz)//数组传参,形参用数组来接收
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
}

void test2(int* arr, int sz)//数组传参,形参用指针来接收
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);//这里的arr[i]其实就是*(arr+i),加i就是跳过i个整型,然后解引用
	}
	printf("\n");
}



int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	test1(arr, sz);
	test2(arr, sz);
	return 0;
}
//结果:
1 2 3 4 5 6 7 8 9 10
1 2 3 4 5 6 7 8 9 10

七:关系操作符

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

需要注意:=和==,前者是赋值操作符

八:逻辑操作符

 &&    逻辑与
 ||    逻辑或

逻辑与就相当于日常生活中说的并且逻辑或就相当于日常生活中的或者逻辑与和逻辑或只关注真假,在C语言中0表示假,非0表示真。逻辑表达式的值只有真或者假,真用1表示,假用0表示

aba&&ba l l b

8.1:一道例题

int main()
{
	int i = 0, a = 0, b = 2, c = 3, d = 4;
	i = a++ && ++b && d++;//其中a++的值是0,因为后面都是逻辑与,并且a++的值为0,对于逻辑与来说,只要有0结果就一定是0,所以此时后面是什么值已经不重要了,不会再执行后面的表达式了,因此就只执行了a++这个表达式
	//对于逻辑与,只要左边为假,右边就不再计算
	//对于逻辑与,只要左边为真,右边就不再计算
	//i = a++||++b||d++;
	printf("a = %d\nb = %d\n
c = %d\nd = %d\n", a, b, c, d);
	return 0;
}   
//结果:
a = 1
b = 2
c = 3
d = 4

九:条件操作符

 exp1 ? exp2 : exp3

其中exp1、exp2、exp3分别对应三个表达式,它们三个组合成条件表达式,如果exp1为真exp2计算exp3不计算条件表达式的值是exp2的结果,如果exp1为假exp2不计算exp3计算条件表达式的值是exp3的结果条件操作符也被叫做三目操作符

//把a和b的较大值赋值给m
int main()
{
	int a = 10;
	int b = 20;
	int m = 0;
	if (a > b)
		m = a;
	else
		m = b;

	m = (a > b ? a : b);//利用条件操作符来实现

	return 0;
}

十:逗号表达式

> exp1, exp2, exp3, …expN

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

//逗号表达式

int main()
{
	//代码1
	int a = 1;
	int b = 2;
	int c = (a > b, a = b + 10, a, b = a + 1);//逗号表达式,从左到右依次计算,整个逗号表达式的结果是最后一个表达式的结果
	printf("%d\n", c);//c最终是13
	return 0;
}
//结果:
13

//代码2
if (a =b + 1, c=a / 2, d > 0)//最终决定if真假的是最后一个表达式d>0,但如果前面的表达式中出现了d,就会影响最终d>0的结果,因此还是要老老实实的从左往右计算


//代码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)
{
         //业务处理
}

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

11.1:下标引用操作符

 [ ] 下标引用操作符

下标引用操作符有两个操作数:一个数组名 + 一个索引值

//下标引用操作符
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	printf("%d\n", arr[4]);//[]就是下标引用操作符
	return 0;
}

11.2:函数调用操作符

( ) 函数调用操作符

函数调用操作符接受一个或者多个操作数:第一个操作数是函数名,剩余的操作数就是传递给函数的参数当函数没有参数的时候就只有函数名这一个操作数

//函数调用操作符
int main()
{
	int len = strlen("abcdef");//()就是函数调用操作符,操作数:strlen,"abcdef".对于函数调用操作符来说至少有一个操作数函数名(函数没有参数的时候),
	return 0;
}

11.3:结构成员访问操作符

访问一个结构的成员

  .  结构体变量.成员名
  -> 结构体指针->成员名
//结构成员访问操作符
//结构体——自定义类型(聚合体)
//生活中有些对象要被描述,不能简单的使用单个内置类型
//书:书名,作者,出版社,定价,……

//类型
struct Book
{
	char name[20];
	char author[30];
	int price;
};

void print1(struct Book* p)
{
	printf("《%s》%s %d\n", (*p).name, (*p).author, (*p).price);
	printf("《%s》%s %d\n", p->name,p->author,p->price);//结构体指针->成员名
}


int main()
{
	struct Book b1 = { "大话数据结构","佚名",66 };
	struct Book b2 = { "C语言","佚名",65 };
	printf("《%s》%s %d\n", b1.name, b1.author, b1.price);//结构体变量.成员名
	printf("《%s》%s %d\n", b2.name, b2.author, b2.price);
	print1(&b1);
	return 0;
}

十二:表达式求值

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

12.1:隐式类型转换

隐式类型转换通俗来说就是偷偷的发生类型转化,我们并没有感知到,但他确确实实在时刻发生着。
C的整型算术运算总是至少以缺省整型类型的精度来进行的。,为了获得这个精度,表达式中的字符短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升

整型提升的意义:

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

12.2:如何进行整型提升?

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

//整型提升
//char short int long……
//  1    2    4
//整型提升针对的是类型的字节数小于整形的,char或者short类型的变量在参与运算的时候,首先会把它们提升为整型
int main()
{
	//当前编译器(vs)char-->signed char(有符号的char,所以它的最高位就是符号位)
	char a = 3;//首先3是一个整数,那它一定是4个字节
	//00000000000000000000000000000011,要把这个存到a里面去,但是a是一个字符型,就只有一个字节,肯定存不下这么多,只能存下最右边的八个比特位
	//00000011 - a中存的
	//四个字节的数据非要放到一个字节的变量里面去,这叫截断

	char b = 127;
	//00000000000000000000000001111111
	//11111111 - b中存的

	char c = a + b;//在计算的时候,首先会把a和b转化为int类型,转化完之后再去加,加的结果也是一个int类型,最终把这个int类型的结果放到c里面
	//00000011 - a中存的
	//01111111 - b中存的
	//接下来就要发生整型提升
	//00000000000000000000000000000011 -- a经过提升后
	//00000000000000000000000001111111 -- b经过提升后
	//接下来就可以进行相加的操作了
	//00000000000000000000000010000010 -- a+b的结果
	//要把这个(四个字节)存到c(字符型,一个字节)里面去,会发生截断
	//10000010 -- c中存的

	printf("%d\n", c);
	//%d -- 打印十进制的整数,所以这里以%d来打印一个字符型变量c也会进行整型提升
	//10000010 -- c中存的
	//11111111111111111111111110000010 -- c经过提升后,注意:这还是内存中存储的,所以这一串二进制是补码,而我们打印需要的是原码
	//10000000000000000000000001111101 -- 符号位不变,其他位按位取反、
	//10000000000000000000000001111110 -- 原码(+1)
	//对应二进制:-126

	return 0;
}
//结果:
-126
  补充:char - - 有符号的char的取值范围是:-128~127
                 无符号的char的取值范围是:0~255  
      short - - 有符号的short的取值范围是:-32768~32767
                无符号的short的取值范围是:0~65535

char类型对应一个字节对应八个比特位,所以一个字符型变量它可以存储的二进制序列是从“00000000~11111111”,一共有256个序列。

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

12.3:整型提升的例子

//整型提升的例子:
int main()
{
	char a = 0xb6;//对应二进制:10110110
	//00000000000000000000000010110110 -- 0xb6
	//10110110 -- a中存的
	
	short b = 0xb600;
	int c = 0xb6000000;

	if (a == 0xb6)
	//这里:a == 0xb6 是一个表达式,在对表达式进行计算的时候,就会进行整型提升
	//10110110 -- a中存的
	//接下来要对a进行整型提升
	//11111111111111111111111110110110 -- a经过整型提升后
	//00000000000000000000000010110110 -- 0xb6
	//显然:a和0xb6并不相等,所以 表达式:a == 0xb6 的值位假,就不会执行下面的打印a

		printf("a\n");
	if (b == 0xb600)
		printf("b\n");
	if (c == 0xb6000000)//这里c本来就是整型,所以就不会进行整型提升
		printf("c\n");
	return 0;
}
//结果:
c
//实例2
int main()
{
	char c = 1;
	printf("%u\n", sizeof(c));
	printf("%u\n", sizeof(+c));//这里+c就是一个表达式,只要是表达式,在参与运算的时候就会进行整型提升,提升之后就变成四个字节了
	printf("%u\n", sizeof(-c));
	return 0;
}
//结果:
1
4
4

在这里插入图片描述

当鼠标放到+c这个表达式上面的时候,可以看出:此时的c已经从字符型提升到无符号的整型了

12.4:算数转化

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

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

如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运算。也就是说:算数转换就高不就低,会把低的转换成高的,也就是把字节数小的转换成字节数大的。

12.5:操作符的属性

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

  1.操作符的优先级
  2.操作符的结合性
  3.是否控制求值顺序

两个相邻的操作符先执行哪个?取决于他们的优先级如果两者的优先级相同,取决于他们的结合性。

12.6:一些问题表达式

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
(需要注意的是:不要把这里的a、b、c、d、e、f只看做单一的变量,它们各自也可以是一个表达式,并且可能有相同的变量,此时先算的可能就会影响到后算的),为了避免出现上面这种情情况,可以加括号来决定到底谁先算。


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

int c=3;
c + --c;
//情形一:
//+左边的c在--c之前就准备好了,此时+左边的c就是3,--c的结果就是2,此时c + --c的结果就是5.
//情形二:
//+左边的c在--c之后才准备好,此时先执行--c,--c的结果是2,此时c变成2,然后+左边的c开始准备,所以+左边的c就是2,此时表达式c + --c的值就是4

//问题代码
int fun()
{
    static int count = 1;
    return ++count;
}
//第一次调用返回2,第二次调用返回3,第三次调用返回4

int main()
{
    int answer;
    answer = fun() - fun() * fun();
    //这里我们只能知道 * 比 + 先算,但是不知道这三个fun函数到底哪一个最先调用
    printf("%d\n", answer);//输出多少?
    return 0;
}

上面这段代码在我的vs编译器上执行的结果是:-10。说明最左边的这个fun函数先调用结果是2,最后的两个fun函数到底谁先调用还是不能确定,因为不管谁先调用结果都是3*4=12.


//问题代码
int main()
{
	int i = 1;
	int ret = (++i) + (++i) + (++i);
	printf("%d\n", ret);
	printf("%d\n", i);
	return 0;
}

上面这段代码按照正常思维,我们算出来的ret值应该是9,也就是最左边的++i先算,结果是2,然后算中间的++i,结果是3,接着算最右边的++i,结果是4,最后计算:ret=2+3+4=9.但是!!!在我的vs编译器中这段代码的执行结果是:ret=12,i=4。!!!而这段代码在Linux环境的结果是:ret=10,i=4


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

到这里,操作符的有关分享就结束啦,喜欢的话可以点赞、评论和收藏哟!

在这里插入图片描述

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

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

相关文章

综合能源系统分析的统一能路理论(三):《稳态与动态潮流计算》(Python代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

spring6笔记3(bean的循环依赖,手写spring框架,ioc注解开发,JdbcTemplate)

第九章、Bean的循环依赖问题 9.1 什么是Bean的循环依赖 A对象中有B属性。B对象中有A属性。这就是循环依赖。我依赖你&#xff0c;你也依赖我。 比如&#xff1a;丈夫类Husband&#xff0c;妻子类Wife。Husband中有Wife的引用。Wife中有Husband的引用。 public class Husband…

【java线程池详解】

java线程池详解线程的基本状态Executor框架Executor框架组成部分Executor框架使用示意图Runnable接口、Callable接口ExecutorsFuture接口和实现Future接口的FutureTask类Future和FutureTask的关系ThreadPoolExecutor类ThreadPoolExecutor 饱和策略&#xff08;拒绝策略&#xf…

MySQL去重,一条SQL语句完美解决【去重留一】

此处以某消费记录表(consume_record)为例&#xff0c;SQL语句如下&#xff1a; DELETE consume_record FROM consume_record, ( SELECT min(id) id, user_id, monetary, con…

Qt第五十五章:Qt Design Studio设计登录页并打包到python运行

目录 一、Qt Design Studio 二、导出所有文件到QRC&#xff08;不要改动默认的QRC文件名称&#xff09; 三、QRC转换成py 1.删除Constants.qml中的 2.将App.qml和Screen01.qml中的 3.转换 4、将QRC文件和转换后的py文件&#xff0c;复制到python项目中使用。 一、Qt Des…

【云原生 Kubernetes】k8s集群部署springboot项目

一、前言 本篇&#xff0c;我们将基于k8s集群&#xff0c;模拟一个比较接近实际业务的使用场景&#xff0c;使用k8s集群部署一个springboot的项目&#xff0c;我们的需求是&#xff1a; 部署SpringBoot项目到阿里云服务器 &#xff1b;基于容器打包&#xff0c;推送私有镜像仓…

Presto 之 BTreeIndex 索引代码走读

一. 前言 本文主要介绍在Presto&#xff08;OpenLookeng&#xff09;中的BTree索引的代码实现。关于BTree索引原理的介绍可以参考官网资料openLooKeng documentation。 二. BTreeIndex 索引建立 在Presto中&#xff0c;BTreeIndex 索引是通过mapdb中的BTreeMap数据结构实现的&a…

【java入门系列一】java基础

学习记录&#x1f914;写在前面JDK\JREPython有没有虚拟机&#xff1f;第一个code规范学习方法转义符号注释讨论总结谢谢点赞交流&#xff01;(❁◡❁)更多代码&#xff1a; Gitee主页&#xff1a;https://gitee.com/GZHzzz博客主页&#xff1a; CSDN&#xff1a;https://blog.…

13---SpringBoot整合JWT,实现登录和拦截

1、 JWT简介 什么是JWT&#xff1f; JWT(JSON Web Token)是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准。它将用户信息加密到token里&#xff0c;服务器不保存任何用户信息。服务器通过使用保存的密钥验证token的正确性&#xff0c;只要正确即通过验证&…

在Ubuntu上安装Azure DevOps代理程序

Contents1 概述2. 安装Ubuntu 18.04操作系统3. 安装Azure DevOps Server 代理3.1 安装Azure DevOps Server 代理3.2 以服务方式运行代理1. 概述Ubuntu是一个以桌面应用为主的Linux操作系统&#xff0c;目前在不适用微软Windows的企业中&#xff0c;ubuntu被广泛应用在个人电脑中…

网络原理4 数据链路层

文章目录mac地址网络原理的总结在数据链路层中&#xff0c;最主要的就是以太网协议这里的目的IP和原地址都是mac地址 mac地址 首先要知道什么是Mac地址&#xff0c;mac地址也叫做物理地址或以太网地址&#xff0c;它是一个用来确认网络设备位置的位置&#xff0c;一个网卡就会…

javaWeb——第一章概述

目录 1.1 软件的分类 1.2 软件架构 1.3 web软件 1.4 web程序 web服务器&#xff1a; Tomcat: 扩展 Java web就是窗口和程序之间的交互&#xff1a; 1.1 软件的分类 系统软件 应用软件 介于两者之间的中间件&#xff08;插件&#xff09; 1.2 软件架构 B/S 服务器与浏…

ZC706P+ADRV9009连接RADIOVERSE详解之三

做好SD卡映像&#xff0c;连接好硬件之后&#xff0c;我们就可以尝试软件操作了。 步骤1&#xff1a;设置好网络 打开软件界面我们看到&#xff0c;板子默认的地址为192.168.1.10 端口号为55555.我们一定也设置跟板子连接的以太网口处于192.168.1网段&#xff0c;并且子网掩码…

【ESP32+freeRTOS学习笔记-(四)任务调度机制】

目录1 、什么是任务的调度机制1.1 概念1.2 三种算法1.3 决定算法的宏2、基本词条解释3、调度算法解释3.1 具有时间片的优先级抢先调度 Prioritized Pre-emptive Scheduling with Time Slicing3.1.1 图解高优先级任务抢占低优先级任务3.1.2 图解具有时间片的优先级抢占3.1.3 总结…

如何通过少量样本推断整体业务情况

在产品运营中非常常见&#xff0c;为了能够解决大量数据时分析效率急剧下降的窘况&#xff0c;我们就必须能够去分析非常小量样本的特征&#xff0c;再用这些特征去评估海量总体数据的特征&#xff0c;我们叫它样本检验。 样本&#xff0c;是指我们需要“分析或考察的数据”的…

MAC(m1)-安装Redis6.2.8

Redis官网&#xff1a;Download | Redis 我准备下载7以前的版本 下载放到如下位置 在这个目录打开终端&#xff1a; 编译测试&#xff0c;执行命令&#xff1a;sudo make test 等待了好久&#xff0c;估计好几分钟 最后出现&#xff1a; 下面准备安装redis&#xff0c;编译安…

计算机网络的定义和性能指标

目录计算机网络的定义计算机网络的分类计算机网络的性能指标速率带宽吞吐量时延时延带宽积往返时间利用率丢包率计算机网络的定义 计算机网络的精确定义并未统一&#xff1b;计算机网络的最简单的定义是&#xff1a;一些互相连接的、自治的计算机的集合&#xff1b; 互连&…

Kubernetes组件_Scheduler_02_二次调度

文章目录一、前言二、二次调度/运行期间调度Descheduler2.1 机器上安装helm2.2 每个机器都要准备好镜像2.3 使用helm部署三、Descheduler需要注意的点(相关理论知识)3.1 descheduler 调度策略3.2 descheduler 有哪些不足3.2.1 基于 Request 计算节点负载并不能反映真实情况3.2.…

【Lua】xLua逻辑热更新

1 前言 Lua基础语法 中系统介绍了 Lua 的语法体系&#xff0c;ToLua逻辑热更新 中介绍了 ToLua 的应用&#xff0c;本文将进一步介绍 Unity3D 中基于 xLua 实现逻辑热更新。 逻辑热更新是指&#xff1a;在保持程序正常运行的情况下&#xff0c;在后台修改代码逻辑&#xff0c;修…

子查询+「EXISTS」 以及 组合查询UNION ALL

目录方便的子查询及EXISTS使用子查询作为计算手段使用子查询过滤数据&#xff08;IN&#xff09;使用子查询过滤数据&#xff08;EXISTS&#xff09;组合查询UNION ALL如何使用UNION ALL合并多个结果集如何使用UNION去除集合的重复记录如何合并2个以上的结果集&#xff1f;方便…