文章目录
- 一、算术操作符
- 二、移位操作符
- 1、 原码、反码、补码
- 2、左移操作符
- 3、右移操作符
- 三、位操作符
- 1、按位与【&】
- 2、按位或【|】
- 3、按位异或【^】
- 4、按位取反【~】
- 5、两道面试题
- 6、进制定位
- 将变量a的第n位置为1
- 将变量a的第n位置为0
- 四、赋值操作符
- 1、复合赋值符
- 五、单目操作符
- 1、单目操作符介绍
- 2、【!】逻辑反操作
- 3、【&】和【*】
- 4、【-】和【+】
- 5、sizeof
- 6、【++】和【- -】
- 7、强制类型转换
- 六、关系操作符
- 七、逻辑操作符
- 一道笔试题~~
- 八、条件操作符
- 九、逗号表达式
- 十、下标引用、函数调用和结构成员
- 1、下标引用操作符 [ ]
- 2、函数调用操作符 ( )
- 3、结构成员调用操作符 . 和 ->
- 十一、表达式求值
- 1、隐式类型转换【整型提升】
- 整型提升的意义
- 2、算术转换
- 3、操作符的属性【附优先级列表】
- 4、问题表达式
- 问题表达式1
- 问题表达式2
- 问题表达式3
- 问题表达式4
- ⑤ 问题表达式5
一、算术操作符
-
除了 【%】 操作符之外,其他的几个操作符可以作用于整数和浮点数。
-
对于【/】 操作符如果两个操作数都为整数,执行整数除法。而只要有浮点数执行的就是浮点数除法。
-
【%】 操作符的两个操作数必须为整数。返回的是整除之后的余数。
-
整数除法:
- 浮点数除法:
- 取余操作符
二、移位操作符
1、 原码、反码、补码
- 其实我们经常能听到2进制、8进制、10进制、16进制这样的讲法,那是什么意思呢?其实2进制、8进制、10进制、16进制是数值的不同表示形式而已。
- 比如:数值15的各种进制的表示形式:
15的2进制:1111
15的8进制:17
15的10进制:15
15的16进制:F
-
对于一个整数来说,在内存中的二进制表示形式有 【原码】【反码】【补码】 三种,写成二进制位的形式也就是32位二进制,例如整数4,它的原码即为
0 0000000000000000000000000000100
。最高位对于32个二进制位来说,叫做符号位
- 符号位是0,表示正整数
- 符号位是1,表示负整数
-
而对于一个正数来说,它的原码、反码、补码都是相同的
-
对于一个负数来说:
- 原码:直接将数值按照正负数的形式翻译成二进制得到的就是原码。
- 反码:将原码的符号位不变,其他位依次按位取反就可以得到反码。
- 补码:反码+1就得到补码。
- 对于整形来说:数据存放内存中其实存放的是补码。
- 为什么呢?
在计算机系统中,数值⼀律⽤补码来表⽰和存储。原因在于,使⽤补码,可以将符号位和数值域统⼀处理;同时,加法和减法也可以统⼀处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。
- 在计算机中都是使用二进制数的补码进行运算的,但是在计算完之后输出的结果都要再转化为原码的形式
2、左移操作符
【移位规则】:左边抛弃、右边补0
- 下面计算一个数字4左移1位后的,将其运算后的结果放到b里面去。
- 在内存中进行计算都是使用补码的形式,因为依次写出
4
的原、反、补码,因为它是正数,所以均是相同的
int main()
{
int a = 4;
//0 0000000000000000000000000000100 - 4的原码
//0 0000000000000000000000000000100 - 4的反码
//0 0000000000000000000000000000100 - 4的补码
int b = a << 1; //把a向左移动一位
printf("a = %d, b = %d\n", a, b);
return 0;
}
- 从上图也可以看出,执行左边丢弃,右边补0,也就是相当于
乘2
,结果就是8
接着我们再来看看负数的情况:
- 对于负数来说,首先要进行的就是取反操作,然后+1就算出了他的补码,然后再进行移位
int main()
{
int a = -4;
//1 0000000000000000000000000000100 - -4的原码
//1 1111111111111111111111111111011 - -4的反码
//1 1111111111111111111111111111100 - -4的补码
int b = a << 1; //把a向左移动一位
//1 1111111111111111111111111111000 - -4移位后的的补码
//1 1111111111111111111111111110111 - -4移位后的的反码
//1 0000000000000000000000000001000 - -4移位后的的原码
printf("a = %d, b = %d\n", a, b);
return 0;
}
- 我们再将移位后的结果再次变成原码的操作,也就是 -1再取反 即可变成原码
- 最后得出的结果再转换为十进制便是
8
3、右移操作符
【移位规则】:
-
逻辑移位
- 左边用0填充,右边丢弃
-
算术移位
- 左边用原该值的符号位填充,右边丢弃
-
对于右移操作符,和左移不同的是它的运算规则比较复杂,在不同编译器下对于移位后的符号位填充是有所不同的,但是在VS下采用的是第二种
算术移位
- 对于右移从结果来看就相当于是缩小的形式
然后我们再来看看负数的情况:
- 对于负数来说,会在计算机内部转化成补码再运算,运算完成后再转换成原码
- 如下图所示:
- 对于移位运算符,不要移动负数位,这个是标准未定义的
int num = 10;
num>>-1;//error
三、位操作符
1、按位与【&】
- 【规则】:全1为1,有0为0
//按位与 - 全1为1,有0为0
int main()
{
int a = 3;
int b = -5;
int c = a & b;
printf("c = %d\n", c);
//00000000000000000000000000000011 - 3的原码、反码、补码
//10000000000000000000000000000101 - -5的原码
//11111111111111111111111111111010 - -5的反码
//11111111111111111111111111111011 - -5的补码
//00000000000000000000000000000011
//11111111111111111111111111111011
//00000000000000000000000000000011 - 3【补码即为原码】
return 0;
}
- 根据
按位与
的运算规则,我们就可以得出最后的结果为3
2、按位或【|】
- 【规则】:有1为1,全0为0
//按位或 - 有1为1,全0为0
int main()
{
int a = 3;
int b = -5;
int c = a | b;
printf("c = %d\n", c);
//00000000000000000000000000000011 - 3的原码、反码、补码
//10000000000000000000000000000101 - -5的原码
//11111111111111111111111111111010 - -5的反码
//11111111111111111111111111111011 - -5的补码
//00000000000000000000000000000011
//11111111111111111111111111111011
// --------------------------------------
//11111111111111111111111111111011 |
//11111111111111111111111111111010 |
//10000000000000000000000000000101 | - 5
return 0;
}
- 根据
按位或
的运算规则,我们就可以得出最后的结果为-5
3、按位异或【^】
- 【规则】:相同为0,相异为1
//按位异或 - 相同为0,相异为1
int main()
{
int a = 3;
int b = -5;
int c = a ^ b;
printf("c = %d\n", c);
//00000000000000000000000000000011 - 3的原码、反码、补码
//10000000000000000000000000000101 - -5的原码
//11111111111111111111111111111010 - -5的反码
//11111111111111111111111111111011 - -5的补码
//00000000000000000000000000000011
//11111111111111111111111111111011
// --------------------------------------
//11111111111111111111111111111000
//11111111111111111111111111110111
//10000000000000000000000000001000 -> -8
return 0;
}
- 根据
按位异或
的运算规则,我们就可以得出最后的结果为-8
注意:
- 两个相同的数异或为0【
a ^ a = 0
】 - 任何数和0异或均为那个数本身【
a ^ 0 = a
】
4、按位取反【~】
【规则】:1变0, 0变1
- 取反直接就可以将1变成0,0变成1
int main()
{
int a = 0;
int b = ~a;
printf("b = %d\n", b);
//00000000000000000000000000000000
//11111111111111111111111111111111 按位取反【补码】
//11111111111111111111111111111110 【反码】
//10000000000000000000000000000001 -> -1【原码】
return 0;
}
- 那么0按位取反之后就变成了
-1
5、两道面试题
两数交换
-
我们之前学的是使用第三方临时变量做一个存放,进行交换
-
还有以一种方法就是通过加减的方式来交换一下这两个数
int main()
{
int a = 3;
int b = 5;
printf("a = %d, b = %d\n", a, b);
a = a + b;
b = a - b;
a = a - b;
printf("a = %d, b = %d\n", a, b);
return 0;
}
- 首先将两个数相加放到a中,再将a-b的值放到b中,此时a中右a和b的值,b中有a的值,最后再将a-b就是b的值
- 上面这种方法有一点不太好,就是当两个很大的数的时候,会出现数据溢出的情况,所以我们还需要另外一种方法:使用异或
- 首先
a = a ^ b
将a和b异或后的结果暂存到a里面去,然后再去异或b的话就相当于是a ^ b ^ b
,根据规则便可以得出结果为a,将其放入b中 - 然后
a = a ^ b
,就相当于是a ^ b ^ a
,那么结果就是b,将其放入a中
int main()
{
int a = 3;
int b = 5;
printf("a = %d, b = %d\n", a, b);
a = a ^ b;
b = a ^ b; //a ^ b ^ b = a ^ 0 = a
a = a ^ b; //a ^ b ^ a = b ^ 0 = b
printf("a = %d, b = %d\n", a, b);
return 0;
}
- 最后打印出来就是交换之后的结果
6、进制定位
将变量a的第n位置为1
思路分析:
- 首先就是要使用到我们上面学习过的
按位或 |
运算,将第三位按位或上一个1,那么这一位就变成了1,但是又不想让其他位置发生变化,那此时就让其他位按位或上一个0
即可,若是那个位上为0,那么就是0,若是那个位上为1,那也为1,那也就是00000000000000000000000000000100
,但是要如何去获取到这个二进制数呢,此时就又需要使用到我们上面讲到过的一个操作符叫做左移<<
那也就是将一个数扩大两倍,这里我们对1进行操作,扩大2倍就是2,再扩大两倍就是我们想要的4,即1 << 2
- 具体的表达式应该为:
[a = a | 1 << 2]
-
此时我们已经完成了第一步,若是你想要置哪个位上的数为1的话,那就修改表达式的最后一个数字即可,这样比较麻烦,需要每次运行前做一个修改,此时我们就来实现题目中的需求:
-
可以再来修改一个位上的数,若是我们要将第5位置为1的话,左移4位即可那也就是
1 << 4
,最后的结果就是26
规律总结
- 若是要置【第3位】的数为1的话,使用数字1左移2位
1 << 2
; - 若是要置【第5位】的数为1的话,使用数字1左移4位
1 << 4
; - 若是要置【第n位】的数为1的话,使用数字1左移(n - 1)位
1 << (n - 1)
;
- 那么此时的话就可以将这个n作为我们自己输入的一个变量,每次想要修改哪一个直接输入即可
int main()
{
int a = 10;
int n = 0;
scanf("%d", &n);
a = a | 1 << (n - 1);
//把变量a的第n为置1
//000000000000000000001010
//000000000000000000010000
//--------------------------------
//000000000000000000001110
printf("a = %d\n", a);
return 0;
}
将变量a的第n位置为0
- 要将一个二进制位置为0的话就又需要使用到我们上面所学习过的
按位与&
,也就是将需要置0的那一位按位与上一个0即可,因为任何数和0进行与都为0,但是呢又不能使得其他二进制位发生改变,那就要使其他二进制位按位与上一个1即可,若是那个位上为0,那么就是0,若是那个位上为1,那也为1,那此时我们再对刚才的第三位进行一个按位与即11111111111111111111111111111011
- 刚才使用的1 << 2,即
00000000000000000000000000000100
,仔细观察就可以看出这两个二进制位其实每个位呈现的都是一个相反的趋势,那么我们在上面使用到的位操作符中哪个具有取反的功能呢,就是按位取反
,那其实只需要将刚才求出的那个表达式外层再加上一个按位取反符就可以了 - 具体的表达式应该为:
[a = a & ~(1 << (n - 1))]
int main()
{
int a = 10;
int n = 0;
while (scanf("%d", &n)!=EOF)
{
a = a | 1 << (n - 1);
//把变量a的第n为置1
//000000000000000000001010
//000000000000000000010000
//--------------------------------
//000000000000000000001110
printf("置1:a = %d\n", a);
a = a & ~(1 << (n - 1));
//把变量a的第n为置0
//000000000000000000001110
//111111111111111111111011
//--------------------------------
//000000000000000000001010
printf("置0:a = %d\n", a);
}
return 0;
}
四、赋值操作符
- 赋值操作符是一个很棒的操作符,他可以让你得到一个你之前不满意的值。也就是你可以给自己重新赋值
int weight = 120; //体重
weight = 89; //不满意就赋值
double salary = 10000.0;
salary = 20000.0; //使用赋值操作符赋值
- 不仅如此,赋值操作符还可以进行连续使用
int b = a += 10;
- 但是对于上面这样的代码其实是不太好的~~
- 但如果写成像下面这样的话就显得非常清爽而且易于调试
a += 10;
b = a;
1、复合赋值符
- 对于赋值操作符来说,还可以和其他操作符进行一个复合的操作
- 例如:【+=】、【-=】、【*=】、【/=】、【%=】、【>>=】、【<<=】、【^=】等等~~
五、单目操作符
1、单目操作符介绍
2、【!】逻辑反操作
- 对于逻辑取反操作符来说,就是
[真变假,假变真]
int main()
{
int flag = 0;
if (!flag)
{
printf("haha\n");
}
return 0;
}
3、【&】和【*】
- 【&】来说叫做取地址操作符,可以获取一个变量在内存中的地址
- 我们还可以将其给到一个指针变量~~
- 那这个时候我要获取到这个指针所指向的地址就是要用到【*】这个解引用操作符
- 这一步操作就是在使这个指针变量重新指向一块新的地址,即
00000014
,转换为十进制也就是【20】
4、【-】和【+】
- 对于这两个操作符并不常用,不能完全说只是单目操作符,在特定的场景下它们也可以算是一个
双目操作符
,例如:-6
的话就只一个单目操作符,8 - 3
的话就是第一个双目操作符,【+】的话也是同理
5、sizeof
-
对于sizeof来说,这个操作符是用来计算数据类型的大小的,字节为单位
-
对于
sizeof()
而言有其特定的返回值打印格式【%zu
】
int a = 10;
int* p;
int arr[10];
printf("%zu\n", sizeof(a)); //int 4
printf("%zu\n", sizeof(p)); //int 4
printf("%zu\n", sizeof(arr)); //特殊,计算的是整个数组的大小
printf("%zu\n", sizeof(arr[0])); //int [10] 40
printf("%zu\n", sizeof(arr[10])); //int 4
sizeof后可省略
- sizeof是一个操作符,而不是函数,函数后面必须带一个括号,而操作符可以不带,下面我们就可以验证一下,后面的()其实是可以省略的
sizeof()内部表达式不参与计算
- 我在sizeof()内部写了一个表达式,这其实也是合法的
int main()
{
short s = 10;
int a = 2;
printf("%d\n", sizeof(s = a + 2));
printf("%d\n", s);
return 0;
}
-
虽然sizeof()内部是可以放表达式的,但是呢这个表达式是不参与运算的
-
我们在看这个sizeof()最后的结果时其实只需要看这个
s
即可,短整型为2个字节,那如果这个表达式不计算的话s的值也就不会发生变化了,所以最后打印出来是10
那为什么这个表达式不参与运算呢?
- 在编译阶段的时候,表达式就不存在了,已经替换成short
sizeof() 与数组
- 我们来看下面的代码~~
#include <stdio.h>
void test1(int arr[])
{
printf("%d\n", sizeof(arr));
}
void test2(char ch[])
{
printf("%d\n", sizeof(ch));
}
int main()
{
int arr[10] = {0};
char ch[10] = {0};
printf("%d\n", sizeof(arr));
printf("%d\n", sizeof(ch));
test1(arr);
test2(ch);
return 0;
}
-
我们要明确,数组名是数组首元素的地址,但是有两个特殊情况
- sizeof(数组名) —— 计算的是
整个数组的大小
【特殊情况1】 - &数组名 —— 获取
整个数组的地址
【特殊情况2】
- sizeof(数组名) —— 计算的是
-
首先第一个【sizeof(arr)】,arr表示数组名,那么计算的就是整个数组的大小,这是一个整型数组,数组中有10个元素,每个元素4个字节,那么整个数组的大小就是40个字节
-
对于【sizeof(ch)】而言也是同理,这是一个字符型数组,数组中有10个元素,每个元素1个字节,那么整个数组的大小就是10个字节
-
接下去就是将数组名传入函数中,除了两种特殊的情况而言,数组名就相当于是首元素地址,然后函数形参中便是指针进行接受,那对于一个指针而言,无论它是【整型指针】、【字符型指针】【浮点型指针】均为4个字节,当然这是在32为系统下,若是在64位系统下就为8个字节,这其实取决于计算机内存中
地址总线
的长度,有兴趣可以去了解一下 -
那既然上面说到一个指针均为4个字节,就可以得出为什么最后一个
sizeof(ch)
为4了,这也只是在求一个指针的大小,不要和字符串数据所占字节数混淆了
6、【++】和【- -】
前置++和后置++
int a = 10;
int b = ++a;
//a = a + 1; b = a;
printf("a = %d, b = %d\n", a, b);
int a = 10;
int b = a++;
//b = a; a = a + 1;
printf("a = %d, b = %d\n", a, b);
- 可以看到,这里的
b = ++a
相当于就是先让a++,然后再把a的值给到b - 可以看到,这里的
b = a++
相当于就是先把a的值给到b,然后再让a++
接着再来看看前置- -和后置- -
int a = 10;
int b = --a;
//a = a - 1; b = a;
printf("a = %d, b = %d\n", a, b);
int a = 10;
int b = a--;
//b = a; a = a - 1;
printf("a = %d, b = %d\n", a, b);
- 对于这里的
b = --a
相当于就是先让a- -,然后再把a的值给到b
- 对于这里的
b = a--
相当于就是先把a的值给到b,然后再让a- -
7、强制类型转换
- 如果我需要将一个浮点类型的数据赋值给一个整形,编译器会报一个警告【精度丢失】,若要强制赋值,就需要做一个强制类型转换
- 可以看到这样就不会出现问题了,要想要把一个不同数据类型的值给到一个另一个数据类型的变量,就可以使用
强制类型转换
int a = (int)3.14;
六、关系操作符
-
关系操作符主要有下面这些:
【>】、【>=】、【<】、【<=】、【! =】、【==】 -
这些我们在使用的时候都会用到,也很简单,就不再多赘述了~~
-
但是这里需要强调的一点就是【==】,在我们平时写代码的时候写这个判断是否等于,容易少一写一个,所以我们建议反着写比如:
a == 10
,我们就可以写成10 == a
七、逻辑操作符
- 这两个操作符很像前面学过的,但是要区分开
- 要区分逻辑与和按位与
- 要区分逻辑或和按位或
特点:
【逻辑与&&】: 表达式两边均为真才是真,若第一个为假,那么整个表达式为假,第二个表达式不参与运算
【逻辑或 ||】: 表达式两边有 一边为真即为真,若第一个为真,那么整个表达式为真,第二个表达式不参与运算
一道笔试题~~
#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 - 这里要注意的一点就是逻辑与前面一个表达式已为假那么第二个表达式是不会参与运算的
八、条件操作符
- 三目操作符:
- 就比如下面这段代码,显示的非常冗余,这个时候我们就可以用到
条件操作符
了
int main()
{
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;
}
- 当条件成立的时候,执行第一个
表达式
,当条件不成立的时候,执行第二个表达式
。
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]
int main()
{
int arr[] = { 1,2,3,4,5 };
int sz = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
for (int i = 0; i < sz; i++)
{
printf("%d ", i[arr]);
}
return 0;
}
- 可以看出两种语法都是可行的
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()
{
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("name = %s\n", ps->writer); //结构体指针->结构体成员名
printf("price = %f\n", ps->price);
- 可以看到对于这三种形式都是可以访问到这个结构体变量的成员
十一、表达式求值
1、隐式类型转换【整型提升】
- C的整型算术运算总是至少以缺省整型类型的精度来进行的
为了获得这个精度,表达式中的字符型和短整型操作数在使用之前被转换为普通整型,这种转换称为[整型提升]
整型提升的意义
- 表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。
- 因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。
- 通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为
int
或unsigned int
,然后才能送入CPU去执行运算
如何进行整型提升
- 整形提升是按照变量的数据类型的符号位来提升的
正数的整形提升
char c1 = 1;
- 正数的原反补码是相同的
- 再将一个整数1赋值给一个char类型的变量c1,此时就会发生截断,字符类型在内存中只占一个字节(8个比特)
- 接下来c1会进行整形提升,在会提升成
00000000000000000000000000000001
达到整型4个字节32个比特位在内存中的要求
负数的整型提升
char c2 = -1;
-
负数在内存中存放的是
11111111111111111111111111111111
,将其给到一个整型的变量之后就会发生截断即为11111111
-
那这个时候再进行一个整型提升就和正数不一样了,因为负数的符号位为1,而整型提升在高位是要补充符号位,所以会在前头加上24个1,那其实也就变回了和之前-1的补码一般的样子,为32个1
2、算术转换
- 如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行
- 如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运算。【排名从上面最高,下面最低】
- 例如
int
和unsigned int
一起进行算术运算的时候这个前者就要转换为后者的类型 - 例如
long int
和long double
一起进行算术运算的时候这个前者就要转换为后者的类型 - 那其实可以看出,在char和short面前称霸的int如今沦落为了小弟
【注意】:
但是算术转换要合理,要不然会有一些潜在的问题
int main()
{
float f = 3.14;
int num = f;//隐式转换,会有精度丢失
printf("%d\n", num);
return 0;
}
3、操作符的属性【附优先级列表】
复杂表达式的求值有三个影响的因素
- 操作符的优先级
- 操作符的结合性
- 是否控制求值顺序
- 两个相邻的操作符先执行哪个?取决于他们的
优先级
。如果两者的优先级相同,取决于他们的结合性
下面有一张关于操作符优先级的列表~~
4、问题表达式
表达式的求值部分由操作符的优先级决定,优先级只能决定先算谁,但是哪个表达式先调用要取决于编译器
- 对于有些表达式而言,其实在不同的编译器上所呈现的结果是不同的,我们将其称作为【问题表达式】
问题表达式1
a*b + c*d + e*f
- 来看上面这段代码,通过上面的优先级列表可以看出
[*]
的优先级一定是比[+]
要来得高,因此可以保证[*]
两端的数字先进行运算,但是却不能保证第三个*比第一个+早执行
问题表达式2
- 继续来看下一个问题表达式
//表达式2
c + --c;
12
- 虽然对于这个
[--]
操作符来说比[+]
操作符的优先级来得高,但是呢我们却不知道在编译器运算的时候这个【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
- 看到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
- 有关
++
和+
结合的问题表达式
//代码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】,在linux里面运行出来是【10】
最后本文就到这里结束了,感谢大家的收看,请多多指点~