整数除法
;只有当两个操作符数中有一个数是小数的时候可以算作是浮点数除法
必须是整数
我们到VS中来验证一下
再来看看取余操作符
接下去我们来说说移位操作符,它是一个为操作符,操作的是二进制数,因为比较重要,所以单独拿出来做讲解
在讲述左移、右移操作符之前,要给读者先介绍一下原码、反码、补码,以便更好地理解二进制位的概念
15
,它的八进制表示形式即为17
,十六进制为F
,二级制为1111
。这个要涉及到每一位上的权重概念,这里的内容很多不好叙述,可以看看——> 进制转换(二进制、八进制、十进制、十六进制)0 0000000000000000000000000000100
。可以看到我将最高位和后面的31位做了一个分隔,因为对于32个二进制位来说,最高位叫做符号位
反码等于原码除符号位外其余按位取反,补码等于反码 + 1
看到这里的读者可能会很好奇,不是要讲移位操作符吗,为什么要先讲这些呢❓
在计算机中都是使用二进制数的补码进行运算的,但是在计算完之后输出的结果都要再转化为原码的形式
有了上面的知识做铺垫,接下去就让我们真正地进入移位操作符的学习
【移位规则】:左边抛弃、右边补0
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接着我们再来看看负数的情况
-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
讲完左移操作符,接下去我们再来讲讲右移操作符
>>
【移位规则】:
① 逻辑移位
左边用0填充,右边丢弃
② 算术移位
左边用原该值的符号位填充,右边丢弃
算术移位
,所以我以此作为讲解然后我们再来看看负数的情况
【注意⚠】
对于移位运算符,不要移动负数位,这个是标准未定义的
int main(void)
{
int a = 5;
int b = a << -1; //error
return 0;
}
对于这个移位操作符来说,你可能会决定它不是很常用,所以也没必要很认真地学习,其实这就错了,那是因为你接触得还不够多,其实在某些特定的场合下,它可以起到非常之大的作用,后面有一道综合例题我会进行讲解✒
好,接下去我们来讲讲位操作符,这也是很多同学长期以来没有搞懂的一块
【规则】:全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【规则】:有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【规则】:相同为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
】
【规则】: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;
}
-1
介绍完了所有的位操作符符,接下去我们马上来检验一下学习的成果, 下面是两道历年面试中的考题,比较复杂而且也很多的位运算在里面,因为拿出啦做讲解
首先第一道比较容易一些,对于两个数的交换相信大家是非常熟悉的
&
异或位运算^
在这之前,我们通过加减的方式来试着交换一下这两个数
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来接受a = a - b
计算出来的结果就是b的值,将其存入变量a中异或^
的方法来运算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
不过没关系,我们慢慢来分析一下
① 思路分析
00000000000000000000000000001010
,那么此时我们先将其第三位置为1,要怎么去实现呢❓按位或 |
运算,将第三位按位或上一个1,那么这一位就变成了1,但是呢又不想让其他位置发生变化,那此时就让其他位按位或上一个0
即可,若是那个位上为0,那么就是0,若是那个位上为1,那也为1,那也就是00000000000000000000000000000100
,但是要如何去获取到这个二进制数呢,此时就又需要使用到我们上面讲到过的一个操作符叫做左移<<
那也就是将一个数扩大两倍,这里我们对1进行操作,扩大2倍就是2,再扩大两倍就是我们想要的4,即1 << 2
[a = a | 1 << 2]
1 << 4
,最后的结果就是261 << 2
;1 << 4
;1 << (n - 1)
;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,但是呢又不能使得其他二进制位发生改变,那就要使其他二进制位按位与上一个1即可,若是那个位上为0,那么就是0,若是那个位上为1,那也为1,那此时我们再对刚才的第三位进行一个按位与即11111111111111111111111111111011
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;
a += 10;
b = a;
复合赋值符
首先来浏览一下所有的单目操作符
[真变假,假变真]
int main(void)
{
int flag = 0;
if (!flag)
{
printf("haha\n");
}
return 0;
}
if(flag == 0)
,不过这样看起来就不像是一个真假的判断if(!root)
或者是if(root == NULL)
,当然如果你听不懂的话可以看看我的二叉树文章,后续对应地去学习一下即可,这里想到里我就顺便说一下指针接收地址
[*]
而言,指的就是此为一个指针变量,而[*]
前面的[int]
表示这个指针变量所指向的是一个整型的地址。那当前这个指针的名字是什么呢,就是pa
,而不是*pa
*pa = 20
[*]
解引用操作符获取到这个对象就为他赋值,那其实编译器会会报出一个Warning说是等号两边的类型级别不同,这其实就是讲一个整型的变量强制给到一个指针变量,才会出现的类型异常问题00000014
,转换为十进制也就是【20】野指针
的内容,因为这一个只有相关很危险☠的东西,也是很多初学者容易犯的*(int*)0x0012f40 = 100
捏造了
一个十六进制的整数,然后将其强制化为一个整型地址,在前面加上一个[*]
解引用操作符,此时我就可以去修改这个地址中存放着的对象了,将其对象的值修改为100,然后可以看到去编译的时候是没有问题的野指针
,因为这个十六进制的数值,这个地址是我随手捏造出来的,操作系统并没有为其分配内存,因此这是一块随机的地址值,是不确定的,若是随便去访问这块地址的话就会造成问题[野指针]
听完这些,相信你一定回忆起了一些有关指针和地址的知识点
对于【-】和【+】这个两个操作符并不常用,其实它们不能完全说只是单目操作符,因为在特定的场景下它们也可以算是一个双目操作符
,例如:-6
的话就只一个单目操作符,8 - 3
的话就是第一个双目操作符,【+】的话也是同理,不做赘述
然后我们再来说说一个操作符,叫做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
int main(void)
{
short s = 10;
int a = 2;
printf("%d\n", sizeof(s = a + 2));
printf("%d\n", s);
return 0;
}
s
即可,短整型为2个字节,那如果这个表达式不计算的话s的值也就不会发生变化了,所以最后打印出来是10
short
数据截断
这个小知识,若是我们不将这个表达式放到sizeof()中去,而是拿到外面来计算,那么最后s的结果还是会发生改变的,不过可以看到对于a + 5
的结果它是一个整型,占4个字节,但是变量s呢它是一个短整型,占2个字节,若是 强行将4个字节的数据放到2个字节中去,其实就会发生一个【数据截断】这个一个现象#include
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;
}
首元素地址
整个数组的大小
【特殊情况1】整个数组的地址
【特殊情况2】地址总线
的长度,有兴趣可以去了解一下sizeof(ch)
为4了,这也只是在求一个指针的大小,不要和字符串数据所占字节数混淆了以上便是对于sizeof()这个操作符而言要介绍的所有内容,希望读者能够理解
首先来看看前置++和后置++
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的值给到bb = 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的值给到bb = a--
相当于就是先把a的值给到b,然后再让a- -最后的话再来说一下强制类型转换
double
类型的数据强制给到一个int
类型的变量,就会出现一种叫做【精度丢失】的现象,若是想要强制给到一个整型的变量,那就要去做一个强制类型转换强制类型转换
int a = (int)3.14;
==
,因为它经常会和赋值操作符=
混淆,其实也不能说是混淆,应该说是【遗漏】,包括是很多资深的程序员在写代码判断一个数是否等于某个值的时候都会犯这样的错误
a == 6
写成a = 6
,若是将这个语句写在if条件判断里的话那么无论a的值为多少都会进入这个条件分支,因为赋值永远都是成立的,也就是为真a == 10
,一般都写成10 == a
对于==
操作符还想再说几句的是我们有时候不仅仅会去比较某个数值,而是去比较两个字符串的内容是否相同或者是比较一些结构体成员的变量,可是呢对于【==】操作符来说是不具备那么强大的功能的,所以要用其他手段去实现
==
去比较的话不是比较的两个字符串的内容,而是比较的两个字符串的首元素地址罢了strcmp
,而对于定义出来的整个结构体成员对象的内容就不好比较了,之后我们在学习了C++之后就知道有个东西叫做[仿函数]
,可以比较自定义的数据类型逻辑操作符很好记,就两个,和我们前面学过的位操作符中的按位与&
和按位或|
很像
32个二进制位上的0和1
逻辑与和或的特点:
✔【逻辑与&】:表达式两边均为真才是真,若第一个为假,那么整个表达式为假,第二个表达式不参与运算
✔【逻辑或 |】:表达式两边有一边为真即为真,若第一个为真,那么整个表达式为真,第二个表达式不参与运算
下面是一道【奇虎360】公司的校招笔试题,请问程序输出的结果是什么?
#include
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++
表达式需要运算之外后面的表达式都不会参与运算,因此最后的结果为1 2 3 4
,【i】的结果即为0现在我将这个题目做几个变形,看看读者是否具有举一反三的能力
题目变形①
int i = 0, a = 1, b = 2, c = 3, d = 4;
i = a++ && ++b && d++;
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++;
题目变形③
int i = 0, a = 1, b = 2, c = 3, d = 4;
i = a++ || ++b || d++;
通过这道奇虎360公司的笔试题以及举一反三的练习,相信你对逻辑操作符一定有了自己的理解
接下去我们来看看条件操作符,不过一般我们都将其叫做条件表达式(三目操作符)
条件操作符
了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;
}
表达式
,当条件不成立的时候,执行第二个表达式
。可以看出我写了后面也可以是一个表达式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
,那么最后再计算便是13d > 0
,那此时我们就要去看看前面一些表达式的运算会不会使得这个d变化,若不会那么这个if判断其实就等价于if(d > 0)
//代码2
if (a = b + 1, c = a / 2, d > 0)
a = get_val()
和 count_val(a)
这两个表达式在while()循环上面调用了一次,然后再while()循环中在调用,显得就有些冗余了,那此时我们就可以使用【逗号表达式】去进行一个优化//代码3
a = get_val();
count_val(a);
while (a > 0)
{
//业务处理
a = get_val();
count_val(a);
}
a = get_val(), count_val(a)
这两个表示,但是呢最后起作用的还是a > 0
,前面两个表达式只是有可能会使a的值发生一个变化罢了while (a = get_val(), count_val(a), a > 0)
{
//业务处理
}
【操作数】:一个数组名 + 一个索引值
arr[1]
,不过既然它一个操作符,那么对于操作数来说其实没有位置的一个限制,其实是可以写成1[arr]
,这个语法也是支持的,访问的都是arr这个数组中的第一个元素接受一个或者多个
操作数:第一个操作数是函数名,剩余的操作数就是传递给函数的参数
test1()
来说它的操作符为()
,只有一个操作数就是函数名test1test2("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;
}
最后再来说说这个结构成员调用操作符【.】和【->】
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);
接下去要讲的这一个隐式类型转换,可以很好地解开你对很多类型转换的一些困惑
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
,然后将其转换为原码的形式,但是通过上面的学习我们可以知道对正数的原、反、补码是相同的,因此这就是它在内存中的形式00000001
填充24个符号位
,以达到整型4个字节32个比特位在内存中的要求,那么此时提升之后的结果便是00000000000000000000000000000001
可以看出又回到了原来的1,不过若是你不去使用这个c1的话是不需要进行整型提升的接下去再来看看负数的整型提升
//负数的整形提升
char c2 = -1;
11111111111111111111111111111111
。同理,将其给到一个整型的变量之后就会发生截断即为11111111
好,说完了该如何去进行整型提升之后我们就可以去代码段中看看到底是如何进行的
首先来看第一个,我们要去计算两个整数的和,但是呢却要放到char类型的变量中去,那会发生什么化学反应呢
int main(void)
{
char a = 5;
char b = 126;
char c = a + b;
}
00000000000000000000000000000101 - 5
——> 00000101 - 5【截断】
00000000000000000000000001111110 - 126
——> 01111110 - 126【截断】
[整型提升]
,在高位补充24个符号位之后就变成了下面这样,然后便可以对它们去进行一个运算了//到了内存中开始计算 —— 整型提升(正数)
00000000000000000000000000000101 - 5
00000000000000000000000001111110 - 126
5 + 126 = 131
%d
做一个打印,那么此时就会输出正确的内容13100000000000000000000000010000011 - 131
10000011 - 131【截断】
可是呢,我就是不用整形去接收,就是玩用字符型去接受,然后再用%d
去打印(“主要还是为了加深知识点的灵活运用”)
printf("c的整数打印形式为:%d\n", c);
printf()
库函数其实也算是一个运算,也要放到内存里面去,然后这个变量c又不是整型,所以此时呢就又会发生一个[整型提升]
了10000011
前面的24个符号位了//整型提升(负数)
11111111111111111111111110000011 - 补码
11111111111111111111111110000010 - 反码
10000000000000000000000001111101 - 原码
-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
,那其没有发生整型提升的原因相信你已经清楚了以上就是有关【整型提升】要介绍的所有内容,看完这些相信你对计算机内部隐式类型转换一定有了一个深刻的了解
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类
型,否则操作就无法进行。下面的层次体系称为【寻常算术转换】
int
和unsigned int
一起进行算术运算的时候这个前者就要转换为后者的类型long int
和long double
一起进行算术运算的时候这个前者就要转换为后者的类型【警告】:
但是算术转换要合理,要不然会有一些潜在的问题
int main(void)
{
float f = 3.14;
int num = f;//隐式转换,会有精度丢失
printf("%d\n", num);
return 0;
}
复杂表达式的求值有三个影响的因素
优先级
。如果两者的优先级相同,取决于他们的结合性
下面有一张关于操作符优先级的列表,可以保存一份日常的参考
表达式的求值部分由操作符的优先级决定,优先级只能决定先算谁,但是哪个表达式先调用要取决于编译器
a*b + c*d + e*f
[*]
的优先级一定是比[+]
要来得高,因此可以保证[*]
两端的数字先进行运算,但是却不能保证第三个*比第一个+早执行//表达式2
c + --c;
[--]
操作符来说比[+]
操作符的优先级来得高,但是呢我们却不知道在编译器运算的时候这个【c】是什么时候准备好--c
就是根据这个【2】来运算;可若是这个--c
先执行的话,后面再去加上这个c结果就不一样了。因此也将其成为问题表达式++
和--
混搭的这种表达式尤其严重,你可以去不同的编译器上运行看看,结果都是不一样的【这种代码不要写,练练思维就行】//代码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 |
count
,我们知道对于静态变量是存放在内存中的【静态区】
,每一次运算都是在上一次的运算的结果后进行一个累加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;
}
-10
,若是后面的fun()先执行的话,最后的结果就是-2
先乘除后加减
,可是呢我们最常用的VS出来的结果都不是我们想要的我们可以到不同编译器上面去观察一下
-10
,而且在大多数的编译器下都是这个,但是呢对于函数的调用先后顺序无法通过操作符的优先级确定,因此这也是一个问题表达式++
和+
结合的问题表达式,这个我在之前的文章中也有提到过,运算出来的结果其实是存在歧义的//代码5
#include
int main()
{
int i = 1;
int ret = (++i) + (++i) + (++i);
printf("%d\n", ret);
printf("%d\n", i);
return 0;
}
一样,我们可以到不同的编译器下去做一个测试
下面我在VS下通过调用反汇编的指令带大家来看一下在底层编译器到底是如何执行的,如果不懂可先看看我的这篇文章——> 反汇编深挖【函数栈帧】的创建和销毁
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
第四条指令
00631872 89 45 F8 mov dword ptr [ebp-8],eax
ebp - 8
这块空间上去。通过上面一条指令我们知道此时eax里面存的值为2,并且ebp - 8
这块地址和变量i的地址是一样的,所以二、三、四条指令也就等价于++i
,只不过是利用寄存器eax做一个转移第五条指令
00631875 8B 4D F8 mov ecx,dword ptr [ebp-8]
ecx = 2
第七条指令
00631878 83 C1 01 add ecx,1
第八条指令
0063187B 89 4D F8 mov dword ptr [ebp-8],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】里面去ebp - 8
里面存放的值,那也就是加上一个i的值,等价于(++i) + (++i)
ebp - 8
里面存放的值,等价于(++i) + (++i) + (++i)
ebp - 14
这块地址中去,通过调试可以看到这块地址和&ret
是一致的,也就是说它们是同一块空间,那也就是将最后的值存放到ret里面去,那么最后打印出来的ret也就是12通过反汇编进行观察调试,这回应该清楚了为什么最后的结果为12了吧
后续会更新Linux下的反汇编调试,单独出文章,敬请期待。。。
好,来总结一下本文所学习的内容✒
加
】、【- 减
】、【* 乘
】、【/ 除
】、【% 取余
】按位与
】、【| 按位或
】、【^ 按位异或
】、【~ 按位取反
】、【<< 按位左移
】、【>> 按位右移
】赋值
】、【+= 复合加
】、【-= 复合减
】、【*= 复合乘
】、【/= 复合除
】、【%= 复合取余
】、【<<= 复合左移
】、【>>= 复合右移
】、【&= 复合按位与
】、【|= 复合按位或
】、【^= 复合按位异或
】、【~= 复合按位取反
】逻辑反
】、【- 负值
】、【+ 正值
】、【& 取地址
】、【sizeof 操作数的类型长度
】、【- - 前置、后置--
】、【++ 前置、后置++
】、【* 间接访问
】、【() 强制类型转换
】大于
】、【>= 大于等于
】、【< 小于
】、【<= 小于等于
】、【!= 不等于
】、【== 等于
】逻辑与
】、【| | 逻辑或
】三目运算符
】exp1, exp2, exp3, …expN
】整个表达式的结果为最后一个逗号后面的表达式.
】、【->
】以上就是本文要介绍的所有内容,感谢您的观看。记得给个三连哦❤️❤️❤️