本篇博客全站热榜最高排名:2
开始操作~
一、算术操作符
因为MarkDown的语法,所以用图片的形式显示
- 对于算术操作符而言有上面这五种,对于前面的【+】、【-】、【*】来说操作数可以是整数或者浮点数
- 对于【/】来说,叫做整除,结果就是我们在数学中说到的商。若是两边都是整数,则执行执行
整数除法
;只有当两个操作符数中有一个数是小数的时候可以算作是浮点数除法
- 对于【%】来说,叫做取余,结果就是我们在数学中说到的余数。它的两个操作数
必须是整数
我们到VS中来验证一下
再来看看取余操作符
二、移位操作符
接下去我们来说说移位操作符,它是一个为操作符,操作的是二进制数,因为比较重要,所以单独拿出来做讲解
1、前言小知识 —— 原码、反码、补码
在讲述左移、右移操作符之前,要给读者先介绍一下原码、反码、补码,以便更好地理解二进制位的概念
- 对于一个数值来说,可以由不同的进制来表示,在计算机内部,有【二进制、八进制、十进制、十六进制】
- 假设一个十进制数
15
,它的八进制表示形式即为17
,十六进制为F
,二级制为1111
。这个要涉及到每一位上的权重概念,这里的内容很多不好叙述,可以看看——> 进制转换(二进制、八进制、十进制、十六进制)
- 对于一个整数来说,在内存中的二级制表示形式有【原码】【反码】【补码】三种,它们写成二进制位的形式也就是32位二进制,例如整数4,它的原码即为
0 0000000000000000000000000000100
。可以看到我将最高位和后面的31位做了一个分隔,因为对于32个二进制位来说,最高位叫做符号位
- 符号位是0,表示正整数
- 符号位是1,表示负整数
- 而对于一个正数来说,它的原码、反码、补码都是相同的;对于一个负数来说,它的
反码等于原码除符号位外其余按位取反,补码等于反码 + 1
看到这里的读者可能会很好奇,不是要讲移位操作符吗,为什么要先讲这些呢❓
- 在这里你需要记住这一句话,它在后面的位操作符中同样适用
在计算机中都是使用二进制数的补码进行运算的,但是在计算完之后输出的结果都要再转化为原码的形式
2、左移操作符【扩大2倍】
有了上面的知识做铺垫,接下去就让我们真正地进入移位操作符的学习📖
📚【移位规则】:左边抛弃、右边补0
- 首先来看到的是要计算一个数字4左移1位后的,将其运算后的结果放到b里面去。那我们知道,在内存中进行计算都是使用补码的形式,因为依次写出4的原、反、补码,因为它是整数,所以均是相同的
int main(void)
{
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倍,因为对于每一个相邻的二进制数说,均是一个2倍的关系,因为其运行出来的结果即为4 * 2 = 8
,也就是b = 8
- 可以看到,这个原来的数字a是不会发生变化的,只是对其进行一个运算,将其运算之后的结果放到b中去而已
接着我们再来看看负数的情况
- 对于正数来说,其实看不出其在内部中的数值变换,因为原、反、补码均是相同的;但是对于负数来说可不一样了,可以看到下面是对
-4
进行一个移位的操作,写出其补码之后就开始了移位操作
int main(void)
{
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;
}
- 但是在移位之后我们还要将二进制数转换为
原码的形式
,上面说到过,虽然在计算机内部是采用补码的形式进行计算的,但是输出打印在屏幕是是采用原码的形式,所以还是要按照负数原、反、补码的规则进行一个转换,最后得出的结果再转换为十进制便是8
3、右移操作符【缩小2倍】
讲完左移操作符,接下去我们再来讲讲右移操作符
>>
📚【移位规则】:
① 逻辑移位
👉左边用0填充,右边丢弃
② 算术移位
👉左边用原该值的符号位填充,右边丢弃
- 可以看到,对于右移操作符,和左移不同的是它的运算规则比较复杂,因为在不同编译器下对于移位后的符号位填充是有所不同的,但是在VS下采用的是第二种
算术移位
,所以我以此作为讲解
- 因为上面已经给出过代码了,因此这里不做展示,从运行结果可以看出对于右移运算来说就是一个缩小的情况,但具体是如何计算的呢,我们通过画图来看看
然后我们再来看看负数的情况
- 对于负数来说还是一样,在计算机内部会将其转化为补码的形式进行运算,然后再内部计算完毕后还要将其转换为原码的形式
- 下面是它在内存中转换的情况
【注意⚠】
👉对于移位运算符,不要移动负数位,这个是标准未定义的
int main(void)
{
int a = 5;
int b = a << -1; //error
return 0;
}
对于这个移位操作符来说,你可能会决定它不是很常用,所以也没必要很认真地学习,其实这就错了,那是因为你接触得还不够多,其实在某些特定的场合下,它可以起到非常之大的作用,后面有一道综合例题我会进行讲解✒
三、位操作符
好,接下去我们来讲讲位操作符,这也是很多同学长期以来没有搞懂的一块
1、按位与【&】
📚【规则】:全1为1,有0为0
- 一样,对于位运算来说,在计算机中进行运算的是一个数的补码形式,然后打印在屏幕上是原码的形式
//按位与 - 全1为1,有0为0
int main(void)
{
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(void)
{
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(void)
{
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(void)
{
int a = 0;
int b = ~a;
printf("b = %d\n", b);
//00000000000000000000000000000000
//11111111111111111111111111111111 按位取反【补码】
//11111111111111111111111111111110 【反码】
//10000000000000000000000000000001 -> -1【原码】
return 0;
}
- 那么0按位取反之后就变成了
-1
😈两道很变态的面试题😈
介绍完了所有的位操作符符,接下去我们马上来检验一下学习的成果, 下面是两道历年面试中的考题,比较复杂而且也很多的位运算在里面,因为拿出啦做讲解
① 两数交换
首先第一道比较容易一些,对于两个数的交换相信大家是非常熟悉的
- 我们可以使用第三方临时变量做一个存放;若是放到个单独的函数中进行交换的操作,那么就要进行对应的传址操作,如果是在C++中的话还可以使用引用操作符
&
- 但是在本文中,我要使用位运算来进行实现,主要是使用到
异或位运算^
在这之前,我们通过加减的方式来试着交换一下这两个数
int main(void)
{
int a = 3;
int b = 5;
printf("a = %d, b = %d\n", a, b);
a = a + b;
b = a - b; //a + b - b
a = a - b; //a + b - a
printf("a = %d, b = %d\n", a, b);
return 0;
}
- 可以看到,变量中的两个数发生了交换,我们来分析一下它们是如何发生交换的
- 首先执行的是
a = a + b
,也就是把a + b的值放到a里面去,接着第二句是b = a - b
,因为经过上面一句的运算,a里面存放的已经是a + b的和了,那么再减去b的话也就是a,因为我们要交换两个值,所以就是要将a的值放到变量b里面去,所以拿b来接受 - 那此时b里面存放的就是a的值了,但是a里面存放的还是a + b的值,所以第三句
a = a - b
计算出来的结果就是b的值,将其存入变量a中 - 那么此时打印出来a与b值便是交换之后的值
- 但其实对于上面这种方法是有缺陷的,若是a与b是两个很大的数时,就会出现数据溢出的情况,所以在下面我要使用
异或^
的方法来运算 - 可以看到使用异或来进行运算的时候总体的思路还是不变的,将加减运算符改成异或运算符之后,就需要使用到我们上面讲到过的异或拓展规则👈
- 首先第一句
a = a ^ b
将a和b异或后的结果暂存到a里面去,然后再去异或b的话就相当于是a ^ b ^ b
,根据规则便可以得出结果为a,将其放入b中 - 然后第三句
a = a ^ b
,就相当于是a ^ b ^ a
,那么结果就是b,将其放入a中
int main(void)
{
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;
}
- 最后打印出来就是交换之后的结果
通过这道题,相信你对异或的运算一定可以掌握得很好,平常在进行OJ刷题的时候,其实也是可以使用到的,但这前提是你要会灵活使用
② 进制定位
接下去第二道是比较困难的,因为结合了我们上面所学习的所有位操作符
【需求1】:将变量a的第n位置为1
- 看到这个需求你可能有点懵,我来解释一下,这个第n位值的不是十进制位,而是二进制位,也就是将一个变量在内存中的32位中的某一位置为1,但是又不能改变其他位置的数字,那有同学瞬间感觉有些烧脑了🌊
不过没关系,我们慢慢来分析一下🔍
① 思路分析
- 首先不去考虑第n位置,先去考虑某个特定的位置,假设我现在有个变量a为10,那么它的二进制位即为
00000000000000000000000000001010
,那么此时我们先将其第三位置为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(void)
{
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;
}
【需求2】:将变量a的第n位置为0
实现了上面这个需求之后,便要去另一个需求,可以将一个二进制数的某一位置为1,那能不能置为0呢❓ 我们来研究研究🔍
- 要将一个二进制位置为0的话就又需要使用到我们上面所学习过的
按位与&
,也就是将需要置0的那一位按位与上一个0即可,因为任何数和0进行与都为0,但是呢又不能使得其他二进制位发生改变,那就要使其他二进制位按位与上一个1即可,若是那个位上为0,那么就是0,若是那个位上为1,那也为1,那此时我们再对刚才的第三位进行一个按位与即11111111111111111111111111111011
- 可是要怎么产生这些个二进制位呢,还记得刚才使用的1 << 2吗,即
00000000000000000000000000000100
,那其实仔细观察就可以看出这两个二进制位其实每个位呈现的都是一个相反的趋势,那么我们在上面使用到的位操作符中哪个具有取反的功能呢❓其实说得很明显了,就是按位取反
,那其实只需要将刚才求出的那个表达式外层再加上一个按位取反符就可以了 - 具体的表达式应该为:
[a = a & ~(1 << (n - 1))]
- 再来看看运行结果
给出整体代码⌨
int main(void)
{
int a = 10;
int n = 0;
scanf("%d", &n);
a = a | 1 << (n - 1);
//把变量a的第n为置1
//00000000000000000000000000001010
//00000000000000000000000000000100
//--------------------------------
//00000000000000000000000000011010
printf("置1:a = %d\n", a);
a = a & ~(1 << (n - 1));
//把变量a的第n为置0
//00000000000000000000000000001110
//11111111111111111111111111111011
//--------------------------------
//00000000000000000000000000001010
printf("置0:a = %d\n", a);
return 0;
}
从上述这个案例来看,真的可以说是淋漓尽致地展现了位运算的奇妙之处💡
四、赋值操作符
- 赋值操作符是一个很棒的操作符,他可以让你得到一个你之前不满意的值。也就是你可以给自己重新赋值
int weight = 120; //体重
weight = 89; //不满意就赋值
double salary = 10000.0;
salary = 20000.0; //使用赋值操作符赋值
- 不仅如此,赋值操作符还可以进行连续使用
int b = a += 10;
- 但是对于上面这样的代码其实是不太好的,将所有的运算都堆积堆积在一起,就无法调试了,一个F10就进入了下一行代码
- 但如果写成像下面这样的话就显得非常清爽而且易于调试
a += 10;
b = a;
复合赋值符
- 对于赋值操作符来说,还可以和其他操作符进行一个复合的操作
- 例如:【+=】、【-=】、【*=】、【/=】、【%=】、【>>=】、【<<=】、【^=】等等。下面举出两个例子👇
五、单目操作符
1、单目操作符介绍
首先来浏览一下所有的单目操作符👀
2、【!】逻辑反操作
- 对于逻辑取反操作符来说,就是
[真变假,假变真]
int main(void)
{
int flag = 0;
if (!flag)
{
printf("haha\n");
}
return 0;
}
- 具体的用法就是像上面这样,若是某个变量为假的时候,就做一些特殊的操作,当然你也可以写成
if(flag == 0)
,不过这样看起来就不像是一个真假的判断 - 对于这个操作符其实我们在后面数据结构的二叉树中也蛮常用的,因为在二叉树进行向下递归的时候需要有递归出口,那就是当前传进来的根节点是否为NULL,就可以写成
if(!root)
或者是if(root == NULL)
,当然如果你听不懂的话可以看看我的二叉树文章,后续对应地去学习一下即可,这里想到里我就顺便说一下
3、【&】和【*】
- 对于【&】来说叫做取地址操作符,可以获取一个变量在内存中的地址。就如下面这样去取到这两个整形变量和字符型变量的地址
- 当然我现在觉得这样去取地址打印查看太费时间了,那我们就可以将这块地址存放到一个指针中去,在初始C语言的时候我有说到过一个对于地址而言其实就是一个指针,所以我们将其给到一个指针变量是完全没有问题的,只是我们广义上说的
指针接收地址
- 再带大家稍微回顾一下指针的有关知识,对于pa前面的
[*]
而言,指的就是此为一个指针变量,而[*]
前面的[int]
表示这个指针变量所指向的是一个整型的地址。那当前这个指针的名字是什么呢,就是pa
,而不是*pa
- 那这个时候我要获取到这个指针所指向的地址中的对象要怎么做呢,那就是要用到【*】这个解引用操作符
- 若是可以获取到这个地址中的对象,那我们就可以对其进行一个修改了,即
*pa = 20
- 但若是你没有使用
[*]
解引用操作符获取到这个对象就为他赋值,那其实编译器会会报出一个Warning说是等号两边的类型级别不同,这其实就是讲一个整型的变量强制给到一个指针变量,才会出现的类型异常问题
- 那其实这一步操作就是在使这个指针变量重新指向一块新的地址,即
00000014
,转换为十进制也就是【20】
- 再来拓展一块有关
野指针
的内容,因为这一个只有相关很危险☠的东西,也是很多初学者容易犯的
*(int*)0x0012f40 = 100
- 对于上面这个语句,首先我
捏造了
一个十六进制的整数,然后将其强制化为一个整型地址,在前面加上一个[*]
解引用操作符,此时我就可以去修改这个地址中存放着的对象了,将其对象的值修改为100,然后可以看到去编译的时候是没有问题的
- 但是一运行起来可以看到发生了写入异常,这其实就是
野指针
,因为这个十六进制的数值,这个地址是我随手捏造出来的,操作系统并没有为其分配内存,因此这是一块随机的地址值,是不确定的,若是随便去访问这块地址的话就会造成问题 - 这其实和我们在释放掉一块申请的地址时然后没有将其置为NULL是一个道理,若是你有一个指针去访问这块随机的地址,那么你的这个指针就叫做
[野指针]
听完这些,相信你一定回忆起了一些有关指针和地址的知识点
4、【-】和【+】
对于【-】和【+】这个两个操作符并不常用,其实它们不能完全说只是单目操作符,因为在特定的场景下它们也可以算是一个双目操作符
,例如:-6
的话就只一个单目操作符,8 - 3
的话就是第一个双目操作符,【+】的话也是同理,不做赘述
5、sizeof [⭐]
然后我们再来说说一个操作符,叫做sizeof,你没听错,那就是个操作符👈
① 写在前面
- 对于
sizeof
来说,是用来计算操作数的类型长度,它以字节为单位。在sizeof后跟一个小括号(),里面就是你要计算的数据类型,但是很多同学看到这个小括号()的时候很多同学就会认为这是一个函数,它确实和函数的样子很类似。 - 对于函数,它后面的这个小括号叫做
函数调用操作符
,在后面我也会说到,可是对于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(void)
{
short s = 10;
int a = 2;
printf("%d\n", sizeof(s = a + 2));
printf("%d\n", s);
return 0;
}
- 看到上面的这个结果你可能会震惊(○´・д・)ノ让我猜猜看你算出来的答案是不是【4】和【7】呢,亦或是其他的答案,虽然sizeof()内部是可以放表达式的,但是呢这个表达式是不参与运算的
- 我们在看这个sizeof()最后的结果时其实只需要看这个
s
即可,短整型为2个字节,那如果这个表达式不计算的话s的值也就不会发生变化了,所以最后打印出来是10
- 那有同学可能会疑惑为什么这个表达式不参与运算呢?又是语法规定吗?
- 答:一方面是这样,但是在内存中看来,在运行阶段的时候,这个表达式其实早就已经不在了,因为它在编译阶段的时候就会替换成了
short
- 答:一方面是这样,但是在内存中看来,在运行阶段的时候,这个表达式其实早就已经不在了,因为它在编译阶段的时候就会替换成了
- 如果想了解为何在编译阶段会发生替换可以看看我的这篇文章 ——> C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】
- 不过这里还要给读者补充的一点是有关
数据截断
这个小知识,若是我们不将这个表达式放到sizeof()中去,而是拿到外面来计算,那么最后s的结果还是会发生改变的,不过可以看到对于a + 5
的结果它是一个整型,占4个字节,但是变量s呢它是一个短整型,占2个字节,若是 强行将4个字节的数据放到2个字节中去,其实就会发生一个【数据截断】这个一个现象
- 下面其实就是在内存中的一个截断操作,强行将一个整型变量给到一个短整型都会造成这样的情况,所以我们在进行赋值的时候要看清楚数据类型
④ sizeof() 与数组
- 最后再来讲讲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;
}
- 答案是下面这4个,你猜对了吗,前面3个相信你一定没问题,但是对于最后一个我猜一猜是不是算成【1】了 (^ _^)
- 正好通过这一块我们再来回顾一下有关数组名的知识
- 对于数组名,一般来说指的都是
首元素地址
- sizeof(数组名) —— 计算的是
整个数组的大小
【特殊情况1】 - &数组名 —— 获取
整个数组的地址
【特殊情况2】 - 有了这些知识,再来分析一下上面的这段代码,首先第一个【sizeof(arr)】,arr表示数组名,那么计算的就是整个数组的大小,这是一个整型数组,数组中有10个元素,每个元素4个字节,那么整个数组的大小就是40个字节
- 对于【sizeof(ch)】而言也是同理,这是一个字符型数组,数组中有10个元素,每个元素1个字节,那么整个数组的大小就是10个字节
- 接下去就是将数组名传入函数中,除了两种特殊的情况而言,数组名就相当于是首元素地址,然后函数形参中便是指针进行接受,那对于一个指针而言,无论它是【整型指针】、【字符型指针】【浮点型指针】均为4个字节,当然这是在32为系统下,若是在64位系统下就为8个字节,这其实取决于计算机内存中
地址总线
的长度,有兴趣可以去了解一下 - 那既然上面说到一个指针均为4个字节,就可以得出为什么最后一个
sizeof(ch)
为4了,这也只是在求一个指针的大小,不要和字符串数据所占字节数混淆了
以上便是对于sizeof()这个操作符而言要介绍的所有内容,希望读者能够理解😚
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、强制类型转换
最后的话再来说一下强制类型转换
- 比方说我这里将一个
double
类型的数据强制给到一个int
类型的变量,就会出现一种叫做【精度丢失】的现象,若是想要强制给到一个整型的变量,那就要去做一个强制类型转换
- 可以看到这样就不会出现问题了,所以你想要把一个不同数据类型的值给到一个另一个数据类型的变量,就可以使用
强制类型转换
int a = (int)3.14;
六、关系操作符
- 关系操作符主要有下面这些👇
【>】、【>=】、【<】、【<=】、【! =】、【==】 - 对于这些操作符来说大家平常在使用的时候也都会碰到过,这里主要强调的一块就是
==
,因为它经常会和赋值操作符=
混淆,其实也不能说是混淆,应该说是【遗漏】,包括是很多资深的程序员在写代码判断一个数是否等于某个值的时候都会犯这样的错误- 例如将
a == 6
写成a = 6
,若是将这个语句写在if条件判断里的话那么无论a的值为多少都会进入这个条件分支,因为赋值永远都是成立的,也就是为真
- 例如将
- 所以我们平常在判断的时候一般不写
a == 10
,一般都写成10 == a
- 因为前者若是稍加了一个【=】的话编译器是不会报出错误的,那后面再调试的时候就会很麻烦
- 可是若后者少加了一个【=】的话编译器就会报出错误,因为一个变量是不可以赋值给到一个常量的,这就属于语法方面的错误了
对于==
操作符还想再说几句的是我们有时候不仅仅会去比较某个数值,而是去比较两个字符串的内容是否相同或者是比较一些结构体成员的变量,可是呢对于【==】操作符来说是不具备那么强大的功能的,所以要用其他手段去实现
- 对于两个字符串的内容,我后面在将字符串内容的时候会说到一个库函数叫做strcmp,它是专门用来比较字符串内容的,若是直接用
==
去比较的话不是比较的两个字符串的内容,而是比较的两个字符串的首元素地址罢了 - 对于结构体成员的比较,也是有独特的方法,不过对于普通的结构体成员,整数、浮点数就直接用【==】比较也是可以的,字符串的话用上面的
strcmp
,而对于定义出来的整个结构体成员对象的内容就不好比较了,之后我们在学习了C++之后就知道有个东西叫做[仿函数]
,可以比较自定义的数据类型
七、逻辑操作符
逻辑操作符很好记,就两个,和我们前面学过的位操作符中的按位与&
和按位或|
很像
- 要区分逻辑与和按位与
- 要区分逻辑或和按位或
- 可以看到对于逻辑与逻辑或来说它们最终的结果只会有两种,那就是【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的整型算术运算总是至少以缺省整型类型的精度来进行的
👉为了获得这个精度,表达式中的字符型和短整型操作数在使用之前被转换为普通整型,这种转换称为[整型提升]
① 整型提升的意义
🎯表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。
🎯因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。
🎯通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为int
或unsigned int
,然后才能送入CPU去执行运算
通过上面的陈述,相信你对整型提升有了一个初步的概念,接下去我们来看看如何去进行一个整型提升👇
② 如何进行整型提升
- 整形提升是按照变量的数据类型的符号位来提升的
对于整型提升来说,整数和负数是不一样的,首先我们来看看正数的整型提升
//正数的整形提升
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类型的变量中去,那会发生什么化学反应呢🔮
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【截断】
可是呢,我就是不用整形去接收,就是玩😆用字符型去接受,然后再用%d
去打印(“主要还是为了加深知识点的灵活运用”)
printf("c的整数打印形式为:%d\n", c);
- 那在若是在这个时候又要去进行打印的话,又要放到内存里面去运算了,调用这个
printf()
库函数其实也算是一个运算,也要放到内存里面去,然后这个变量c又不是整型,所以此时呢就又会发生一个[整型提升]
了 - 此时就需要去补充
10000011
前面的24个符号位了
//整型提升(负数)
11111111111111111111111110000011 - 补码
11111111111111111111111110000010 - 反码
10000000000000000000000001111101 - 原码
- 然后变要将这个32个二进制位以十进制的形式打印出来,可是计算机中的运算是采取补码的形式,打印输出的话就要采取补码的形式了,所以此时就需要将这个补码转化为原码了,可以看到这是一个负数的补码,所以转化为原码的时候要小心了,需要将补码-1然后再除符号位外均做取反
- 此时再去转化为十进制的形式输出便是
-125
,我们来看看结果【全体起立👮】
接下去再来看看第二个栗子🌰
- 上面呢我们只说到了字符类型的整型提升,下面呢我们再来看看短整型,它们都是属于整型数据类型的一种
- 可以看到定义了三个变量,分别是字符型、短整型、整型,然后初始化了一个十六进制的数据,那我们可以将其转换为二进制的形式便为
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 a
和short b
它们均不是一个整型int类型的数据,所以都会发生一个[整型提升]
,不过int c
它就是一个整型的变量,所以是不会发生变化的
通过这个例子相信你对整型提升一定有了更加深刻的理解
最后一个小案例我们和sizeof做一个结合,顺便再回顾一下前面的知识点📜
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,可以对于下三个结果为什么会发生整型提升呢?我们来分析一下🔍 - 对于
c + 1
来说它是一个表达式,上面说到过若是一个char类型或者是short类型定义的变量参与了运算,那在内存中就会发生一个整型提升,那如果这是一个表达式的话也就相当于是参与了运算,整型提升后变为4个字节,具体细节不做展开 - 那对于
+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、算术转换
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类
型,否则操作就无法进行。下面的层次体系称为【寻常算术转换】
- 其实很好理解。如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运算。【排名从上面最高,下面最低】
- 例如
int
和unsigned int
一起进行算术运算的时候这个前者就要转换为后者的类型 - 例如
long int
和long double
一起进行算术运算的时候这个前者就要转换为后者的类型 - 那其实可以看出,在char和short面前称霸的int如今沦落为了小弟🐒
【警告】:
但是算术转换要合理,要不然会有一些潜在的问题
int main(void)
{
float f = 3.14;
int num = f;//隐式转换,会有精度丢失
printf("%d\n", num);
return 0;
}
- 可以看到,在编译的阶段就出现了一个精度丢失的Warning⚠
3、操作符的属性【附优先级列表】
复杂表达式的求值有三个影响的因素
- 操作符的优先级
- 操作符的结合性
- 是否控制求值顺序
- 两个相邻的操作符先执行哪个?取决于他们的
优先级
。如果两者的优先级相同,取决于他们的结合性
下面有一张关于操作符优先级的列表,可以保存一份日常的参考
👴聊聊问题表达式
表达式的求值部分由操作符的优先级决定,优先级只能决定先算谁,但是哪个表达式先调用要取决于编译器
- 对于有些表达式而言,其实在不同的编译器上所呈现的结果是不同的,我们将其称作为【问题表达式】
① 问题表达式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
上面这五条指令一起说,因为和上面三条一样是行云流水式的
- 首先将
ebp -8
里面的值存放到寄存器【eax】里面去
- 然后给【eax】的值加上一个
ebp - 8
里面存放的值,那也就是加上一个i的值,等价于(++i) + (++i)
- 然后再给【eax】的值加上一个
ebp - 8
里面存放的值,等价于(++i) + (++i) + (++i)
- 最后将上面计算出来eax里面的值存放到
ebp - 14
这块地址中去,通过调试可以看到这块地址和&ret
是一致的,也就是说它们是同一块空间,那也就是将最后的值存放到ret里面去,那么最后打印出来的ret也就是12
通过反汇编进行观察调试,这回应该清楚了为什么最后的结果为12了吧
- 下面是在Linux环境下通过
objdump
进行反汇编观察
objdump -S a.out
- 在Linux下的反汇编调试这一块比较复杂,就不展开细讲,这里你只需要知道对于操作符而言只有优先级和结合性,没法确定唯一计算路径,所以这是一个问题表达式
十二、总结与提炼【最后的舞台】
好,来总结一下本文所学习的内容✒
- 本文我总共讲到了46种操作符,可以说是很全了,请读者观赏👇
- 算术操作符:【+
加
】、【-减
】、【*乘
】、【/除
】、【%取余
】
- 位操作符:【&
按位与
】、【|按位或
】、【^按位异或
】、【~按位取反
】、【<<按位左移
】、【>>按位右移
】
- 赋值操作符:【=
赋值
】、【+=复合加
】、【-=复合减
】、【*=复合乘
】、【/=复合除
】、【%=复合取余
】、【<<=复合左移
】、【>>=复合右移
】、【&=复合按位与
】、【|=复合按位或
】、【^=复合按位异或
】、【~=复合按位取反
】
- 单目操作符:【!
逻辑反
】、【-负值
】、【+正值
】、【&取地址
】、【sizeof操作数的类型长度
】、【- -前置、后置--
】、【++前置、后置++
】、【*间接访问
】、【()强制类型转换
】
- 关系操作符:【>
大于
】、【>=大于等于
】、【<小于
】、【<=小于等于
】、【!=不等于
】、【==等于
】
- 逻辑操作符:【&&
逻辑与
】、【| |逻辑或
】
- 条件操作符:【?
三目运算符
】
- 逗号表达式:【
exp1, exp2, exp3, …expN
】整个表达式的结果为最后一个逗号后面的表达式
- 下标引用操作符:【[ ]】
- 函数调用操作符:【( )】
- 结构成员调用操作符:【
.
】、【->
】
- 最后的话是讲到了有关表达式的求值相关的概念。为读者介绍了隐式类型转换中的【整型提升】,知道了原来短整型和字符型的数据在内存中是这样变化的;然后说到【算术转换】,清楚了再两个不同等级的数据类型一起操作的时候等级低的会转化为等级高的;最后说到了各种各样的【问题表达式】,也带大家通过反汇编观察了编译器的执行逻辑
以上就是本文要介绍的所有内容,感谢您的观看。记得给个三连哦❤️❤️❤️