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

img
img

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

需要这份系统化资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

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


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

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

课后习题


(15)- 位运算 | 的应用

一、位或运算符

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

2

2

=

4

2^2 = 4

22=4 种情况。

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

#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。

  • 这个问题,我们很容易联想到位或。
  • 我们分析一下题目意思,如果第 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。

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

  • 其它位不能变,所以位与上1;第5位要置零,所以位与上0;
  • 这样写有个问题,就是这串数字太长了,一点都不美观,而且容易写错,当然我们也可以转换成 十六进制,转换的过程也有可能出错。
  • 而我们利用位或,只能将第5位设置成1,怎么把它设置成0呢?

我们可以配合减法来用。分成以下两步:
  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)就是题目所求的 “低位连续零变一” 。

4、低位首零变一

【例题4】给定一个整数

x

x

x,将它低位第一个 0 变成 1。

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

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

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

课后习题


(16)- 位运算 ^ 的应用

一、异或运算符

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

2

2

=

4

2^2 = 4

22=4 种情况。

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

#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。

  • 这个问题,我们很容易联想到异或。
  • 我们分析一下题目意思,如果第 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的值。

  • 从而实现了变量ab的交换。

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、丢失的数

【例题4】给定一个

n

1

n-1

n−1 个数,分别代表 1 到

n

n

n 的其中

n

1

n-1

n−1 个,求丢失的那个数。

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

5、简单加密

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

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

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

课后习题


(17)- 位运算 ~ 的应用

一、取反运算符

  • 取反运算符是一个单目位运算符,也就是只有一个操作数,表示为~x
  • 取反运算会对操作数的每一位按照如下表格进行运算,对于每一位只有 0 或 1 两种情况。
操作数取反结果
01
10
#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
...

二、取反运算符的应用

1、0 的取反

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

  • 首先对源码进行取反,得到:
 ~ 00000000 00000000 00000000 00000000
 --------------------------------------
   11111111 11111111 11111111 11111111

  • 这个问题,我们刚讨论完,这个答案为

2

32

1

2^{32}-1

232−1。但是实际输出时,你会发现,它的值是-1

  • 这是为什么?
  • 搞得我一头雾水。
  • 原因是因为在C语言中有两种类型的int,分别为unsigned intsigned 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


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

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

课后习题


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

一、左移运算符

1、左移的二进制形态

  • 左移运算符是一个二元的位运算符,也就是有两个操作数,表示为x << y。其中xy均为整数。
  • 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;
}

  • 我们盲猜一下,最高位的 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 同余的。更多关于同余的知识,可以参考我的算法系列文章:夜深人静写算法(三)- 初等数论入门(学生党记得找我开试读)。

二、左移运算符的应用

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)

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))

3)标记位取反

【例题3】对于

x

x

x 这个数,我们希望对它二进制位的第

k

k

k 位(从0开始,从低到高数)取反。

  • 取反操作,联想到的是 异或 运算。
  • 它的特点是:异或上 1,结果取反;异或上 0,结果不变。
  • 所以我们对标记码的要求是:第

k

k

k 位为1,其余位为 0,其值为(1 << k)。那么将 第

k

k

k 位 取反的语句可以写成:x ^ (1 << k)

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)


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

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

课后习题


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

一、右移运算符

1、右移的二进制形态

  • 右移运算符是一个二元的位运算符,也就是有两个操作数,表示为x >> y。其中xy均为整数。
  • 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是等价的。

【例题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


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

k

k

k 位的值;

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

课后习题


(20)- 赋值运算符

一、赋值运算符概览

1、赋值运算符

  • 今天我们来讲一下赋值运算符。
  • 对于赋值运算符,主要分为两类:简单赋值运算符 和 复合赋值运算符。如下图所示:
  • 简单赋值运算符,我们之前在讲 光天化日学C语言(03)- 变量 的时候就已经遇到了,它的表示形式如下:

=

=

\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;
} 

  • 运行结果为:
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

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)- 算术运算符
  • 而算术赋值运算符就是先进行算术运算,再进行赋值。算术赋值运算符的表格如下:
运算符简称描述举例
+=加且赋值运算符右边操作数 加上 左边操作数 的结果赋值给 左边操作数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、位赋值运算符

运算符简称描述举例
&=按位与且赋值运算符左边操作数 按位与上 右边操作数 的结果赋值给 左边操作数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(当然,这个例子比较极端,实际编码中千万不要写出这样的代码哦)。

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

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

课后习题


(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结构的括号内的第一个表达式,用于给多个局部变量赋值。
  • 一段对 110的数求立方和的代码,如下:
#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;

三、逗号运算符注意事项

  • 需要注意的是,逗号运算符的优先级非常低,甚至比赋值运算符还要低,所以当它和赋值运算符相遇时,是优先计算赋值运算的,如下代码所示:
#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;
}

  • 这段代码中ab的的赋值,只差了一个括号,但是结果截然不同。
  • 输出的结果为:
3 1

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

a

=

3

a=3

a=3,

b

=

1

b=1

b=1。


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

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

课后习题


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

一、运算符简介

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

二、运算符分类

  • 将按功能分类,可以分为:后缀运算符、单目运算符、算术运算符、关系运算符、位运算符、逻辑运算符、条件运算符、赋值运算符、逗号运算符。
  • 在之前的章节也有介绍了很多运算符,这里简单做个总结:
运算符类型运算符举例参考文章
后缀运算符[]下标运算会在数组章节讲解,待更新
单目运算符(type)强制转换光天化日学C语言(12)- 类型转换
算术运算符+加号光天化日学C语言(09)- 算术运算符
移位运算符<<左移光天化日学C语言(18)- 位运算 << 的应用
关系运算符<小于光天化日学C语言(10)- 关系运算符
双目位运算符&位与光天化日学C语言(14)- 位运算 & 的应用
双目逻辑运算符&&光天化日学C语言(11)- 逻辑运算符
条件运算符? :会在if语句章节讲解,待更新
赋值运算符<<=左移后赋值光天化日学C语言(20)- 赋值运算符与赋值表达式
逗号运算符,逗号光天化日学C语言(21)- 逗号运算符

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

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
2sizeof取长度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``等于表达式\表达式
11&&逻辑与表达式&&表达式a && b
12``逻辑与
13?:条件运算符表达式1? 表达式2: 表达式3a>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`=`位或后赋值变量`
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了。


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

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

课后习题


第三章

数据类型的存储方式

(23)- 整数的存储

一、整数简介

1、符号位 和 数值位

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

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

2、整型的加减运算

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

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

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

  • 所以,我们需要做的就是设计一种简单的、不用区分符号位和数值位的加法电路,就能同时实现加法和减法运算。首先让我们看几个计算机中的概念。

二、机器数和真值

1、机器数

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

2、真值

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

三、计算机编码

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、反码

1)定义

【定义】 正数反码 就是它的 原码负数反码原码 的每一位的 0变11变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、补码

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

四、为什么要引入补码

  • 最后,我们来讲一下引入补码的真实意图是什么。
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 根本不会被存进计算机中,也就是表现出来的结果就是 零!

  • 而且,补码的这个运算,和我们之前提到的定义吻合。
  • 综上所述,补码解决了整数加法带来的所有问题。

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

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

课后习题


(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、浮点数类型

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

4、浮点数的输出

  • 我们可以用printf对浮点数进行格式化输出,如下表格所示:
控制符浮点类型表示形式
%ffloat十进制表示
%efloat指数表示,输出结果中的 e小写
%Efloat指数表示,输出结果中的 E大写
%lfdouble十进制表示
%ledouble指数表示,输出结果中的e小写
%lEdouble指数表示,输出结果中的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;

  • 我们发现,为了表示成科学计数法,小数点的位置发生了浮动,这就是浮点数的由来。

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

=

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)由于floatdouble分配给指数位的比特位不同,所以需要分情况讨论;
  • 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

  • 接下来,我们拿具体floatdouble的实际位数来举例说明实际内存中的存储方式。

3、浮点数存储内存结构

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

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


  • 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

]

[-27+1,27]

[−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) 第二步,分三个小步骤:

(

a

)

(2.a)

(2.a) &value代表取value这个值的地址;

(

b

)

(2.b)

(2.b) (float *)&value代表将这个地址转换成float类型;

(

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) 第二步,分三个小步骤:

(

a

)

(2.a)

(2.a) &value代表取value这个值的地址;

(

b

)

(2.b)

(2.b) (double *)&value代表将这个地址转换成double类型;

(

c

)

(2.c)

(2.c) *(double *)&value代表将这个地址里的值按照double类型解析得到一个double数;

  • 没错,运行结果也是:
14.375000

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

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

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

课后习题


(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 的小数才有可能转换成有限位数的二进制。
  • 在之前的章节中,我们知道floatdouble的尾数部分是有限的,可定无法容纳无限的二进制数,即使能够转换成有限的位数,也可能会超出给定的尾数部分的长度,这时候就必须进行舍弃。这时候,由于和原数并不是完全相等,就出现了精度问题。

3、四舍五入

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

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

  • 所以对于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 位。

浮点数类型尾数个数(二进制)十进制位数
float23 ~ 247 ~ 8
double52 ~ 5315 ~ 16

二、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

e

n

t

exponent

exponent。

  • 然而,当指数

e

x

p

exp

exp 的所有二进制位都为 0 时,尾数隐含的整数部分变成了 0,并且用 1 减去中间值

2

n

1

1

2^{n-1}-1

2n−1−1 才能还原真实的指数

e

x

p

o

n

e

n

t

exponent

exponent。

  • 为什么会有非规格化浮点数的出现呢?
  • 来看这样一个例子!
  • 在规格化浮点数中,浮点数的尾数不应当包含前导 0。如果全部用十进制表示,对于类似0.012的浮点数,规格化的表示应为1.2e-2。但对于某些过小的数,如1.2e-130,允许的指数位数不能满足指数的需要,可能就会在尾数前添加前导0,如将其表示为0.00012e-126

总结如下:
  1)当指数

e

x

p

exp

exp 的所有二进制位都是 0 时,我们将这样的浮点数称为“非规格化浮点数”;
  2)当指数

e

x

p

exp

exp 的所有二进制位既不全为 0 也不全为 1 时,我们称之为“规格化浮点数”;
  3)当指数 exp 的所有二进制位都是 1 时,作为特殊值对待。
换言之,究竟是规格化浮点数,还是非规格化浮点数,还是特殊值,完全看指数

e

x

p

exp

exp。

三、浮点数判定

1、精度定义

  • 在 C++ 中,

1

e

6

1e-6

1e−6 代表

1

0

6

10^{-6}

10−6,即

0.000001

0.000001

0.000001,是一个比较合适的精度值;

#define eps 1e-6

2、相等判定

  • 介于浮点数的表示方式,不能用 ‘==’ 进行相等判定,必须将两数相减,取绝对值以后,根据结果是否小于某个精度来判定两者是否相等;
bool EQ(double a, double b) {   // EQual
	return fabs(a - b) < eps;
}

3、不相等判定

  • ‘不相等’ 就是 ‘相等’ 的 ‘非’(取反);
bool NEQ(double a, double b) {  // NotEQual
	return !EQ(a, b);
}

4、大于等于判定

  • ‘大于等于’ 就是 ‘大于 或 等于’ 的意思,需要拆分成如下形式:
bool GET(double a, double b) {    // GreaterEqualThan
	return a > b || EQ(a, b);
}

5、小于等于判定

  • ‘小于等于’ 就是 ‘小于 或 等于’ 的意思,需要拆分成如下形式:
bool SET(double a, double b) {   // SmallerEqualThan
	return a < b || EQ(a, b);
}

6、小于判定

  • ‘小于’ 就是 ‘大于等于’ 的 ‘非’,需要拆分成如下形式:
  • 注意:千万不能直接用

a

<

b

a < b

a<b;

bool ST(double a, double b) {   // SmallerThan
	return a < b && NEQ(a, b);
}

7、大于判定

  • ‘大于’ 就是 ‘小于等于’ 的 ‘非’,需要拆分成如下形式:
  • 注意:千万不能直接用

a

b

a > b

a>b;

bool GT(double a, double b) {   // GreaterThan
	return a > b && NEQ(a, b);
}


通过这一章,我们学会了:
  浮点数的精度及其判定;

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

课后习题


第四章

控制流

(26)- if else 语句

一、语句

1、简单语句

  • 例如:a = 6是一个赋值表达式,而a = 6;则代表一个赋值语句。
  • 如下代码所示:
#include <stdio.h>
int main() {
    int a = 1;                // (1)
    int b = ++a;              // (2)
    int c = a + b;            // (3)
    printf("%d\n", a, b, c);  // (4)
    return 0;                 // (5)
}

  • 以上就是五条简单语句。

2、复合语句

  • 将一些简单语句用一对{}括起来,就构成了一个复合语句。复合语句的作用和单个简单语句分开写是一致的,有时候也叫程序块。并且,花括号的后面不需要跟分号。
  • 如下代码所示:
#include <stdio.h>
int main() {
    {
        int a = 1;                // (1)
        int b = ++a;              // (2)
        int c = a + b;            // (3)
        printf("%d\n", a, b, c);  // (4)
    }
    return 0;                     // (5)
}

  • 这段代码和上面那段代码的执行结果是一致的。
  • 并且是按照顺序一条一条执行下来的。但是有些时候,我们期望达到的结果,并不是顺序结构就能满足的,我们需要引入一些更加多样化的结构,比如 分支结构(或者可以叫 选择结构)。
  • 这一章,我们就来学习一下 C语言 中实现分支结构的判断语句 if - else。

二、if - else 语句

1、if - else 常规语法

  • if - else 语句用于条件判定,它的语法如下:
    if (表达式)
        语句1
    else
        语句2

这个语句在执行时,先计算 表达式 的值:
  1)如果 表达式 的值 非零,则执行 语句1
  2)如果 表达式 的值 为零,则执行 语句2

  • 例如,下面的代码:
#include <stdio.h>
int main() {
    int n;
    scanf("%d", &n);
    if(n % 2 == 1)
        printf("%d 是奇数!", n);
    else 
        printf("%d 是偶数!", n);
    return 0;
} 

  • n % 2 == 1是由算术运算符%和关系运算符==组成的表达式,它的含义是

n

n

n 模 2 的值是否为 1。如果为 1 即表达式成立,则输出

n

n

n 是奇数,否则输出

n

n

n 是偶数。

  • 这就是最简单的 if - else 语句了。

2、加上花括号

  • 这时候,如果我们想要在判断奇偶性之后,做一些更加复杂的事情,比如:当

n

n

n 为奇数的时候,将

n

n

n 乘上 3,再减去 4;当

n

n

n 为偶数的时候,将

n

n

n 减去 5,再乘上 6;我们就可以用到上文中所说的复合语句了。代码实现如下:

#include <stdio.h>
int main() {
    int n;
    scanf("%d", &n);
    if(n % 2 == 1) 
    {
        n \*= 3;
        n -= 4;
    }
    else 
    {
        n -= 5;
        n \*= 6;
    }
    return 0;
} 

  • 这一下就多出了好多行,对于屏幕不够大的小伙伴,这样的写法就很别扭。所以,我们可以将左花括号{放在条件判断的后面,并且将 else放在右花括号}的后面。因为 C语言 对空格和换行不是很敏感,得到代码如下:
#include <stdio.h>
int main() {
    int n;
    scanf("%d", &n);
    if(n % 2 == 1) {
        n \*= 3;
        n -= 4;
    } else {
        n -= 5;
        n \*= 6;
    }
    return 0;
} 

3、表达式的省略语法

  • 对表达式,我们可以对写法进行简化,例如下面两种写法是等价的:
    if (表达式)
        语句1
    else
        语句2

    if (表达式 != 0)
        语句1
    else
        语句2

  • 所以,上面的代码,可以改成:
#include <stdio.h>
int main() {
    int n;
    scanf("%d", &n);
    if(n % 2) {            // (1)
        n \*= 3;
        n -= 4;
    } else {
        n -= 5;
        n \*= 6;
    }
    return 0;
} 

  • (

1

)

(1)

(1) 这是唯一的一行区别,因为n % 2 == 1等价于n % 2 != 0,等价于n % 2

4、else 可选

  • 所以也可以表示成:
    if (表达式)
        语句

  • 例如,当

n

n

n 为奇数的时候需要打印出

n

n

n 的值,为偶数时什么都不用干,就可以这么写:

#include <stdio.h>
int main() {
    int n;
    scanf("%d", &n);
    if(n % 2) {
        printf("%d\n", n);
    }
    return 0;
} 

三、else - if 语句

1、else - if 常规语法

  • 在 C语言中,我们经常会遇到多路判断结构,语法如下:
    if (表达式1)
        语句1
    else if(表达式2)
        语句2
    else if(表达式3)
        语句3
    else if(...)
        ...
    else
        语句n

  • 这种语句是编写多路判断的常用方法,其中每个表达式会按照顺序进行依次求值,一旦某个表达式的结果为真,就会进入它对应的语句进行执行,并且对于后面所有的else if后的表达式 和 语句 都不会进行计算了。
  • 而最后一个else表示的则是 “上述条件均不成立” 的情况,当然,如果不需要处理这种情况,也是可以省略的。

【例题1】通过输入字符,判断它的类型:
  1)如果是数字,输出串 “number”;
  2)如果是大写字母,输出串 “upper letter”;
  3)如果是小写字母,输出串 “lowwer letter”;
在这里插入图片描述

  • 我们可以用 if - else 和 else - if 语句将这个问题组合出来。代码实现如下:
#include <stdio.h>
int main(){
    char c;
    c = getchar();
    if(c >= '0' && c <= '9')
        printf("number\n");
    else if(c >= 'A' && c <= 'Z')
        printf("upper letter\n");
    else if(c >= 'a' && c <= 'z')
        printf("lowwer letter\n");
    else
        printf("other\n");
    return 0;
}

四、嵌套 if 语句

1、嵌套 if 语句常规语法

  • 这个概念比较简单,就是把之前的 if - else 语句的 语句1 替换成一个新的 if 语句,就形成了嵌套,如下:
    if (表达式1) {
        if(表达式2)
            语句1
    } else
        语句n

2、else 的匹配问题

  • 对于一个语句里面有多个if,但是只有一个else时,这个else到底是和哪个if匹配的,我们通过一个例子来说明。代码如下:
#include <stdio.h>
int main() {
    int a = 1, b = 2;
    if(a)                   // (1) 
        if(a > b)           // (2)
           printf("a > b\n");
    else                    // (3)
        printf("a == 0\n");
    return 0;
}

  • 想想看,这段代码输出的是什么?

  • 是的,运行结果应该为:
a == 0

  • 为什么不是什么都不输出呢?
  • 主要原因还是因为我们被缩进给骗了。C语言中的缩进是不能改变语义的,所以这段代码实际应该是这样的:
#include <stdio.h>
int main() {
    int a = 1, b = 2;
    if(a)                   // (1) 
        if(a > b)           // (2)
            printf("a > b\n");
        else                // (3)
            printf("a == 0\n");
    return 0;
}

  • 也就是

(

3

)

(3)

(3)的else,是匹配的

(

2

)

(2)

(2)的if,不是匹配

(

1

)

(1)

(1)的if

  • 结论:else与最近的前一个没有与else匹配的if匹配。所以怕产生歧义的最好办法,就是多加{}

通过这一章,我们学会了:
  1)if else 语句的用法;
  2)else if 语句的用法;

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

课后习题


(27)- 条件运算符

一、条件运算符

if (a > b) {
    x = a;
}else {
    x = b;
}

  • 在 C语言中,提供了一种更加简单的语法,被称为 条件运算符,语法如下:
    表达式1 ? 表达式2 : 表达式3

  • 以上由 条件运算符 组成的式子被称为 条件表达式
  • 条件运算符是 C语言 中唯一的一个三目运算符,其求值规则为:如果 表达式1 的值为 ,则以 表达式2 的值作为整个 条件表达式 的值,否则以 表达式3 的值作为整个 条件表达式 的值。
  • 条件表达式通常用于赋值语句之中。
  • 所以上面的if else语句,完全可以用 条件运算符 来实现,如下:
    x = (a > b) ? a : b;

  • 注意:这里的?:是一个整体,是不能分隔的,不可单独使用。

二、条件运算符的嵌套

  • 由于条件表达式中的每个表达式可以又是一个条件表达式,所以是会产生嵌套的。
  • 例如,我们用((a > c) ? a : c)来对 表达式2 进行替换,得到:
    x = (a > b) ? ((a > c) ? a : c) : b;

  • 也可以再用(b < c) ? b : c来替换 表达式3,得到:
    x = (a > b) ? ((a > c) ? a : c) : ((b < c) ? b : c);

  • 那么让我们来看看以下这段代码的输出吧:
#include <stdio.h>
int main() {
    int a = 3, b = 4, c = 5;
    int x =  (a > b) ? ((a > c) ? a : c) : ((b < c) ? b : c);
    printf("%d\n", x);
    return 0;
}

  • 首先计算的是 (a > b)是否为真,(3 > 5)结果自然不为真,所以我们走的是后面的分支,即((b < c) ? b : c),得到的结果为求bc的最小值,所以为 4,而这也就是整个条件表达式的值。
  • 所以这段代码的运行结果为:
4

  • 当然,这样的代码可读性就很差,实际编码中,我们也不推荐这样的写法。
  • 后面我们学了函数以后,实现这段逻辑时,代码就能清晰很多。

三、条件运算符的值类型

  • 条件运算符对应的值类型如何确定呢?
#include <stdio.h>

int main() {
    double a = 0 ? 1 : 1.5;
    printf("%lf\n", a);
    return 0;
}

  • 这段代码的运行结果为:
1.500000

  • 这样,起码我们可以确定的是 0 ? 1 : 1.5的类型是double,因为如果是整数的话,最后不可能返回一个浮点数。事实上,它的类型是由 表达式2表达式3 决定的,类型转换遵循以下规则:
  • 也就是说 表达式2int表达式3unsigned,则条件表达式的值返回的就是unsigned类型的。

四、条件运算符的优先级和结合性

1、优先级

条件运算符的优先级比较低,仅高于 赋值运算符 和 逗号运算符,所以使用的时候不用担心优先级问题。

#include <stdio.h>

int main() {
    int a = 3, b = 4;
    int x =  a > b ? a : b;    // (1)
    int y = (a > b) ? a : b;   // (2)
    printf("%d %d\n", x, y);
    return 0;
}

  • 所以上文中的

(

1

)

(1)

(1) 和

(

2

)

(2)

(2) 是等价的,因为关系运算符>的优先级高于?:,所以先计算a > b这个表达式的值,最后得到的结果为4

2、结合性

条件运算符的结合性是右结合的。

#include <stdio.h>
int main() {
    int a = 3, b = 4, c = 5;
    int x =  a < b ? a > c ? a : c : b;
    printf("%d\n", x);
    return 0;
}

  • 第一眼看到这个的时候,一脸懵逼,比如< b ? a >是个什么鬼???
  • 当然,这不重要,我们只需要知道结合性是又结合的,就能把这个表达式转换成如下形式:
  • a < b ? (a > c ? a : c) : b
  • 这样就转化成了条件表达式的嵌套,首先a < b条件满足,所以我们只需要知道(a > c ? a : c)的值,就是最终条件表达式的值了,即3 > 5 ? 3 : 5的值,即5

五、条件运算符的特点总结

条件运算符 和 条件表达式 的几个特点总结如下:
  1)条件运算符是唯一的三目运算符;
  2)条件运算符是右结合的;
  3)条件运算符的优先级非常低,仅高于赋值运算符 和 逗号运算符;


通过这一章,我们学会了:
  条件表达式;

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

课后习题


(28)- switch case 语句

一、分支语句

【例题1】根据输入的整型a,如果范围在

[

1

,

6

]

[1, 6]

[1,6],则输出它的英文形式(OneTwo、…),否则输出 Other

  • 可以利用 if else 语句轻松写出来,代码实现如下:
#include <stdio.h>

int main() {
    int a;
    scanf("%d", &a);
    if(a == 1) {
        printf("One\n"); 
    }else if(a == 2) {
        printf("Two\n"); 
    }else if(a == 3) {
        printf("Three\n"); 
    }else if(a == 4) {
        printf("Four\n"); 
    }else if(a == 5) {
        printf("Five\n"); 
    }else if(a == 6) {
        printf("Six\n"); 
    }else {
        printf("Other\n"); 
    }
    return 0;
}

二、switch case 语句

1、switch case 引例

  • 对于这种情况,我们可以利用 switch 语句来替代,实现如下:
#include <stdio.h>
int main() {
    int a;
    scanf("%d", &a);
    switch(a){
        case 1: 
            printf("One\n"); 
            break;
        case 2: 
            printf("Two\n"); 
            break;
        case 3: 
            printf("Three\n"); 
            break;
        case 4: 
            printf("Four\n"); 
            break;
        case 5: 
            printf("Five\n"); 
            break;
        case 6: 
            printf("Six\n"); 
            break;
        default:
            printf("Other\n");
            break;
    }
    return 0;
}

  • 修改后的代码实现如下:
#include <stdio.h>
int main() {
    int a;
    scanf("%d", &a);
    switch(a){
        case 1:  printf("One\n");   break;
        case 2:  printf("Two\n");   break;
        case 3:  printf("Three\n"); break;
        case 4:  printf("Four\n");  break;
        case 5:  printf("Five\n");  break;
        case 6:  printf("Six\n");   break;
        default: printf("Other\n"); break;
    }
    return 0;
}

  • 当然,这就是 代码规范 范畴的问题了,看团队里面怎么执行了,我这里就不多加讨论了。如果是个人代码,不打算给别人看,那当然怎么舒服怎么来。
  • 上面的代码,在进行相应的输入后,运行结果如下:
4↙
Four

2、switch case 用法

  • switch 是另外一种选择结构的语句,用来代替简单的、拥有多个分枝的 if else 语句,基本格式如下:
switch(表达式){
    case 整数1: 语句1;
    case 整数2: 语句2;
          ...
    case 整数n: 语句n;
    default: 语句n + 1;
}

对于 switch case 语句的执行过程,如下:
  1)首先,计算 表达式 的值,假设为

x

x

x;
  2)然后,从第一个 case 开始,比较

x

x

x 和 整数1 的值,如果相等,就执行冒号后面的所有语句,也就是从 语句1 一直执行到 语句n + 1,而不管后面的 case 是否匹配成功(这是平时开发的易错点,需要特别注意)。
  3)如果

x

x

x 和 整数1 不相等,就跳过冒号后面的 语句1,继续比较第二个 case、第三个 case……,一旦发现和某个 整数 相等了,就会执行后面所有的语句。假设

x

x

x 和 整数6 相等,那么就会从 语句6 一直执行到 语句n + 1
  4)如果直到最后一个 整数n 都没有找到相等的值,那么就执行 default 后的 语句n + 1

3、switch case 易错点

  • 当和某个 整数 匹配成功后,会执行该分支以及后面所有分支的语句。例如,如果写成下面的代码,那么,结果可能不是我们想要的:
#include <stdio.h>
int main(){
    int a;
    scanf("%d", &a);
    switch(a){
        case 1: printf("One\n");
        case 2: printf("Two\n");
        case 3: printf("Three\n");
        case 4: printf("Four\n");
        case 5: printf("Five\n");
        case 6: printf("Six\n");
        default:printf("Other\n");
    }
    return 0;
}


  • 运行结果如下:
3↙
Three
Four
Five
Six
Other

  • 输入3以后,程序发现和第 3 个分支匹配成功,于是就执行第 3 个分支以及后面的所有分支语句。这显然不是我们想要的结果,我们希望只执行第 3 个分支,而跳过后面的其他分支。
  • 为了达到这个目标,必须要在每个分支最后添加break;语句。

4、switch case 中的 break

  • break是 C语言 中的一个关键字,用于跳出 switch 语句。所谓 跳出,是指一旦遇到 break,就不再执行 switch 中的任何语句,包括当前分支中的语句和其他分支中的语句;也就是说,整个 switch 执行结束了,接着会执行整个 switch 后面的代码。
#include <stdio.h>
int main(){
    int a;
    scanf("%d", &a);
    switch(a){
        case 1: printf("One\n"); break;
        case 2: printf("Two\n"); break;
        case 3: printf("Three\n"); break;
        case 4: printf("Four\n"); break;
        case 5: printf("Five\n"); break;
        case 6: printf("Six\n"); break;
        default:printf("Other\n"); break;
    }
    return 0;
}

  • 得到运行结果如下:
3↙
Three

  • 由于 default 是最后一个分支,匹配后不会再执行其他分支,所以也可以不添加break;语句也可。

  • 关于break语句的更多内容,我会在后续讲 循环语句 时继续讲解。

5、switch case 语句中的 case 类型

  • case 后面跟的整数类型,必须是 整数 或者 常整表达式,如下:
#include <stdio.h>
int main(){
    int a;
    scanf("%d", &a);
    switch(a){ 
        case 1: printf("One\n"); break;          // (1) 
        case '2' - '0': printf("Two\n"); break;  // (2)
        case 1 + 1 + 1: printf("Three\n"); break;// (3)
        case 4.0: printf("Four\n"); break;       // (4)
        case a: printf("Five\n"); break;         // (5)
        case a + 1: printf("Six\n"); break;      // (6)
        default:printf("Other\n"); break;        
    }
    return 0;
}

  • (

1

)

(1)

(1) 正确,为整数;

  • (

2

)

(2)

(2) 正确,字符减法,得到整数;

  • (

3

)

(3)

(3) 正确,表达式结果为整数;

  • (

4

)

(4)

(4) 错误,结果为浮点数;

  • (

5

)

(5)

(5) 错误,结果为变量;

  • (

6

)

(6)

(6) 错误,结果为变量表达式;


通过这一章,我们学会了:
  switch case 语句的用法;

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

课后习题


(29)- while 语句

一、程序结构概述

在C语言中,共有三大常用的程序结构:
  顺序结构: 代码从前往后执行,没有任何跳转;
  选择结构 : 也叫分支结构,即 if else、switch 以及条件运算符;
  循环结构: 即重复执行同一段代码。

二、循环结构简介

#include <stdio.h>
int main() {
    printf("I love you!\n");
    printf("I love you!\n");
    printf("I love you!\n");
    printf("I love you!\n");
    printf("I love you!\n");
    printf("I love you!\n");
    printf("I love you!\n");
    printf("I love you!\n");
    return 0;
} 

  • 重要的是,当输出语句为 100 条时,如果还用上面的思路进行输出,整个代码就会显得非常臃肿和冗余。所以,我们需要引入一种能够循环执行代码的结构。

1、while 语句

1)语法规则
  • while 语句的一般形式为:
while(表达式){
    语句块
}

它的含义是:
  1)首先,计算 表达式 的值,当值为真时, 执行 语句块 的内容;
  2)然后,再次计算 表达式 的值,如果为真,继续执行 语句块……
  这个过程会一直重复,直到表达式的值为假,则退出循环,执行 while 后面的代码。

  • 我们通常将上述的 表达式 称为循环条件,把 语句块 称为循环体,整个循环的过程就是不停判断 循环条件、并执行 循环体代码 的过程。
  • 例如,我们可以把输出 100 条 “I love you!” ,写成如下代码:
#include <stdio.h>
int main() {
    int iCount = 0;                // (1)
    while(iCount < 100) {          // (2)
        printf("I love you!\n");   // (3)
        ++iCount;                  // (4)
    }
    return 0;
} 

  • (

1

)

(1)

(1) 初始化一个计数器iCount为 0;

img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

需要这份系统化资料的朋友,可以戳这里获取


* 在 C语言中,提供了一种更加简单的语法,被称为 **条件运算符**,语法如下:



表达式1 ? 表达式2 : 表达式3

* 以上由 **条件运算符** 组成的式子被称为 **条件表达式**。
* 条件运算符是 C语言 中唯一的一个三目运算符,其求值规则为:如果 **表达式1** 的值为 **真**,则以 **表达式2** 的值作为整个 **条件表达式** 的值,否则以 **表达式3** 的值作为整个 **条件表达式** 的值。
* 条件表达式通常用于赋值语句之中。
* 所以上面的`if else`语句,完全可以用 **条件运算符** 来实现,如下:



x = (a > b) ? a : b;

* 注意:这里的`?`和`:`是一个整体,是不能分隔的,不可单独使用。


## 二、条件运算符的嵌套


* 由于条件表达式中的每个表达式可以又是一个条件表达式,所以是会产生嵌套的。
* 例如,我们用`((a > c) ? a : c)`来对 **表达式2** 进行替换,得到:



x = (a > b) ? ((a > c) ? a : c) : b;

* 也可以再用`(b < c) ? b : c`来替换 **表达式3**,得到:



x = (a > b) ? ((a > c) ? a : c) : ((b < c) ? b : c);

* 那么让我们来看看以下这段代码的输出吧:



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


* 首先计算的是 `(a > b)`是否为真,`(3 > 5)`结果自然不为真,所以我们走的是后面的分支,即`((b < c) ? b : c)`,得到的结果为求`b`和`c`的最小值,所以为 4,而这也就是整个条件表达式的值。
* 所以这段代码的运行结果为:



4


* 当然,这样的代码可读性就很差,实际编码中,我们也不推荐这样的写法。
* 后面我们学了函数以后,实现这段逻辑时,代码就能清晰很多。


## 三、条件运算符的值类型


* 条件运算符对应的值类型如何确定呢?



#include <stdio.h>

int main() {
double a = 0 ? 1 : 1.5;
printf(“%lf\n”, a);
return 0;
}


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



1.500000


* 这样,起码我们可以确定的是 `0 ? 1 : 1.5`的类型是`double`,因为如果是整数的话,最后不可能返回一个浮点数。事实上,它的类型是由 **表达式2** 和 **表达式3** 决定的,类型转换遵循以下规则:  
 ![](https://i-blog.csdnimg.cn/blog_migrate/304ce3f15f6a15524d206878cc29a9e5.png#pic_center)
* 也就是说 **表达式2** 是`int`,**表达式3** 是`unsigned`,则条件表达式的值返回的就是`unsigned`类型的。


## 四、条件运算符的优先级和结合性


### 1、优先级



> 
> 条件运算符的优先级比较低,仅高于 赋值运算符 和 逗号运算符,所以使用的时候不用担心优先级问题。
> 
> 
> 



#include <stdio.h>

int main() {
int a = 3, b = 4;
int x = a > b ? a : b; // (1)
int y = (a > b) ? a : b; // (2)
printf(“%d %d\n”, x, y);
return 0;
}


* 所以上文中的  
 
 
 
 
 ( 
 
 
 1 
 
 
 ) 
 
 
 
 (1) 
 
 
 (1) 和  
 
 
 
 
 ( 
 
 
 2 
 
 
 ) 
 
 
 
 (2) 
 
 
 (2) 是等价的,因为关系运算符`>`的优先级高于`?:`,所以先计算`a > b`这个表达式的值,最后得到的结果为`4`。


### 2、结合性



> 
> 条件运算符的结合性是右结合的。
> 
> 
> 



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


* 第一眼看到这个的时候,一脸懵逼,比如`< b ? a >`是个什么鬼???
* 当然,这不重要,我们只需要知道结合性是又结合的,就能把这个表达式转换成如下形式:
* `a < b ? (a > c ? a : c) : b`
* 这样就转化成了条件表达式的嵌套,首先`a < b`条件满足,所以我们只需要知道`(a > c ? a : c)`的值,就是最终条件表达式的值了,即`3 > 5 ? 3 : 5`的值,即`5`。


## 五、条件运算符的特点总结



> 
> 条件运算符 和 条件表达式 的几个特点总结如下:  
>   1)条件运算符是唯一的三目运算符;  
>   2)条件运算符是右结合的;  
>   3)条件运算符的优先级非常低,仅高于赋值运算符 和 逗号运算符;
> 
> 
> 




---


![](https://i-blog.csdnimg.cn/blog_migrate/77cd40549825465e6083a1d071b9585a.gif#pic_center)



> 
> 通过这一章,我们学会了:  
>   条件表达式;
> 
> 
> 


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




---


## 课后习题


![](https://i-blog.csdnimg.cn/blog_migrate/d2cccdbee96eb8e7f54a516fceb15355.gif#pic_center)


* [【第04题】给定 a 和 b,问 a 能否被 b 整除](https://bbs.csdn.net/topics/618545628)




---




**(28)- switch case 语句**

## 一、分支语句


![](https://i-blog.csdnimg.cn/blog_migrate/7faa38d6357546e70590a0e9990e4832.png#pic_center)


* 在学习 [☀️光天化日学C语言☀️(26)- if else 语句](https://bbs.csdn.net/topics/618545628) 时,我们已经知道了对于不同的条件调用不同的语句进行处理。来看个例题。



> 
> 【例题1】根据输入的整型`a`,如果范围在  
>  
>  
>  
>  
>  [ 
>  
>  
>  1 
>  
>  
>  , 
>  
>  
>  6 
>  
>  
>  ] 
>  
>  
>  
>  [1, 6] 
>  
>  
>  [1,6],则输出它的英文形式(`One`、`Two`、…),否则输出 `Other`。
> 
> 
> 


* 可以利用 if else 语句轻松写出来,代码实现如下:



#include <stdio.h>

int main() {
int a;
scanf(“%d”, &a);
if(a == 1) {
printf(“One\n”);
}else if(a == 2) {
printf(“Two\n”);
}else if(a == 3) {
printf(“Three\n”);
}else if(a == 4) {
printf(“Four\n”);
}else if(a == 5) {
printf(“Five\n”);
}else if(a == 6) {
printf(“Six\n”);
}else {
printf(“Other\n”);
}
return 0;
}


## 二、switch case 语句


![](https://i-blog.csdnimg.cn/blog_migrate/21700950cf67460c147f0a9b738cffa7.png#pic_center)


### 1、switch case 引例


* 对于这种情况,我们可以利用 switch 语句来替代,实现如下:



#include <stdio.h>
int main() {
int a;
scanf(“%d”, &a);
switch(a){
case 1:
printf(“One\n”);
break;
case 2:
printf(“Two\n”);
break;
case 3:
printf(“Three\n”);
break;
case 4:
printf(“Four\n”);
break;
case 5:
printf(“Five\n”);
break;
case 6:
printf(“Six\n”);
break;
default:
printf(“Other\n”);
break;
}
return 0;
}


![](https://i-blog.csdnimg.cn/blog_migrate/a336ef8395371758cfd7d5aa0ed1e6d7.png)


* 修改后的代码实现如下:



#include <stdio.h>
int main() {
int a;
scanf(“%d”, &a);
switch(a){
case 1: printf(“One\n”); break;
case 2: printf(“Two\n”); break;
case 3: printf(“Three\n”); break;
case 4: printf(“Four\n”); break;
case 5: printf(“Five\n”); break;
case 6: printf(“Six\n”); break;
default: printf(“Other\n”); break;
}
return 0;
}


* 当然,这就是 **代码规范** 范畴的问题了,看团队里面怎么执行了,我这里就不多加讨论了。如果是个人代码,不打算给别人看,那当然怎么舒服怎么来。
* 上面的代码,在进行相应的输入后,运行结果如下:



4↙
Four


### 2、switch case 用法


* switch 是另外一种选择结构的语句,用来代替简单的、拥有多个分枝的 if else 语句,基本格式如下:



switch(表达式){
case 整数1: 语句1;
case 整数2: 语句2;

case 整数n: 语句n;
default: 语句n + 1;
}



> 
> 对于 switch case 语句的执行过程,如下:  
>   1)首先,计算 **表达式** 的值,假设为  
>  
>  
>  
>  
>  x 
>  
>  
>  
>  x 
>  
>  
>  x;  
>   2)然后,从第一个 **case** 开始,比较  
>  
>  
>  
>  
>  x 
>  
>  
>  
>  x 
>  
>  
>  x 和 **整数1** 的值,如果相等,就执行冒号后面的所有语句,也就是从 **语句1** 一直执行到 **语句n + 1**,而不管后面的 **case** 是否匹配成功(这是平时开发的易错点,需要特别注意)。  
>   3)如果  
>  
>  
>  
>  
>  x 
>  
>  
>  
>  x 
>  
>  
>  x 和 **整数1** 不相等,就跳过冒号后面的 **语句1**,继续比较第二个 case、第三个 case……,一旦发现和某个 **整数** 相等了,就会执行后面所有的语句。假设  
>  
>  
>  
>  
>  x 
>  
>  
>  
>  x 
>  
>  
>  x 和 **整数6** 相等,那么就会从 **语句6** 一直执行到 **语句n + 1**。  
>   4)如果直到最后一个 **整数n** 都没有找到相等的值,那么就执行 **default** 后的 **语句n + 1**。  
>  ![](https://i-blog.csdnimg.cn/blog_migrate/8d12f19815277b84f92f62023d5cce9d.png#pic_center)
> 
> 
> 


### 3、switch case 易错点


* 当和某个 **整数** 匹配成功后,会执行该分支以及后面所有分支的语句。例如,如果写成下面的代码,那么,结果可能不是我们想要的:



#include <stdio.h>
int main(){
int a;
scanf(“%d”, &a);
switch(a){
case 1: printf(“One\n”);
case 2: printf(“Two\n”);
case 3: printf(“Three\n”);
case 4: printf(“Four\n”);
case 5: printf(“Five\n”);
case 6: printf(“Six\n”);
default:printf(“Other\n”);
}
return 0;
}


* 运行结果如下:



3↙
Three
Four
Five
Six
Other


* 输入3以后,程序发现和第 3 个分支匹配成功,于是就执行第 3 个分支以及后面的所有分支语句。这显然不是我们想要的结果,我们希望只执行第 3 个分支,而跳过后面的其他分支。
* 为了达到这个目标,必须要在每个分支最后添加`break;`语句。


### 4、switch case 中的 break


* `break`是 C语言 中的一个关键字,用于跳出 switch 语句。所谓 **跳出**,是指一旦遇到 `break`,就不再执行 switch 中的任何语句,包括当前分支中的语句和其他分支中的语句;也就是说,整个 switch 执行结束了,接着会执行整个 switch 后面的代码。



#include <stdio.h>
int main(){
int a;
scanf(“%d”, &a);
switch(a){
case 1: printf(“One\n”); break;
case 2: printf(“Two\n”); break;
case 3: printf(“Three\n”); break;
case 4: printf(“Four\n”); break;
case 5: printf(“Five\n”); break;
case 6: printf(“Six\n”); break;
default:printf(“Other\n”); break;
}
return 0;
}


* 得到运行结果如下:



3↙
Three


* 由于 default 是最后一个分支,匹配后不会再执行其他分支,所以也可以不添加`break;`语句也可。


![](https://i-blog.csdnimg.cn/blog_migrate/7f977839764db54ff9246bd9a7c5e7aa.png#pic_center)


* 关于`break`语句的更多内容,我会在后续讲 **循环语句** 时继续讲解。


### 5、switch case 语句中的 case 类型


* case 后面跟的整数类型,必须是 **整数** 或者 **常整表达式**,如下:



#include <stdio.h>
int main(){
int a;
scanf(“%d”, &a);
switch(a){
case 1: printf(“One\n”); break; // (1)
case ‘2’ - ‘0’: printf(“Two\n”); break; // (2)
case 1 + 1 + 1: printf(“Three\n”); break;// (3)
case 4.0: printf(“Four\n”); break; // (4)
case a: printf(“Five\n”); break; // (5)
case a + 1: printf(“Six\n”); break; // (6)
default:printf(“Other\n”); break;
}
return 0;
}


* ( 
 
 
 1 
 
 
 ) 
 
 
 
 (1) 
 
 
 (1) 正确,为整数;
* ( 
 
 
 2 
 
 
 ) 
 
 
 
 (2) 
 
 
 (2) 正确,字符减法,得到整数;
* ( 
 
 
 3 
 
 
 ) 
 
 
 
 (3) 
 
 
 (3) 正确,表达式结果为整数;
* ( 
 
 
 4 
 
 
 ) 
 
 
 
 (4) 
 
 
 (4) 错误,结果为浮点数;
* ( 
 
 
 5 
 
 
 ) 
 
 
 
 (5) 
 
 
 (5) 错误,结果为变量;
* ( 
 
 
 6 
 
 
 ) 
 
 
 
 (6) 
 
 
 (6) 错误,结果为变量表达式;




---


![](https://i-blog.csdnimg.cn/blog_migrate/77cd40549825465e6083a1d071b9585a.gif#pic_center)



> 
> 通过这一章,我们学会了:  
>   switch case 语句的用法;
> 
> 
> 


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




---


## 课后习题


![](https://i-blog.csdnimg.cn/blog_migrate/d2cccdbee96eb8e7f54a516fceb15355.gif#pic_center)


* [【第55题】根据不同输入进行不同输出 | switch case 分支语句的应用](https://bbs.csdn.net/topics/618545628)




---




**(29)- while 语句**

## 一、程序结构概述



> 
> 在C语言中,共有三大常用的程序结构:  
>   **顺序结构:** 代码从前往后执行,没有任何跳转;  
>   **选择结构 :** 也叫分支结构,即 if else、switch 以及条件运算符;  
>   **循环结构:** 即重复执行同一段代码。
> 
> 
> 


## 二、循环结构简介


![](https://i-blog.csdnimg.cn/blog_migrate/5634f7a7eb202f16e710ca5e1aac3f8c.png#pic_center)



#include <stdio.h>
int main() {
printf(“I love you!\n”);
printf(“I love you!\n”);
printf(“I love you!\n”);
printf(“I love you!\n”);
printf(“I love you!\n”);
printf(“I love you!\n”);
printf(“I love you!\n”);
printf(“I love you!\n”);
return 0;
}


![](https://i-blog.csdnimg.cn/blog_migrate/0629ce03e60e2b297d7be7da4fb09aa3.png#pic_center)


* 重要的是,当输出语句为 100 条时,如果还用上面的思路进行输出,整个代码就会显得非常臃肿和冗余。所以,我们需要引入一种能够循环执行代码的结构。


### 1、while 语句


#### 1)语法规则


* while 语句的一般形式为:



while(表达式){
语句块
}



> 
> 它的含义是:  
>   1)首先,计算 **表达式** 的值,当值为真时, 执行 **语句块** 的内容;  
>   2)然后,再次计算 **表达式** 的值,如果为真,继续执行 **语句块**……  
>   这个过程会一直重复,直到表达式的值为假,则退出循环,执行 while 后面的代码。
> 
> 
> 


* 我们通常将上述的 **表达式** 称为循环条件,把 **语句块** 称为循环体,整个循环的过程就是不停判断 **循环条件**、并执行 **循环体代码** 的过程。
* 例如,我们可以把输出 100 条 “I love you!” ,写成如下代码:



#include <stdio.h>
int main() {
int iCount = 0; // (1)
while(iCount < 100) { // (2)
printf(“I love you!\n”); // (3)
++iCount; // (4)
}
return 0;
}


* ( 
 
 
 1 
 
 
 ) 
 
 
 
 (1) 
 
 
 (1) 初始化一个计数器`iCount`为 0;


[外链图片转存中...(img-A3VzJr4Y-1715692691121)]
[外链图片转存中...(img-swod2d3m-1715692691121)]
[外链图片转存中...(img-VJfw3fY3-1715692691121)]

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!**

**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值