1 二进制
• 2进制的数字每一位都是由0~1的数字组成
• 2进制中满2进1
1.1.1 2进制转10进制
1.1.2 10进制转2进制
1.2.1 2进制转8进制
8进制的每一位数字都是0~7,而数字0~7各自写成2进制最多有3个2进制位就足够了,所以2进制转8进制时,从2进制序列中右边低位开始向左每3个2进制位就换算成一个8进制位,剩余不够3个2进制位的直接换算。
如:2进制数01101011,换算成8进制就为0153。
8进制在C语言中表示的时候,数字前会加0。
1.2.2 2进制转16进制
16进制的每一位数字都是0~9、A~F,各自写成2进制,最多有4个2进制位就足够了,所以2进制转16进制时,从2进制序列中右边低位开始向左每4个2进制位就换算成一个16进制位,剩余不够4个2进制位的直接换算。
如:2进制数01101011,换算成16进制就为0x6b。
16进制在C语言中表示的时候,数字前会加0x。
2 原码、反码、补码
整数的2进制表示方法有三种,即原码、反码和补码
三种表示方法均有符号位和数字位两部分,符号位用“0”表示“正”,用“1”表示“负”。
C语言中,一个整形数的最高位是符号位,其余都是数字位。
正整数的原码、反码和补码都相同。
负整数的原码:直接将数值按照正负数的形式转换成2进制得到即为原码。
负整数的反码:原码的符号位不变,数字位按位取反即得反码。
负整数的补码:反码加1即得补码。
对于整形来说,数据存放在内存中其实存放的是补码。
因为使用补码,可以将符号位和数值位统一处理,同时,加法和减法也可以统一处理(CPU中只有加法器)。
此外,补码与原码相互转换,其运算过程相同,不需要额外的硬件电路。
3 移位操作符
<< 左移操作符
>> 右移操作符
注:移位操作符的操作数只能是整数,不能是小数。
3.1 左移操作符
移位规则:左边抛弃,右边补0。
#include <stdio.h>
int main()
{
int a = 10;
int b = a << 1;
printf("a=%d\n", a);
printf("b=%d\n", b);
return 0;
}
输出结果:
3.2 右移操作符
1.逻辑右移:左边补0,右边抛弃。
2.算术右移:左边用原该值的符号位填充,右边抛弃。
到底采用哪种右移,是不确定的,取决于编译器,大部分编译器采用算术右移。
#include <stdio.h>
int main()
{
int a = -1;
int b = a >> 1;
printf("a=%d\n", a);
printf("b=%d\n", b);
return 0;
}
输出结果:
注意:对于移位操作符,不能移动负数位,否则会报错。
例:
int num = 10;
num>>-1;//error
4 位操作符
& //按位与
| //按位或
^ //按位异或
~ //按位取反
注:它们的操作数必须是整数,不能是小数。
4.1 按位与
#include <stdio.h>
int main()
{
int a = 5;
int b = -6;
int c = a & b;
//00000000000000000000000000000101 5的补码
//11111111111111111111111111111010 -6的补码
//按位与:对应的二进制位同时为1则为1,有0则为0
printf("a=%d\n", a);
printf("b=%d\n", b);
printf("c=%d\n", c);
return 0;
}
输出结果:
应用:
1.如果想看某个二进制数的最低位是几,可以采用 a&1 的操作。
2.如果想看某个二进制数从右往左数的第n位是几,可以先 a>>n-1 ,再 a&1。
例:
#include <stdio.h>
int main()
{
int a = 0;
int n = 0;
printf("请输入一个数以及你想观察的位数:");
scanf("%d %d", &a, &n);
printf("%d二进制形式下从右往左数的第%d位是%d\n", a, n, a >> n - 1 & 1);
return 0;
}
输出结果:
4.2 按位或
#include <stdio.h>
int main()
{
int a = 5;
int b = -6;
int c = a | b;
//00000000000000000000000000000101 5的补码
//11111111111111111111111111111010 -6的补码
//按位或:对应的二进制位同时为0则为0,有1则为1
printf("a=%d\n", a);
printf("b=%d\n", b);
printf("c=%d\n", c);
return 0;
}
输出结果:
应用:将指定位变为1。
例:
#include <stdio.h>
int main()
{
int a = 0;
int n = 0;
printf("输入一个数,以及你想让它的第几位变为1?\n");
scanf("%d %d", &a, &n);
a = a | (1 << n-1);//将a的二进制中第n位改成1
printf("这个数的第%d位变为1后产生的数为:%d", n, a);
return 0;
}
输出结果:
4.3 按位异或
#include <stdio.h>
int main()
{
int a = 5;
int b = -6;
int c = a ^ b;
//00000000000000000000000000000101 5的补码
//11111111111111111111111111111010 -6的补码
//按位异或:对应的二进制位相同则为0,相异则为1
printf("a=%d\n", a);
printf("b=%d\n", b);
printf("c=%d\n", c);
return 0;
}
输出结果:
注意:异或是支持交换律的!
例:
3^3^5=5
3^5^3=5
面试题:不创建临时变量(第三个变量),实现两个数的交换。
方法一:
#include <stdio.h>
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("交换前:a=%d,b=%d\n", a, b);
a = a + b;//a加b的和暂时放在a里面去
b = a - b;//实际上为a加b减b
a = a - b;//实际上是a加b减a
printf("交换后:a=%d,b=%d\n", a, b);
return 0;
}
缺陷:a和b两个数字过大的时候容易导致越界。
输出结果:
方法二:
#include <stdio.h>
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("交换前:a=%d,b=%d\n", a, b);
a = a ^ b;
b = a ^ b;//相当于a^b^b,而b^b=0,所以a^b^b=a^0=a
a = a ^ b;//相当于a^b^a,而由异或运算支持交换律得到a^b^a=a^a^b=b
printf("交换后:a=%d,b=%d\n", a, b);
return 0;
}
这种异或操作具有局限性:
1.只能作用于整数交换;
2.代码的可读性差;
3.执行的效率低于采用临时变量的方法。
输出结果:
4.4 按位取反
#include <stdio.h>
int main()
{
int n = 0;
int a = ~n;
//00000000000000000000000000000000
//按位取反:对应的二进制位是0的则为1,是1的则为0
printf("%d\n", a);
return 0;
}
输出结果:
应用:将指定位变0。
例:
#include <stdio.h>
int main()
{
int a = 0;
int n = 0;
printf("输入一个数,以及你想让它的第几位变为0?\n");
scanf("%d %d", &a, &n);
a = a & ~(1 << n - 1);//将a的二进制中第n位改成0
printf("这个数的第%d位变为0后产生的数为:%d", n, a);
return 0;
}
输出结果:
4.5 练习
谷歌面试题:求一个整数以2进制形式存储在内存中时1的个数。
方法一:
分析:在10进制中,我们采用模10除10的办法来得到10进制数中的每一位数。那么推而广之,对于2进制数,可以采用模2除2的办法得到2进制数中的每一位数。
#include <stdio.h>
int main()
{
int a = 0;
int count = 0;//计数
scanf("%d", &a);
while (a)
{
if (a % 2 == 1)//判断当前最低位是否为1
count++;
a = a / 2;//将a去掉一位,再赋给a
}
printf("这个数在二进制形式中1的个数 = %d\n", count);
return 0;
}
存在缺陷,因为当输入的数为负数时,计算会出错
方法二:
#include <stdio.h>
int main()
{
int a = 0;
int count = 0;//计数
scanf("%d", &a);
int i = 0;
for (i = 0; i < 32; i++)
{
if (((a >> i) & 1) == 1)//判断右移i位后当前最低位是否为1
{
count++;
}
}
printf("这个数在二进制形式中1的个数 = %d\n", count);
return 0;
}
方法三:
#include <stdio.h>
int main()
{
int a = 0;
int count = 0;//计数
scanf("%d", &a);
while (a)
{
count++;
a = a & (a - 1);//只要执行1次,a的二进制序列中,最右边的1就消失了,能执行几次就说明有几个1
}
printf("这个数在二进制形式中1的个数 = %d\n", count);
return 0;
}
输出结果:
练习:写一个代码,判断一个数是否是2的n次方
分析:2^n这个数的二进制形式中只有一个1,如果去掉一个1后二进制中1的个数为0,则说明这个数是2的n次方
#include <stdio.h>
int main()
{
int n = 0;
while (scanf("%d", &n) != EOF)
{
if ((n & (n - 1)) == 0)//去掉一个1后判断二进制中1的个数是否为0
{
printf("yes\n");
}
else
printf("no\n");
}
return 0;
}
输出结果:
5 逗号表达式
exp1, exp2, exp3, … ,expN
逗号表达式,就是用逗号隔开的多个表达式,是优先级最低的操作符。
逗号表达式中的表达式从左向右依次执行,整个表达式的结果是最后一个表达式的结果。
//代码1
int a = 1;
int b = 2;
int c = (a>b, a=b+10, a, b=a+1);//逗号表达式
c是多少?
//代码2
if (a =b + 1, c=a / 2, 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)
{
//业务处理
}
6 下标引用[]和函数调用()操作符
6.1 []下标引用操作符
操作数:一个数组名+一个索引值。
int arr[10];//创建数组。
arr[9] = 10;//[ ]的两个操作数是arr和9。
6.2 ()函数调用操作符
函数调用操作符接受一个或者多个操作数,第一个操作数是函数名,剩余的操作数就是传递给函数的参数。
函数调用操作符至少有一个操作数,即操作数中至少要有函数名。
#include <stdio.h>
int Add(int x, int y)
{
return x + y;
}
int main()
{
int ret = Add(2, 3);//函数调用操作符
int n = sizeof(ret);//注意:sizeof是一个操作符而不是函数,因为它后面的括号其实可以省略。
printf("%d\n", ret);
return 0;
}
7 操作符的属性
7.1 优先级
优先级指的是,如果一个表达式包含多个运算符,哪个运算符应该优先执行。各种运算符的优先级有所不同。
例:
3 + 4 * 5;
上面示例中,表达式 3 + 4 * 5 里面既有加法运算符(+),又有乘法运算符(*)。由于乘法的优先级高于加法,所以会先计算 4 * 5 ,而不是先计算 3 + 4。
7.2 结合性
如果两个运算符的优先级相同,优先级没办法确定计算顺序时,这时候就要靠结合性来进行确定。
结合性指的是,根据运算符是左结合,还是右结合来决定执行顺序。大部分运算符是左结合(从左向右执行),少数操作符是右结合(从右向左执行),比如赋值运算符(=)。
常见操作符的优先级:
• 圆括号( () )
• 自增运算符( ++ ),自减运算符( -- )
• 一元运算符( + 和 - )
• 乘法( * ),除法( / )
• 加法( + ),减法( - )
• 关系运算符( < 、 > 等)
• 赋值运算符( = )
由于圆括号的优先级最高,所以可以使用它来改变其他运算符的优先级。
部分运算符的优先级:
8 表达式求值
表达式求值之前首先要进行类型转换,当表达式中的值转换到适当的类型时,才开始计算。
两种类型转换:
1.整型提升
2.算术转换
8.1 整形提升
C语言中整形算术运算总是至少以缺省整型类型(int)的精度来进行。
为了获得这个精度,表达式中的字符和短整型操作数在使用之前将被转换为普通整型(int、unsigned int),这种转换就称为整型提升。
整型提升的原因:
表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通⽤寄存器的⻓度。
因此,即使两个char类型的相加,在CPU执⾏时实际上也要先转换为CPU内整型操作数的标准长度。
通⽤CPU(general-purpose CPU)是难以直接实现两个8⽐特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)的。所以,表达式中各种长度可能⼩于int⻓度的整型值,都必须先转换为int或unsigned int,然后才能送⼊CPU去执行运算。
整型提升的方式:
1.有符号整数提升是按照变量的数据类型的符号位来提升。
2.无符号整数提升,高位补0。
例1:
//负数的整形提升
char c1 = -1;
变量c1的二进制位(补码)中只有8个⽐特位:
1111111
因为 char 为有符号的 char
所以整形提升的时候,高位补充符号位,即为1
提升之后的结果是:
11111111111111111111111111111111
//正数的整形提升
char c2 = 1;
变量c2的二进制位(补码)中只有8个⽐特位:
00000001
因为 char 为有符号的 char
所以整形提升的时候,高位补充符号位,即为0
提升之后的结果是:
00000000000000000000000000000001
//无符号整形提升,⾼位补0
例2:
#include <stdio.h>
int main()
{
char a = 5;
//5 —— int —— 4个字节 —— 32bit
//a —— char —— 1个字节 —— 8bit
//00000000000000000000000000000101 —— 5的补码
//5要存储到a中,需要截断,所以存在a中的实际上为:00000101
char b = 127;
//127 —— int —— 4个字节 —— 32bit
//b —— char —— 1个字节 —— 8bit
//00000000000000000000000001111111 —— 127的补码
//127要存储到b中,需要截断,所以存在a中的实际上为:01111111
char c = a + b;//a和b都为有符号的char类型,如要相加就会发生整型提升,且两个符号位为0,所以高位补0
//a —— 00000000000000000000000000000101
//b —— 00000000000000000000000001111111
//a+b —— 00000000000000000000000010000100
//a+b要存储到c中,需要截断,所以存在c中的实际上为:10000100
printf("%d\n", c);//%d是按照十进制的形式打印有符号的整数,
//而c为有符号的char类型,如要打印也会发生整型提升,且c的符号位为1,所以高位补1
//c —— 11111111111111111111111110000100 —— 补码
//负数取反加一得原码 —— 10000000000000000000000001111100 —— -124的原码
return 0;
}
8.2 算术转换
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数转换为另一个操作数的类型,否则操作就无法进行。
下面的层次体系称为寻常算术转换。
long double
double
float
unsigned long int
long int
unsigned int
int
如果某个操作数的类型在上面这个列表中排名靠后,那么首先要转换为另外一个操作数的类型后再执行运算。
8.3 问题表达式解析
8.3.1 表达式1
//表达式的求值部分由操作符的优先级决定。
a*b + c*d + e*f
表达式1在计算的时候,由于 * 比 + 的优先级高,只能保证 * 的计算比 + 要早,但是优先级并不能决定第三个 * 比第一个 + 早执行。
所以表达式的计算顺序就可以能是:
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
所以仅通过操作符的属性,无法确定该表达式的唯一计算路径。
8.3.2 表达式2
c + --c;
同上,操作符的优先级只能决定 – 运算在 + 运算的前面,但是我们没办法确定左边的c是 – 之前的值还是 --之后的值,所以结果是不可预测的,会有歧义。
8.3.3 表达式3
int main()
{
int i = 10;
i = i-- - --i * ( i = -3 ) * i++ + ++i;
printf("i = %d\n", i);
return 0;
}
表达式3在不同编译器中测试结果:
8.3.4 表达式4
#include <sdtio.h>
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() 中,从操作符的优先级得知:先算乘法,再算减法。
但是函数调用的先后顺序无法通过操作符的优先级来确定,所以这个代码也存在问题。
8.3.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;
}
gcc编译器执行结果:
VS2019执行结果:
同样的代码产生了不同的结果,这是因为依靠操作符的优先级和结合性无法决定代码中的第一个 + 和第三个前置 ++ 的先后顺序,所以第一个 + 在执行的时候,第三个 ++ 是否执行是不确定的。