1.了解二进制
其实二进制;八进制;十进制和十六进制都是数值的不同表示形式而已
- 二进制:基数为2,由0和1两个数字组成,逢2进1。
- 八进制:基数为8,由0~7八个数字组成,逢8进1。
- 十进制:基数为10,由0~9十个数字组成,逢10进1。
- 十六进制:基数为16,由0~9十个数字和A/a(10);B/b(11);C/c(12);D/d(13);E/e(14);F/f(15)六个数字共同组成,逢16进1。
(1)二进制转十进制
位权:以十进制为例
百位 | 十位 | 个位 | |
---|---|---|---|
十进制的位 | 1 | 2 | 3 |
权 | 10^2 | 10^1 | 10^0 |
按权展开 | 100 | 10 | 1 |
求值 | 1*100+ | 2*10+ | 3*1 |
注意:
- 基数的多少次幂,这样的数被称为位权
- 任何数的1次方都等于其本身;任何数的0次方都等于1
因此,二进制转十进制的做法如下:以二进制的111为例
二进制的位 | 1 | 1 | 1 |
---|---|---|---|
权 | 2^2 | 2^1 | 2^0 |
按权展开 | 4 | 2 | 1 |
求值 | 1*4+ | 1*2+ | 1*1 |
(2)十进制转二进制
口诀:转2除2,倒取余,从下往上依次排序
如:将十进制的10转为二进制
注意: 当里面的数小于外面的基数时,也就是除不开的时候,里面的数就是最后的余数
(3)二进制转八进制
方法:将3个二进制数划为一组,按421这个上标的顺序标上标(分组是从后往前分,标上标是从前往后标),不足3位的在最前边补0(0标上标后依旧是0,不参与运算)其余的每组上标相加得出结果,最后每个组的结果合并就是转换完成的八进制数
如:将二进制数1101111转换为八进制的数
二进制数: | 1101111 |
---|---|
分组后 | 001 101 111 |
上标 | 421 421 421 |
八进制数结果 | 1 + 5 + 7 = 157 |
注意: 以0开头的数字会被当做八进制
(4)二进制转十六进制
方法:将4个二进制数划为一组,按8421这个上标的顺序标上标(分组是从后往前分,标上标是从前往后标),不足4位的在最前边补0(0标上标后依旧是0,不参与运算)其余的每组上标相加得出结果,最后每个组的结果合并就是转换完成的十六进制数
如:将二进制数110110111转换为十六进制的数
二进制数: | 0001 1011 0111 |
---|---|
分组后 | 0001 1011 0111 |
上标 | 8421 8421 8421 |
十六进制数结果 | 1 + b + 7 = 0x1b7 |
注意:
- 转换成十六进制时,10~15这些数字必须用字母表示
- 表示十六进制的时候前面要加
0x
2.原码、反码和补码
整数的二进制表示方法有3种:原码、反码和补码
这三种表示方法均有符号位和数值位两部分,符号位都是用0表示正、用1表示负;而数值位最高位的一位是被当做符号位,剩余的都是数值位。
正整数的原码、反码和补码都相同,但负整数的三种表示方法各不相同:
- 原码:直接将数值按照正负数的形式翻译成二进制得到的就是原码
- 反码:将原码的符号位(从左往右数第一位就是符号位)不变,数值位(除去符号位的其它位)依次按位取反就可以得到反码
- 补码:反码+1就得到补码
如:
int
类型 5 的原码;反码和补码
如:
int
类型 -5 的原码;反码和补码
为什么表示5的是32位二进制数?
因为1字节等于8比特,而1个int类型占4个字节,也就是32比特位,所以用32个二进制数来表示
对于整型来说,数据存放内存中其实存放的是补码。 因为在计算机系统中,数值一律用补码来表示和存储。原因在于使用补码可以将符号位和数值域统一处理;同时,加法和减法也可以统一处理(CPU只有加法器);此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路
示例:
int main()
{
int a = 1;
int b = -1;
// 1 + (-1)
// 0000000000 0000000000 000000000001 -- 1原码/补码
// 1000000000 0000000000 000000000001 -- -1原码
// 1111111111 1111111111 111111111110 -- -1反码
// 1111111111 1111111111 111111111111 -- -1补码
}
3.移位操作符
<<
左移操作符>>
右移操作符
(1)左移操作符
移位规则:左边丢弃,右边补上0
代码示例:将
num
左移一位
#include <stdio.h>
int main()
{
int num = 10;
// num的二进制表示:0000000000 0000000000 0000001010
int ret = num << 1;
// num << 1的移位结果:000000000 0000000000 00000010100
printf("num = %d\n",num);
printf("ret = %d\n",ret);
return 0;
}
注意:
- 整个过程
num
本身的值是不会发生改变的,只有将移位过程赋值给num
本身的时候值才会发生改变 - 无论是正数还是负数,在内存中移位运算的都是补码
(2)右移操作符
- 逻辑右移:左边用0填充,右边丢弃
- 算术右移:左边用原该值的符号位填充,右边丢弃
注意:
右移所采用的是逻辑右移,还是算术右移是不确定的,取决于编译器。但是大部分的编译器是采用算术右移的
代码示例:将
num
右移一位(逻辑右移)
#include <stdio.h>
int main()
{
int num = -1;
// num的二进制表示:1111111111 1111111111 1111111111
int ret = num >> 1;
// num >> 1逻辑右移的移位结果:01111111111 1111111111 111111111
printf("num = %d\n", num);
printf("ret = %d\n", ret);
return 0;
}
代码示例:将
num
右移一位(算术右移)
#include <stdio.h>
int main()
{
int num = -1;
// num的二进制表示:1111111111 1111111111 1111111111
int ret = num >> 1;
// num >> 1算术右移的移位结果:1111111111 1111111111 1111111111
printf("num = %d\n", num);
printf("ret = %d\n", ret);
return 0;
}
注意:
- 移位操作符的操作数只能是整数,不能是浮点数
- 对于移位操作符,不要移动负数位,这个标准是未定义的
//如:
int num = 10;
num >> -1; //移动负数位是错误的
4.位操作符
位操作符有:
& // 按位与
| // 按位或
^ // 按位异或
~ // 按位取反操作符
注意:
- 以上位操作符的操作数只能是整数
- 位指的是二进制位
多说无益,代码示例:
(1)按位与的运算规则:对应的二进制位上,有0则为0,两个同时为1才为1
#include <stdio.h>
int main()
{
// &
int a = 5;
int b = -6;
int c = a & b; // 按位与
// a的二进制表示:0000000000 0000000000 0000000101
// b的二进制表示:1111111111 1111111111 1111111010
printf("%d\n",c);
// a & b:0000000000 0000000000 0000000000
return 0;
}
(2)按位或的运算规则:对应的二进制位上,有1则为1,两个同时为0才为0
#include <stdio.h>
int main()
{
// |
int a = 5;
int b = -6;
int c = a | b; // 按位或
// a的二进制表示:0000000000 0000000000 0000000101
// b的二进制表示:1111111111 1111111111 1111111010
printf("%d\n",c);
// a | b:1111111111 1111111111 1111111111
return 0;
}
(3)按位异或的运算规则:对应的二进制位上,相同则为0,相异则为1
#include <stdio.h>
int main()
{
// ^
int a = 5;
int b = -6;
int c = a ^ b; // 按位异或
// a的二进制表示:0000000000 0000000000 0000000101
// b的二进制表示:1111111111 1111111111 1111111010
printf("%d\n",c);
// a ^ b:1111111111 1111111111 1111111111
return 0;
}
(4)按位取反的运算规则:二进制是0的变成1,是1的变成0
#include <stdio.h>
int main()
{
int n = 0; // 0000000000 0000000000 000000000000
int a = ~n; //按位取反 1111111111 1111111111 111111111111
printf("%d\n",a);
return 0;
}
练习1:实现两个数的交换
#include <stdio.h>
int main()
{
int a = 3;
int b = 5;
int tmp = 0;
printf("交换前:a = %d b = %d\n",a,b);
tmp = a;
a = b;
b = tmp;
printf("交换后:a = %d b = %d\n",a,b);
return 0;
}
练习2:不能创建临时变量(第三个变量),实现两个数的交换
// 第一种方法
#include <stdio.h>
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和b的数特别大的时候会导致范围越界
// 第二种方法
#include <stdio.h>
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;
}
1. 异或运算符的特点:
- a ^ a = 0
- 0 ^ a = a
- a ^ a ^ b = b
- a ^ b ^ a = b
通过以上的特点分析得出一个结论:异或是支持交换律的
2. 异或运算的局限性:
- 异或只能用于整数交换
- 代码的可读性较差
- 代码的执行效率也是低于使用第3变量的方法的
练习3:求一个整数存储在内存中的二进制中1的个数
// 第一种方法
#include <stdio.h>
int main()
{
int a = 15;
int count = 0;
while(a)
{
if(a % 2 == 1)
{
count++;
}
a = a / 2;
}
printf("count = %d\n",count);
return 0;
}
// 第二种方法
#include <stdio.h>
int main()
{
int a = 0;
scanf("%d",&a);
int i = 0;
int count = 0;
for(i<0; i<32; i++)
{
if(((a>>i) & 1) == 1)
{
count++;
}
}
printf("count = %d\n",count);
return 0;
}
// 第三种方法
#include <stdio.h>
int main()
{
int n = 0;
int count = 0;
scanf("%d",&n);
while(n)
{
n = n & (n-1);
count++;
}
printf("count = %d\n",count);
return 0;
}
练习4:判断一个数是否是2^n次方
#include <stdio.h>
int main()
{
int n = 0;
scanf("%d",&n);
if(n & (n - 1) == 0)
{
printf("yes\n");
}
else
{
printf("no\n");
}
return 0;
}
练习5:将a = 13的二进制位中第五位数字改为1,并且其它位保持不变
#include <stdio.h>
int main()
{
int a = 13;
// 0000000000 0000000000 000000001101
// 0000000000 0000000000 000000010000
// 1 << 4
// 0000000000 0000000000 000000011101
a = a | (1 << 4);
printf("a = %d\n",a);
return 0;
}
练习6:将a = 29的二进制位中第五位数字改为0,并且其它位保持不变
#include <stdio.h>
int main()
{
int a = 29;
// 0000000000 0000000000 000000011101
a = a & ~(1 << 4);
// 1111111111 1111111111 111111101111
printf("a = %d\n",a);
// 0000000000 0000000000 000000001101
return 0;
}
5.逗号表达式
- 逗号表达式就是用逗号隔开的多个表达式
- 逗号表达式从左向右依次执行,整个表达式的结果是最后一个表达式的结果
- 逗号操作符(
,
)的优先级是最低级的
代码示例:
#include <stdio.h>
int main()
{
int a = 1;
int b = 2;
int c = (a > b, a = b + 10, b = a + 1);
printf("%d\n",c);
return 0;
}
6.下标访问和函数调用
(1)下标引用操作符:[]
操作数:一个数组名 + 一个索引值
#include <stdio.h>
int main()
{
int arr[5] = {1,2,3,4,5}; // 创建一个数组
printf("%d\n",arr[4]); // 使用下标引用操作符
// 下标引用操作符( [] )的两个操作数就是 arr 和 4
return 0;
}
在上述的代码中,下标引用操作符([]
)的两个操作数就是 a 和 9
(2)函数调用操作符:()
可以接收一个或者多个操作数:第一个操作数是函数名;剩余的操作数就是传递给函数的参数
#include <stdio.h>
int Add(int x, int y)
{
return x+y;
}
int main()
{
//函数的调用
int ret = Add(1,2); // 使用函数调用操作符
// 这个函数调用操作符:( () )的操作数就是 Add, 1, 2
printf("%d\n",ret);
return 0;
}
注意:
-
函数调用操作符至少要有一个操作数
-
sizeof()
不是函数,是操作符,因为sizeof
后面的括号是可以省略掉的,而函数后面的括号是坚决不可以省略的。
// 代码示例:
#include <stdio.h>
int main()
{
int a = 10;
int n = sizeof a;
printf("%d\n", n);
return 0;
}
7.操作符的属性
C语言的操作符有两个重要的属性:优先级和结合性。这两个属性一定程度上决定了表达式求值的计算顺序。
(1)优先级
优先级指的是如果一个表达式包含多个运算符,那么哪个运算符应该优先执行。每种运算符的优先级是不一样的
常用运算符的优先级顺序(从上到下,优先级是由高到低的):
- 圆括号:
()
- 自增运算符:
++
;自减运算符:--
- 单目运算符:
+
和-
- 乘法:
*
;除法:/
- 加法:
+
;减法:-
- 关系运算符:
<
;>
等 - 赋值运算符:
=
注意:
-
关于优先级,我们一般讨论的是两个相邻运算符的优先级
-
由于圆括号的优先级最高,因此可以使用它来改变其它运算符的优先级
(2)结合性
如果两个运算符的优先级相同,没办法确定先计算哪个,这时候就要看结合性了。根据运算符是左结合;还是右结合,决定执行顺序。大部分的运算符都是左结合(从左到右执行);只有少数运算符是右结合(从右到左执行)。
参考链接: C 运算符优先级 - cppreference.com
级和结合性。这两个属性一定程度上决定了表达式求值的计算顺序。
(1)优先级
优先级指的是如果一个表达式包含多个运算符,那么哪个运算符应该优先执行。每种运算符的优先级是不一样的
常用运算符的优先级顺序(从上到下,优先级是由高到低的):
- 圆括号:
()
- 自增运算符:
++
;自减运算符:--
- 单目运算符:
+
和-
- 乘法:
*
;除法:/
- 加法:
+
;减法:-
- 关系运算符:
<
;>
等 - 赋值运算符:
=
注意:
-
关于优先级,我们一般讨论的是两个相邻运算符的优先级
-
由于圆括号的优先级最高,因此可以使用它来改变其它运算符的优先级
(2)结合性
如果两个运算符的优先级相同,没办法确定先计算哪个,这时候就要看结合性了。根据运算符是左结合;还是右结合,决定执行顺序。大部分的运算符都是左结合(从左到右执行);只有少数运算符是右结合(从右到左执行)。
参考链接: C 运算符优先级 - cppreference.com