《C语言动漫对话教程(入门篇)》_int a=2 a百分号等于4-1则表达式a加等于a乘等于a减等于a乘于三

printf("%lf\n", c); 
return 0;

}


* 我们只需要将`a`或`b`其中之一转换成`double`,然后再参与运算即可。
* 当然,我们还可以这么写:



#include <stdio.h>
int main(){
int a = 10;
int b = 3;
double c = (a + 0.0) / b;
printf(“%lf\n”, c);
return 0;
}


* 或者这么写:



#include <stdio.h>
int main(){
int a = 10;
int b = 3;
double c = (a * 1.0) / b;
printf(“%lf\n”, c);
return 0;
}


* 核心就是:不改变原有表达式的值,在其中添加一些`double`类型的数,使得整个表达式转换成`double`。
* 使用强制类型转换时,有时候可能不是编译器想要的那样,因为这是写代码的人自己的行为,所以程序员自己要意识到其中潜在的风险。
* 比如将指针转换成整型,或者将`double`转换成指针,当然,有些强制转换可能直接导致程序崩溃。




---


![](https://img-blog.csdnimg.cn/202106131045347.gif#pic_center)



> 
> 通过这一章,我们学会了:  
>   1)类型转换;  
>   2)自动类型转换;  
>   3)强制类型转换;
> 
> 
> 


* 希望对你有帮助哦 ~ 祝大家早日成为 C 语言大神!




---


## 课后习题


![](https://img-blog.csdnimg.cn/20210627104419151.gif#pic_center)


* [【第01题】A + B | 四种输入方式,开启刷题的序章](https://bbs.csdn.net/topics/618317507)
* [【第10题】给定 n 和 n 个正整数,输出它们的平均数](https://bbs.csdn.net/topics/618317507)
* [【第12题】给定 r,求以 r 为半径的圆的周长和面积](https://bbs.csdn.net/topics/618317507)




---




**(13)- 位运算概览**

## 一、再谈二进制


* 我们在学习 [光天化日学C语言(06)- 进制转换入门](https://bbs.csdn.net/topics/618317507) 的时候,曾经提到过二进制。


![](https://img-blog.csdnimg.cn/20210630104852147.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1doZXJlSXNIZXJvRnJvbQ==,size_16,color_FFFFFF,t_70)


* 在计算机中,非零即一。


### 1、二进制数值表示


* 例如,在计算机中,我们可以用单纯的 0 和 1 来表示数字。



> 
> 1、101、1100011、100101010101 都是二进制数。  
>  123、423424324、101020102101AF 则不是,因为有 0 和 1 以外的数字位。
> 
> 
> 


* 一般为了不产生二义性,我们会在数字的右下角写上它的进制,例如:
* 101 
 
 
 
 0 
 
 
 
 ( 
 
 
 10 
 
 
 ) 
 
 
 
 
 
 1010\_{(10)} 
 
 
 1010(10)​
* 代表的是十进制下的 1010,也就是十进制下的 “一千零一十”。
* 101 
 
 
 
 0 
 
 
 
 ( 
 
 
 2 
 
 
 ) 
 
 
 
 
 
 1010\_{(2)} 
 
 
 1010(2)​
* 代表的是二进制下的 1010,也就是十进制下的 “十”。


![](https://img-blog.csdnimg.cn/20210630105129813.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1doZXJlSXNIZXJvRnJvbQ==,size_16,color_FFFFFF,t_70)


### 2、二进制加法



> 
> 二进制加法采用从低到高的位依次相加,当相加的和为2时,则向高位进位。
> 
> 
> 


* 例如,在二进制中,加法如下: 
 
 
 
 
 
 1 
 
 
 
 ( 
 
 
 2 
 
 
 ) 
 
 
 
 
 + 
 
 
 
 1 
 
 
 
 ( 
 
 
 2 
 
 
 ) 
 
 
 
 
 = 
 
 
 1 
 
 
 
 0 
 
 
 
 ( 
 
 
 2 
 
 
 ) 
 
 
 
 
 
 
 1 
 
 
 
 ( 
 
 
 2 
 
 
 ) 
 
 
 
 
 + 
 
 
 
 0 
 
 
 
 ( 
 
 
 2 
 
 
 ) 
 
 
 
 
 = 
 
 
 
 1 
 
 
 
 ( 
 
 
 2 
 
 
 ) 
 
 
 
 
 
 
 0 
 
 
 
 ( 
 
 
 2 
 
 
 ) 
 
 
 
 
 + 
 
 
 
 1 
 
 
 
 ( 
 
 
 2 
 
 
 ) 
 
 
 
 
 = 
 
 
 
 1 
 
 
 
 ( 
 
 
 2 
 
 
 ) 
 
 
 
 
 
 
 0 
 
 
 
 ( 
 
 
 2 
 
 
 ) 
 
 
 
 
 + 
 
 
 
 0 
 
 
 
 ( 
 
 
 2 
 
 
 ) 
 
 
 
 
 = 
 
 
 
 0 
 
 
 
 ( 
 
 
 2 
 
 
 ) 
 
 
 
 
 
 1\_{(2)} + 1\_{(2)} = 10\_{(2)} \\ 1\_{(2)} + 0\_{(2)} = 1\_{(2)} \\ 0\_{(2)} + 1\_{(2)} = 1\_{(2)} \\ 0\_{(2)} + 0\_{(2)} = 0\_{(2)} 
 
 
 1(2)​+1(2)​=10(2)​1(2)​+0(2)​=1(2)​0(2)​+1(2)​=1(2)​0(2)​+0(2)​=0(2)​


### 3、二进制减法



> 
> 二进制减法采用从低到高的位依次相减,当遇到 0 减 1 的情况,则向高位借位。
> 
> 
> 


* 例如,在二进制中:减法如下: 
 
 
 
 
 
 1 
 
 
 
 ( 
 
 
 2 
 
 
 ) 
 
 
 
 
 − 
 
 
 
 1 
 
 
 
 ( 
 
 
 2 
 
 
 ) 
 
 
 
 
 = 
 
 
 
 0 
 
 
 
 ( 
 
 
 2 
 
 
 ) 
 
 
 
 
 
 
 1 
 
 
 
 ( 
 
 
 2 
 
 
 ) 
 
 
 
 
 − 
 
 
 
 0 
 
 
 
 ( 
 
 
 2 
 
 
 ) 
 
 
 
 
 = 
 
 
 
 1 
 
 
 
 ( 
 
 
 2 
 
 
 ) 
 
 
 
 
 
 1 
 
 
 
 0 
 
 
 
 ( 
 
 
 2 
 
 
 ) 
 
 
 
 
 − 
 
 
 
 1 
 
 
 
 ( 
 
 
 2 
 
 
 ) 
 
 
 
 
 = 
 
 
 
 1 
 
 
 
 ( 
 
 
 2 
 
 
 ) 
 
 
 
 
 
 
 0 
 
 
 
 ( 
 
 
 2 
 
 
 ) 
 
 
 
 
 − 
 
 
 
 0 
 
 
 
 ( 
 
 
 2 
 
 
 ) 
 
 
 
 
 = 
 
 
 
 0 
 
 
 
 ( 
 
 
 2 
 
 
 ) 
 
 
 
 
 
 1\_{(2)} - 1\_{(2)} = 0\_{(2)} \\ 1\_{(2)} - 0\_{(2)} = 1\_{(2)} \\ 10\_{(2)} - 1\_{(2)} = 1\_{(2)} \\ 0\_{(2)} - 0\_{(2)} = 0\_{(2)} 
 
 
 1(2)​−1(2)​=0(2)​1(2)​−0(2)​=1(2)​10(2)​−1(2)​=1(2)​0(2)​−0(2)​=0(2)​
* 而我们今天要讲的位运算正是基于二进制展开的。


## 二、位运算简介


* 位运算可以理解成对二进制数字上的每一个位进行操作的运算。
* 位运算分为 布尔位运算符 和 移位位运算符。
* 布尔位运算符又分为 位与(&)、位或(|)、异或(^)、按位取反(~);移位位运算符分为 左移(<<) 和 右移(>>)。
* 如图所示:  
 ![](https://img-blog.csdnimg.cn/20210630085908172.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1doZXJlSXNIZXJvRnJvbQ==,size_16,color_FFFFFF,t_70#pic_center)


![](https://img-blog.csdnimg.cn/20210630105928941.gif#pic_center)


## 三、位运算概览


* 今天,我们先来对位运算进行一个初步的介绍。后面会对每个运算符的应用做详细介绍,包括刷题的时候如何运用位运算来加速等等。


### 1、布尔位运算


* 对于布尔位运算,总共有四个,如下表所示:




| C语言运算符表示 | 含义 | 示例 |
| --- | --- | --- |
| `&` | 位与 | `x & y` |
| `|` | 位或 | `x | y` |
| `^` | 异或 | `x ^ y` |
| `~` | 按位取反 | `x ~ y` |


#### 1)位与


* 位与就是对操作数的每一位按照如下表格进行运算,对于每一位只有 0 或 1 两种情况,所以组合出来总共  
 
 
 
 
 
 2 
 
 
 2 
 
 
 
 = 
 
 
 4 
 
 
 
 2^2 = 4 
 
 
 22=4 种情况。




| 左操作数 | 右操作数 | 结果 |
| --- | --- | --- |
| 0 | 0 | 0 |
| 0 | 1 | 0 |
| 1 | 0 | 0 |
| 1 | 1 | 1 |


![](https://img-blog.csdnimg.cn/20210630110208203.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1doZXJlSXNIZXJvRnJvbQ==,size_16,color_FFFFFF,t_70)



#include <stdio.h>
int main() {
int a = 0b1010; // (1)
int b = 0b0110; // (2)
printf(“%d\n”, (a & b) ); // (3)
return 0;
}


* ( 
 
 
 1 
 
 
 ) 
 
 
 
 (1) 
 
 
 (1) 在C语言中,以`0b`作为前缀,表示这是一个二进制数。那么`a`的实际值就是  
 
 
 
 
 ( 
 
 
 1010 
 
 
 
 ) 
 
 
 2 
 
 
 
 
 (1010)\_2 
 
 
 (1010)2​。
* ( 
 
 
 2 
 
 
 ) 
 
 
 
 (2) 
 
 
 (2) 同样的,`b`的实际值就是 
 
 
 
 
 ( 
 
 
 0110 
 
 
 
 ) 
 
 
 2 
 
 
 
 
 (0110)\_2 
 
 
 (0110)2​;
* ( 
 
 
 3 
 
 
 ) 
 
 
 
 (3) 
 
 
 (3) 那么这里`a & b`就是对 
 
 
 
 
 ( 
 
 
 1010 
 
 
 
 ) 
 
 
 2 
 
 
 
 
 (1010)\_2 
 
 
 (1010)2​ 和  
 
 
 
 
 ( 
 
 
 0110 
 
 
 
 ) 
 
 
 2 
 
 
 
 
 (0110)\_2 
 
 
 (0110)2​ 的每一位做表格中的`&`运算。
* 所以最后输出结果为:



2


* 因为输出的是十进制数,它的二进制表示为:  
 
 
 
 
 ( 
 
 
 0010 
 
 
 
 ) 
 
 
 2 
 
 
 
 
 (0010)\_2 
 
 
 (0010)2​。
* 注意:这里的 **前导零** 可有可无,作者写上前导零只是为了对齐以及让读者更加清楚位与的运算方式。


#### 2)位或


* 位或的运算结果如下:




| 左操作数 | 右操作数 | 结果 |
| --- | --- | --- |
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 1 |


![](https://img-blog.csdnimg.cn/2021063011050432.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1doZXJlSXNIZXJvRnJvbQ==,size_16,color_FFFFFF,t_70)


* 我们来看以下这段程序:



#include <stdio.h>
int main() {
int a = 0b1010;
int b = 0b0110;
printf(“%d\n”, (a | b) );
return 0;
}


* 以上程序的输出结果为:



14


* 即二进制下的  
 
 
 
 
 ( 
 
 
 1110 
 
 
 
 ) 
 
 
 2 
 
 
 
 
 (1110)\_2 
 
 
 (1110)2​ 。


#### 3)异或


* 异或的运算结果如下:




| 左操作数 | 右操作数 | 结果 |
| --- | --- | --- |
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |


![](https://img-blog.csdnimg.cn/20210630110608782.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1doZXJlSXNIZXJvRnJvbQ==,size_16,color_FFFFFF,t_70)


* 我们来看以下这段程序:



#include <stdio.h>
int main() {
int a = 0b1010;
int b = 0b0110;
printf(“%d\n”, (a ^ b) );
return 0;
}


* 以上程序的输出结果为:



12


* 即二进制下的  
 
 
 
 
 ( 
 
 
 1100 
 
 
 
 ) 
 
 
 2 
 
 
 
 
 (1100)\_2 
 
 
 (1100)2​ 。


#### 4)按位取反


* 按位取反其实就是 0 变 1, 1 变 0。
* 同样,我们来看一段程序。



#include <stdio.h>
int main() {
int a = 0b1;
printf(“%d\n”, ~a );
return 0;
}


* 这里我想卖个关子,同学们可以自己试一下运行结果。
* 至于为什么会输出这个结果,我会在 [光天化日学C语言(17)- 位运算 ~ 的应用](https://bbs.csdn.net/topics/618317507) 中进行详细讲解,敬请期待。


### 2、移位位运算


* 对于移位位运算,总共有两个,如下表所示:




| C语言运算符表示 | 含义 | 示例 |
| --- | --- | --- |
| `<<` | 左移 | `x << y` |
| `>>` | 右移 | `x >> y` |


#### 1)左移


* 其中`x << y`代表将二进制的  
 
 
 
 
 x 
 
 
 
 x 
 
 
 x 的末尾添加  
 
 
 
 
 y 
 
 
 
 y 
 
 
 y 个零,就好比向左移动了  
 
 
 
 
 y 
 
 
 
 y 
 
 
 y 位。
* 比如  
 
 
 
 
 ( 
 
 
 1011 
 
 
 
 ) 
 
 
 2 
 
 
 
 
 (1011)\_2 
 
 
 (1011)2​ 左移三位的结果为: 
 
 
 
 
 ( 
 
 
 1011000 
 
 
 
 ) 
 
 
 2 
 
 
 
 
 (1011000)\_2 
 
 
 (1011000)2​。


#### 2)右移


* 其中`x >> y`代表将二进制的  
 
 
 
 
 x 
 
 
 
 x 
 
 
 x 从右边开始截掉  
 
 
 
 
 y 
 
 
 
 y 
 
 
 y 个数,就好比向右移动了  
 
 
 
 
 y 
 
 
 
 y 
 
 
 y 位。
* 比如  
 
 
 
 
 ( 
 
 
 101111 
 
 
 
 ) 
 
 
 2 
 
 
 
 
 (101111)\_2 
 
 
 (101111)2​ 右移三位的结果为: 
 
 
 
 
 ( 
 
 
 101 
 
 
 
 ) 
 
 
 2 
 
 
 
 
 (101)\_2 
 
 
 (101)2​。




---


![](https://img-blog.csdnimg.cn/202106131045347.gif#pic_center)



> 
> 通过这一章,我们学会了:  
>   1)位与 & ;  
>   2)位或 |   
>   3)异或 ^;  
>   4)按位取反 ~;  
>   5)左移 <<;  
>   6)右移 >>;
> 
> 
> 


* 希望对你有帮助哦 ~ 祝大家早日成为 C 语言大神!




---


## 课后习题


![](https://img-blog.csdnimg.cn/20210627104419151.gif#pic_center)


* [【第01题】A + B | 四种输入方式,开启刷题的序章](https://bbs.csdn.net/topics/618317507)
* [【第39题】位与 & 的应用 | 一句话消除末尾连续的 1](https://bbs.csdn.net/topics/618317507)




---




**(14)- 位运算 & 的应用**

## 一、位与运算符


* 位与运算符是一个二元的位运算符,也就是有两个操作数,表示为`x & y`。
* 位与运算会对操作数的每一位按照如下表格进行运算,对于每一位只有 0 或 1 两种情况,所以组合出来总共  
 
 
 
 
 
 2 
 
 
 2 
 
 
 
 = 
 
 
 4 
 
 
 
 2^2 = 4 
 
 
 22=4 种情况。




| 左操作数 | 右操作数 | 结果 |
| --- | --- | --- |
| 0 | 0 | 0 |
| 0 | 1 | 0 |
| 1 | 0 | 0 |
| 1 | 1 | 1 |


* 通过这个表,我们得出一些结论:
* 1)无论是 0 或 1,只要位与上 1,还是它本身;
* 2)无论是 0 或 1,只要位与上 0,就变成 0;  
 ![](https://img-blog.csdnimg.cn/20210630110208203.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1doZXJlSXNIZXJvRnJvbQ==,size_16,color_FFFFFF,t_70)



#include <stdio.h>
int main() {
int a = 0b1010; // (1)
int b = 0b0110; // (2)
printf(“%d\n”, (a & b) ); // (3)
return 0;
}


* ( 
 
 
 1 
 
 
 ) 
 
 
 
 (1) 
 
 
 (1) 在C语言中,以`0b`作为前缀,表示这是一个二进制数。那么`a`的实际值就是  
 
 
 
 
 ( 
 
 
 1010 
 
 
 
 ) 
 
 
 2 
 
 
 
 
 (1010)\_2 
 
 
 (1010)2​。
* ( 
 
 
 2 
 
 
 ) 
 
 
 
 (2) 
 
 
 (2) 同样的,`b`的实际值就是 
 
 
 
 
 ( 
 
 
 0110 
 
 
 
 ) 
 
 
 2 
 
 
 
 
 (0110)\_2 
 
 
 (0110)2​;
* ( 
 
 
 3 
 
 
 ) 
 
 
 
 (3) 
 
 
 (3) 那么这里`a & b`就是对 
 
 
 
 
 ( 
 
 
 1010 
 
 
 
 ) 
 
 
 2 
 
 
 
 
 (1010)\_2 
 
 
 (1010)2​ 和  
 
 
 
 
 ( 
 
 
 0110 
 
 
 
 ) 
 
 
 2 
 
 
 
 
 (0110)\_2 
 
 
 (0110)2​ 的每一位做表格中的`&`运算。
* 所以最后输出结果为:



2


* 因为输出的是十进制数,它的二进制表示为:  
 
 
 
 
 ( 
 
 
 0010 
 
 
 
 ) 
 
 
 2 
 
 
 
 
 (0010)\_2 
 
 
 (0010)2​。
* 注意:这里的 **前导零** 可有可无,作者写上前导零只是为了对齐以及让读者更加清楚位与的运算方式。


## 二、位与运算符的应用


### 1、奇偶性判定


* 我们判断一个数是奇数还是偶数,往往是通过取模`%`来判断的,如下:



#include <stdio.h>
int main() {
if(5 % 2 == 1) {
printf(“5是奇数\n”);
}
if(6 % 2 == 0) {
printf(“6是偶数\n”);
}
return 0;
}


* 然而,我们也可以这么写:



#include <stdio.h>
int main() {
if(5 & 1) {
printf(“5是奇数\n”);
}
if( (6 & 1) == 0 ) {
printf(“6是偶数\n”);
}
return 0;
}


* 哇,好神奇!
* 这是利用了奇数和偶数分别的二进制数的特性,如下表所示:




| - | 二进制末尾位 |
| --- | --- |
| 奇数 | 1 |
| 偶数 | 0 |


* 所以,我们对任何一个数,通过将它和 `0b1`进行位与,结果为零,则必然这个数的二进制末尾位为0,根据以上表就能得出它是偶数了;否则,就是奇数。
* 注意,由于`if`语句我们还没有实际提到过,所以这里简单提一下,后面会有系统的讲解:



if( expr ) { body }

* 对于以上语句,`expr`代表的是一个表达式,表达式的值最后只有 零 或 非零,如果值为非零,才会执行`body`中的内容。


### 2、取末五位



> 
> 【例题1】给定一个数,求它的二进制表示的末五位,以十进制输出即可。
> 
> 
> 


![](https://img-blog.csdnimg.cn/20210701092721550.gif#pic_center)


* 这个问题的核心就是:我们只需要末五位,剩下的位我们是不需要的,所以可以将给定的数 位与上`0b11111`,这样一来就直接得到末五位的值了。
* 代码实现如下:



#include <stdio.h>
int main() {
int x;
scanf(“%d”, &x);
printf(“%d\n”, (x & 0b11111) );
return 0;
}


![](https://img-blog.csdnimg.cn/20210701093358159.gif#pic_center)



> 
> 【例题2】如果是想得到末七位、末九位、末十四位、末 K 位,应该如何实现呢?
> 
> 
> 


### 3、消除末尾五位



> 
> 【例题3】给定一个 32 位整数,要求消除它的末五位。
> 
> 
> 


* 还是根据位与的性质,消除末五位的含义,有两层:
* 1)末五位,要全变成零;
* 2)剩下的位不变;
* 那么,根据位运算的性质,我们需要数,它的高27位都为1,低五位都为 0,则这个数就是:
* ( 
 
 
 11111111111111111111111111100000 
 
 
 
 ) 
 
 
 2 
 
 
 
 
 (11111111111111111111111111100000)\_2 
 
 
 (11111111111111111111111111100000)2​
* 但是如果要这么写,代码不疯掉,人也会疯掉,所以一般我们把它转成十六进制,每四个二进制位可以转成一个十六进制数,所以得到十六进制数为`0xffffffe0`。
* 代码实现如下:



#include <stdio.h>
int main() {
int x;
scanf(“%d”, &x);
printf(“%d\n”, (x & 0xffffffe0) );
return 0;
}


![](https://img-blog.csdnimg.cn/2021070110055027.gif#pic_center)


### 4、消除末尾连续1


![](https://img-blog.csdnimg.cn/2021070109371968.gif#pic_center)



> 
> 【例题4】给出一个整数,现在要求将这个整数转换成二进制以后,将末尾连续的1都变成0,输出改变后的数(以十进制输出即可)。
> 
> 
> 


* 我们知道,这个数的二进制表示形式一定是:
* . 
 
 
 . 
 
 
 . 
 
 
 0 
 
 
 
 
 11...11 
 
 
 ⏟ 
 
 
 
 k 
 
 
 
 
 ...0\underbrace{11...11}\_{\rm k} 
 
 
 ...0k








11...11​​
* 如果,我们把这个二进制数加上1,得到的就是:
* . 
 
 
 . 
 
 
 . 
 
 
 1 
 
 
 
 
 00...00 
 
 
 ⏟ 
 
 
 
 k 
 
 
 
 
 ...1\underbrace{00...00}\_{\rm k} 
 
 
 ...1k








00...00​​
* 我们把这两个数进行位与运算,得到:
* . 
 
 
 . 
 
 
 . 
 
 
 0 
 
 
 
 
 00...00 
 
 
 ⏟ 
 
 
 
 k 
 
 
 
 
 ...0\underbrace{00...00}\_{\rm k} 
 
 
 ...0k








00...00​​
* 所以,你学会了吗?


### 5、2的幂判定



> 
> 【例题5】请用一句话,判断一个正数是不是2的幂。
> 
> 
> 


* 如果一个数是 2 的幂,它的二进制表示必然为以下形式:
* 1 
 
 
 
 
 00...00 
 
 
 ⏟ 
 
 
 
 k 
 
 
 
 
 1\underbrace{00...00}\_{\rm k} 
 
 
 1k








00...00​​
* 这个数的十进制值为  
 
 
 
 
 
 2 
 
 
 k 
 
 
 
 
 2^k 
 
 
 2k。
* 那么我们将它减一,即  
 
 
 
 
 
 2 
 
 
 k 
 
 
 
 − 
 
 
 1 
 
 
 
 2^k-1 
 
 
 2k−1 的二进制表示如下(参考二进制减法的借位):
* 0 
 
 
 
 
 11...11 
 
 
 ⏟ 
 
 
 
 k 
 
 
 
 
 0\underbrace{11...11}\_{\rm k} 
 
 
 0k








11...11​​
* 于是 这两个数位与的结果为零,于是我们就知道了如果一个数  
 
 
 
 
 x 
 
 
 
 x 
 
 
 x 是 2 的幂,那么`x & (x-1)`必然为零。而其他情况则不然。
* 所以本题的答案为:



(x & (x-1)) == 0



---


![](https://img-blog.csdnimg.cn/202106131045347.gif#pic_center)



> 
> 通过这一章,我们学会了:  
>   1)用位运算 & 来做奇偶性判定;  
>   2)用位运算 & 获取一个数的末五位,末七位,末K位;  
>   3)用位运算 & 消除某些二进制位;  
>   4)用位运算 & 消除末尾连续 1;
> 
> 
> 


* 希望对你有帮助哦 ~ 祝大家早日成为 C 语言大神!




---


## 课后习题


![](https://img-blog.csdnimg.cn/20210627104419151.gif#pic_center)


* [【第39题】位与 & 的应用 | 一句话消除末尾连续的 1](https://bbs.csdn.net/topics/618317507)




---




**(15)- 位运算 | 的应用**

## 一、位或运算符


* 位或运算符是一个二元的位运算符,也就是有两个操作数,表示为`x | y`。
* 位或运算会对操作数的每一位按照如下表格进行运算,对于每一位只有 0 或 1 两种情况,所以组合出来总共  
 
 
 
 
 
 2 
 
 
 2 
 
 
 
 = 
 
 
 4 
 
 
 
 2^2 = 4 
 
 
 22=4 种情况。




| 左操作数 | 右操作数 | 结果 |
| --- | --- | --- |
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 1 |


* 通过这个表,我们得出一些结论:
* 1)无论是 0 或 1,只要位或上 1,就变成1;
* 2)只有当两个操作数都是0的时候,才变成 0;


![](https://img-blog.csdnimg.cn/2021063011050432.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1doZXJlSXNIZXJvRnJvbQ==,size_16,color_FFFFFF,t_70)



#include <stdio.h>
int main() {
int a = 0b1010; // (1)
int b = 0b0110; // (2)
printf(“%d\n”, (a | b) ); // (3)
return 0;
}


* ( 
 
 
 1 
 
 
 ) 
 
 
 
 (1) 
 
 
 (1) 在C语言中,以`0b`作为前缀,表示这是一个二进制数。那么`a`的实际值就是  
 
 
 
 
 ( 
 
 
 1010 
 
 
 
 ) 
 
 
 2 
 
 
 
 
 (1010)\_2 
 
 
 (1010)2​。
* ( 
 
 
 2 
 
 
 ) 
 
 
 
 (2) 
 
 
 (2) 同样的,`b`的实际值就是 
 
 
 
 
 ( 
 
 
 0110 
 
 
 
 ) 
 
 
 2 
 
 
 
 
 (0110)\_2 
 
 
 (0110)2​;
* ( 
 
 
 3 
 
 
 ) 
 
 
 
 (3) 
 
 
 (3) 那么这里`a | b`就是对 
 
 
 
 
 ( 
 
 
 1010 
 
 
 
 ) 
 
 
 2 
 
 
 
 
 (1010)\_2 
 
 
 (1010)2​ 和  
 
 
 
 
 ( 
 
 
 0110 
 
 
 
 ) 
 
 
 2 
 
 
 
 
 (0110)\_2 
 
 
 (0110)2​ 的每一位做表格中的`|`运算。
* 所以最后输出结果为:



14


* 因为输出的是十进制数,它的二进制表示为:  
 
 
 
 
 ( 
 
 
 1110 
 
 
 
 ) 
 
 
 2 
 
 
 
 
 (1110)\_2 
 
 
 (1110)2​。


## 二、位或运算符的应用


### 1、设置标记位



> 
> 【例题1】给定一个数,判断它二进制低位的第 5 位,如果为 0,则将它置为 1。
> 
> 
> 


![](https://img-blog.csdnimg.cn/20210701092721550.gif#pic_center)


* 这个问题,我们很容易联想到位或。
* 我们分析一下题目意思,如果第 5 位为 1,不用进行任何操作;如果第 5 位为 0,则置为 1。言下之意,无论第五位是什么,我们都直接置为 1即可,代码如下:



#include <stdio.h>
int main() {
int x;
scanf(“%d”, &x);
printf(“%d\n”, x | 0b10000);
return 0;
}


### 2、置空标记位



> 
> 【例题2】给定一个数,判断它二进制低位的第 5 位,如果为 1,则将它置为 0。
> 
> 
> 


* 这个问题,我们在学过 [光天化日学C语言(14)- 位运算 & 的应用](https://bbs.csdn.net/topics/618317507) 以后,很容易得出这样一种做法:



#include <stdio.h>
int main() {
int x;
scanf(“%d”, &x);
printf(“%d\n”, x & 0b11111111111111111111111111101111);
return 0;
}


* 其它位不能变,所以位与上1;第5位要置零,所以位与上0;
* 这样写有个问题,就是这串数字太长了,一点都不美观,而且容易写错,当然我们也可以转换成 十六进制,转换的过程也有可能出错。
* 而我们利用位或,只能将第5位设置成1,怎么把它设置成0呢?  
 ![](https://img-blog.csdnimg.cn/20210702072210343.gif#pic_center)



> 
> 我们可以配合减法来用。分成以下两步:  
>   1)首先,强行将低位的第5位置成1;  
>   2)然后,强行将低位的第5位去掉;
> 
> 
> 


* 第  
 
 
 
 
 ( 
 
 
 1 
 
 
 ) 
 
 
 
 (1) 
 
 
 (1) 步可以采用位或运算,而第  
 
 
 
 
 ( 
 
 
 2 
 
 
 ) 
 
 
 
 (2) 
 
 
 (2) 步,我们可以直接用减法即可。
* 代码实现如下:



#include <stdio.h>
int main() {
int x;
int a = 0b10000;
scanf(“%d”, &x);
printf(“%d\n”, (x | a) - a );
return 0;
}


* 注意:直接减是不行的,因为我们首先要保证那一位为 1,否则贸然减会产生借位,和题意不符。


### 3、低位连续零变一



> 
> 【例题3】给定一个整数  
>  
>  
>  
>  
>  x 
>  
>  
>  
>  x 
>  
>  
>  x,将它低位连续的 0 都变成 1。
> 
> 
> 


* 假设这个整数低位连续有  
 
 
 
 
 k 
 
 
 
 k 
 
 
 k 个零,二进制表示如下:
* . 
 
 
 . 
 
 
 . 
 
 
 1 
 
 
 
 
 00...00 
 
 
 ⏟ 
 
 
 
 k 
 
 
 
 
 ...1\underbrace{00...00}\_{\rm k} 
 
 
 ...1k








00...00​​
* 那么,如果我们对它进行减一操作,得到的二进制数就是:
* . 
 
 
 . 
 
 
 . 
 
 
 0 
 
 
 
 
 11...11 
 
 
 ⏟ 
 
 
 
 k 
 
 
 
 
 ...0\underbrace{11...11}\_{\rm k} 
 
 
 ...0k








11...11​​
* 我们发现,只要对这两个数进行位或,就能得到:
* . 
 
 
 . 
 
 
 . 
 
 
 1 
 
 
 
 
 11...11 
 
 
 ⏟ 
 
 
 
 k 
 
 
 
 
 ...1\underbrace{11...11}\_{\rm k} 
 
 
 ...1k








11...11​​
* 也正是题目所求,所以代码实现如下:



#include <stdio.h>
int main() {
int x;
scanf(“%d”, &x);
printf(“%d\n”, x | (x-1) ); // (1)
return 0;
}


* ( 
 
 
 1 
 
 
 ) 
 
 
 
 (1) 
 
 
 (1) `x | (x-1)`就是题目所求的 “低位连续零变一” 。  
 ![](https://img-blog.csdnimg.cn/2021070110055027.gif#pic_center)


### 4、低位首零变一


![](https://img-blog.csdnimg.cn/20210702074508165.gif#pic_center)



> 
> 【例题4】给定一个整数  
>  
>  
>  
>  
>  x 
>  
>  
>  
>  x 
>  
>  
>  x,将它低位第一个 0 变成 1。
> 
> 
> 


* 记得在评论区留下你的答案哦 ~




---


![](https://img-blog.csdnimg.cn/202106131045347.gif#pic_center)



> 
> 通过这一章,我们学会了:  
>   1)用位运算 | 来做标记位的设置;  
>   2)用位运算 | 来做标记位的清除;  
>   3)用位运算 | 将低位连续的零变成一;
> 
> 
> 


* 希望对你有帮助哦 ~ 祝大家早日成为 C 语言大神!




---


## 课后习题


![](https://img-blog.csdnimg.cn/20210627104419151.gif#pic_center)


* [【第40题】位或 | 的应用 | 低位首零变一](https://bbs.csdn.net/topics/618317507)




---




**(16)- 位运算 ^ 的应用**

## 一、异或运算符


* 异或运算符是一个二元的位运算符,也就是有两个操作数,表示为`x ^ y`。
* 异或运算会对操作数的每一位按照如下表格进行运算,对于每一位只有 0 或 1 两种情况,所以组合出来总共  
 
 
 
 
 
 2 
 
 
 2 
 
 
 
 = 
 
 
 4 
 
 
 
 2^2 = 4 
 
 
 22=4 种情况。




| 左操作数 | 右操作数 | 结果 |
| --- | --- | --- |
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |


* 通过这个表,我们得出一些结论:
* 1)两个相同的十进制数异或的结果一定为零。
* 2)任何一个数和 0 的异或结果一定是它本身。
* 3)异或运算满足结合律和交换律。


![](https://img-blog.csdnimg.cn/20210702215615543.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1doZXJlSXNIZXJvRnJvbQ==,size_16,color_FFFFFF,t_70)



#include <stdio.h>
int main() {
int a = 0b1010; // (1)
int b = 0b0110; // (2)
printf(“%d\n”, (a ^ b) ); // (3)
return 0;
}


* ( 
 
 
 1 
 
 
 ) 
 
 
 
 (1) 
 
 
 (1) 在C语言中,以`0b`作为前缀,表示这是一个二进制数。那么`a`的实际值就是  
 
 
 
 
 ( 
 
 
 1010 
 
 
 
 ) 
 
 
 2 
 
 
 
 
 (1010)\_2 
 
 
 (1010)2​。
* ( 
 
 
 2 
 
 
 ) 
 
 
 
 (2) 
 
 
 (2) 同样的,`b`的实际值就是 
 
 
 
 
 ( 
 
 
 0110 
 
 
 
 ) 
 
 
 2 
 
 
 
 
 (0110)\_2 
 
 
 (0110)2​;
* ( 
 
 
 3 
 
 
 ) 
 
 
 
 (3) 
 
 
 (3) 那么这里`a ^ b`就是对 
 
 
 
 
 ( 
 
 
 1010 
 
 
 
 ) 
 
 
 2 
 
 
 
 
 (1010)\_2 
 
 
 (1010)2​ 和  
 
 
 
 
 ( 
 
 
 0110 
 
 
 
 ) 
 
 
 2 
 
 
 
 
 (0110)\_2 
 
 
 (0110)2​ 的每一位做表格中的`^`运算。
* 所以最后输出结果为:



12


* 因为输出的是十进制数,它的二进制表示为:  
 
 
 
 
 ( 
 
 
 1100 
 
 
 
 ) 
 
 
 2 
 
 
 
 
 (1100)\_2 
 
 
 (1100)2​。


## 二、异或运算符的应用


### 1、标记位取反



> 
> 【例题1】给定一个数,将它的低位数起的第 4 位取反,0 变 1,1 变 0。
> 
> 
> 


![](https://img-blog.csdnimg.cn/20210701092721550.gif#pic_center)


* 这个问题,我们很容易联想到异或。
* 我们分析一下题目意思,如果第 4 位为 1,则让它异或上 `0b1000`就能变成 0;如果第 4 位 为 0,则让它异或上 `0b1000`就能变成 1,也就是无论如何都是异或上 `0b1000`,代码如下:



#include <stdio.h>
int main() {
int x;
scanf(“%d”, &x);
printf(“%d\n”, x ^ 0b1000);
return 0;
}


### 2、变量交换



> 
> 【例题2】给定两个数  
>  
>  
>  
>  
>  a 
>  
>  
>  
>  a 
>  
>  
>  a 和  
>  
>  
>  
>  
>  b 
>  
>  
>  
>  b 
>  
>  
>  b,用异或运算交换它们的值。
> 
> 
> 


* 这个是比较老的面试题了,直接给出代码:



#include <stdio.h>
int main() {
int a, b;
while (scanf(“%d %d”, &a, &b) != EOF) {
a = a ^ b; // (1)
b = a ^ b; // (2)
a = a ^ b; // (3)
printf(“%d %d\n”, a, b);
}
return 0;
}


* 我们直接来看  
 
 
 
 
 ( 
 
 
 1 
 
 
 ) 
 
 
 
 (1) 
 
 
 (1) 和  
 
 
 
 
 ( 
 
 
 2 
 
 
 ) 
 
 
 
 (2) 
 
 
 (2) 这两句话,相当于`b`等于`a ^ b ^ b`,根据异或的几个性质,我们知道,这时候的`b`的值已经变成原先`a`的值了。
* 而再来看第  
 
 
 
 
 ( 
 
 
 3 
 
 
 ) 
 
 
 
 (3) 
 
 
 (3) 句话,相当于`a`等于`a ^ b ^ a`,还是根据异或的几个性质,这时候,`a`的值已经变成了原先`b`的值。
* 从而实现了变量`a`和`b`的交换。


### 3、出现奇数次的数



> 
> 【例题3】输入  
>  
>  
>  
>  
>  n 
>  
>  
>  
>  n 
>  
>  
>  n 个数,其中只有一个数出现了奇数次,其它所有数都出现了偶数次。求这个出现了奇数次的数。
> 
> 
> 


* 根据异或的性质,两个一样的数异或结果为零。也就是所有出现偶数次的数异或都为零,那么把这  
 
 
 
 
 n 
 
 
 
 n 
 
 
 n 个数都异或一下,得到的数就一定是一个出现奇数次的数了。



#include <stdio.h>
int main() {
int n, x, i, ans;
scanf(“%d”, &n);
ans = 0;
for(i = 0; i < n; ++i) {
scanf(“%d”, &x);
ans = (ans ^ x);
}
printf(“%d\n”, ans);
return 0;
}


### 4、丢失的数


![](https://img-blog.csdnimg.cn/20210702074508165.gif#pic_center)



> 
> 【例题4】给定一个  
>  
>  
>  
>  
>  n 
>  
>  
>  − 
>  
>  
>  1 
>  
>  
>  
>  n-1 
>  
>  
>  n−1 个数,分别代表 1 到  
>  
>  
>  
>  
>  n 
>  
>  
>  
>  n 
>  
>  
>  n 的其中  
>  
>  
>  
>  
>  n 
>  
>  
>  − 
>  
>  
>  1 
>  
>  
>  
>  n-1 
>  
>  
>  n−1 个,求丢失的那个数。
> 
> 
> 


* 记得在评论区留下你的答案哦 ~


### 5、简单加密


* 基于 **两个数异或为零**,**任何数和零异或为其本身** 这两个特点,异或还可以用来做简单的加密。
* 将明文异或上一个固定的数变成密文以后,可以通过继续异或上这个数,再将密文转变成明文。




---


![](https://img-blog.csdnimg.cn/202106131045347.gif#pic_center)



> 
> 通过这一章,我们学会了:  
>   1)用位运算 ^ 来做标记位的取反;  
>   2)用位运算 ^ 来做变量交换;  
>   3)用位运算 ^ 找出出现奇数次的数;  
>   4)用位运算 ^ 的加密解密;
> 
> 
> 


* 希望对你有帮助哦 ~ 祝大家早日成为 C 语言大神!




---


## 课后习题


![](https://img-blog.csdnimg.cn/20210627104419151.gif#pic_center)


* [【第41题】异或 ^ 的应用 | 丢失的那个数](https://bbs.csdn.net/topics/618317507)




---




**(17)- 位运算 ~ 的应用**

## 一、取反运算符


* 取反运算符是一个单目位运算符,也就是只有一个操作数,表示为`~x`。
* 取反运算会对操作数的每一位按照如下表格进行运算,对于每一位只有 0 或 1 两种情况。




| 操作数 | 取反结果 |
| --- | --- |
| 0 | 1 |
| 1 | 0 |



#include <stdio.h>
int main() {
int a = 0b1;
printf(“%d\n”, ~a );
return 0;
}


* 这里`~a`代表的是对二进制数 1 进行取反,直观感受应该是 0。
* 但是实际输出的却是:



-2


* 这是为什么呢?
* 那是因为,这是一个 32 位整数,实际的取反操作是这样的:



~ 00000000 00000000 00000000 00000001

11111111 11111111 11111111 11111110


* 32位整数的二进制表示,前导零也要参与取反。
* 而对于一个有符号的 32 位整数,我们需要用最高位来代表符号位,即最高位为 0,则代表正数;最高位为 1,则代表负数;
* 这时候我们就需要引入补码的概念了。


### 1、补码


* 在计算机中,二进制编码是采用补码的形式表示的,补码定义如下:



> 
> 正数的补码是它本身,符号位为 0;负数的补码为正数数值二进制位取反后加一,符号位为一;
> 
> 
> 


### 2、补码举例


* 根据补码的定义,`-2`的补码计算,需要经过两步:
* 1)对 2 的二进制进行按位取反,如下:



~ 00000000 00000000 00000000 00000010

11111111 11111111 11111111 11111101


* 2)然后加上 1,如下:



11111111 11111111 11111111 11111101

  • 00000000 00000000 00000000 00000001

11111111 11111111 11111111 11111110


* 结果正好为我们开始提到的`~1`的结果。


### 3、补码的真实含义


* 补码的真实含义,其实体现在 “补” 这个字上,在数学上,两个互为相反数的数字相加等于 0,而在计算机中,两个互为相反数的数字相加等于  
 
 
 
 
 
 2 
 
 
 n 
 
 
 
 
 2^n 
 
 
 2n。
* 换言之,互为相反数的两个数互补,补成  
 
 
 
 
 
 2 
 
 
 n 
 
 
 
 
 2^n 
 
 
 2n。
* 对于 32位整型, 
 
 
 
 
 n 
 
 
 = 
 
 
 32 
 
 
 
 n = 32 
 
 
 n=32;对于 64位整型, 
 
 
 
 
 n 
 
 
 = 
 
 
 64 
 
 
 
 n = 64 
 
 
 n=64。所以补码也可以表示成如下形式:
* [ 
 
 
 x 
 
 
 
 ] 
 
 
 补 
 
 
 
 = 
 
 
 
 { 
 
 
 
 
 
 
 x 
 
 
 
 
 
 
 
 ( 
 
 
 0 
 
 
 ≤ 
 
 
 x 
 
 
 < 
 
 
 
 2 
 
 
 
 n 
 
 
 − 
 
 
 1 
 
 
 
 
 ) 
 
 
 
 
 
 
 
 
 
 
 
 2 
 
 
 n 
 
 
 
 + 
 
 
 x 
 
 
 
 
 
 
 
 
 ( 
 
 
 − 
 
 
 
 2 
 
 
 
 n 
 
 
 − 
 
 
 1 
 
 
 
 
 ≤ 
 
 
 x 
 
 
 < 
 
 
 0 
 
 
 ) 
 
 
 
 
 
 
 
 
 
 [x]\_补 = \begin{cases}x & (0 \le x \lt 2^{n-1})\\ 2^{n} + x & (-2^{n-1} \le x \lt 0)\\ \end{cases} 
 
 
 [x]补​={x2n+x​(0≤x<2n−1)(−2n−1≤x<0)​
* 于是,对于`int`类型,就有:
* x 
 
 
 + 
 
 
 ( 
 
 
 − 
 
 
 x 
 
 
 ) 
 
 
 = 
 
 
 
 2 
 
 
 32 
 
 
 
 
 x + (-x) = 2^{32} 
 
 
 x+(−x)=232
* 因此, 
 
 
 
 
 − 
 
 
 2 
 
 
 = 
 
 
 
 2 
 
 
 32 
 
 
 
 − 
 
 
 2 
 
 
 
 -2 = 2^{32} - 2 
 
 
 −2=232−2。
* 于是,我们开始数数……



2^32 = 1 00000000 00000000 00000000 00000000
2^32 - 1 = 11111111 11111111 11111111 11111111
2^32 - 2 = 11111111 11111111 11111111 11111110


* 近一步了解了`-2`的二进制表示。
* 关于补码的深入内容,详细可以参考这篇文章:[《C/C++ 面试 100 例》(九)补码全网最全总结](https://bbs.csdn.net/topics/618317507)。


## 二、取反运算符的应用


### 1、0 的取反



> 
> 【例题1】0 的取反结果为多少呢?
> 
> 
> 


![](https://img-blog.csdnimg.cn/20210701092721550.gif#pic_center)


* 首先对源码进行取反,得到:



~ 00000000 00000000 00000000 00000000

11111111 11111111 11111111 11111111


* 这个问题,我们刚讨论完,这个答案为  
 
 
 
 
 
 2 
 
 
 32 
 
 
 
 − 
 
 
 1 
 
 
 
 2^{32}-1 
 
 
 232−1。但是实际输出时,你会发现,它的值是`-1`。
* 这是为什么?
* 搞得我一头雾水。
* 原因是因为在C语言中有两种类型的`int`,分别为`unsigned int`和`signed int`,我们之前讨论的`int`都是`signed int`的简称。


#### 1)有符号整型


* 对于有符号整型`signed int`而言,最高位表示符号位,所以只有31位能表示数值,能够表示的数值范围是: 
 
 
 
 
 − 
 
 
 
 2 
 
 
 31 
 
 
 
 ≤ 
 
 
 x 
 
 
 ≤ 
 
 
 
 2 
 
 
 31 
 
 
 
 − 
 
 
 1 
 
 
 
 -2^{31} \le x \le 2^{31}-1 
 
 
 −231≤x≤231−1
* 所以,对于有符号整型,输出采用`%d`,如下:



#include <stdio.h>
int main() {
printf(“%d\n”, ~0 );
return 0;
}


* 结果为:



-1


#### 2)无符号整型


* 对于无符号整型`unsigned int`而言,由于不需要符号位,所以总共有32位表示数值,数值范围为:
* 0 
 
 
 ≤ 
 
 
 x 
 
 
 ≤ 
 
 
 
 2 
 
 
 32 
 
 
 
 − 
 
 
 1 
 
 
 
 0 \le x \le 2^{32}-1 
 
 
 0≤x≤232−1
* 对于无符号整型,输出采用`%u`,如下:



#include <stdio.h>
int main() {
printf(“%u\n”, ~0 );
return 0;
}


* 结果为:



4294967295


* 即  
 
 
 
 
 
 2 
 
 
 32 
 
 
 
 − 
 
 
 1 
 
 
 
 2^{32}-1 
 
 
 232−1。


### 2、相反数



> 
> 【例题2】给定一个`int`类型的正数  
>  
>  
>  
>  
>  x 
>  
>  
>  
>  x 
>  
>  
>  x,求  
>  
>  
>  
>  
>  x 
>  
>  
>  
>  x 
>  
>  
>  x 的相反数(注意:不能用负号)。
> 
> 
> 


* 这里,我们可以直接利用补码的定义,对于正数  
 
 
 
 
 x 
 
 
 
 x 
 
 
 x,它的相反数的补码就是  
 
 
 
 
 x 
 
 
 
 x 
 
 
 x 二进制取反加一。即:`~x + 1`。



#include <stdio.h>
int main() {
int x = 18;
printf(“%d\n”, ~x + 1 );
return 0;
}


* 运行结果如下:



-18


### 3、代替减法



> 
> 【例题3】给定两个`int`类型的正数  
>  
>  
>  
>  
>  x 
>  
>  
>  
>  x 
>  
>  
>  x 和  
>  
>  
>  
>  
>  y 
>  
>  
>  
>  y 
>  
>  
>  y,实现  
>  
>  
>  
>  
>  x 
>  
>  
>  − 
>  
>  
>  y 
>  
>  
>  
>  x - y 
>  
>  
>  x−y(注意:不能用减号)。
> 
> 
> 


* 这个问题比较简单,如果上面的相反数已经理解了,那么,`x - y`其实就可以表示成`x + (-y)`,而`-y`又可以表示成`~y + 1`,所以减法 `x - y`就可以用`x + ~y + 1`来代替。
* 代码实现如下:



#include <stdio.h>
int main() {
int a = 8;
int b = 17;
printf(“%d\n”, a + ~b + 1 );
return 0;
}


* 运行结果为:



-9


### 4、代替加法



> 
> 【例题4】给定两个`int`类型的正数  
>  
>  
>  
>  
>  x 
>  
>  
>  
>  x 
>  
>  
>  x 和  
>  
>  
>  
>  
>  y 
>  
>  
>  
>  y 
>  
>  
>  y,实现  
>  
>  
>  
>  
>  x 
>  
>  
>  + 
>  
>  
>  y 
>  
>  
>  
>  x + y 
>  
>  
>  x+y(注意:不能用加号)。
> 
> 
> 


* 我们可以把`x + y`变成`x - (-y)`,而`-y`又可以替换成 `~y + 1`;
* 所以`x + y`就变成了`x - ~y - 1`,不用加号实现了加法运算。



#include <stdio.h>
int main() {
int x = 18;
int y = 7;
printf(“%d\n”, x - ~y - 1 );
return 0;
}


* 运行结果为:



25




---


![](https://img-blog.csdnimg.cn/202106131045347.gif#pic_center)



> 
> 通过这一章,我们学会了:  
>   1)按位取反运算符;  
>   2)补码的运算;  
>   3)有符号整型和无符号整型;  
>   4)相反数、加法、减法、等于判定的另类解法;
> 
> 
> 


* 希望对你有帮助哦 ~ 祝大家早日成为 C 语言大神!




---


## 课后习题


![](https://img-blog.csdnimg.cn/20210627104419151.gif#pic_center)


* [【第42题】按位取反~的应用 | 相反数](https://bbs.csdn.net/topics/618317507)




---




**(18)- 位运算 << 的应用**

## 一、左移运算符


### 1、左移的二进制形态


* 左移运算符是一个二元的位运算符,也就是有两个操作数,表示为`x << y`。其中`x`和`y`均为整数。
* `x << y`念作:“将  
 
 
 
 
 x 
 
 
 
 x 
 
 
 x 左移  
 
 
 
 
 y 
 
 
 
 y 
 
 
 y 位”,这里的位当然就是二进制位了,那么它表示的意思也就是:先将  
 
 
 
 
 x 
 
 
 
 x 
 
 
 x 用二进制表示,然后再左移  
 
 
 
 
 y 
 
 
 
 y 
 
 
 y 位,并且在尾部添上  
 
 
 
 
 y 
 
 
 
 y 
 
 
 y 个零。
* 举个例子:对于二进制数  
 
 
 
 
 2 
 
 
 
 3 
 
 
 10 
 
 
 
 = 
 
 
 ( 
 
 
 10111 
 
 
 
 ) 
 
 
 2 
 
 
 
 
 23\_{10} = (10111)\_2 
 
 
 2310​=(10111)2​ 左移  
 
 
 
 
 y 
 
 
 
 y 
 
 
 y 位的结果就是:  
  
 
 
 
 
 ( 
 
 
 10111 
 
 
 
 
 0...0 
 
 
 ⏟ 
 
 
 
 y 
 
 
 
 
 ) 
 
 
 2 
 
 
 
 
 (10111\underbrace{0...0}\_{\rm y})\_2 
 
 
 (10111y








0...0​​)2​


### 2、左移的执行结果


* `x << y`的执行结果等价于:
* x 
 
 
 × 
 
 
 
 2 
 
 
 y 
 
 
 
 
 x \times 2^y 
 
 
 x×2y
* 如下代码:



#include <stdio.h>
int main() {
int x = 3;
int y = 5;
printf(“%d\n”, x << y);
return 0;
}


* 输出结果为:



96


* 正好符合这个左移运算符的实际含义:
* 96 
 
 
 = 
 
 
 3 
 
 
 × 
 
 
 
 2 
 
 
 5 
 
 
 
 
 96 = 3 \times 2^5 
 
 
 96=3×25



> 
> 最常用的就是当  
>  
>  
>  
>  
>  x 
>  
>  
>  = 
>  
>  
>  1 
>  
>  
>  
>  x = 1 
>  
>  
>  x=1 时,`1 << y`代表的就是  
>  
>  
>  
>  
>  
>  2 
>  
>  
>  y 
>  
>  
>  
>  
>  2^y 
>  
>  
>  2y,即 2 的幂。
> 
> 
> 


### 3、负数左移的执行结果


* 所谓负数左移,就是`x << y`中,当`x`为负数的情况,代码如下:



#include <stdio.h>
int main() {
printf(“%d\n”, -1 << 1);
return 0;
}


* 它的输出如下:



-2


* 我们发现同样是满足  
 
 
 
 
 x 
 
 
 × 
 
 
 
 2 
 
 
 y 
 
 
 
 
 x \times 2^y 
 
 
 x×2y 的,这个可以用补码来解释,`-1`的补码为:
* 11111111 
 
 
   
 
 
 11111111 
 
 
   
 
 
 11111111 
 
 
   
 
 
 11111111 
 
 
 
 11111111 \ 11111111 \ 11111111 \ 11111111 
 
 
 11111111 11111111 11111111 11111111
* 左移一位后,最高位的 1 就没了,低位补上 0,得到:
* 11111111 
 
 
   
 
 
 11111111 
 
 
   
 
 
 11111111 
 
 
   
 
 
 11111110 
 
 
 
 11111111 \ 11111111 \ 11111111 \ 11111110 
 
 
 11111111 11111111 11111111 11111110
* 而这,正好是 `-2`的补码,同样,继续左移 1 位,得到:
* 11111111 
 
 
   
 
 
 11111111 
 
 
   
 
 
 11111111 
 
 
   
 
 
 11111100 
 
 
 
 11111111 \ 11111111 \ 11111111 \ 11111100 
 
 
 11111111 11111111 11111111 11111100
* 这是`-4`的补码,以此类推,所以负整数的左移结果同样也是  
 
 
 
 
 x 
 
 
 × 
 
 
 
 2 
 
 
 y 
 
 
 
 
 x \times 2^y 
 
 
 x×2y。



> 
> 可以理解成 `- (x << y)`和`(-x) << y`是等价的。
> 
> 
> 


### 4、左移负数位是什么情况


* 刚才我们讨论了  
 
 
 
 
 x 
 
 
 < 
 
 
 0 
 
 
 
 x < 0 
 
 
 x<0 的情况,那么接下来,我们试下  
 
 
 
 
 y 
 
 
 < 
 
 
 0 
 
 
 
 y < 0 
 
 
 y<0 的情况会是如何?
* 是否同样满足: 
 
 
 
 
 x 
 
 
 × 
 
 
 
 2 
 
 
 y 
 
 
 
 
 x \times 2^y 
 
 
 x×2y 呢?
* 如果还是满足,那么两个整数的左移就有可能产生小数了。
* 看个例子:



#include <stdio.h>
int main() {
printf(“%d\n”, 32 << -1); // 16
printf(“%d\n”, 32 << -2); // 8
printf(“%d\n”, 32 << -3); // 4
printf(“%d\n”, 32 << -4); // 2
printf(“%d\n”, 32 << -5); // 1
printf(“%d\n”, 32 << -6); // 0
printf(“%d\n”, 32 << -7); // 0
return 0;
}


* 虽然能够正常运行,但是结果好像不是我们期望的,而且会报警告如下:



> 
> [Warning] left shift count is negative [-Wshift-count-negative]
> 
> 
> 


* 实际上,编辑器告诉我们尽量不用左移的时候用负数,但是它的执行结果不能算错误,起码例子里面对了,结果不会出现小数,而是取整了。
* 左移负数位其实效果和右移对应正数数值位一致,右移相关的内容,我们会在 **光天化日学C语言(19)- 位运算 >> 的应用** 中讲到。


### 5、左移时溢出会如何


* 我们知道,`int`类型的数都是 32 位的,最高位代表符号位,那么假设最高位为 1,次高位为 0,左移以后,符号位会变成 0,会产生什么问题呢?
* 举个例子,对于  
 
 
 
 
 − 
 
 
 
 2 
 
 
 31 
 
 
 
 + 
 
 
 1 
 
 
 
 -2^{31}+1 
 
 
 −231+1 的二进制表示为:最高位和最低位为1,其余为零。



#include <stdio.h>
int main() {
int x = 0b10000000000000000000000000000001;
printf(“%d\n”, x); // -2147483647
return 0;
}


* 输出结果为:



-2147483647


* 那么,将它进行左移一位以后,得到的结果是什么呢?



#include <stdio.h>
int main() {
int x = 0b10000000000000000000000000000001;
printf(“%d\n”, x << 1);
return 0;
}


![](https://img-blog.csdnimg.cn/20210706082829292.gif#pic_center)


* 我们盲猜一下,最高位的 1 被移出去,最低位补上 0,结果应该是`0b10`。
* 实际输出的结果,的确是:



2


* 但是如果按照  
 
 
 
 
 x 
 
 
 × 
 
 
 
 2 
 
 
 y 
 
 
 
 
 x \times 2^y 
 
 
 x×2y 答案应该是  
 
 
 
 
 ( 
 
 
 − 
 
 
 
 2 
 
 
 31 
 
 
 
 + 
 
 
 1 
 
 
 ) 
 
 
 × 
 
 
 2 
 
 
 = 
 
 
 − 
 
 
 
 2 
 
 
 32 
 
 
 
 + 
 
 
 2 
 
 
 
 (-2^{31}+1) \times 2 = -2^{32}+2 
 
 
 (−231+1)×2=−232+2
* 这里又回到了补码的问题上,事实上,在计算机中,`int`整型其实是一个环,溢出以后又会回来,而环的长度正好是  
 
 
 
 
 
 2 
 
 
 32 
 
 
 
 
 2^{32} 
 
 
 232,所以 
 
 
 
 
 − 
 
 
 
 2 
 
 
 32 
 
 
 
 + 
 
 
 2 
 
 
 = 
 
 
 2 
 
 
 
 -2^{32}+2 = 2 
 
 
 −232+2=2,这个就有点像同余的概念,这两个数是模  
 
 
 
 
 
 2 
 
 
 32 
 
 
 
 
 2^{32} 
 
 
 232 同余的。更多关于同余的知识,可以参考我的算法系列文章:[夜深人静写算法(三)- 初等数论入门](https://bbs.csdn.net/topics/618317507)(学生党记得找我开试读)。


## 二、左移运算符的应用


### 1、取模转化成位运算


* 对于  
 
 
 
 
 x 
 
 
 
 x 
 
 
 x 模上一个 2 的次幂的数  
 
 
 
 
 y 
 
 
 
 y 
 
 
 y,我们可以转换成位与上  
 
 
 
 
 
 2 
 
 
 y 
 
 
 
 − 
 
 
 1 
 
 
 
 2^y-1 
 
 
 2y−1。
* 即在数学上的:
* x 
 
 
   
 
 
 m 
 
 
 o 
 
 
 d 
 
 
   
 
 
 
 2 
 
 
 y 
 
 
 
 
 x \ mod \ 2^y 
 
 
 x mod 2y
* 在计算机中就可以用一行代码表示:`x & ((1 << y) - 1)`。


### 2、生成标记码



> 
>   我们可以用左移运算符来实现标记码,即`1 << k`作为第  
>  
>  
>  
>  
>  k 
>  
>  
>  
>  k 
>  
>  
>  k 个标记位的标记码,这样就可以通过一句话,实现对标记位置 0、置 1、取反等操作。
> 
> 
> 


#### 1)标记位置1



> 
> 【例题1】对于  
>  
>  
>  
>  
>  x 
>  
>  
>  
>  x 
>  
>  
>  x 这个数,我们希望对它二进制位的第  
>  
>  
>  
>  
>  k 
>  
>  
>  
>  k 
>  
>  
>  k 位(从0开始,从低到高数)置为 1。
> 
> 
> 


* 置 1 操作,让我们联想到了 位或 运算。
* 它的特点是:位或上 1,结果为 1;位或上0,结果不变。
* 所以我们对标记码的要求是:第  
 
 
 
 
 k 
 
 
 
 k 
 
 
 k 位为 1,其它位为 0,正好是`(1 << k)`,那么将 第  
 
 
 
 
 k 
 
 
 
 k 
 
 
 k 位 置为 1 的语句可以写成:`x | (1 << k)`。
* 有关位或运算的更多内容,可以参考:[光天化日学C语言(15)- 位运算 | 的应用](https://bbs.csdn.net/topics/618317507)。


#### 2)标记位置0



> 
> 【例题2】对于  
>  
>  
>  
>  
>  x 
>  
>  
>  
>  x 
>  
>  
>  x 这个数,我们希望对它二进制位的第  
>  
>  
>  
>  
>  k 
>  
>  
>  
>  k 
>  
>  
>  k 位(从0开始,从低到高数)置为 0。
> 
> 
> 


* 置 0 操作,让我们联想到了 位与 运算。
* 它的特点是:位与上 0,结果为 0;位与上 1,结果不变。
* 所以在我们对标记码的要求是:第  
 
 
 
 
 k 
 
 
 
 k 
 
 
 k 位为 0,其它位为 1,我们需要的是`(~(1 << k))`,那么将 第  
 
 
 
 
 k 
 
 
 
 k 
 
 
 k 位 置为 0 的语句可以写成:`x & (~(1 << k))`。
* 有关位与运算的更多内容,可以参考:[光天化日学C语言(14)- 位运算 & 的应用](https://bbs.csdn.net/topics/618317507)。
* 有关 按位取反 运算的更多内容,可以参考:[光天化日学C语言(17)- 位运算 ~ 的应用](https://bbs.csdn.net/topics/618317507)。


#### 3)标记位取反



> 
> 【例题3】对于  
>  
>  
>  
>  
>  x 
>  
>  
>  
>  x 
>  
>  
>  x 这个数,我们希望对它二进制位的第  
>  
>  
>  
>  
>  k 
>  
>  
>  
>  k 
>  
>  
>  k 位(从0开始,从低到高数)取反。
> 
> 
> 


* 取反操作,联想到的是 异或 运算。
* 它的特点是:异或上 1,结果取反;异或上 0,结果不变。
* 所以我们对标记码的要求是:第  
 
 
 
 
 k 
 
 
 
 k 
 
 
 k 位为1,其余位为 0,其值为`(1 << k)`。那么将 第  
 
 
 
 
 k 
 
 
 
 k 
 
 
 k 位 取反的语句可以写成:`x ^ (1 << k)`。
* 有关 异或 运算的更多内容,可以参考:[光天化日学C语言(16)- 位运算 ^ 的应用](https://bbs.csdn.net/topics/618317507)。


### 3、生成掩码


* 同样,我们可以用左移来生成一个掩码,完成对某个数的二进制末  
 
 
 
 
 k 
 
 
 
 k 
 
 
 k 位执行一些操作。
* 对于`(1 << k)`的二进制表示为:1 加上 k 个 0,那么 `(1 << k) - 1`的二进制则代表  
 
 
 
 
 k 
 
 
 
 k 
 
 
 k 个 1。
* 把末尾的  
 
 
 
 
 k 
 
 
 
 k 
 
 
 k 位都变成 1,可以写成:`x | ((1 << k) - 1)`。
* 把末尾的  
 
 
 
 
 k 
 
 
 
 k 
 
 
 k 为都变成 0,可以写成:`x & ~((1 << k) - 1)`。
* 把末尾的  
 
 
 
 
 k 
 
 
 
 k 
 
 
 k 位都取反,可以写成:`x ^ ((1 << k) - 1)`。




---


![](https://img-blog.csdnimg.cn/202106131045347.gif#pic_center)



> 
> 通过这一章,我们学会了:  
>   1)位运算 << 的用法;  
>   2)用 << 来生成标记位;  
>   3)用 << 来生成掩码;
> 
> 
> 


* 希望对你有帮助哦 ~ 祝大家早日成为 C 语言大神!




---


## 课后习题


![](https://img-blog.csdnimg.cn/20210627104419151.gif#pic_center)


* [【第43题】左移的应用 | 一句话判断一个数是否是 2 的幂](https://bbs.csdn.net/topics/618317507)




---




**(19)- 位运算 >> 的应用**

## 一、右移运算符


### 1、右移的二进制形态


* 右移运算符是一个二元的位运算符,也就是有两个操作数,表示为`x >> y`。其中`x`和`y`均为整数。
* `x >> y`念作:“将  
 
 
 
 
 x 
 
 
 
 x 
 
 
 x 右移  
 
 
 
 
 y 
 
 
 
 y 
 
 
 y 位”,这里的位当然就是二进制位了,那么它表示的意思也就是:先将  
 
 
 
 
 x 
 
 
 
 x 
 
 
 x 用二进制表示,对于正数,右移  
 
 
 
 
 y 
 
 
 
 y 
 
 
 y 位;对于负数,右移  
 
 
 
 
 y 
 
 
 
 y 
 
 
 y 位后高位都补上 1。
* 举个例子:对于二进制数  
 
 
 
 
 8 
 
 
 
 7 
 
 
 10 
 
 
 
 = 
 
 
 ( 
 
 
 1010111 
 
 
 
 ) 
 
 
 2 
 
 
 
 
 87\_{10} = (1010111)\_2 
 
 
 8710​=(1010111)2​ 左移  
 
 
 
 
 y 
 
 
 
 y 
 
 
 y 位的结果就是:  
  
 
 
 
 
 ( 
 
 
 1010 
 
 
 
 ) 
 
 
 2 
 
 
 
 
 (1010)\_2 
 
 
 (1010)2​


### 2、右移的执行结果


* `x >> y`的执行结果等价于:
* ⌊ 
 
 
 
 x 
 
 
 
 2 
 
 
 y 
 
 
 
 
 ⌋ 
 
 
 
 \lfloor \frac x {2^y} \rfloor 
 
 
 ⌊2yx​⌋
* 其中  
 
 
 
 
 ⌊ 
 
 
 a 
 
 
 ⌋ 
 
 
 
 \lfloor a\rfloor 
 
 
 ⌊a⌋ 代表对  
 
 
 
 
 a 
 
 
 
 a 
 
 
 a 取下整。
* 如下代码:



#include <stdio.h>
int main() {
int x = 0b1010111;
int y = 3;
printf(“%d\n”, x >> y);
return 0;
}


* 输出结果为:



10


* 正好符合这个右移运算符的实际含义:
* 10 
 
 
 = 
 
 
 ⌊ 
 
 
 
 87 
 
 
 
 2 
 
 
 3 
 
 
 
 
 ⌋ 
 
 
 
 10 = \lfloor \frac {87} {2^3} \rfloor 
 
 
 10=⌊2387​⌋



> 
> 由于除法可能造成不能整除,所以才会有 取下整 这一步运算。
> 
> 
> 


### 3、负数右移的执行结果


* 所谓负数右移,就是`x >> y`中,当`x`为负数的情况,代码如下:



#include <stdio.h>
int main() {
printf(“%d\n”, -1 >> 1);
return 0;
}


* 它的输出如下:



-1


* 我们发现同样是满足  
 
 
 
 
 ⌊ 
 
 
 
 x 
 
 
 
 2 
 
 
 y 
 
 
 
 
 ⌋ 
 
 
 
 \lfloor \frac x {2^y} \rfloor 
 
 
 ⌊2yx​⌋ 的(注意,负数的 取下整 和 正数 是正好相反的),这个可以用补码来解释,`-1`的补码为:
* 11111111 
 
 
   
 
 
 11111111 
 
 
   
 
 
 11111111 
 
 
   
 
 
 11111111 
 
 
 
 11111111 \ 11111111 \ 11111111 \ 11111111 
 
 
 11111111 11111111 11111111 11111111
* 右移一位后,由于是负数,高位补上 1,得到:
* 11111111 
 
 
   
 
 
 11111111 
 
 
   
 
 
 11111111 
 
 
   
 
 
 11111111 
 
 
 
 11111111 \ 11111111 \ 11111111 \ 11111111 
 
 
 11111111 11111111 11111111 11111111
* 而这,正好是 `-1`的补码,同样,继续右移 1 位,得到:



> 
> 可以理解成 `- (x >> y)`和`(-x) >> y`是等价的。
> 
> 
> 


![](https://img-blog.csdnimg.cn/20210707082137532.gif#pic_center)



> 
> 【例题1】要求不运行代码,肉眼看出这段代码输出多少。
> 
> 
> 



#include <stdio.h>
int main() {
int x = (1 << 31) | (1 << 30) | 1;
int y = (1 << 31) | (1 << 30) | (1 << 29);
printf(“%d\n”, (x >> 1) / y);
return 0;
}


### 4、右移负数位是什么情况


* 刚才我们讨论了  
 
 
 
 
 x 
 
 
 < 
 
 
 0 
 
 
 
 x < 0 
 
 
 x<0 的情况,那么接下来,我们试下  
 
 
 
 
 y 
 
 
 < 
 
 
 0 
 
 
 
 y < 0 
 
 
 y<0 的情况会是如何?
* 是否同样满足: 
 
 
 
 
 ⌊ 
 
 
 
 x 
 
 
 
 2 
 
 
 y 
 
 
 
 
 ⌋ 
 
 
 
 \lfloor \frac x {2^y} \rfloor 
 
 
 ⌊2yx​⌋ 呢?
* 如果还是满足,那么两个整数的左移就有可能产生小数了。
* 看个例子:



#include <stdio.h>
int main() {
printf(“%d\n”, 1 >> -1); // 2
printf(“%d\n”, 1 >> -2); // 4
printf(“%d\n”, 1 >> -3); // 8
printf(“%d\n”, 1 >> -4); // 16
printf(“%d\n”, 1 >> -5); // 32
printf(“%d\n”, 1 >> -6); // 64
printf(“%d\n”, 1 >> -7); // 128
return 0;
}


* 虽然能够正常运行,但是结果好像不是我们期望的,而且会报警告如下:



> 
> [Warning] right shift count is negative [-Wshift-count-negative]
> 
> 
> 


* 实际上,编辑器告诉我们尽量不用右移的时候用负数,但是它的执行结果不能算错误,起码例子里面对了。
* 右移负数位其实效果和左移对应正数数值位一致。


## 二、右移运算符的应用


### 1、去掉低 k 位



> 
> 【例题2】给定一个数  
>  
>  
>  
>  
>  x 
>  
>  
>  
>  x 
>  
>  
>  x,去掉它的低  
>  
>  
>  
>  
>  k 
>  
>  
>  
>  k 
>  
>  
>  k 位以后进行输出。
> 
> 
> 


* 这个问题,可以直接通过右移来完成,如下:`x >> k`。


### 2、取低位连续 1



> 
> 【例题3】获取一个数  
>  
>  
>  
>  
>  x 
>  
>  
>  
>  x 
>  
>  
>  x 低位连续的 1 并且输出。
> 
> 
> 


* 对于一个数  
 
 
 
 
 x 
 
 
 
 x 
 
 
 x,假设低位有连续  
 
 
 
 
 k 
 
 
 
 k 
 
 
 k 个 1。如下:
* ( 
 
 
 . 
 
 
 . 
 
 
 . 
 
 
 0 
 
 
 
 
 1...1 
 
 
 ⏟ 
 
 
 
 k 
 
 
 
 
 ) 
 
 
 2 
 
 
 
 
 (...0\underbrace{1...1}\_{\rm k})\_2 
 
 
 (...0k








1...1​​)2​
* 然后我们将它加上 1 以后,得到的就是:
* ( 
 
 
 . 
 
 
 . 
 
 
 . 
 
 
 1 
 
 
 
 
 0...0 
 
 
 ⏟ 
 
 
 
 k 
 
 
 
 
 ) 
 
 
 2 
 
 
 
 
 (...1\underbrace{0...0}\_{\rm k})\_2 
 
 
 (...1k








0...0​​)2​
* 这时候将这两个数异或结果为:
* ( 
 
 
 
 
 1...1 
 
 
 ⏟ 
 
 
 
 
 k 
 
 
 + 
 
 
 1 
 
 
 
 
 
 ) 
 
 
 2 
 
 
 
 
 (\underbrace{1...1}\_{\rm {k+1}})\_2 
 
 
 (k+1








1...1​​)2​
* 这时候,再进行右移一位,就得到了 连续  
 
 
 
 
 k 
 
 
 
 k 
 
 
 k 个 1 的值,也正是我们所求。
* 所以可以用以下语句来求:`(x ^ (x + 1)) >> 1`。


### 3、取第k位的值



> 
> 【例题4】获取一个数  
>  
>  
>  
>  
>  x 
>  
>  
>  
>  x 
>  
>  
>  x 的第  
>  
>  
>  
>  
>  k 
>  
>  
>  ( 
>  
>  
>  0 
>  
>  
>  ≤ 
>  
>  
>  k 
>  
>  
>  ≤ 
>  
>  
>  30 
>  
>  
>  ) 
>  
>  
>  
>  k(0 \le k \le 30) 
>  
>  
>  k(0≤k≤30) 位的值并且输出。
> 
> 
> 


* 对于二进制数来说,第  
 
 
 
 
 k 
 
 
 
 k 
 
 
 k 位的值一定是 0 或者 1。
* 而 对于 1 到  
 
 
 
 
 k 
 
 
 − 
 
 
 1 
 
 
 
 k-1 
 
 
 k−1 位的数字,对于我们来说是没有意义的,我们可以用右移来去掉,再用位与运算符来获取二进制的最后一位是 0 还是 1,如下:`(x >> k) & 1`。




---


![](https://img-blog.csdnimg.cn/202106131045347.gif#pic_center)



> 
> 通过这一章,我们学会了:  
>   1)位运算 >> 的用法;  
>   2)用 >> 来取低位连续 1;  
>   3)用 >> 取第  
>  
>  
>  
>  
>  k 
>  
>  
>  
>  k 
>  
>  
>  k 位的值;
> 
> 
> 


* 希望对你有帮助哦 ~ 祝大家早日成为 C 语言大神!




---


## 课后习题


![](https://img-blog.csdnimg.cn/20210627104419151.gif#pic_center)


* [【第44题】右移的应用 | (更新中)](https://bbs.csdn.net/topics/618317507)




---




**(20)- 赋值运算符**

## 一、赋值运算符概览


### 1、赋值运算符


* 今天我们来讲一下赋值运算符。
* 对于赋值运算符,主要分为两类:简单赋值运算符 和 复合赋值运算符。如下图所示:  
 ![](https://img-blog.csdnimg.cn/2021070808043845.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1doZXJlSXNIZXJvRnJvbQ==,size_16,color_FFFFFF,t_70#pic_center)
* 简单赋值运算符,我们之前在讲 [光天化日学C语言(03)- 变量](https://bbs.csdn.net/topics/618317507) 的时候就已经遇到了,它的表示形式如下: 
 
 
 
 
 
 
 
 
 变 
 
 
 量 
 
 
 
 
 
 
 
 
 
 = 
 
 
 常 
 
 
 量 
 
 
 
 
 
 
 
 
 
 
 变 
 
 
 量 
 
 
 
 
 
 
 
 
 
 = 
 
 
 表 
 
 
 达 
 
 
 式 
 
 
 
 
 
 
 
 \begin{aligned}变量 &= 常量 \\ 变量 &= 表达式\end{aligned} 
 
 
 变量变量​=常量=表达式​
* 即将赋值符号`=`右边的操作数的值赋值给左边的操作数。


### 2、赋值表达式


* 类似这样的表达式,我们称之为 **赋值表达式**。
* 例如:



a = 10189;
a = a + 5;


* 任何表达式都是有值的,赋值表达式也不例外,它的值就是`=`右边的值。
* 试想一下这段代码的输出是多少?



#include <stdio.h>
int main() {
int a = 5;
int b = (a = 5);
printf(“%d\n”, b);
return 0;
}


![](https://img-blog.csdnimg.cn/20210708075859822.gif#pic_center)


* 运行结果为:



5


* 原因就是因为表达式`a = 5`的值为`5`,从而等价于`b = 5`。


### 3、赋值运算的自动类型转换


* 赋值运算符会进行**自动类型转换**,转换类型就是左边操作数的类型。



#include <stdio.h>
int main() {
int a = 0;
a = a + 1.5;
printf(“%d\n”, a);
return 0;
}


* 输出的结果为:



1


* 有关类型转换的内容,可以参考 [光天化日学C语言(12)- 类型转换](https://bbs.csdn.net/topics/618317507)。


### 4、连续赋值


* 我们来看一个例子,如下:



#include <stdio.h>
int main() {
int a, b, c, d = 0;
a = b = c = d = d == 0;
printf(“%d\n”, a);
return 0;
}


* 这段代码的运行结果为:



1


* 为什么呢?
* 它其实等价于:



#include <stdio.h>
int main() {
int a, b, c, d = 0;
a = ( b = (c = ( d = (d == 0) ) ) );
printf(“%d\n”, a);
return 0;
}


* 这里涉及到两个概念:运算符优先级、运算符结合性。
* 具体的内容,我们会在后续内容中详细讲解。现在你只需要知道 赋值运算符`=`的优先级低于关系运算符`==`,所以`d = d == 0`等价于`d = (d == 0)`;而赋值运算符`=`的结合性是从右到左,所以`a = b = c`等价于`a = (b = c)`。


## 二、复合赋值运算符


* 首先来看一个赋值语句,如下:



int love;
love = love + 1314;

* 像这种**表达式左边的变量**重复出现在**表达式的右边**,则可以缩写成:



int love;
love += 1314;

* 而这里的`+=`就是复合赋值运算符,类似的复合赋值运算符还有很多,总共分为两大类:算术赋值运算符、位赋值运算符。


### 1、算术赋值运算符


* 算术运算符我们之前已经了解过了,具体可以参考这篇文章:[光天化日学C语言(09)- 算术运算符](https://bbs.csdn.net/topics/618317507)。
* 而算术赋值运算符就是先进行算术运算,再进行赋值。算术赋值运算符的表格如下:




| 运算符 | 简称 | 描述 | 举例 |
| --- | --- | --- | --- |
| `+=` | 加且赋值运算符 | 将 **右边操作数** 加上 **左边操作数** 的结果赋值给 **左边操作数** | `a += b`等价于`a = a + b` |
| `-=` | 减且赋值运算符 | 将 **左边操作数** 减去 **右边操作数** 的结果赋值给 **左边操作数** | `a -= b`等价于`a = a - b` |
| `*=` | 乘且赋值运算符 | 将 **右边操作数** 乘以 **左边操作数** 的结果赋值给 **左边操作数** | `a *= b`等价于`a = a * b` |
| `/=` | 除且赋值运算符 | 将 **左边操作数** 除以 **右边操作数** 的结果赋值给 **左边操作数** | `a /= b`等价于`a = a / b` |
| `%=` | 求模且赋值运算符 | 求 **两个操作数的模**,并将结果赋值给 **左边操作数** | `a %= b`等价于`a = a % b` |


### 2、位赋值运算符


* 位运算符我们之前已经了解过了,具体可以参考这篇文章:[光天化日学C语言(13)- 位运算概览](https://bbs.csdn.net/topics/618317507)。
* 而位赋值运算符就是先进行位运算,再进行赋值。位赋值运算符的表格如下:




| 运算符 | 简称 | 描述 | 举例 |
| --- | --- | --- | --- |
| `&=` | 按位与且赋值运算符 | 将 **左边操作数** 按位与上 **右边操作数** 的结果赋值给 **左边操作数** | `a &= b`等同于`a = a & b` |
| `|=` | 按位或且赋值运算符 | 将 **左边操作数** 按位或上 **右边操作数** 的结果赋值给 **左边操作数** | `a |= b`等同于`a = a | b` |
| `^=` | 按位异或且赋值运算符 | 将 **左边操作数** 按位异或上 **右边操作数** 的结果赋值给 **左边操作数** | `a ^= b`等同于`a = a ^ b` |
| `<<=` | 左移且赋值运算符 | 将 **左边操作数** 左移 **右边操作数** 的位数后的结果赋值给 **左边操作数** | `a <<= b`等同于`a = a << b` |
| `>>=` | 右移且赋值运算符 | 将 **左边操作数** 右移 **右边操作数** 的位数后的结果赋值给 **左边操作数** | `a >>= b`等同于`a = a >> b` |


## 三、复合赋值表达式


* 对于两个表达式  
 
 
 
 
 
 e 
 
 
 1 
 
 
 
 
 e\_1 
 
 
 e1​ 和  
 
 
 
 
 
 e 
 
 
 2 
 
 
 
 
 e\_2 
 
 
 e2​,有复合赋值表达式:
* e 
 
 
 1 
 
 
 
 
   
 
 
 
 o 
 
 
 p 
 
 
 = 
 
 
 
 
   
 
 
 
 e 
 
 
 2 
 
 
 
 
 e\_1 \ \_{op=} \ e\_2 
 
 
 e1​ op=​ e2​
* 等价于:
* e 
 
 
 1 
 
 
 
 = 
 
 
 ( 
 
 
 
 e 
 
 
 1 
 
 
 
 ) 
 
 
 
   
 
 
 
 o 
 
 
 p 
 
 
 
 
   
 
 
 ( 
 
 
 
 e 
 
 
 2 
 
 
 
 ) 
 
 
 
 e\_1 = (e\_1) \ \_{op} \ (e\_2) 
 
 
 e1​=(e1​) op​ (e2​)
* 其中  
 
 
 
 
 o 
 
 
 p 
 
 
 
 op 
 
 
 op 就是上文提到的那 10 个 复合赋值运算符。



> 
> 这样写的好处有三个:  
>   1)前一种形式, 
>  
>  
>  
>  
>  
>  e 
>  
>  
>  1 
>  
>  
>  
>  
>  e\_1 
>  
>  
>  e1​ 只计算一次;第二种形式要计算两次。  
>   2)前一种形式,不需要加上圆括号;第二种形式的圆括号不可少。  
>   3)看起来简洁清晰;
> 
> 
> 


* 举个极端的例子:
* `a.b.c.d.e.f[ 1024 + g.h.i.j.k.l ] = a.b.c.d.e.f[ 1024 + g.h.i.j.k.l ] + 5`

 炸裂的🤣🤣🤣!!!
* 利用复合赋值表达式,我们就可以写成:`a.b.c.d.e.f[ 1024 + g.h.i.j.k.l ] += 5`(当然,这个例子比较极端,实际编码中千万不要写出这样的代码哦)。




---


![](https://img-blog.csdnimg.cn/202106131045347.gif#pic_center)



> 
> 通过这一章,我们学会了:  
>   1)赋值运算符;  
>   2)赋值表达式;
> 
> 
> 


* 希望对你有帮助哦 ~ 祝大家早日成为 C 语言大神!




---


## 课后习题


![](https://img-blog.csdnimg.cn/20210627104419151.gif#pic_center)


* [【第15题】给定一个整数,对它进行逆序输出 | 完美的栈思想](https://bbs.csdn.net/topics/618317507)




---




**(21)- 逗号运算符**

## 一、逗号运算符


* 今天,我们就来看下逗号运算符和逗号表达式吧。
* 在 C语言 中,可以把多个表达式用逗号连接起来,构成一个更大的表达式。其中的逗号称为 **逗号运算符**,所构成的表达式称为 **逗号表达式**。逗号表达式中用逗号分开的表达式分别求值,以最后一个表达式的值作为整个表达式的值。



> 
> 简单来说,逗号表达式遵循两点原则:  
>   1)以逗号分隔的表达式单独计算;  
>   2)逗号表达式的值为最后一个表达式的值;
> 
> 
> 


## 二、逗号运算符的应用


### 1、连续变量定义


* 逗号运算通常用于变量的连续定义,如下:



#include <stdio.h>
int main() {
int a = 1, b = 2, c = 3, d = 1 << 6, e;
printf(“%d\n”, a + b + c + d);
return 0;
}


* 这里的`int a = 1, b = 2, c = 3, d = 1 << 6, e`就是逗号表达式。


### 2、循环语句赋初值


* 逗号运算通常用于`for`结构的括号内的第一个表达式,用于给多个局部变量赋值。
* 一段对 `1`到 `10`的数求立方和的代码,如下:



#include <stdio.h>
int main() {
int i, s;
for(i = 1, s = 0; i <= 10; ++i) {
s += i*i*i;
}
printf(“%d\n”, s);
return 0;
}


* 这里的`i = 1, s = 0`就是逗号表达式。
* 有关于`for`的内容,会在后面的章节来介绍,暂时只需要知道可以使用逗号表达式来对一些变量赋予初值。


### 3、交换变量


* 我们在实现交换变量的时候,往往需要三句话:



int tmp;
tmp = a;
a = b;
b = tmp;


* 有了逗号表达式,我们就可以这么写:



int tmp;
tmp = a, a = b, b = tmp;


![](https://img-blog.csdnimg.cn/20210710135950445.gif#pic_center)


## 三、逗号运算符注意事项


* 需要注意的是,逗号运算符的优先级非常低,甚至比赋值运算符还要低,所以当它和赋值运算符相遇时,是优先计算赋值运算的,如下代码所示:



#include <stdio.h>
int main() {
int x, y, a, b;
a = (1, x = 2, y = 3);
b = 1, x = 9, y = 3;
printf(“%d %d\n”, a, b);
return 0;
}


* 这段代码中`a`和`b`的的赋值,只差了一个括号,但是结果截然不同。
* 输出的结果为:



3 1


* 原因是因为`(1, x = 2, y = 3)`表达式的值为以逗号分隔的最后一个表达式的值,即`3`;而在`b = 1, x = 9, y = 3`中,由于逗号运算符的优先级很低,导致表达式分成了三部分:`b = 1`、`x = 9`、`y = 3`,所以才有  
 
 
 
 
 a 
 
 
 = 
 
 
 3 
 
 
 
 a=3 
 
 
 a=3, 
 
 
 
 
 b 
 
 
 = 
 
 
 1 
 
 
 
 b=1 
 
 
 b=1。




---


![](https://img-blog.csdnimg.cn/202106131045347.gif#pic_center)



> 
> 通过这一章,我们学会了:  
>   1)逗号运算符;  
>   2)逗号表达式;
> 
> 
> 


* 希望对你有帮助哦 ~ 祝大家早日成为 C 语言大神!




---


## 课后习题


![](https://img-blog.csdnimg.cn/20210627104419151.gif#pic_center)


* [【第32题】给定一个字符串,原地对它进行翻转并输出](https://bbs.csdn.net/topics/618317507)




---




**(22)- 运算符优先级和结合性**

## 一、运算符简介


* 运算符用于执行程序代码运算,会针对一个、两个或多个操作数来进行运算。例如:1 + 2,其操作数是 1 和 2,而运算符则是 “+”(加号)。
* C语言把除了 **控制语句** 和 **输入输出** 以外的几乎所有的基本操作都作为运算符处理,可见一斑。


## 二、运算符分类


* 将按功能分类,可以分为:后缀运算符、单目运算符、算术运算符、关系运算符、位运算符、逻辑运算符、条件运算符、赋值运算符、逗号运算符。  
 ![](https://img-blog.csdnimg.cn/20210710133157266.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1doZXJlSXNIZXJvRnJvbQ==,size_16,color_FFFFFF,t_70#pic_center)
* 在之前的章节也有介绍了很多运算符,这里简单做个总结:




| 运算符类型 | 运算符举例 | 参考文章 |
| --- | --- | --- |
| 后缀运算符 | `[]`下标运算 | 会在数组章节讲解,待更新 |
| 单目运算符 | `(type)`强制转换 | [光天化日学C语言(12)- 类型转换](https://bbs.csdn.net/topics/618317507) |
| 算术运算符 | `+`加号 | [光天化日学C语言(09)- 算术运算符](https://bbs.csdn.net/topics/618317507) |
| 移位运算符 | `<<`左移 | [光天化日学C语言(18)- 位运算 << 的应用](https://bbs.csdn.net/topics/618317507) |
| 关系运算符 | `<`小于 | [光天化日学C语言(10)- 关系运算符](https://bbs.csdn.net/topics/618317507) |
| 双目位运算符 | `&`位与 | [光天化日学C语言(14)- 位运算 & 的应用](https://bbs.csdn.net/topics/618317507) |
| 双目逻辑运算符 | `&&` | [光天化日学C语言(11)- 逻辑运算符](https://bbs.csdn.net/topics/618317507) |
| 条件运算符 | `? :` | 会在`if`语句章节讲解,待更新 |
| 赋值运算符 | `<<=`左移后赋值 | [光天化日学C语言(20)- 赋值运算符与赋值表达式](https://bbs.csdn.net/topics/618317507) |
| 逗号运算符 | `,`逗号 | [光天化日学C语言(21)- 逗号运算符](https://bbs.csdn.net/topics/618317507) |


## 三、运算符的优先级和结合性


### 1、运算符优先级表




| 优先级 | 运算符 | 名称 | 形式 | 举例 |
| --- | --- | --- | --- | --- |
| 1 | `[]` | 数组下标 | 数组名[常量表达式] | `a[2]` |
| 1 | `()` | 圆括号 | (表达式) 或 函数名(形参表) | `(a+1)` |
| 1 | `.` | 对象的成员选择 | 对象.成员名 | `a.b` |
| 1 | `->` | 指针的成员选择 | 指针.成员名 | `a->b` |
| 2 | `+` | 正号 | +表达式 | `+5` |
| 2 | `-` | 负号 | -表达式 | `-5` |
| 2 | `(type)` | 强制类型转换 | (数据类型)表达式 | `(int)a` |
| 2 | `++` | 自增运算符 | ++变量名 / 变量名++ | `++i` |
| 2 | `--` | 自增运算符 | –变量名 / 变量名– | `--i` |
| 2 | `!` | 逻辑非 | !表达式 | `!a[0]` |
| 2 | `~` | 按位取反 | ~表达式 | `~a` |
| 2 | `&` | 取地址 | &变量名 | `&a` |
| 2 | `*` | 解引用 | \*指针变量名 | `*a` |
| 2 | `sizeof` | 取长度 | sizeof(表达式) | `sizeof(a)` |
| 3 | `*` | 乘 | 表达式 \* 表达式 | `3 * 5` |
| 3 | `/` | 除 | 表达式 / 表达式 | `3 / 5` |
| 3 | `%` | 模 | 整型表达式 % 整型非零表达式 | `3 % 5` |
| 4 | `+` | 加 | 表达式 + 表达式 | `a + b` |
| 4 | `-` | 减 | 表达式 - 表达式 | `a - b` |
| 5 | `<<` | 左移 | 变量<<表达式 | `1<<5` |
| 5 | `>>` | 右移 | 变量>>表达式 | `x>>1` |
| 6 | `<` | 小于 | 表达式<表达式 | `1 < 2` |
| 6 | `<=` | 小于等于 | 表达式<=表达式 | `1 <= 2` |
| 6 | `>` | 大于 | 表达式>表达式 | `1 > 2` |
| 6 | `>=` | 大于等于 | 表达式>=表达式 | `1 >= 2` |
| 7 | `==` | 等于 | 表达式==表达式 | `1 == 2` |
| 7 | `!=` | 不等于 | 表达式!=表达式 | `1 != 2` |
| 8 | `&` | 等于 | 表达式&表达式 | `1 & 2` |
| 9 | `^` | 等于 | 表达式^表达式 | `1 ^ 2` |
| 10 | `|` | 等于 | 表达式\表达式 | `1 | 2` |
| 11 | `&&` | 逻辑与 | 表达式&&表达式 | `a && b` |
| 12 | `||` | 逻辑与 | 表达式`||`表达式 | `a || b` |
| 13 | `?:` | 条件运算符 | 表达式1? 表达式2: 表达式3 | `a>b?a:b` |
| 14 | `=` | 赋值 | 变量=表达式 | `a = b` |
| 14 | `+=` | 加后赋值 | 变量+=表达式 | `a += b` |
| 14 | `-=` | 减后赋值 | 变量-=表达式 | `a -= b` |
| 14 | `*=` | 乘后赋值 | 变量\*=表达式 | `a *= b` |
| 14 | `/=` | 除后赋值 | 变量/=表达式 | `a /= b` |
| 14 | `%=` | 模后赋值 | 变量%=表达式 | `a %= b` |
| 14 | `>>=` | 右移后赋值 | 变量>>=表达式 | `a >>= b` |
| 14 | `<<=` | 左移后赋值 | 变量<<=表达式 | `a <<= b` |
| 14 | `&=` | 位与后赋值 | 变量&=表达式 | `a &= b` |
| 14 | `^=` | 异或后赋值 | 变量^=表达式 | `a ^= b` |
| 14 | `|=` | 位或后赋值 | 变量`|=`表达式 | `a |= b` |
| 15 | `,` | 逗号运算符 | 表达式1,表达式2,… | `a+b,a-b` |


### 2、结合性



> 
> 结合方向只有 **3** 个是 **从右往左**,其余都是 **从左往右**(比较符合人的直观感受)。  
>   (1)一个是单目运算符;  
>   (2)一个是双目运算符中的 赋值运算符;  
>   (3)一个条件运算符,也就是C语言中唯一的三目运算符。
> 
> 
> 


### 3、优先级



> 
> 后缀运算符和单目运算符优先级一般最高,逗号运算符的优先级最低。快速记忆如下:
> 
> 
> 
>  单目逻辑运算符 > 算术运算符 > 关系运算符 > 双目逻辑运算符 > 赋值运算符 
>  
> 
> 


## 四、运算符的优先级和结合性举例




---



**🧡例题1🧡**


#include <stdio.h>
int main() {
int a = 1, b = 2, c = 3;
a <<= b <<= c;
printf(“%d\n”, a );
return 0;
}



> 
> 【运行结果】65536  
> 【结果答疑】`a <<= b <<= c`的计算方式等价于`a = (a << (b << c))`,结果为`1 << 16`。
> 
> 
> 




---



**🧡例题2🧡**


#include <stdio.h>
int main() {
int a = 1, b = 2;
printf(“%d\n”, a > b ? a + b : a - b );
return 0;
}



> 
> 【运行结果】-1  
> 【结果答疑】条件运算符的优先级较低,低于关系运算符和算术运算符,所以`a > b ? a + b : a - b`等价于`1 > 2 ? 3 : -1`。
> 
> 
> 




---



**🧡例题3🧡**


#include <stdio.h>
int main() {
int a = 1;
–a && --a;
printf(“%d\n”, a);
return 0;
}



> 
> 【运行结果】0  
> 【结果答疑】这个例子是展示逻辑与运算符`&&`从左往右计算过程中,一旦遇到 0 就不再进行运算了,所以`--a`实际上只执行了一次。
> 
> 
> 




---



**🧡例题4🧡**


#include <stdio.h>
int main() {
int x = 0b010000;
printf(“%d\n”, x | x - 1 );
return 0;
}



> 
> 【运行结果】31  
> 【结果答疑】这个例子是是将低位连续的零变成一,但是一般这样的写法会报警告,因为编译程序并不知道你的诉求,到底是想先计算 | 还是先计算 `-`,由于这个问题我们实际要计算的是`x | (x - 1)`,并且减法运算符`-`优先级高于位或运算符 | ,所以括号是可以省略的。
> 
> 
> 




---



**🧡例题5🧡**


#include <stdio.h>
int main() {
int a = 0b1010;
int b = 0b0101;
int c = 0b1001;
printf(“%d\n”, a | b ^ c );
return 0;
}



> 
> 【运行结果】14  
> 【结果答疑】这个例子表明了异或运算符`^`高于位或运算符 | 。
> 
> 
> 




---



**🧡例题6🧡**


#include <stdio.h>
int main() {
int a = 0b1010;
int b = 0b0110;
printf(“%d\n”, a & b == 2);
return 0;
}



> 
> 【运行结果】0  
> 【结果答疑】延续【例题59】继续看,之前`a & b`输出的是`2`,那为什么加上等于`==`判定后,输出结果反而变成`0`了呢?原因是因为`==`的优先级高于位与`&`,所以相当于进行了`a & 0`的操作,结果自然就是0了。
> 
> 
> 




---


![](https://img-blog.csdnimg.cn/202106131045347.gif#pic_center)



> 
> 通过这一章,我们学会了:  
>   1)运算符的优先级;  
>   2)运算符的结合性;
> 
> 
> 


* 希望对你有帮助哦 ~ 祝大家早日成为 C 语言大神!




---


## 课后习题


![](https://img-blog.csdnimg.cn/20210627104419151.gif#pic_center)


* [【第13题】给定三个数 a,b,c,从小到大输出这三个数](https://bbs.csdn.net/topics/618317507)




---



**第三章**


**数据类型的存储方式**



**(23)- 整数的存储**

## 一、整数简介


### 1、符号位 和 数值位


* 我们知道 整数 分为 有符号整型 和 无符号整型。
* 有符号整型,程序需要区分 **符号位** 和 **数值位**。
* 对我们人类来说,很容易分辨;而对计算机而言,就要设计专门的电路,这就增加了硬件的复杂性,从而增加了计算的时间。



> 
>   所以,如果能够将 **符号位** 和 **数值位** 联合起来,让它们共同参与运算,不再加以区分,这样硬件电路就会变得更加简单。
> 
> 
> 


### 2、整型的加减运算


* 其次,**加法** 和 **减法** 的引入,也将问题变得复杂。而由于减去一个数相当于加上这个数的相反数,例如:`1 - 2`等价于 `1 + (-2)`,`1 - (-2)`等价于`1 + 2`。



> 
>   所以,它们可以合并为一种运算,即只保留加法运算。
> 
> 
> 


* 相反数是指 数值位 相同,符号位 不同的两个数,例如,1 和 -1 就是一对相反数。




---


* 所以,我们需要做的就是设计一种简单的、不用区分符号位和数值位的加法电路,就能同时实现加法和减法运算。首先让我们看几个计算机中的概念。  
 ![](https://img-blog.csdnimg.cn/20210712230350612.gif#pic_center)


## 二、机器数和真值


### 1、机器数


* 我们知道计算机是内部由 0 和 1 组成的编码,无论是整数还是浮点数,都会涉及到负数,对于机器来说是不知道正负的,而 “正” 和 “负” 正好是两种对立的状态,所以规定用 “0” 表示 “正”,“1” 表示 “负”,这样符号就被数字化了,并且将它放在有效数字的前面,就成了有符号数;
* 把符号 “数字化” 的数称为 机器数;


### 2、真值


* 而带有 “+” 或者 “-” 的数称为 真值;
* 然而,当符号位和数值部分放在一起后,如何让它一起参与运算呢?那就要涉及到接下来要讲的计算机的各种编码了。


## 三、计算机编码


![](https://img-blog.csdnimg.cn/2021071305411288.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1doZXJlSXNIZXJvRnJvbQ==,size_16,color_FFFFFF,t_70)


### 1、原码


#### 1)定义


* 这里的原码并不是源码(源代码)的意思,而是机器数中最简单的一种表示形式;为了快速理解,这里只介绍 32位整数;



> 
> **【定义】** **符号位** 为 **0** 代表 **正数**,**符号位** 为 **1** 代表 **负数**,**数值位** 为 **真值的绝对值**。
> 
> 
> 


#### 2)举例


* 1)对于十进制数 37,它的 真值 和 原码 关系如下:



真值:+ 00000000 00000000 00000000 00100101
原码: 00000000 00000000 00000000 00100101


* 2)对于十进制数 -37,它的 真值 和 原码 的关系如下:



真值:- 00000000 00000000 00000000 00100101
原码: 10000000 00000000 00000000 00100101


* 我们发现,对于负数的情况,原码 加上 真值(注意,这里真值为负数)后,二进制数正好等于  
 
 
 
 
 1 
 
 
 ( 
 
 
 
 
 0...0 
 
 
 ⏟ 
 
 
 
 31 
 
 
 
 
 ) 
 
 
 2 
 
 
 
 
 1(\underbrace{0...0}\_{31})\_2 
 
 
 1(31








0...0​​)2​, 即  
 
 
 
 
 
 2 
 
 
 31 
 
 
 
 
 2^{31} 
 
 
 231,表示成公式如下: 
 
 
 
 
 [ 
 
 
 x 
 
 
 
 ] 
 
 
 原 
 
 
 
 + 
 
 
 x 
 
 
 = 
 
 
 
 2 
 
 
 31 
 
 
 
 
 [x]\_原 + x = 2^{31} 
 
 
 [x]原​+x=231


#### 3)公式


* 因此,我们可以通过移项,得出原码的十进制计算公式如下:



> 
>  
>  
>  
>  
>  
>  [ 
>  
>  
>  x 
>  
>  
>  
>  ] 
>  
>  
>  原 
>  
>  
>  
>  = 
>  
>  
>  
>  { 
>  
>  
>  
>  
>  
>  
>  x 
>  
>  
>  
>  
>  
>  
>  
>  ( 
>  
>  
>  0 
>  
>  
>  ≤ 
>  
>  
>  x 
>  
>  
>  < 
>  
>  
>  
>  2 
>  
>  
>  
>  n 
>  
>  
>  − 
>  
>  
>  1 
>  
>  
>  
>  
>  ) 
>  
>  
>  
>  
>  
>  
>  
>  
>  
>  
>  
>  2 
>  
>  
>  
>  n 
>  
>  
>  − 
>  
>  
>  1 
>  
>  
>  
>  
>  − 
>  
>  
>  x 
>  
>  
>  
>  
>  
>  
>  
>  
>  ( 
>  
>  
>  − 
>  
>  
>  
>  2 
>  
>  
>  
>  n 
>  
>  
>  − 
>  
>  
>  1 
>  
>  
>  
>  
>  < 
>  
>  
>  x 
>  
>  
>  ≤ 
>  
>  
>  0 
>  
>  
>  ) 
>  
>  
>  
>  
>  
>  
>  
>  
>  
>  [x]\_原 = \begin{cases} x & (0 \le x < 2^{n-1})\\ 2^{n-1} - x & (-2^{n-1} < x \le 0) \end{cases} 
>  
>  
>  [x]原​={x2n−1−x​(0≤x<2n−1)(−2n−1<x≤0)​   这里  
>  
>  
>  
>  
>  x 
>  
>  
>  
>  x 
>  
>  
>  x 代表真值,而  
>  
>  
>  
>  
>  n 
>  
>  
>  
>  n 
>  
>  
>  n 的取值是  
>  
>  
>  
>  
>  8 
>  
>  
>  、 
>  
>  
>  16 
>  
>  
>  、 
>  
>  
>  32 
>  
>  
>  、 
>  
>  
>  64 
>  
>  
>  
>  8、16、32、64 
>  
>  
>  8、16、32、64,我们通常说的整型`int`都是 32位 的,本文就以  
>  
>  
>  
>  
>  n 
>  
>  
>  = 
>  
>  
>  32 
>  
>  
>  
>  n = 32 
>  
>  
>  n=32 的情况进行阐述;
> 
> 
> 


* 原码是最贴近人类的编码方式,并且很容易和真值进行转换,但是让计算机用原码进行加减运算过于繁琐,如果两个数符号位不同,需要先判断绝对值大小,然后用绝对值大的减去绝对值小的,并且符号以绝对值大的数为准,本来是加法却需要用减法来实现。




---


### 2、反码


![](https://img-blog.csdnimg.cn/20210713054304153.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1doZXJlSXNIZXJvRnJvbQ==,size_16,color_FFFFFF,t_70)


#### 1)定义



> 
> **【定义】** **正数** 的 **反码** 就是它的 **原码**;**负数** 的 **反码** 为 **原码** 的每一位的 **0变1**、**1变0**(即位运算中的按位取反);
> 
> 
> 


#### 2)举例


* 1)对于十进制数 37,它的 真值 和 反码 关系如下:



真值:+ 00000000 00000000 00000000 00100101
反码: 00000000 00000000 00000000 00100101


* 2)对于十进制数 -37,它的 真值 和 反码 的关系如下:



真值:- 00000000 00000000 00000000 00100101
反码: 11111111 11111111 11111111 11011010


* 我们发现,对于负数的情况,反码 减去 真值(注意,这里真值为负数)后,负负得正,转换成二进制位相加正好等于  
 
 
 
 
 ( 
 
 
 
 
 1...1 
 
 
 ⏟ 
 
 
 
 32 
 
 
 
 
 ) 
 
 
 2 
 
 
 
 
 (\underbrace{1...1}\_{32})\_2 
 
 
 (32








1...1​​)2​, 即  
 
 
 
 
 
 2 
 
 
 32 
 
 
 
 − 
 
 
 1 
 
 
 
 2^{32}-1 
 
 
 232−1,表示成公式如下: 
 
 
 
 
 [ 
 
 
 x 
 
 
 
 ] 
 
 
 反 
 
 
 
 − 
 
 
 x 
 
 
 = 
 
 
 
 2 
 
 
 32 
 
 
 
 − 
 
 
 1 
 
 
 
 [x]\_反 - x = 2^{32}-1 
 
 
 [x]反​−x=232−1


#### 3)公式


* 因此,通过移项,我们可以得出反码的十进制计算公式如下:



> 
>  
>  
>  
>  
>  
>  [ 
>  
>  
>  x 
>  
>  
>  
>  ] 
>  
>  
>  反 
>  
>  
>  
>  = 
>  
>  
>  
>  { 
>  
>  
>  
>  
>  
>  
>  x 
>  
>  
>  
>  
>  
>  
>  
>  ( 
>  
>  
>  0 
>  
>  
>  ≤ 
>  
>  
>  x 
>  
>  
>  < 
>  
>  
>  
>  2 
>  
>  
>  
>  n 
>  
>  
>  − 
>  
>  
>  1 
>  
>  
>  
>  
>  ) 
>  
>  
>  
>  
>  
>  
>  
>  
>  
>  
>  
>  2 
>  
>  
>  n 
>  
>  
>  
>  − 
>  
>  
>  1 
>  
>  
>  + 
>  
>  
>  x 
>  
>  
>  
>  
>  
>  
>  
>  
>  ( 
>  
>  
>  − 
>  
>  
>  
>  2 
>  
>  
>  
>  n 
>  
>  
>  − 
>  
>  
>  1 
>  
>  
>  
>  
>  < 
>  
>  
>  x 
>  
>  
>  ≤ 
>  
>  
>  0 
>  
>  
>  ) 
>  
>  
>  
>  
>  
>  
>  
>  
>  
>  [x]\_反 = \begin{cases} x & (0 \le x < 2^{n-1})\\ 2^{n}-1 + x & (-2^{n-1} < x \le 0) \end{cases} 
>  
>  
>  [x]反​={x2n−1+x​(0≤x<2n−1)(−2n−1<x≤0)​   这里  
>  
>  
>  
>  
>  x 
>  
>  
>  
>  x 
>  
>  
>  x 代表真值,而  
>  
>  
>  
>  
>  n 
>  
>  
>  
>  n 
>  
>  
>  n 的取值是  
>  
>  
>  
>  
>  8 
>  
>  
>  、 
>  
>  
>  16 
>  
>  
>  、 
>  
>  
>  32 
>  
>  
>  、 
>  
>  
>  64 
>  
>  
>  
>  8、16、32、64 
>  
>  
>  8、16、32、64,我们通常说的整型`int`都是 32位 的,本文就以  
>  
>  
>  
>  
>  n 
>  
>  
>  = 
>  
>  
>  32 
>  
>  
>  
>  n = 32 
>  
>  
>  n=32 的情况进行阐述;
> 
> 
> 


* 反码有个很难受的点,就是  
 
 
 
 
 ( 
 
 
 0 
 
 
 
 
 0...0 
 
 
 ⏟ 
 
 
 
 31 
 
 
 
 
 ) 
 
 
 2 
 
 
 
 
 (0\underbrace{0...0}\_{31})\_2 
 
 
 (031








0...0​​)2​ 和  
 
 
 
 
 ( 
 
 
 1 
 
 
 
 
 0...0 
 
 
 ⏟ 
 
 
 
 31 
 
 
 
 
 ) 
 
 
 2 
 
 
 
 
 (1\underbrace{0...0}\_{31})\_2 
 
 
 (131








0...0​​)2​ 都代表零,就是我们常说的 正零 和 负零。正如公式中看到的,当真值为 0 的时候,有两种情况,这就产生了二义性,而且浪费了一个整数表示形式。




---


### 3、补码


![](https://img-blog.csdnimg.cn/20210713054414215.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1doZXJlSXNIZXJvRnJvbQ==,size_16,color_FFFFFF,t_70)


#### 1)定义



> 
> **【定义】** **正数** 的 **补码** 就是它的 **原码**;**负数** 的 **补码** 为 它的**反码加一**;
> 
> 
> 


#### 2)举例


* 1)对于十进制数 37,它的 真值 和 补码 关系如下:



真值:+ 00000000 00000000 00000000 00100101
补码: 00000000 00000000 00000000 00100101


* 2)对于十进制数 -37,它的 真值 和 反码 的关系如下:



真值:- 00000000 00000000 00000000 00100101
补码: 11111111 11111111 11111111 11011011


* 我们发现,对于负数的情况,反码 减去 真值(注意,这里真值为负数)后,负负得正,转换成二进制位相加正好等于  
 
 
 
 
 1 
 
 
 ( 
 
 
 
 
 0...0 
 
 
 ⏟ 
 
 
 
 32 
 
 
 
 
 ) 
 
 
 2 
 
 
 
 
 1(\underbrace{0...0}\_{32})\_2 
 
 
 1(32








0...0​​)2​, 即  
 
 
 
 
 
 2 
 
 
 32 
 
 
 
 
 2^{32} 
 
 
 232,表示成公式如下: 
 
 
 
 
 [ 
 
 
 x 
 
 
 
 ] 
 
 
 补 
 
 
 
 − 
 
 
 x 
 
 
 = 
 
 
 
 2 
 
 
 32 
 
 
 
 
 [x]\_补 - x = 2^{32} 
 
 
 [x]补​−x=232


#### 3)公式


* 因此,通过移项,我们可以得出补码的十进制计算公式如下:



> 
>  
>  
>  
>  
>  
>  [ 
>  
>  
>  x 
>  
>  
>  
>  ] 
>  
>  
>  补 
>  
>  
>  
>  = 
>  
>  
>  
>  { 
>  
>  
>  
>  
>  
>  
>  x 
>  
>  
>  
>  
>  
>  
>  
>  ( 
>  
>  
>  0 
>  
>  
>  ≤ 
>  
>  
>  x 
>  
>  
>  < 
>  
>  
>  
>  2 
>  
>  
>  
>  n 
>  
>  
>  − 
>  
>  
>  1 
>  
>  
>  
>  
>  ) 
>  
>  
>  
>  
>  
>  
>  
>  
>  
>  
>  
>  2 
>  
>  
>  n 
>  
>  
>  
>  + 
>  
>  
>  x 
>  
>  
>  
>  
>  
>  
>  
>  
>  ( 
>  
>  
>  − 
>  
>  
>  
>  2 
>  
>  
>  
>  n 
>  
>  
>  − 
>  
>  
>  1 
>  
>  
>  
>  
>  ≤ 
>  
>  
>  x 
>  
>  
>  < 
>  
>  
>  0 
>  
>  
>  ) 
>  
>  
>  
>  
>  
>  
>  
>  
>  
>  [x]\_补 = \begin{cases} x & (0 \le x < 2^{n-1})\\ 2^{n} + x & (-2^{n-1} \le x < 0) \end{cases} 
>  
>  
>  [x]补​={x2n+x​(0≤x<2n−1)(−2n−1≤x<0)​   这里  
>  
>  
>  
>  
>  x 
>  
>  
>  
>  x 
>  
>  
>  x 代表真值,而  
>  
>  
>  
>  
>  n 
>  
>  
>  
>  n 
>  
>  
>  n 的取值是  
>  
>  
>  
>  
>  8 
>  
>  
>  、 
>  
>  
>  16 
>  
>  
>  、 
>  
>  
>  32 
>  
>  
>  、 
>  
>  
>  64 
>  
>  
>  
>  8、16、32、64 
>  
>  
>  8、16、32、64,我们通常说的整型`int`都是 32位 的,本文就以  
>  
>  
>  
>  
>  n 
>  
>  
>  = 
>  
>  
>  32 
>  
>  
>  
>  n = 32 
>  
>  
>  n=32 的情况进行阐述;
> 
> 
> 


#### 4、编码总结



> 
> 对于三种编码方式,总结如下:  
>   1)这三种机器数的最高位均为符号位;  
>   2)当真值为正数时,原码、反码、补码的表示形式相同,符号位用 “0” 表示,数值部分真值相同;  
>   3)当真值为负数时,原码、反码、补码的表示形式不同,但是符号位都用 “1” 表示,数值部分:反码是原码的 “按位取反”,补码是反码加一;
> 
> 
> 


**正数**



真值:+ 00000000 00000000 00000000 00100101
原码: 00000000 00000000 00000000 00100101
反码: 00000000 00000000 00000000 00100101
补码: 00000000 00000000 00000000 00100101


**负数**



真值:- 00000000 00000000 00000000 00100101
原码: 10000000 00000000 00000000 00100101
反码: 11111111 11111111 11111111 11011010
补码: 11111111 11111111 11111111 11011011


## 四、为什么要引入补码


![](https://img-blog.csdnimg.cn/20210713054619609.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1doZXJlSXNIZXJvRnJvbQ==,size_16,color_FFFFFF,t_70)


* 最后,我们来讲一下引入补码的真实意图是什么。


#### 1、主要目的


* 计算机的四则运算希望设计的尽量简单。但是引入 **符号位** 的概念,对于计算机来说还要考虑正负数相加,等于引入了减法,所以希望是计算机底层 **只设计一个加法器**,就能把加法和减法都做了。


#### 2、原码运算


* 对于原码的加法,两个正数相加的情况如下:



+1 的原码:00000000 00000000 00000000 00000001
+1 的原码:00000000 00000000 00000000 00000001

+2 的原码:00000000 00000000 00000000 00000010


* 好像没有什么问题?于是人们开始探索减法,但是起初设计的人的初衷是希望不用减法,只用加法运算就能够将加法和减法都包含进来,于是,我们尝试用原码的负数表示来做运算;
* 将 `1 - 2`表示成`1 + (-2)`,然后用原码相加得到:



+1 的原码:00000000 00000000 00000000 00000001
-2 的原码:10000000 00000000 00000000 00000010

-3 的原码:10000000 00000000 00000000 00000011


* 我们发现`1 + (-2) = -3`,计算结果明显是错的,所以为了解决减法问题,引入了反码;


#### 3、反码运算


* 对于正数的加法,两个正数反码相加的情况和原码相加一致,不会有问题。
* 对于正数的减法,转换成一正一负两数相加。
* 将 `1 - 2`表示成`1 + (-2)`,情况如下:



+1 的反码:00000000 00000000 00000000 00000001
-2 的反码:11111111 11111111 11111111 11111101

-1 的反码:11111111 11111111 11111111 11111110


* 没有什么问题?但是某种情况下,反码会有歧义,当两个相同的数相减时,即`1 - 1`表示成`1 + (-1)`,情况 如下:



+1 的反码:00000000 00000000 00000000 00000001
-1 的反码:11111111 11111111 11111111 11111110

-0 的反码:11111111 11111111 11111111 11111111


* 这里出现了一个奇怪的概念,就是 “负零”,反码运算过程中会出现有两个编码表示零这个数值。
* 为了解决正负零的问题引入了补码的概念。


#### 4、补码运算



> 
> 1)两个正数的补码相加。
> 
> 
> 


* 其和等于 它们的原码相加,已经验证过,不会有问题;



> 
> 2)一正一负两个数相加,且 **答案非零** 。
> 
> 
> 



+1 的补码:00000000 00000000 00000000 00000001
-2 的补码:11111111 11111111 11111111 11111110

-1 的补码:11111111 11111111 11111111 11111111


* 结果正确;



> 
> 3)一正一负两个数相加,且 **答案为零**。
> 
> 
> 



+1 的补码 00000000 00000000 00000000 00000001
-1 的补码: 11111111 11111111 11111111 11111111

0 的补码:1 00000000 00000000 00000000 00000000


* 两个互为相反数的数相加后,得到的数的补码为  
 
 
 
 
 
 2 
 
 
 n 
 
 
 
 
 2^n 
 
 
 2n(可以认为是是溢出了),所以那个 1 根本不会被存进计算机中,也就是表现出来的结果就是 零!
* 而且,补码的这个运算,和我们之前提到的定义吻合。
* 综上所述,补码解决了整数加法带来的所有问题。




---


![](https://img-blog.csdnimg.cn/202106131045347.gif#pic_center)



> 
> 通过这一章,我们学会了:  
>   1)原码的表示形式;  
>   2)反码的表示形式;  
>   3)补码的表示形式;
> 
> 
> 


* 希望对你有帮助哦 ~ 祝大家早日成为 C 语言大神!




---


## 课后习题


![](https://img-blog.csdnimg.cn/20210627104419151.gif#pic_center)


* [【第11题】给出四个数,输出四个数的和 | 溢出了怎么办?](https://bbs.csdn.net/topics/618317507)




---




**(24)- 浮点数的存储**

## 一、浮点数简介


### 1、数学中的小数


* 数学中的小数分为整数部分和小数部分,它们由点号`.`分隔,我们将它称为 **十进制表示**。例如  
 
 
 
 
 0.0 
 
 
 
 0.0 
 
 
 0.0、 
 
 
 
 
 1314.520 
 
 
 
 1314.520 
 
 
 1314.520、 
 
 
 
 
 − 
 
 
 1.234 
 
 
 
 -1.234 
 
 
 −1.234、 
 
 
 
 
 0.0001 
 
 
 
 0.0001 
 
 
 0.0001 等都是合法的小数,这是最常见的小数形式。
* 小数也可以采用 **指数表示**,例如  
 
 
 
 
 1.23. 
 
 
 × 
 
 
 1 
 
 
 
 0 
 
 
 2 
 
 
 
 
 1.23.\times 10^2 
 
 
 1.23.×102、 
 
 
 
 
 0.0123 
 
 
 × 
 
 
 1 
 
 
 
 0 
 
 
 5 
 
 
 
 
 0.0123 \times 10^5 
 
 
 0.0123×105、 
 
 
 
 
 1.314 
 
 
 × 
 
 
 1 
 
 
 
 0 
 
 
 
 − 
 
 
 2 
 
 
 
 
 
 1.314 \times 10^{-2} 
 
 
 1.314×10−2 等。


### 2、C语言中的小数


* 在 C语言 中的小数,我们称为浮点数。
* 其中,十进制表示相同,而指数表示,则略有不同。
* 对于数学中的  
 
 
 
 
 a 
 
 
 × 
 
 
 1 
 
 
 
 0 
 
 
 n 
 
 
 
 
 a \times 10^n 
 
 
 a×10n。在C语言中的指数表示如下:



aEn 或者 aen


* 其中  
 
 
 
 
 a 
 
 
 
 a 
 
 
 a 为尾数部分,是一个十进制数; 
 
 
 
 
 n 
 
 
 
 n 
 
 
 n 为指数部分,是一个十进制整数; 
 
 
 
 
 E 
 
 
 
 E 
 
 
 E、 
 
 
 
 
 e 
 
 
 
 e 
 
 
 e 是固定的字符,用于分割 尾数部分 和 指数部分。




| 数学 | C语言 |
| --- | --- |
|  
 
 
 
 
 1.5 
 
 
 
 1.5 
 
 
 1.5 |  
 
 
 
 
 1.5 
 
 
 E 
 
 
 1 
 
 
 
 1.5E1 
 
 
 1.5E1 |
|  
 
 
 
 
 1990 
 
 
 
 1990 
 
 
 1990 |  
 
 
 
 
 1.99 
 
 
 e 
 
 
 3 
 
 
 
 1.99e3 
 
 
 1.99e3 |
|  
 
 
 
 
 − 
 
 
 0.054 
 
 
 
 -0.054 
 
 
 −0.054 |  
 
 
 
 
 − 
 
 
 0.54 
 
 
 e 
 
 
 − 
 
 
 1 
 
 
 
 -0.54e-1 
 
 
 −0.54e−1 |


### 3、浮点数类型


* 常用浮点数有两种类型,分别是`float`和`double`;
* `float`称为单精度浮点型,占 4 个字节;`double`称为双精度浮点型,占 8 个字节。


### 4、浮点数的输出


* 我们可以用`printf`对浮点数进行格式化输出,如下表格所示:




| 控制符 | 浮点类型 | 表示形式 |
| --- | --- | --- |
| `%f` | `float` | 十进制表示 |
| `%e` | `float` | 指数表示,输出结果中的 `e`小写 |
| `%E` | `float` | 指数表示,输出结果中的 `E`大写 |
| `%lf` | `double` | 十进制表示 |
| `%le` | `double` | 指数表示,输出结果中的`e`小写 |
| `%lE` | `double` | 指数表示,输出结果中的`E`大写 |


* 来看一段代码加深理解:



#include <stdio.h>

int main() {
float f = 520.1314f;
double d = 520.1314;

printf("%f\n", f);
printf("%e\n", f);
printf("%E\n", f);

printf("%lf\n", d);
printf("%le\n", d);
printf("%lE\n", d);
return 0;

}


* 这段代码的输出如下:



520.131409
5.201314e+02
5.201314E+02
520.131400
5.201314e+02
5.201314E+02


* 1)`%f`和 `%lf`默认保留六位小数,不足六位以 0 补齐,超过六位按四舍五入截断。
* 2)以指数形式输出浮点数时,输出结果为科学计数法。也就是说,尾数部分的取值为:
* 0 
 
 
 ≤ 
 
 
 尾 
 
 
 数 
 
 
 < 
 
 
 10 
 
 
 
 0 \le 尾数 \lt 10 
 
 
 0≤尾数<10
* 3)以上六个输出,对应的是表格中的六种输出方式,但是我们发现第一种输出方式中,并不是我们期望的结果,这是由于这个数超出了`float`能够表示的范围,从而产生了精度误差,而`double`的范围更大一些,所以就能正确表示,所以平时编码过程中,如果对效率要求较高,对精度要求较低,可以采用`float`;反之,对效率要求一般,但是对精度要求较高,则需要采用`double`。


## 二、浮点数的存储


### 1、科学计数法


* C语言中,浮点数在内存中是以科学计数法进行存储的,科学计数法是一种指数表示,数学中常见的科学计数法是基于十进制的,例如  
 
 
 
 
 5.2 
 
 
 × 
 
 
 1 
 
 
 
 0 
 
 
 11 
 
 
 
 
 5.2 × 10^{11} 
 
 
 5.2×1011;计算机中的科学计数法可以基于其它进制,例如  
 
 
 
 
 1.11 
 
 
 × 
 
 
 
 2 
 
 
 7 
 
 
 
 
 1.11 × 2^7 
 
 
 1.11×27 就是基于二进制的,它等价于  
 
 
 
 
 ( 
 
 
 11100000 
 
 
 
 ) 
 
 
 2 
 
 
 
 
 (11100000)\_2 
 
 
 (11100000)2​。
* 科学计数法的一般形式如下:
* v 
 
 
 a 
 
 
 l 
 
 
 u 
 
 
 e 
 
 
 = 
 
 
 ( 
 
 
 − 
 
 
 1 
 
 
 
 ) 
 
 
 
 s 
 
 
 i 
 
 
 g 
 
 
 n 
 
 
 
 
 × 
 
 
 f 
 
 
 r 
 
 
 a 
 
 
 c 
 
 
 t 
 
 
 i 
 
 
 o 
 
 
 n 
 
 
 × 
 
 
 b 
 
 
 a 
 
 
 s 
 
 
 
 e 
 
 
 
 e 
 
 
 x 
 
 
 p 
 
 
 o 
 
 
 n 
 
 
 e 
 
 
 n 
 
 
 t 
 
 
 
 
 
 value = (-1)^{sign} \times fraction \times base^{exponent} 
 
 
 value=(−1)sign×fraction×baseexponent



> 
>    
>  
>  
>  
>  
>  v 
>  
>  
>  a 
>  
>  
>  l 
>  
>  
>  u 
>  
>  
>  e 
>  
>  
>  
>  value 
>  
>  
>  value:代表要表示的浮点数;  
>    
>  
>  
>  
>  
>  s 
>  
>  
>  i 
>  
>  
>  g 
>  
>  
>  n 
>  
>  
>  
>  sign 
>  
>  
>  sign:代表  
>  
>  
>  
>  
>  v 
>  
>  
>  a 
>  
>  
>  l 
>  
>  
>  u 
>  
>  
>  e 
>  
>  
>  
>  value 
>  
>  
>  value 的正负号,它的取值只能是 0 或 1:取值为 0 是正数,取值为 1 是负数;  
>    
>  
>  
>  
>  
>  b 
>  
>  
>  a 
>  
>  
>  s 
>  
>  
>  e 
>  
>  
>  
>  base 
>  
>  
>  base:代表基数,或者说进制,它的取值大于等于 2;  
>    
>  
>  
>  
>  
>  f 
>  
>  
>  r 
>  
>  
>  a 
>  
>  
>  c 
>  
>  
>  t 
>  
>  
>  i 
>  
>  
>  o 
>  
>  
>  n 
>  
>  
>  
>  fraction 
>  
>  
>  fraction:代表尾数,或者说精度,是  
>  
>  
>  
>  
>  b 
>  
>  
>  a 
>  
>  
>  s 
>  
>  
>  e 
>  
>  
>  
>  base 
>  
>  
>  base 进制的小数,并且  
>  
>  
>  
>  
>  1 
>  
>  
>  ≤ 
>  
>  
>  f 
>  
>  
>  r 
>  
>  
>  a 
>  
>  
>  c 
>  
>  
>  t 
>  
>  
>  i 
>  
>  
>  o 
>  
>  
>  n 
>  
>  
>  < 
>  
>  
>  b 
>  
>  
>  a 
>  
>  
>  s 
>  
>  
>  e 
>  
>  
>  
>  1 \le fraction \lt base 
>  
>  
>  1≤fraction<base,这意味着,小数点前面只能有一位数字;  
>    
>  
>  
>  
>  
>  e 
>  
>  
>  x 
>  
>  
>  p 
>  
>  
>  o 
>  
>  
>  n 
>  
>  
>  e 
>  
>  
>  n 
>  
>  
>  t 
>  
>  
>  
>  exponent 
>  
>  
>  exponent:代表指数,是一个整数,可正可负,并且为了直观一般采用 **十进制** 表示。
> 
> 
> 


#### 1)十进制的科学计数法


* 以  
 
 
 
 
 14.375 
 
 
 
 14.375 
 
 
 14.375 这个小数为例,根据初中学过的知识,想要把它转换成科学计数法,只要移动小数点的位置。如果小数点左移一位,则指数  
 
 
 
 
 e 
 
 
 x 
 
 
 p 
 
 
 o 
 
 
 n 
 
 
 e 
 
 
 n 
 
 
 t 
 
 
 
 exponent 
 
 
 exponent 加一;如果小数点右移一位,则指数  
 
 
 
 
 e 
 
 
 x 
 
 
 p 
 
 
 o 
 
 
 n 
 
 
 e 
 
 
 n 
 
 
 t 
 
 
 
 exponent 
 
 
 exponent 减一;
* 所以它在十进制下的科学计数法,根据上述公式,计算结果为:
* ( 
 
 
 14.375 
 
 
 
 ) 
 
 
 10 
 
 
 
 = 
 
 
 1.4375 
 
 
 × 
 
 
 1 
 
 
 
 0 
 
 
 1 
 
 
 
 
 (14.375)\_{10} = 1.4375 \times 10^1 
 
 
 (14.375)10​=1.4375×101
* 其中  
 
 
 
 
 v 
 
 
 a 
 
 
 l 
 
 
 u 
 
 
 e 
 
 
 = 
 
 
 14.375 
 
 
 
 value = 14.375 
 
 
 value=14.375、 
 
 
 
 
 s 
 
 
 i 
 
 
 g 
 
 
 n 
 
 
 = 
 
 
 0 
 
 
 
 sign = 0 
 
 
 sign=0、 
 
 
 
 
 b 
 
 
 a 
 
 
 s 
 
 
 e 
 
 
 = 
 
 
 10 
 
 
 
 base = 10 
 
 
 base=10、 
 
 
 
 
 f 
 
 
 r 
 
 
 a 
 
 
 c 
 
 
 t 
 
 
 i 
 
 
 o 
 
 
 n 
 
 
 = 
 
 
 1.4375 
 
 
 
 fraction = 1.4375 
 
 
 fraction=1.4375、 
 
 
 
 
 e 
 
 
 x 
 
 
 p 
 
 
 o 
 
 
 n 
 
 
 e 
 
 
 n 
 
 
 t 
 
 
 = 
 
 
 1 
 
 
 
 exponent = 1 
 
 
 exponent=1;
* 这是我们数学中最常见的科学计数法。


#### 2)二进制的科学计数法


* 同样以  
 
 
 
 
 14.375 
 
 
 
 14.375 
 
 
 14.375 这个小数为例,我们将它转换成二进制,按照两部分进行转换:整数部分和小数部分。
* **整数部分**:整数部分等于 14,不断除 2 取余数,转换成 2 的幂的求和如下:
* ( 
 
 
 14 
 
 
 
 ) 
 
 
 10 
 
 
 
 = 
 
 
 1 
 
 
 × 
 
 
 
 2 
 
 
 3 
 
 
 
 + 
 
 
 1 
 
 
 × 
 
 
 
 2 
 
 
 2 
 
 
 
 + 
 
 
 1 
 
 
 × 
 
 
 
 2 
 
 
 1 
 
 
 
 + 
 
 
 0 
 
 
 × 
 
 
 
 2 
 
 
 0 
 
 
 
 
 (14)\_{10} = 1 \times 2^3 + 1 \times 2^2 + 1 \times 2^1 + 0 \times 2^0 
 
 
 (14)10​=1×23+1×22+1×21+0×20
* 所以 14 的二进制表示为  
 
 
 
 
 ( 
 
 
 1110 
 
 
 
 ) 
 
 
 2 
 
 
 
 
 (1110)\_2 
 
 
 (1110)2​。
* **小数部分**:小数部分等于 0.375,不断乘 2 取整数部分的值,转换成 2 的幂的求和如下:
* ( 
 
 
 0.375 
 
 
 
 ) 
 
 
 10 
 
 
 
 = 
 
 
 0 
 
 
 × 
 
 
 
 2 
 
 
 
 − 
 
 
 1 
 
 
 
 
 + 
 
 
 1 
 
 
 × 
 
 
 
 2 
 
 
 
 − 
 
 
 2 
 
 
 
 
 + 
 
 
 1 
 
 
 × 
 
 
 
 2 
 
 
 
 − 
 
 
 3 
 
 
 
 
 
 (0.375)\_{10} = 0 \times 2^{-1} + 1 \times 2^{-2} +1 \times 2^{-3} 
 
 
 (0.375)10​=0×2−1+1×2−2+1×2−3
* 所以 0.375 的二进制表示为  
 
 
 
 
 ( 
 
 
 0.011 
 
 
 
 ) 
 
 
 2 
 
 
 
 
 (0.011)\_2 
 
 
 (0.011)2​
* 将 整数部分 和 小数部分 相加,得到的就是它的二进制表示:
* ( 
 
 
 1110.011 
 
 
 
 ) 
 
 
 2 
 
 
 
 
 (1110.011)\_2 
 
 
 (1110.011)2​
* 同样,我们参考十进制科学计数法的表示方式,通过移动小数点的位置,将它表示成二进制的科学计数法,对于这个数,我们需要将它的小数点左移三位。得到:
* ( 
 
 
 1110.011 
 
 
 
 ) 
 
 
 2 
 
 
 
 = 
 
 
 ( 
 
 
 1.110011 
 
 
 
 ) 
 
 
 2 
 
 
 
 × 
 
 
 
 2 
 
 
 3 
 
 
 
 
 (1110.011)\_2 = (1.110011)\_2 \times 2^3 
 
 
 (1110.011)2​=(1.110011)2​×23
* 其中  
 
 
 
 
 v 
 
 
 a 
 
 
 l 
 
 
 u 
 
 
 e 
 
 
 = 
 
 
 14.375 
 
 
 
 value = 14.375 
 
 
 value=14.375、 
 
 
 
 
 s 
 
 
 i 
 
 
 g 
 
 
 n 
 
 
 = 
 
 
 0 
 
 
 
 sign = 0 
 
 
 sign=0、 
 
 
 
 
 b 
 
 
 a 
 
 
 s 
 
 
 e 
 
 
 = 
 
 
 2 
 
 
 
 base = 2 
 
 
 base=2、 
 
 
 
 
 f 
 
 
 r 
 
 
 a 
 
 
 c 
 
 
 t 
 
 
 i 
 
 
 o 
 
 
 n 
 
 
 = 
 
 
 ( 
 
 
 1.110011 
 
 
 
 ) 
 
 
 2 
 
 
 
 
 fraction = (1.110011)\_2 
 
 
 fraction=(1.110011)2​、 
 
 
 
 
 e 
 
 
 x 
 
 
 p 
 
 
 o 
 
 
 n 
 
 
 e 
 
 
 n 
 
 
 t 
 
 
 = 
 
 
 3 
 
 
 
 exponent = 3 
 
 
 exponent=3;
* 我们发现,为了表示成科学计数法,小数点的位置发生了浮动,这就是浮点数的由来。


![](https://img-blog.csdnimg.cn/202107130815492.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1doZXJlSXNIZXJvRnJvbQ==,size_16,color_FFFFFF,t_70)


### 2、浮点数存储概述


* 计算机中的浮点数表示都是采用二进制的。上面的科学计数法公式中,除了  
 
 
 
 
 b 
 
 
 a 
 
 
 s 
 
 
 e 
 
 
 
 base 
 
 
 base 确定是 2 以外,符号位  
 
 
 
 
 s 
 
 
 i 
 
 
 g 
 
 
 n 
 
 
 
 sign 
 
 
 sign、尾数位  
 
 
 
 
 f 
 
 
 r 
 
 
 a 
 
 
 c 
 
 
 t 
 
 
 i 
 
 
 o 
 
 
 n 
 
 
 
 fraction 
 
 
 fraction、指数位  
 
 
 
 
 e 
 
 
 x 
 
 
 p 
 
 
 o 
 
 
 n 
 
 
 e 
 
 
 n 
 
 
 t 
 
 
 
 exponent 
 
 
 exponent 都是未知数,都需要在内存中体现出来。还是以  
 
 
 
 
 14.375 
 
 
 
 14.375 
 
 
 14.375 为例,我们来看下它的几个关键数值的存储。


#### 1)符号的存储


* 符号位的存储类似存储整型一样,单独分配出一个比特位来,用 0 表示正数,1 表示负数。对于  
 
 
 
 
 14.375 
 
 
 
 14.375 
 
 
 14.375,符号位的值是 0。


#### 2)尾数的存储


* 根据科学计数法的定义,尾数部分的取值范围为  
 
 
 
 
 1 
 
 
 ≤ 
 
 
 f 
 
 
 r 
 
 
 a 
 
 
 c 
 
 
 t 
 
 
 i 
 
 
 o 
 
 
 n 
 
 
 < 
 
 
 2 
 
 
 
 1 \le fraction \lt 2 
 
 
 1≤fraction<2
* 这代表尾数的整数部分一定为 1,是一个恒定的值,这样就无需在内存中提现出来,可以将其直接截掉,只要把小数点后面的二进制数字放入内存中即可,这个设计可真是省(扣)啊。
* 对于  
 
 
 
 
 ( 
 
 
 1.110011 
 
 
 
 ) 
 
 
 2 
 
 
 
 
 (1.110011)\_2 
 
 
 (1.110011)2​,就是把`110011`放入内存。我们将内存中存储的尾数命名为  
 
 
 
 
 f 
 
 
 
 f 
 
 
 f,真正的尾数命名为  
 
 
 
 
 f 
 
 
 r 
 
 
 a 
 
 
 c 
 
 
 t 
 
 
 i 
 
 
 o 
 
 
 n 
 
 
 
 fraction 
 
 
 fraction,则么它们之间的关系为:  
 
 
 
 
 f 
 
 
 r 
 
 
 a 
 
 
 c 
 
 
 t 
 
 
 i 
 
 
 o 
 
 
 n 
 
 
 = 
 
 
 1. 
 
 
 f 
 
 
 
 fraction = 1.f 
 
 
 fraction=1.f
* 这时候,我们就可以发现,如果  
 
 
 
 
 b 
 
 
 a 
 
 
 s 
 
 
 e 
 
 
 
 base 
 
 
 base 采用其它进制,那么尾数的整数部分就不是固定的,它有多种取值的可能,以十进制为例,尾数的整数部分可能是  
 
 
 
 
 1 
 
 
 → 
 
 
 9 
 
 
 
 1 \to 9 
 
 
 1→9 之间的任何一个值,如此一来,尾数的整数部分就无法省略,必须在内存中表示出来。但是将  
 
 
 
 
 b 
 
 
 a 
 
 
 s 
 
 
 e 
 
 
 
 base 
 
 
 base 设置为 2,就可以节省掉一个比特位的内存,这也是采用二进制的优势。


#### 3)指数的存储


* 指数是一个整数,并且有正负之分,不但需要存储它的值,还得能区分出正负号来。所以存储时需要考虑到这些。
* 那么它是参照补码的形式来存储的吗?
* 答案是否。
* 指数的存储方式遵循如下步骤:
* 1)由于`float`和`double`分配给指数位的比特位不同,所以需要分情况讨论;
* 2)假设分配给指数的位数为  
 
 
 
 
 n 
 
 
 
 n 
 
 
 n 个比特位,那么它能够表示的指数的个数就是  
 
 
 
 
 
 2 
 
 
 n 
 
 
 
 
 2^n 
 
 
 2n;
* 3)考虑到指数有正负之分,并且我们希望正负指数的个数尽量平均,所以取一半, 
 
 
 
 
 
 2 
 
 
 
 n 
 
 
 − 
 
 
 1 
 
 
 
 
 
 2^{n-1} 
 
 
 2n−1 表示负数, 
 
 
 
 
 
 2 
 
 
 
 n 
 
 
 − 
 
 
 1 
 
 
 
 
 
 2^{n-1} 
 
 
 2n−1 表示正数。
* 4)但是,我们发现还有一个 0,需要表示,所以负数的表示范围将就一点,就少了一个数;
* 5)于是,如果原本的指数位  
 
 
 
 
 x 
 
 
 
 x 
 
 
 x,实际存储到内存的值就是: 
 
 
 
 
 x 
 
 
 + 
 
 
 
 2 
 
 
 
 n 
 
 
 − 
 
 
 1 
 
 
 
 
 − 
 
 
 1 
 
 
 
 x + 2^{n-1} - 1 
 
 
 x+2n−1−1
* 接下来,我们拿具体`float`和`double`的实际位数来举例说明实际内存中的存储方式。


### 3、浮点数存储内存结构


* 浮点数的内存分布主要分成了三部分:符号位、指数位、尾数位。浮点数的类型确定后,每一部分的位数就是固定的。浮点数的类型,是指它是`float`还是`double`。
* 对于`float`类型,内存分布如下:


![](https://img-blog.csdnimg.cn/20210713205208545.png#pic_center)


* 对于`double`类型,内存分布如下:


![](https://img-blog.csdnimg.cn/20210713205236558.png#pic_center)




---


* 1)符号位:只有两种取值:0 或 1,直接放入内存中;
* 2)指数位:将指数本身的值加上  
 
 
 
 
 
 2 
 
 
 
 n 
 
 
 − 
 
 
 1 
 
 
 
 
 − 
 
 
 1 
 
 
 
 2^{n-1}-1 
 
 
 2n−1−1 转换成 二进制,放入内存中;
* 3)尾数位:将小数部分放入内存中;




| 浮点数类型 | 指数位数 | 指数范围 | 尾数位数 | 尾数范围 |
| --- | --- | --- | --- | --- |
| `float` |  
 
 
 
 
 8 
 
 
 
 8 
 
 
 8 |  
 
 
 
 
 [ 
 
 
 − 
 
 
 
 2 
 
 
 7 
 
 
 
 + 
 
 
 1 
 
 
 , 
 
 
 
 2 
 
 
 7 
 
 
 
 ] 
 
 
 
 [-2^7+1,2^7] 
 
 
 [−27+1,27] |  
 
 
 
 
 23 
 
 
 
 23 
 
 
 23 |  
 
 
 
 
 [ 
 
 
 ( 
 
 
 0 
 
 
 
 ) 
 
 
 2 
 
 
 
 , 
 
 
 ( 
 
 
 
 
 1...1 
 
 
 ⏟ 
 
 
 
 23 
 
 
 
 
 ) 
 
 
 2 
 
 
 
 ] 
 
 
 
 [(0)\_2, (\underbrace{1...1}\_{23})\_2] 
 
 
 [(0)2​,(23








1...1​​)2​] |
| `double` |  
 
 
 
 
 11 
 
 
 
 11 
 
 
 11 |  
 
 
 
 
 [ 
 
 
 − 
 
 
 
 2 
 
 
 10 
 
 
 
 + 
 
 
 1 
 
 
 , 
 
 
 
 2 
 
 
 10 
 
 
 
 ] 
 
 
 
 [-2^{10}+1,2^{10}] 
 
 
 [−210+1,210] |  
 
 
 
 
 52 
 
 
 
 52 
 
 
 52 |  
 
 
 
 
 [ 
 
 
 ( 
 
 
 0 
 
 
 
 ) 
 
 
 2 
 
 
 
 , 
 
 
 ( 
 
 
 
 
 1...1 
 
 
 ⏟ 
 
 
 
 52 
 
 
 
 
 ) 
 
 
 2 
 
 
 
 ] 
 
 
 
 [(0)\_2, (\underbrace{1...1}\_{52})\_2] 
 
 
 [(0)2​,(52








1...1​​)2​] |


### 4、内存结构验证举例


* 以上文求得的  
 
 
 
 
 14.375 
 
 
 
 14.375 
 
 
 14.375 为例,我们将它转换成二进制,表示成科学计数法,如下:
* ( 
 
 
 1110.011 
 
 
 
 ) 
 
 
 2 
 
 
 
 = 
 
 
 ( 
 
 
 1.110011 
 
 
 
 ) 
 
 
 2 
 
 
 
 × 
 
 
 
 2 
 
 
 3 
 
 
 
 
 (1110.011)\_2 = (1.110011)\_2 \times 2^3 
 
 
 (1110.011)2​=(1.110011)2​×23
* 其中 值  
 
 
 
 
 v 
 
 
 a 
 
 
 l 
 
 
 u 
 
 
 e 
 
 
 = 
 
 
 14.375 
 
 
 
 value = 14.375 
 
 
 value=14.375、符号位  
 
 
 
 
 s 
 
 
 i 
 
 
 g 
 
 
 n 
 
 
 = 
 
 
 0 
 
 
 
 sign = 0 
 
 
 sign=0、基数  
 
 
 
 
 b 
 
 
 a 
 
 
 s 
 
 
 e 
 
 
 = 
 
 
 2 
 
 
 
 base = 2 
 
 
 base=2、尾数  
 
 
 
 
 f 
 
 
 r 
 
 
 a 
 
 
 c 
 
 
 t 
 
 
 i 
 
 
 o 
 
 
 n 
 
 
 = 
 
 
 ( 
 
 
 1.110011 
 
 
 
 ) 
 
 
 2 
 
 
 
 
 fraction = (1.110011)\_2 
 
 
 fraction=(1.110011)2​、指数  
 
 
 
 
 e 
 
 
 x 
 
 
 p 
 
 
 o 
 
 
 n 
 
 
 e 
 
 
 n 
 
 
 t 
 
 
 = 
 
 
 3 
 
 
 
 exponent = 3 
 
 
 exponent=3;


#### 1)float 的内存验证


* 为了方便阅读,我采用了颜色来表示数字,橙色代表符号位,蓝色代表指数位,红色代表尾数,绿色代表尾数补齐位;并且 八位一分隔,增强可视化。
* 符号位的内存:0
* 指数的内存(加上127后等于130,再转二进制):10000010
* 尾数的内存(不足23位补零):1100110 00000000 00000000
* 按顺序组织到一起后得到:01000001 01100110 00000000 00000000



#include <stdio.h>
int main() {
int value = 0b01000001011001100000000000000000; // (1)
printf(“%f\n”, *(float *)(&value) ); // (2)
return 0;
}



> 
> 运算结果如下:  
>    
>  
>  
>  
>  
>  ( 
>  
>  
>  1 
>  
>  
>  ) 
>  
>  
>  
>  (1) 
>  
>  
>  (1) 第一步,就是把上面那串二进制的 01串 直接拷贝下来,然后在前面加上`0b`前缀,代表了  
>  
>  
>  
>  
>  v 
>  
>  
>  a 
>  
>  
>  l 
>  
>  
>  u 
>  
>  
>  e 
>  
>  
>  
>  value 
>  
>  
>  value 这个四字节的内存结构就是这样的;  
>    
>  
>  
>  
>  
>  ( 
>  
>  
>  2 
>  
>  
>  ) 
>  
>  
>  
>  (2) 
>  
>  
>  (2) 第二步,分三个小步骤:  
>     
>  
>  
>  
>  
>  ( 
>  
>  
>  2. 
>  
>  
>  a 
>  
>  
>  ) 
>  
>  
>  
>  (2.a) 
>  
>  
>  (2.a) `&value`代表取`value`这个值的地址;  
>     
>  
>  
>  
>  
>  ( 
>  
>  
>  2. 
>  
>  
>  b 
>  
>  
>  ) 
>  
>  
>  
>  (2.b) 
>  
>  
>  (2.b) `(float *)&value`代表将这个地址转换成`float`类型;  
>     
>  
>  
>  
>  
>  ( 
>  
>  
>  2. 
>  
>  
>  c 
>  
>  
>  ) 
>  
>  
>  
>  (2.c) 
>  
>  
>  (2.c) `*(float *)&value`代表将这个地址里的值按照`float`类型解析得到一个`float`数;
> 
> 
> 


* 运行结果为:



14.375000


* (有关取地址和指针相关的内容,由于前面章节还没有涉及,如果读者看不懂,也没有关系,后面在讲解指针时会详细讲解这块内容,敬请期待)。


#### 2)double 的内存验证


* 为了方便阅读,我采用了颜色来表示数字,橙色代表符号位,蓝色代表指数位,红色代表尾数,绿色代表尾数补齐位;并且 八位一分隔,增强可视化。
* 符号位的内存:0
* 指数的内存(加上1023后等于1026,再转二进制):100 00000010
* 尾数的内存(不足52位补零):1100 11000000 00000000 00000000 00000000 00000000 00000000
* 按顺序组织到一起后得到:01000000 00101100 11000000 00000000 00000000 00000000 00000000 00000000



#include <stdio.h>
int main() {
long long value = 0b0100000000101100110000000000000000000000000000000000000000000000; // (1)
printf(“%lf\n”, *(double *)(&value) ); // (2)
return 0;
}



> 
> 运算结果如下:  
>    
>  
>  
>  
>  
>  ( 
>  
>  
>  1 
>  
>  
>  ) 
>  
>  
>  
>  (1) 
>  
>  
>  (1) 第一步,就是把上面那串二进制的 01串 直接拷贝下来,然后在前面加上`0b`前缀,代表了  
>  
>  
>  
>  
>  v 
>  
>  
>  a 
>  
>  
>  l 
>  
>  
>  u 
>  
>  
>  e 
>  
>  
>  
>  value 
>  
>  
>  value 这个八字节的内存结构就是这样的;  
>    
>  
>  
>  
>  
>  ( 
>  
>  
>  2 
>  
>  
>  ) 
>  
>  
>  
>  (2) 
>  
>  
>  (2) 第二步,分三个小步骤:  
>     
>  
>  
>  
>  
>  ( 
>  
>  
>  2. 
>  
>  
>  a 
>  
>  
>  ) 
>  
>  
>  
>  (2.a) 
>  
>  
>  (2.a) `&value`代表取`value`这个值的地址;  
>     
>  
>  
>  
>  
>  ( 
>  
>  
>  2. 
>  
>  
>  b 
>  
>  
>  ) 
>  
>  
>  
>  (2.b) 
>  
>  
>  (2.b) `(double *)&value`代表将这个地址转换成`double`类型;  
>     
>  
>  
>  
>  
>  ( 
>  
>  
>  2. 
>  
>  
>  c 
>  
>  
>  ) 
>  
>  
>  
>  (2.c) 
>  
>  
>  (2.c) `*(double *)&value`代表将这个地址里的值按照`double`类型解析得到一个`double`数;
> 
> 
> 


* 没错,运行结果也是:



14.375000


* 这块内容,如果你看的有点懵,没有关系,等我们学了指针的内容以后,再来回顾这块内容,你就会如茅塞一样顿开了!
* 你学废了吗?🤣




---


![](https://img-blog.csdnimg.cn/202106131045347.gif#pic_center)



> 
> 通过这一章,我们学会了:  
>   浮点数的科学计数法和内存存储方式;
> 
> 
> 


* 希望对你有帮助哦 ~ 祝大家早日成为 C 语言大神!




---


## 课后习题


![](https://img-blog.csdnimg.cn/20210627104419151.gif#pic_center)


* [【第06题】给定两个点的坐标 (x1, y1) 和 (x2, y2),求两点间的距离](https://bbs.csdn.net/topics/618317507)




---




**(25)- 浮点数的精度问题**

## 一、精度问题的原因


* 对于十进制的数转换成二进制时,整数部分和小数部分转换方式是不同的。


### 1、整数转二进制


* 对于整数而言,采用的是 “展除法”,即不断的除以 2,取余数。
* 举例, 
 
 
 
 
 ( 
 
 
 11 
 
 
 
 ) 
 
 
 10 
 
 
 
 
 (11)\_{10} 
 
 
 (11)10​ 通过不断除2,取余数,得到的余数序列为  
 
 
 
 
 1 
 
 
   
 
 
 1 
 
 
   
 
 
 0 
 
 
   
 
 
 1 
 
 
 
 1 \ 1 \ 0 \ 1 
 
 
 1 1 0 1,然后逆序一下, 
 
 
 
 
 ( 
 
 
 1011 
 
 
 
 ) 
 
 
 2 
 
 
 
 
 (1011)\_2 
 
 
 (1011)2​ 就是它的二进制表示了。
* 所以对于一个有限位数的整数,一定能够转换成有限位数的二进制。


### 2、小数转二进制


* 而对于小数而言,采用的是 “乘二取整法”,即 不断乘以 2,取整数。一个有限位数的小数不一定能够转换成有限位数的二进制。只有末尾是 5 的小数才有可能转换成有限位数的二进制。
* 在之前的章节中,我们知道`float`和`double`的尾数部分是有限的,可定无法容纳无限的二进制数,即使能够转换成有限的位数,也可能会超出给定的尾数部分的长度,这时候就必须进行舍弃。这时候,由于和原数并不是完全相等,就出现了精度问题。


### 3、四舍五入


* 对与`float`类型,是一个四字节的浮点数,也就是32个比特位,具体内存存储方式如下图所示:


![](https://img-blog.csdnimg.cn/20210713205208545.png#pic_center)


* 而对于`double`类型,是一个八字节的浮点数,也就是64个比特位,具体内存存储方式如下图所示:


![](https://img-blog.csdnimg.cn/20210713205236558.png#pic_center)


* 所以对于`float`的二进制表示,尾数23位,加上一位隐藏的1,总共24位,最后一位可能是精确数字,也可能是近似数字;而其余的 23 位都是精确数字。从二进制的角度看,这种浮点格式的小数,最多有 24 位有效数字,但是能保证的是 23 位;也就是说,整体的精度为 23 ~ 24 位。如果转换成十进制, 
 
 
 
 
 
 2 
 
 
 24 
 
 
 
 = 
 
 
 16777216 
 
 
 
 2^{24} = 16777216 
 
 
 224=16777216,一共 8 位;也就是说,最多有 8 位有效数字(十进制),但是能保证的是 7 位,从而得出整体精度为 7 ~ 8 位。对于 double,同理可得,二进制形式的精度为 52 ~ 53 位,十进制形式的精度为 15 ~ 16 位。




| 浮点数类型 | 尾数个数(二进制) | 十进制位数 |
| --- | --- | --- |
| `float` | 23 ~ 24 | 7 ~ 8 |
| `double` | 52 ~ 53 | 15 ~ 16 |


## 二、IEEE 754 标准


* 浮点数除了 [光天化日学C语言(24)- 浮点数的存储](https://bbs.csdn.net/topics/618317507) 讲到的存储方式以外,还遵循 IEEE 754 标准。
* IEEE 754 标准规定,当指数  
 
 
 
 
 e 
 
 
 x 
 
 
 p 
 
 
 o 
 
 
 n 
 
 
 e 
 
 
 n 
 
 
 t 
 
 
 
 exponent 
 
 
 exponent 的所有位都为 1 时,不再作为 “正常” 的浮点数对待,而是作为特殊值处理。


### 1、负无穷大


* 如果此时尾数  
 
 
 
 
 f 
 
 
 r 
 
 
 a 
 
 
 c 
 
 
 t 
 
 
 i 
 
 
 o 
 
 
 n 
 
 
 
 fraction 
 
 
 fraction 的二进制位都为 0,且符号 sign 为 1,则表示负无穷大;



#include <stdio.h>

int main() {
int ninf = 0b11111111100000000000000000000000;
printf(“%f\n”, *(float *)&ninf );
return 0;
}


* 运行结果为:



-inf


### 2、正无穷大


* 如果此时尾数  
 
 
 
 
 f 
 
 
 r 
 
 
 a 
 
 
 c 
 
 
 t 
 
 
 i 
 
 
 o 
 
 
 n 
 
 
 
 fraction 
 
 
 fraction 的二进制位都为 0,且符号 sign 为 0,则表示正无穷大。



#include <stdio.h>

int main() {
int pinf = 0b01111111100000000000000000000000;
printf(“%f\n”, *(float *)&pinf );
return 0;
}


* 运行结果为:



inf


### 3、Not a Number


* 如果此时尾数  
 
 
 
 
 f 
 
 
 r 
 
 
 a 
 
 
 c 
 
 
 t 
 
 
 i 
 
 
 o 
 
 
 n 
 
 
 
 fraction 
 
 
 fraction 的二进制位不全为 0,则表示 NaN (Not a Number),也即这是一个无效的数字,或者该数字未经初始化。



#include <stdio.h>

int main() {
int nan = 0b11111111100000000000000000001010;
printf(“%f\n”, *(float *)&nan );
return 0;
}


* 运行结果如下,符合我们的预期:



nan


### 4、浮点数的规格化


* 当指数  
 
 
 
 
 e 
 
 
 x 
 
 
 p 
 
 
 o 
 
 
 n 
 
 
 e 
 
 
 n 
 
 
 t 
 
 
 
 exponent 
 
 
 exponent 的所有二进制位都为 0 时,情况也比较特殊。对于 “正常” 的浮点数,尾数  
 
 
 
 
 f 
 
 
 r 
 
 
 a 
 
 
 c 
 
 
 t 
 
 
 i 
 
 
 o 
 
 
 n 
 
 
 
 fraction 
 
 
 fraction 隐含的整数部分为 1,并且在读取浮点数时,内存中的指数  
 
 
 
 
 e 
 
 
 x 
 
 
 p 
 
 
 
 exp 
 
 
 exp 要减去中间值  
 
 
 
 
 
 2 
 
 
 
 n 
 
 
 − 
 
 
 1 
 
 
 
 
 − 
 
 
 1 
 
 
 
 2^{n-1}-1 
 
 
 2n−1−1 才能还原真实的指数  
 
 
 
 
 e 
 
 
 x 
 
 
 p 
 
 
 o 
 
 
 n 
 
 
### 一、Python所有方向的学习路线

Python所有方向路线就是把Python常用的技术点做整理,形成各个领域的知识点汇总,它的用处就在于,你可以按照上面的知识点去找对应的学习资源,保证自己学得较为全面。



![](https://img-blog.csdnimg.cn/img_convert/9f49b566129f47b8a67243c1008edf79.png)



### 二、学习软件

工欲善其事必先利其器。学习Python常用的开发软件都在这里了,给大家节省了很多时间。



![](https://img-blog.csdnimg.cn/img_convert/8c4513c1a906b72cbf93031e6781512b.png)



### 三、入门学习视频



我们在看视频学习的时候,不能光动眼动脑不动手,比较科学的学习方法是在理解之后运用它们,这时候练手项目就很适合了。



![](https://img-blog.csdnimg.cn/afc935d834c5452090670f48eda180e0.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA56iL5bqP5aqb56eD56eD,size_20,color_FFFFFF,t_70,g_se,x_16#pic_center)




**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化学习资料的朋友,可以戳这里无偿获取](https://bbs.csdn.net/topics/618317507)**

**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**
  • 7
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值