前言
你能分清按位与和逻辑与吗?
你了解右移操作符左端应该补什么吗?
你了解逗号表达式吗?
你知道操作符的优先级吗?
你知道隐式类型转换——整型提升吗?
嗨害!小帅来喽!
本文,小帅将为你详细解读操作符的世界!希望大家可以上机实验。
篇幅较长,但干货很多,请大家耐心阅读!
操作符的分类
算术操作符 |
位移操作符 |
位操作符 |
赋值操作符 |
单目操作符 |
关系操作符 |
逻辑操作符 |
条件操作符 |
逗号表达式 |
下标引用、函数调用和结构成员 |
1.算术操作符
+ | - | * | / | % |
代码示例:
#include<stdio.h>
int main()
{
int a = 10;
int b = 20;
printf("%d\n", a + b);
printf("%d\n", a - b);
printf("%d\n", a * b);
printf("%d\n", a / b);
printf("%d\n", a % b);
return 0;
}
代码结果:
我们可以从结果发现,相加减乘的结果都符合我们的预期,但是为什么相除的结果不等于理论上的0.5呢?这里有两点注意:
- printf()中格式我们用的控制字符串是%d,整数值用%d 输出,实数用%f 输出
- 这里隐含类型转换问题,即10和20都是整数,它们是整数之间的运算,只要运算符的两边都是整数,则运算结果也会是整数。正因为这样,10 / 20的值才是0,而不是0.5。而当我们计算10.0 / 20.0,这里10.0和20.0是浮点数,浮点数之间的运算结果是浮点数,因此10.0 / 20.0 = 0.5。即整数 / 整数 = 整数,浮点数 / 浮点数 = 浮点数。注意,这里的运算符“/”其实是“多面手”,它既可以做整数除法,又可以做浮点数除法。
那么5 - 0.1的值是什么?5 / 2.0的值是什么?“整数 - 浮点数”是整数还是浮点数?
整数-浮点数=浮点数,确切的说法是:整数先“变”成浮点数,然后浮点数-浮点数=浮点数。整数 / 浮点数相同。
问题的思考:
#include<stdio.h>
int main()
{
printf("%.1f\n", 8.0/5.0);
return 0;
}
思考1 :字符串%.1f 不变,把 8.0/5.0 改成原来的 8/5,结果如何?
思考2 :字符串%.1f 改成原来的%d,8.0/5.0 不变,结果如何?
我们可以上手实验一下:
思考1答案:
思考2答案:
这两个答案的真正原因涉及整数和浮点数编码,相信多数初学者对此都不感兴趣。原因并不重要(当然我们也可以自行探索),重要的是规范:根据规范做事,则一切尽在掌握中。
小结:
- 除了 % 操作符之外,其他的几个操作符可以作用于整数和浮点数。
- 对于 / 操作符如果两个操作数都为整数,执行整数除法。而只要有浮点数执行的就是浮点数除法。
- % 操作符的两个操作数必须为整数。返回的是整除之后的余数。
2.移位操作符
<< | 左移操作符 |
>> | 右移操作符 |
注意:移位操作符的操作数只能为整数,操作对象为二进制数。
这里我们简单补充一下原码、反码、补码的知识:
原码(true form)是一种计算机中对数字的二进制定点表示方法。原码表示法在数值前面增加了一位符号位(即最高位为符号位):正数该位为0,负数该位为1(0有两种表示:+0和-0),其余位表示数值的大小。
正数:
原码、反码、补码相同
例如:(十进制)7——(二进制)00000000 00000000 00000000 00000111——原码
00000000 00000000 00000000 00000111——反码
00000000 00000000 00000000 00000111——补码
负数:
原码的符号位不变,其他位按位取反,得到反码
反码加1,得到补码
例如:(十进制)-1——(二进制)10000000 00000000 00000000 00000001——原码
11111111 11111111 11111111 11111110——反码
11111111 11111111 11111111 11111111——补码
注意:内存单元中存储的是补码,但在输出时是原码
2.1左移操作符
移位规则:
左边抛弃、右边补0
(即按二进制形式把所有的数字向左移动对应的位数,高位移出(舍弃),低位的空位补零)
代码示例(<<):
#include<stdio.h>
int main()
{
int a = 10;
int b = a << 1;
//00000000000000000000000000001010 -补码
printf("%d\n", b);
printf("%d\n", a);
return 0;
}
代码结果:
数学意义:
在数字没有溢出的前提下,对于正数和负数,左移一位都相当于乘以2的1次方,左移n位就相当于乘以2的n次方。
2.2右移操作符
右移运算分为两种:
算数移位(常见有符号数)移位规则:
按二进制形式把所有的数字向右移动对应位移位数,低位移出(舍弃),高位的空位补符号位,即正数补零,负数补1。
逻辑移位(无符号数)移位规则:
按二进制形式把所有的数字向右移动对应位数,低位移出(舍弃),高位的空位补零。对于正数来说和带符号右移相同,对于负数来说不同。
代码示例(>>):
#include<stdio.h>
int main()
{
int a = -1;
//10000000000000000000000000000001-原码
//11111111111111111111111111111110-反码
//11111111111111111111111111111111-补码
int b = a >> 1;
printf("%d\n", b);
printf("%d\n", a);
return 0;
}
代码结果:
注意:
对于移位运算符,我们不能想当然的移动负数位,这个是标准未定义的。
例如:7>>-1,向右移-1位,不能认为是左移1位,并且移动负数位也是标准未定义的。
数学意义:
右移一位相当于除2,右移n位相当于除以2的n次方取整。
3.位操作符
& | 按位与 |
| | 按位或 |
^ | 按位异或 |
注意:它们的操作数必须是整数,操作对象是二进制数。
3.1 按位与 &
代码示例:
//& - 按2进制位与
//对应的二进制位有0,则为0,两个同时为1,才为1
#include<stdio.h>
int main()
{
int a = 3;
//00000000000000000000000000000011-原反补码
int b = -5;
//10000000000000000000000000000101 -原码
//11111111111111111111111111111010 -反码
//11111111111111111111111111111011 -补码
//
int c = a & b;//两数对应的二进制位补码有0,则为0,两个同时为1,才为1
//00000000000000000000000000000011 -a的补码
//11111111111111111111111111111011 -b的补码
//00000000000000000000000000000011 -c的补码
//
printf("%d\n", c);
return 0;
}
代码结果:
3.2 按位或 |
代码示例:
#include<stdio.h>
//按2进制位或
//对应的二进制位有1则为1,两个同时为0则为0
int main()
{
int a = 3;
//00000000000000000000000000000011 -原反补码
int b = -5;
//10000000000000000000000000000101 -原码
//11111111111111111111111111111010 -反码
//11111111111111111111111111111011 -补码
//
int c = a | b;//两数对应的二进制位补码有1则为1,两个同时为0则为0
//00000000000000000000000000000011 -a的补码
//11111111111111111111111111111011 -b的补码
//11111111111111111111111111111011 -c的补码
//
printf("%d\n", c);
return 0;
}
代码结果:
3.3按位异或 ^
异或操作符的特点:
- a ^ a = 0
- a ^ 0 = a
- 异或支持交换律
代码示例:
#include<stdio.h>
//^ - 按2进制位异或
//对应的二进制位:相同为0,相异为1
int main()
{
int a = 3;
//00000000000000000000000000000011 -原反补码
int b = -5;
//10000000000000000000000000000101 -原码
//11111111111111111111111111111010 -反码
//11111111111111111111111111111011 -补码
//
int c = a ^ b;
//00000000000000000000000000000011 -a的补码
//11111111111111111111111111111011 -b的补码
//11111111111111111111111111111000 -c的补码
//11111111111111111111111111110111
//10000000000000000000000000001000 -c的原码
//-8
printf("%d\n", c);
return 0;
}
代码结果:
3.4 为了更好的了解移位操作符和位操作符,下面我们做些经典题来加深印象。
例题一:不创建临时变量(第三个变量),实现两个整数的交换。(某大厂面试题)
方法一:加减法交换
#include<stdio.h>
int main()
{
int a = 3;
int b = 5;
printf("%d %d\n", a, b);
a = a + b;
b = a - b;//由上一个语句得,b = a + b - b = a
a = a - b;//由b = a, a = a + b,得a = a + b - a = b
printf("%d %d\n", a, b);
return 0;
}
代码结果:
方法一弊端:
- 两数相加可能超过基本数据类型的范围
- 可读性差
方法二:异或交换
代码示例:
#include<stdio.h>
int main()
{
int a = 3;
int b = 5;
printf("%d %d\n", a, b);
a = a ^ b;
b = a ^ b;//由上一个语句,得b = a ^ b ^ b(由异或交换律,a ^ a = 0,a ^ 0 =a),可得b = a
a = a ^ b;//由a = a ^ b, b = a, 得a = a ^ b ^ a = b
printf("%d %d\n", a, b);
return 0;
}
代码结果:
方法二弊端:
- 异或只能对整数间进行交换
- 效率不如使用临时变量(在平时交换变量时)
- 可读性差
例题二 :统计一个整数得二进制位中有几个1。
方法一:
类比求一个十进制数整数有几位数程序(循环模10除10.......直至该数等于0),我们将整数循环模2除2......若等于1则count++直至该整数等于0。
代码示例:
#include<stdio.h>
int Numberof(int n)//换成无符号数
{
int count = 0;
while (n != 0)
{
if (n % 2 == 1)
{
count++;
}
n /= 2;//每次除2,直至n = 0
}
return count;
}
int main()
{
int n = 0, count = 0;
scanf("%d", &n);
printf("%d\n\n", Numberof(n));
return 0;
}
我们输入十进制数——7,预计打印3
代码结果:
符合预期。我们也可以上手试试其他整数。
那我们输入十进制数—— -1 ,预计打印32
代码结果:
不符合预期,即代码设计有错。为什么正数没错 ,而负数有误呢?我们推理发现,-1在第一次循环条件判断时就不符合,-1 % 2 = 0不进入循环,又count = (-1) / 2 = 0,故输出0。
那这个程序只能统计正数,不能再完善而包含负数吗?答案是可以的,我们可以在函数形参接收实参将形参接收得类型定义为无符号类型(最高位将不再是符号位),这样负数就变为正数!
改善代码如下:
#include<stdio.h>
int Numberof(unsigned int n)//换成无符号数
{
int count = 0;
while (n != 0)
{
if (n % 2 == 1)
{
count++;
}
n /= 2;
}
return count;
}
int main()
{
int n = 0, count = 0;
scanf("%d", &n);
printf("%d\n\n", Numberof(n));
return 0;
}
输入-1
代码结果:
方法二:
将整数与1按位与后,将1左移1位再次与整数按位与。只要比较的二进制位有1,则按位与的结果就大于等于1。
代码示例:
#include<stdio>
int main()
{
int res = 0;
int n = 0;
scanf("%d", &n);
for (int i = 0; i < 32; i++)
{
//按位比较,每次1左移1位
if ((n & (1 << i)) != 0)
res++;
}
printf("%d", res);
return 0;
}
输入-1 代码结果:
思考:还能不能更加优化,因为这里必须循环32次。
方法三:
n & ( n - 1 )
#include<stdio.h>
int main()
{
int n = 0,count=0;
scanf("%d", &n);
while (n)
{
count++;
n &= n - 1;
}
printf("%d", count);
return 0;
}
举例:7—— 00000000 00000000 00000000 00000111——原反补码
6(7-1) —— 00000000 00000000 00000000 00000110——原反补码
按位与后: 00000000 00000000 00000000 00000110——补码
解读:二进制位最初一位必定是0或1,而减1后二进制中1的总数必定小于等于原先1的总数,两者按位与后有0即为0,两者为1即为1。
该方法较前两种效率最高,望小伙伴们理解掌握。
4.赋值操作符
赋值操作符是一个很棒的操作符,它可以让你修改一个你之前不满意的值,也就是你可以给自己重新赋值。
= | 赋值操作符 |
举例:
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;//连续赋值
这样的代码感觉怎么样?
那同样的语义,你看看:
x = y+1;
a = x;
这样的写法是不是更加清晰爽朗而且易于调试。
4.1 复合赋值符
复合赋值符 |
---|
+= |
-= |
*= |
/= |
%= |
>>= |
<<= |
&= |
|= |
^= |
这些运算符都可以写成复合的效果。
例如:
int x = 10;
x = x + 10;
x += 10;//复合赋值
int a = a / 10;
a /= 10;
a %= 10;
//其他运算符一样的道理。这样写更加简洁!
5.单目操作符
5.1单目操作符介绍
! | 逻辑反操作 |
- | 负值 |
+ | 正值 |
& | 取地址 |
sizeof | 操作数的类型长度(以字节为单位) |
~ | 对一个数的二进制按位取反 |
-- | 前置、后置-- |
++ | 前置、后置++ |
* | 间接访问操作符(解引用操作符) |
(类型) | 强制类型转换 |
注意:单目操作符,只有一个操作数。操作符有2个操作数的为双目操作符。
c语言中0表示假,非0表示真
下面用代码展示上述操作符作用:
1. !
#include<stdio.h>
int main()
{
//C语言中0表示假,非0表示真
int flag = 0;
if (flag)//flag如果为真,做.....
{
printf("hehe\n");
}
if (!flag)
{
printf("hehe\n");
}
printf("%d\n", flag);
printf("%d\n", !flag);
return 0;
}
代码结果:
2. -
int main()
{
int a = -10;
printf("%d\n", a);
printf("%d\n", -a);
return 0;
}
代码结果:
3. +
int main()
{
int a = -10;
printf("%d\n", a);
printf("%d\n", +a);
return 0;
}
代码结果:
4. &(取地址)
int main()
{
int a = 10;
printf("%p\n", &a);// &取出a的地址,%p打印地址
int* pa = &a;
char ch = 'w';//定义字符变量ch,赋值为‘w’
char*pc = &ch;//定义字符指针pc,存储ch的地址
char arr[10] = { 0 };//定义字符数组arr
char* p2 = arr; //定义字符指针p2,存储arr数组首元素地址
char* p3 = &arr[0]; //取arr数组第一个元素的地址,赋值给字符指针p3,p2,p3相同
const char* p = "abcdef";//定义字符指针p存储字符串首字符地址
printf("%p\n", p); //打印p中存储的地址,即字符串首字符地址
printf("%c\n", *p); //打印指针中地址指向的字符a
return 0;
}
5. sizeof
函数调用的时候,要写()
但是sizeof后边的括号可以省略,说明sizeof不是函数
//函数调用的时候,要写()
//但是sizeof后边的括号可以省略,说明sizeof不是函数
int main()
{
int a = 10;
printf("%d\n", sizeof(a));
printf("%d\n", sizeof a);//ok
printf("%d\n", sizeof(int));
int arr[10] = {0};
printf("%d\n", sizeof arr);//ok
printf("%d\n", sizeof(arr));//ok
printf("%d\n", sizeof(int[10]));//ok
return 0;
}
代码结果:
大家可以思考下面这段程序结果:
int main()
{
int a = 10;
short s = 5;
printf("%d\n", sizeof(s = a + 3));
printf("%d\n", s);
return 0;
}
代码结果:
解析:sizeof是关键字也是操作符,并且sizeof内部的表达式是不计算的,所以sizeof( s = a + 3)只会计算s的字节大小。
6. ~
注意:
- ~ 操作数是整数,操作对象是二进制。
- 将二进制所有位按位取反
//~ 按位取反
//00000000000000000000000000000000
//11111111111111111111111111111111 - 补码是全1
//-1
int main()
{
int a = 0;
printf("%d\n", ~a);//?
return 0;
}
代码结果:
至此,我们学完了所有对二进制数操作的操作符
下面给出一道题,来综合运用以上操作符。
& | ^ >> << ~
例题: 给出整数 (十进制)9——(二进制)00000000 00000000 00000000 00001001,编写程序将该二进制的第5位0转变为1。
int main()
{
int a = 9;
//00000000000000000000000000001001
//00000000000000000000000000010000 1<<4
//00000000000000000000000000011001
//
//把a的二进制中第5位改成1
a |= (1<<4);
printf("%d\n", a);
//把a的二进制中的第5位改回来,变成0
//00000000000000000000000000011001
//11111111111111111111111111101111
//00000000000000000000000000001001
a &= (~(1 << 4));
printf("%d\n", a);//9
return 0;
}
7. ++(前置++,后置++)
前置++:先将该变量+1,在使用该变量
后置++:先使用该变量,再将该变量+1
-- 使用与 ++ 相同
代码示例:
int main()
{
int a = 10;
int d = 10;
int b = a++;//后置++,先使用,再++
int c = ++d;//前置++,先++,后使用
printf("%d\n", a);
printf("%d\n", b);
printf("%d\n", c);
return 0;
}
我们再来看函数传参时:
道理与上面相同
#include <stdio.h>
void test(int b)
{
printf("b = %d\n", b);
}
int main()
{
int a = 10;
//test(a++);
test(++a);
return 0;
}
代码结果:
注意:++ -- 带有副作用,即使用后该变量会改变大小,如果不想改变原有变量的大小就将该变量加1进行赋值操作即可。
8. ()
注意:强制类型转换可能会使精度缺失!
int main()
{
int a = (int)3.14;//将浮点数强制类型转换为整数
printf("%d\n", a);
return 0;
}
//int main()
//{
// //time_t;
// srand((unsigned int)time(NULL));//平时使用的随机数也是使用强制类型转换
//
// return 0;
//}
6. 关系操作符
> |
>= |
< |
<= |
!= |
== |
这些关系运算符比较简单,没有什么可讲的,但是我们要注意一些运算符使用时的陷阱。
注意:在编程的过程中 == 和= 不小心写错,将会导致程序错误。
7.逻辑操作符
&& | 逻辑与(并且) |
|| | 逻辑或(或者) |
区分逻辑与和按位与
区分逻辑或与按位或
- 按位与或,操作数是整数,操作对象是二进制序列
- 逻辑与或,操作对象是表达式
a | b | a&&b |
真 | 真 | 真 |
假 | 真 | 假 |
真 | 假 | 假 |
假 | 假 | 假 |
a | b | a||b |
真 | 真 | 真 |
假 | 真 | 真 |
真 | 假 | 真 |
假 | 假 | 假 |
//1. 能被4整除,并且不能被100整除
//2. 能被400整除
//判断闰年
int main()
{
int y = 2048;
if (((y % 4 == 0) && (y % 100 != 0)) || (y % 400 == 0))
{
printf("Yes\n");
}
return 0;
}
和算术表达式类似,逻辑表达式也由运算符和值构成, 例如“||”运算符称为“逻辑或”,a || b 表示 a 为真,或者 b 为真。换句话说,a 和 b 只要 有一个为真,a || b 就为真;如果 a 和 b 都为真,则 a || b 也为真。和其他语言不同的是,在 C 语言中单个整数也可以表示真假,其中 0 为假,其他值为真。
细心的读者也许发现了,如果 a 为真,则无论 b 的值如何,a || b 均为真。换句话说, 一旦发现 a 为真,就不必计算 b 的值C 语言正是采取了这样的策略,称为短路(short-circuit)。
注意: C 语言中的逻辑运算符都是短路运算符。一旦能够确定整个表达式的值,就不 再继续计算。
笔试题:
#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;
}
这里就考到了短路,结果如下:
你能算出下面这一段程序的结果吗?
#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\nb = %d\nc = %d\nd = %d\n", a, b, c, d);
return 0;
}
结果如下:
8.条件操作符(三目操作符)
exp1 ? exp2 : exp3(与if - else语句作用相似)
如果exp1 为真,则表达式结果为exp2
如果exp1 为假,则表达式结果为exp3
举例:
int main()
{
int a = 0;
int b = (a > 5 ? 3 : -3);
printf("%d", b);
return 0;
}
结果如下:
练习:
if (a > 5)
b = 3;
else
b = -3;
9.逗号表达式
exp1, exp2, exp3, ......expN
- 逗号表达式,就是用逗号隔开的多个表达式。
- 逗号表达式,从左到右依次执行。整个表达式的结果是最后一个表达式 的结果。
大家可以计算下面一段程序结果
int main()
{
int a = 1;
int b = 2;
int c = (a > b, a = b + 10, a, b = a + 1);//逗号表达式
printf("c=%d\n", c);
return 0;
}
10.下标引用、函数调用和结构成员
1. [ ] 下标引用操作符
操作数:一个数组名 + 一个索引值
int main()
{
int arr[10] = { 1,2,3,4,5 };
printf("%d\n", arr[4]);//[] - 下标引用操作符,操作数是:arr , 4
//3 + 4;
return 0;
}
思考下面一段程序时是否正确(大家可以上手实践):
int main()
{
int arr[10] = { 1,2,3,4,5 };
printf("%d\n", 4[arr]);//[] - 下标引用操作符,操作数是:arr , 4
//3 + 4;
return 0;
}
结果:
看到这里,可能有许多人有疑问,数组访问还能这样写?
解析:[ ] 是个操作符,i 和 arr 是 [ ]这个操作符的操作数而已,就如同a + b == b + a 一样,所以也可以将两者交换位置,但不建议这样写,因为可读性差,这里只是简单介绍一下它的意义。
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;
}
3.访问一个结构的成员
. 结构体.成员名
-> 结构体指针->成员名
代码举例:
#include <stdio.h>
struct Stu
{
char name[10];
int age;
char sex[5];
double score;
};
void set_age1(struct Stu stu)
{
stu.age = 18;
}
void set_age2(struct Stu* pStu)
{
pStu->age = 18;//结构成员访问
}
int main()
{
struct Stu stu;
struct Stu* pStu = &stu;//结构成员访问
stu.age = 20;//结构成员访问
set_age1(stu);//实参为结构体
pStu->age = 20;//结构成员访问
set_age2(pStu);//实参为结构体指针
return 0;
}
11.表达式求值
表达式求值的顺序一部分是由操作符的优先级和结合性决定。
同样,有些表达式的操作数在求值的过程中可能需要转换为其他类型。
11.1 隐式类型转换
c语言的整型算术运算总是至少以缺少整型类型的精度来进行的。
为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整形,这种转换成为整型提升。
整型提升的意义 :表达式的整型运算要在 CPU 的相应运算器件内执行, CPU 内整型运算器 (ALU) 的操作数的字节长度一般就是 int 的字节长度,同时也是 CPU 的通用寄存器的长度。因此,即使两个 char 类型的相加,在 CPU 执行时实际上也要先转换为 CPU 内整型操作数的标准长度。通用 CPU ( general-purpose CPU )是难以直接实现两个 8 比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以,表达式中各种长度可能小于 int 长度的整型值,都必须先转换为 int 或 unsigned int ,然后才能送入 CPU 去执行运算。
如何进行整型提升呢?
负数的整形提升
char c1 = - 1 ;变量 c1 的二进制位 ( 补码 ) 中只有 8 个比特位:1111111因为 char 为有符号的 char所以整形提升的时候, 高位补充符号位 ,即为 1提升之后的结果是:11111111111111111111111111111111正数的整形提升
char c2 = 1 ;变量 c2 的二进制位 ( 补码 ) 中只有 8 个比特位:00000001因为 char 为有符号的 char所以整形提升的时候, 高位补充符号位 ,即为 0提升之后的结果是:00000000000000000000000000000001无符号整形提升,高位补0
代码举例:
int main()
{
//char --> signed char
char a = 3;//表达式,整型3存入字符a中要进行截断
//截断
//00000000000000000000000000000011
//00000011 - a
//
char b = 127;
//00000000000000000000000001111111
//01111111 - b
char c = a + b;
//00000011
//01111111
//整型提升
//00000000000000000000000000000011
//00000000000000000000000001111111
//00000000000000000000000010000010
//10000010 - c
printf("%d\n", c);
//%d 是打印十进制的整数
//11111111111111111111111110000010 - 补码
//11111111111111111111111110000001
//10000000000000000000000001111110 - 原码
//-126
return 0;
}
解析:
int a = 3; 为一个表达式,3为整型对应二进制为 00000000000000000000000000000011
将整数放进char类型变量 ,就好比9米长杆子放进三米宽的房子,是要折断放进去的,故3截断为00000011放进a中。同理127折断为 01111111 放进b中。char c = a + b ;该表达式运算前要整型提升,a为 00000000 00000000 00000000 00000011 ,b为 00000000 00000000 00000000 01111111 相加为 00000000 00000000 00000000 10000010 截断后为 10000010放进c中,在打印c时打印十进制数,因为是表达式先整型提升,c的最高位为1即为负数,所以高位补充符号位1后c为 11111111111111111111111110000010——补码
补码经过取反加1后为原码 10000000000000000000000001111110,我们发现两个正数相加因为整型提升而结果变为了负数,由此可见整型提升的重要性!
这里补充有符号char与无符号char的取值范围
- 有符号的char的取值范围:-128~127
- 无符号的char的取值范围:0~255
整型提升的例子1:
//实例1
int main()
{
char a = 0xb6; //0x为十六进制数,每两个16进制位占一个字节,因为15用二进制位1111表示
short b = 0xb600;
int c = 0xb6000000;
if (a == 0xb6)
printf("a");
if (b == 0xb600)
printf("b");
if (c == 0xb6000000)
printf("c");
return 0;
}
大家认为屏幕上会打印什么呢?
实例1中的a,b要进行整形提升,但是c不需要整形提升,因为c本身就为整型4个字节。
- a,b整形提升之后,变成了负数,所以表达式 a==0xb6 , b==0xb600 的结果是假。
- 但是c不发生整形提升,则表达式 c==0xb6000000 的结果是真。
整型提升的例子2:
//实例2
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;//隐式转换,会有精度丢失
例题:
#include <stdio.h>
int i;
int main()
{
i--;
if (i > sizeof(i))
{
printf(">\n");
}
else
{
printf("<\n");
}
return 0;
}
这里隐藏着算术转换!由于sizeof的返回值为无符号整数并且i 的默认值为0,故 i --后为-1,算术转换后 -1补码 11111111 11111111 11111111 11111111数值大小为2^32-1远大于4,故输出 >
11.3 操作符的属性
复杂表达式的求职有三个影响因素。
- 操作符的优先级
- 操作符的结合性
- 是否控制求值顺序
两个相邻的操作符先执行哪个?
取决于它们的优先级。如果两者的优先级相同,取决于它们的结合性。
操作符优先级
操作符 | 描述 | 用法示例 | 结果类型 | 结合性 | 是否控制求值顺序 |
() | 聚组 | (表达式) | 与表达式相同 | N/A | 否 |
() | 函数调用 | rexp (rexp, ...rexp) | rexp | L-R | 否 |
[ ] | 下标引用 | rexp[rexp] | lexp | L-R | 否 |
. | 访问结构成员 | rexp.member_name | lexp | L-R | 否 |
-> |
访问结构指针成员
|
rexp->member_name
| rexp | 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 | 否 |
& |
取地址
| & lexp | rexp | R-L | 否 |
sizof |
取其长度,以字节
表示
| sizeof rexp sizeof(类型) | rexp | R-L | 否 |
(类型) |
类型转换
| (类型)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 | 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 | 是 |
一些问题表达式
示例1:
//表达式1
c + -- c ;
注释:操作符的优先级只能决定自减--的运算在+的运算的前面,但是我们并没有办法得知,+操作符的左操作数的获取在右操作数之前还是之后求值,所以结果是不可预测的,是有歧义的。
//代码2-非法表达式
int main()
{
int i = 10;
i = i-- - --i * ( i = -3 ) * i++ + ++i;
printf("i = %d\n", i);
return 0;
}
结果:该段程序在不同编译器中测试结果各不相同!
示例3:
//代码3
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() ; 中我们只能通过操作符的优先级得知:先算乘法, 再算减法。函数的调用先后顺序无法通过操作符的优先级确定 。
示例4:
//代码5
#include <stdio.h>
int main()
{
int i = 1;
int ret = (++i) + (++i) + (++i);
printf("%d\n", ret);
printf("%d\n", i);
return 0;
}
结果:
也是不同编译器产生不同的结果。
这段代码中的第一个 + 在执行的时候,第三个++ 是否执行,这个是不确定的,因为依靠操作符的优先级 和结合性是无法决定第一个 + 和第 三个前置 ++ 的先后顺序。
总结 :我们写出的表达式 如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题的。
总结
本文至此,小帅用超长的篇幅详细的解读了所有操作符,希望 学习完操作符的世界,我们的编程能力会更上一层楼,对代码深层的理解也会随之加深!大家可以收藏,以便查阅,熟记于心。
最后,如果小帅的本文哪里有错误,还请大家指出,请在评论区留言(ps:抱大佬的腿),新手创作,实属不易,如果满意,还请给个免费的赞,三连也不是不可以(流口水幻想)嘿!那我们下期再见喽,拜拜!