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

学习路线:

这个方向初期比较容易入门一些,掌握一些基本技术,拿起各种现成的工具就可以开黑了。不过,要想从脚本小子变成黑客大神,这个方向越往后,需要学习和掌握的东西就会越来越多以下是网络渗透需要学习的内容:
在这里插入图片描述

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

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

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

4、其它进制

七进制:7天 = 1周;
十二进制:12瓶啤酒 = 1打;
二十四进制:24小时 = 1天;
六十进制:60秒 = 1分钟;60分钟 = 1小时;

三、计算机中的进制

  • 在计算机中常用的进制有哪些呢?

1、二进制

  • C语言中,我们如果想表示一个二进制数,可以用0b作为前缀,然后跟上01组成的数字,我们来看一段代码:
#include <stdio.h>
int main() {
	int a = 0b101;
	printf("%d\n", a);
	return 0;
}

  • 这段代码中,输出的结果如下:
5

  • 因为%d代表输出的数是十进制,所以我们需要将二进制转换成十进制以后输出,0b101的数学表示如下:

(

101

)

2

(101)_2

(101)2​

  • 它在十进制下的值为 5。
  • 因为数字比较小,所以我们可以简单列出二进制和十进制的对应关系如下:
进制
二进制011011100101
十进制012345
  • 也就是二进制下101对应于十进制下的 5。

2、八进制

  • 讲八进制之前,我们还是先来看一段代码:
#include <stdio.h>
int main() {
	int a = 0123;
	printf("%d\n", a);
	return 0;
} 

  • 那么,这段代码的输出值为多少呢?

83

  • 为什么呢?参考二进制的表示法,八进制的表示法是前缀1个0,然后跟上0-7的数字;
  • 换言之,我们需要把 123这个八进制数转换成十进制后再输出。而转换结果就是83,由于这里数字较大,我们已经无法一个一个数出来了,所以需要进行进制转换,关于进制转换,在第四节进制转换初步里再来讲解。

3、十六进制

  • 同样的,对于十六进制数,表示方式为:以0x或者0X作为前缀,跟上0-9a-fA-F的数字,其中大小写字母的含义相同,分别代表从1015的数字。如下表所示:
小写字母大写字母代表数字
aA10
bB11
cC12
dD13
eE14
fF15
  • 我们看看这段代码的输出:
#include <stdio.h>
int main() {
	int a = 0X123;
	printf("%d\n", a);
	return 0;
} 

  • 对于这段代码,输出的是:
291

在这里插入图片描述

四、进制转换初步

1、X进制 转 十进制

对于 X 进制的数来说,我们定义以下几个概念:
  【概念1】对于数字部分从右往左编号为 0 到

n

n

n,第

i

i

i 个数字位表示为

d

i

d_i

di​,这个数字就是

d

n

.

.

.

d

1

d

0

d_{n}…d_1d_0

dn​…d1​d0​;
  【概念2】每个数字位有一个权值;
  【概念3】第

i

i

i 个数字位的权值为

X

i

X^i

Xi;

  • 基于以上几个概念, X进制 转 十进制的值为 每一位数字 和 它的权值的乘积的累加和,如下:

i

=

0

n

X

i

d

i

\sum_{i=0}^{n} X^id_i

i=0∑n​Xidi​

\sum

∑ 是个求和符号,不必惊慌!

  • 举个例子,对于上文提到的八进制的数0123,转换成十进制,只需要套用公式:

i

=

0

n

X

i

d

i

=

i

=

0

2

8

i

d

i

=

8

2

×

1

8

1

×

2

8

0

×

3

=

64

16

3

=

83

\begin{aligned}\sum_{i=0}^{n} X^id_i &= \sum_{i=0}^{2} 8^id_i \ &= 8^2 \times 1 + 8^1 \times 2 + 8^0 \times 3 \ &= 64 + 16 + 3 \ &= 83\end{aligned}

i=0∑n​Xidi​​=i=0∑2​8idi​=82×1+81×2+80×3=64+16+3=83​

  • 再如,上文提到的十六进制数0X123,转换成十进制,套用同样的公式,如下:

i

=

0

n

X

i

d

i

=

i

=

0

2

1

6

i

d

i

=

1

6

2

×

1

1

6

1

×

2

1

6

0

×

3

=

256

32

3

=

291

\begin{aligned}\sum_{i=0}^{n} X^id_i &= \sum_{i=0}^{2} 16^id_i \ &= 16^2 \times 1 + 16^1 \times 2 + 16^0 \times 3 \ &= 256 + 32 + 3 \ &= 291\end{aligned}

i=0∑n​Xidi​​=i=0∑2​16idi​=162×1+161×2+160×3=256+32+3=291​

2、十进制 转 X进制

  • 对于 十进制 转 X进制 的问题,我们可以这么来考虑:
  • 从 X进制 转 十进制 的原理可知,任何一个十进制数字都是由 X进制 的幂的倍数累加而成。所以,一个数一定有

X

0

X^0

X0 这部分,而这部分,可以通过原数除上

X

X

X 的余数得到。然后我们把原数除上

X

X

X 后得到的数,肯定又有

X

0

X^0

X0 的部分,就这样重复的试除,直到得到的商为 零 时结束,过程中的余数,逆序一下就是对应进制的数了。

  • 还是一上文的例子来说,对于291我们可以通过如下方式,转换成 十六进制。
291 除 16  ========== 余 3
18 除 16  =========== 余 2
1 除 16  ============ 余 1

  • 而对于十进制的83,我们可以通过如下方式,转换成 八进制。
83 除 8 ============ 余 3
10 除 8 ============ 余 2
1 除 8 ============= 余 1

  • 那么,等我们后面学习了循环语句以后,就可以教大家如何用计算机来实现进制转换了,目前阶段只需要了解下进制转换的基本原理即可。

通过这一章,我们学会了:
  1)二进制的表示方式为:0b作为前缀,加上0-1组成的数字;
  2)八进制的表示方式为:0作为前缀,加上0-7组成的数字;
  3)十六进制的表示方式为:0x或者0X作为前缀,加上0-9a-fA-F组成的数字;
  4)X进制转换成十进制;
  5)十进制转换成X进制;

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

课后习题

在这里插入图片描述


(07)- ASCII码

一、ASCII 码简介

1、ASCII 码的定义

  • ASCII 码(即 American Standard Code for Information Interchange),翻译过来是美国信息交换标准代码。
  • 我们一般念成 ask 2 马。

在这里插入图片描述

2、ASCII 码的起源

  • 它是一套编码系统。
  • 由于计算机用 高电平 和 低电平 分别表示 1 和 0,所以,在计算机中所有的数据在存储和运算时都要使用二进制数表示,例如,像a-zA-Z这样的52个字母以及0-9的数字还有一些常用的符号(例如?*#@!@#$%^&*()等)在计算机中存储时也要使用二进制数来表示,具体用哪些二进制数字表示哪个符号,每个人都可以约定自己的一套规则,这就叫编码。
  • 即 一个数字 和 一个字符 的一一映射。
  • 为了通信而不造成混淆,所以需要所有人都使用相同的规则。

3、ASCII 码的表示方式

  • 标准ASCII 码,使用 7 位二进制数(剩下的1位二进制为0)来表示所有的大写和小写字母,数字09、标点符号,以及在英语中使用的特殊控制字符。
  • 简单的就可以认为:一个数字对应一个字符。具体如下表所示:

二、ASCII 码的输出

  • ASCII 码对应的字符用单引号括起来,并且是可以按照两种方式来输出的,分别为:字符形式 和 整数形式。

1、字符

  • 当成字符用的时候,用格式化输出%c来控制,如下:
#include <stdio.h>
int main() {
	printf("%c\n", '0');
	printf("%c\n", 'A');
	printf("%c\n", 'a');
	printf("%c\n", '$');
	return 0;
} 

  • 得到的输出结果如下:
0
A
a
$

2、整数

  • 当成整数用的时候,用格式化输出%d来控制,如下:
#include <stdio.h>
int main() {
	printf("%d\n", '0');
	printf("%d\n", 'A');
	printf("%d\n", 'a');
	printf("%d\n", '$');
	return 0;
} 

  • 得到的输出结果如下:
48
65
97
36

  • 这是因为一个字符代表的是一个整数到符号的映射,它本质上还是一个整数,所以我们可以用整数的形式来输出。字符'0'的整数编码为48,字符'1'的整数编码为49,以此类推。

三、ASCII 码的运算

  • 既然当成了整数,那么就可以进行简单的四则运算了。
  • 我们简单来看下下面这段代码:
#include <stdio.h>
int main() {
	printf("%c\n", '0' + 5);
	printf("%c\n", 'A' + 3);
	printf("%c\n", 'a' + 5);
	printf("%c\n", '$' + 1);
	return 0;
} 

  • 它的输出如下:
5
D
f
%

  • 字符加上一个数字,我们可以认为是对字符编码进行了一个对应数字的偏移,字符'0'向右偏移 5 个单位,就是字符'5';同样的,'A'向右偏移3个单位,就是字符'D'
  • 有加法当然也有减法,接下来让我们看个例题。

【例题1】给出如下代码,给出它的输出结果。

#include <stdio.h>
int main() {
	printf("%c\n", 'A' - 10);
	return 0;
}

  • 建议先想想,然后再敲代码看看结果,是否和你想的一致。

四、ASCII 码的比较

  • ASCII 码既然可以和整数无缝切换,那么自然也可以进行比较了。
  • 通过上一节,我们了解到了 '0'加上1以后等于'1',那么顺理成章可以得出:'0' < '1'
  • 同样可以知道:'a' < 'b''X' < 'Y'
  • 那么,我们再来看个问题。

【例题2】请问 'a' < 'A'还是 'a' > 'A'

  • 这个问题的答案,就交给评论区吧,通过以上的教学,相信你一定能回答对。

通过这一章,我们学会了:
  1)ASCII 码的表示;
  2)ASCII 码的运算;
  3)ASCII 码的比较;

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

课后习题

在这里插入图片描述


(08)- 常量

一、常量简介

  • C语言中的常量,主要分为以下几种类型:

二、数值常量

  • 数值常量分为整数和浮点数,整数一般称为整型常量,浮点数则称为实型常量。

1、整型常量

  • 整型常量分为二进制、八进制、十进制 和 十六进制。
  • 每个整型常量分为三部分:前缀部分、数字部分、后缀部分。
  • 如下表所示:
进制前缀部分数字部分后缀部分
二进制0b0-1ulll
八进制00-7ulll
十进制0-9ulll
十六进制0x0X0-9a-fA-Fulll

  • 换言之,无符号整型就是非负整数。
  • 待时机成熟,我会对整数的存储结构进行一个非常详细的介绍。

【例题1】说出以下整型常量中,哪些是非法的,为什么非法。

1314
520u
0xFoooooL
0XFeeeul
018888
0987UU
0520
0x4b
1024llul
30ll
030ul

2、实型常量

  • 实型常量又分为 小数形式 和 指数形式。
1)小数形式
  • 小数形式由三部分组成:整数部分、小数点、小数部分。例如:
	3.1415927
	4.5f
	.1314

  • 其中 f后缀代表 float,用于区分double
  • .1314等价于0.1314
2)指数形式
  • 指数形式的典型格式为xey,如下:
	1e9
	5.2e000000
	5.2e-1
	1.1e2

  • 它表示的数值是:
  • x

×

1

0

y

x \times 10^{y}

x×10y

  • 其中

y

y

y 代表的是数字10的指数部分,所以是支持负数的。

三、字符常量

  • 字符常量可以是一个普通的字符、一个转义序列,或一个通用的字符。
  • 每个字符都对应一个 ASCII 码值。
1)普通字符
  • 普通字符就是用单引号括引起来的单个字符,如下:
	'a'
	'Q'
	'8'
	'?'
	'+'
	' '

2)转义字符
  • 转义字符是用引号引起来,并且内容为 斜杠 + 字符,例如我们之前遇到的用 '\n'代表换行,\t代表水平制表符(可理解为键盘上的 tab 键),'\\'代表一个反斜杠,等等;
  • 当然还可以用 '\ooo'来代替一个字符,其中一个数字o代表一个八进制数;也可以用 '\xhh'来代表一个字符,具体见如下代码:
#include <stdio.h>
int main() {
    char a = 65;
    char b = '\101';
    char c = '\x41';
    printf("%c %c %c\n", a, b, c);
    return 0;
} 

  • 以上的代码输出结果为:
A A A

  • 这是因为八进制下的101和十六进制的41在十进制下都是65,代表的是大写字母'A'的ASCII 码值。

【例题1】请问如何输出一个单引号?

四、字符串常量

  • 字符串常量,又称为字符串字面值,是括在双引号""中的。一个字符串包含类似于字符常量的字符:普通字符、转义序列。

1、单个字符串常量

#include <stdio.h>
int main() {
    printf( "光天化日学\x43语言!\n" );
    return 0;
} 

  • 我们可以用转义的'\x43'代表'C'和其它字符组合,变成一个字符串常量。以上代码输出为:
光天化日学C语言!

【例题2】如果我想要如下输出结果,请问,代码要怎么写?

"光天化日学C语言!"

2、字符串常量分行

  • 两个用""引起来的字符串,是可以无缝连接的,如下代码:
#include <stdio.h>
int main() {
	printf( 
		"光天化日学" 
		"C语言!\n" 
	);
	return 0;
} 

  • 这段代码的结果也是:
光天化日学C语言!

五、符号常量

1、#define

  • 利用 #define预处理器可以定义一个常量如下:
#include <stdio.h>
#define TIPS "光天化日学\x43语言!\n" 
#define love 1314
int main() {
    printf( TIPS );
    printf("%d\n", love);
    return 0;
} 

  • 以上这段代码,会将所有TIPS都原文替换为"光天化日学\x43语言!\n";将所有love替换为1314

2、const

  • const的用法也非常广泛,而且涉及到很多概念,这里只介绍最简单的用法,后面会开辟一个新的章节专门来讲它的用法。
#include <stdio.h> 
const int love = 1314;
int main() {
	printf( "%d\n", love );
	return 0;
}

  • 我们可以在普通变量定义前加上const,这样就代表它是个常量了,在整个运行过程中都不能被修改。

【例题3】下面这段代码会发生什么情况,自己编程试一下吧。

#include <stdio.h> 
const int love = 1314;
int main() {
    love = 520;
    printf( "%d\n", love );
    return 0;
}


  • 通过这一章,我们学会了 各种类型 的常量,希望对你有帮助哦 ~ 祝大家早日成为 C 语言大神!

课后习题

在这里插入图片描述


第二章

运算符和表达式

(09)- 算术运算符

一、算术运算符

  • 算术运算符主要包含以下几个:
  • 1)四则运算符,也就是数学上所说的加减乘除;
  • 2)取余符号;
  • 3)自增和自减。
  • 那么接下来让我们一个一个来看看吧。

1、四则运算符

  • 数学上的加减乘除和C语言的加减乘除的含义类似,但是符号表示方法不尽相同,对比如下:
/加法减法乘法除法
数学+-

×

\times

× | ÷ |
| C语言 | + | - | * | / |

1)加法
  • a + b代表两个操作数相加,代码如下:
#include <stdio.h>
int main() {
    int a = 1, b = 2;
    double c = 1.005, d = 1.995;
    printf("a + b = %d\n", a + b );
    printf("c + d = %.3lf\n", c + d);
    printf("a + c = %.3lf\n", a + c);
    return 0;
}


  • 这段代码的输出为:
a + b = 3
c + d = 3.000
a + c = 2.005

2)减法
  • a - b代表从第一个操作数中减去第二个操作数,代码如下:
#include <stdio.h>
int main() {
    int a = 1, b = 2;
    double c = 1.005, d = 1.995;
    printf("a - b = %d\n", a - b );
    printf("c - d = %.3lf\n", c - d);
    printf("a - c = %.3lf\n", a - c);
    return 0;
}

  • 这段代码的输出为:
a - b = -1
c - d = -0.990
a - c = -0.005

3)乘法
  • a * b代表两个操作数相乘,代码如下:
#include <stdio.h>
int main() {
    int a = 1, b = 2;
    double c = 1.005, d = 1.995;
    printf("a \* b = %d\n",    a \* b);
    printf("c \* d = %.3lf\n", c \* d);
    printf("a \* c = %.3lf\n", a \* c);
    return 0;
}


  • 这段代码的输出为:
a \* b = 2
c \* d = 2.005
a \* c = 1.005

4)除法

不同类型的除数和被除数会导致不同类型的运算结果。
  1)当 除数 和 被除数 都是整数时,运算结果也是整数;
    1.a)如果能整除,结果就是它们相除的商;
    1.b)如果不能整除,那么就直接丢掉小数部分,只保留整数部分,即数学上的 取下整
  2)除数和被除数中有一个是小数,那么运算结果也是小数,并且是 double 类型的小数。

  • 我们来看一段代码:
#include <stdio.h>
int main() {
    int a = 6, b = 3, c = 4;
    double d = 4;
    printf("a / b = %d\n", a / b );
    printf("a / c = %d\n", a / c);
    printf("a / d = %.3lf\n", a / d);
    return 0;
}

  • 输出结果如下:
a / b = 2
a / c = 1
a / d = 1.500

  • a能被整除b,所以第一行输出它们的商,即 2
  • a不能被整除c,所以第二行输出它们相除的下整,即 1
  • ad中,d为浮点数,所以相除得到的也是浮点数;

#include <stdio.h>
int main() {
    int a = 5, b = 0;
    int c = a / b;
    return 0;
}

  • 这里会触发一个异常,即 除零错。这种情况在 C语言中是不允许的,但是由于变量的值只有在运行时才会确定,编译器是没办法帮你把这个错误找出来的,平时写代码的时候一定要注意。

2、取余符号

  • 取余,也就是求余数,使用的运算符是%。C语言中的取余运算只能针对整数,也就是说,%两边都必须是整数,不能出现小数,否则会出现编译错误。
  • 例如:5 % 3 = 27 % 2 = 1

当然,余数可以是正数也可以是负数,由%左边的整数决定:
  1)如果%左边是正数,那么余数也是正数;
  2)如果%左边是负数,那么余数也是负数。

  • 我们继续来看一段代码:
#include <stdio.h>
int main()
{
    printf(
        "9%%4=%d\n"
        "9%%-4=%d\n"
        "-9%%4=%d\n"
        "-9%%-4=%d\n",
        9%4, 
        9%-4, 
        -9%4, 
        -9%-4
    );
    return 0;
}


  • 光天化日学C语言(08)- 常量 这一章中,我们提到的两个用""引起来的字符串是可以无缝连接的,所以这段代码里面四个字符串相当于一个。而%printf中是用来做格式化的,所以想要输出到屏幕上,需要用%%。于是,我们得到输出结果如下:
9%4=1
9%-4=1
-9%4=-1
-9%-4=-1

  • 印证了最后的符号是跟着左边的数走的。

3、自增和自减

  • 自增和自减的情况类似,所以我们只介绍自增即可。

	x = x + 1;

  • 我们也可以写成:
	x++;

  • 当然,也可以写成:
	++x;

  • 这两者的区别是什么呢?我们来看一段代码:
#include <stdio.h>
int main()
{
    int x = 1;
    printf( "x = %d\n", x++ );
    printf( "x = %d\n", x );
    return 0;
}

  • 输出结果是:
x = 1
x = 2

  • 这是因为x在自增前,就已经把值返回了,所以输出的是原值。我们再来看另一种情况:
#include <stdio.h>
int main()
{
    int x = 1;
    printf( "x = %d\n", ++x );
    printf( "x = %d\n", x );
    return 0;
}

  • 输出结果是:
x = 2
x = 2

  • 这是因为x先进行了自增,再把值返回,所以输出的是自增后的值。
  • 当然,自减也是同样的道理,大家可以自己写代码实践一下。

通过这一章,我们学会了:
  1)四则运算符;
  2)取余运算符;
  3)自增和自减运算符;

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

课后习题

在这里插入图片描述


(10)- 关系运算符

一、关系运算符

1、概览

  • 关系运算符是用来判断符号两边的数据的大小关系的。
  • C语言中的关系运算符主要有六个,如下:

2、表示方式

  • C语言中的关系运算符和数学中的含义相同,但是表示方法略有不同,区别如下:
关系运算符释义C语言表示数学表示
大于>>
大于等于>=
等于===
不等于!=
小于<<
小于等于<=
  • 关系运算符的两边可以是变量、数值 或 表达式,例如:
1)变量
  • a > b
  • a != b
2)数值
  • 3 > 5
  • a == 6
3)表达式
  • a + b > 4
  • a > a + b

二、关系运算符的应用

1、运算结果

  • 关系运算符的运算结果只有 0 或 1。当条件成立时结果为 1,条件不成立结果为 0。
  • 我们来看一段代码,如下:
#include <stdio.h>
int main() {
    printf("%d\n", 1 > 2);
    printf("%d\n", 1 < 2);
    return 0;
}

  • 得到的输出结果为:
0
1

  • 原因就是1 > 2在数学上是不成立的,所以结果为0;而1 < 2在数学上是不成立的,所以结果为1

2、运算符嵌套

  • 关系运算符是允许嵌套使用的,即运算的结果可以继续作为关系运算符的运算参数,例如以下代码:
#include <stdio.h>
int main() {
    printf("%d\n", 1 > 2 > -1);
    return 0;
}

  • 输出结果是多少呢?
  • 由于1 > 2的结果为0,所以1 > 2 > -1等价于0 > -1,显然是成立的,所以输出的结果为:
1

  • 有关于结合性的内容,会在运算符的内容都讲完后,就运算符优先级和运算符结合性进行一个统一讲解,现在这个阶段,你只需要知道,关系运算符都是左结合,即存在多个运算符,有没有括号的情况下,一律从左往右计算。

【例题1】给出以下代码,问输出的结果是什么。

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

3、运算符优先级

  • !===的优先级低于><>=<=
  • 优先级是什么呢?
  • 看个例子就能明白。
#include <stdio.h>
int main() {
    printf("%d\n", 1 < 2 == 1);
    return 0;
}

  • 我们可以做出两种假设:
  • 假设1==优先级低于<1 < 2优先计算,则表达式等价于1 == 1,成立,输出1
  • 假设2==优先级高于<2 == 1优先计算,则表达式等价于1 < 0,不成立,输出0
  • 实际上,这段代码的结果为:
1

  • ==的优先级低于<,当然,同学们可以试下 !=和其它符号的关系。
  • 另外,关系表达式会进场用在条件判断 if语句中,例如:
if(a < b) {
    // TODO
}

  • 我们会在将if语句的时候继续复习关系运算符相关的知识哦~

4、== 和 =

  • 初学者最容易犯的错是把===搞混,前者是判断相等与否,而后者是赋值。
  • 看一段代码,就能知道:
#include <stdio.h>
int main() {
    int a = 0; 
    printf("%d\n", a = 0);
    printf("%d\n", a == 0);
    return 0;
}

  • 以上这段代码的输出结果是:
0
1

  • 神不神奇,意不意外?!

通过这一章,我们学会了:
  1)6种关系运算符;
  2)关系运算符的嵌套;
  3)关系运算符的优先级;

  • 希望对你有帮助哦 ~ 当然,要多写代码尝试下文中提到的各种情况哦,祝大家早日成为 C 语言大神!

课后习题


(11)- 逻辑运算符

一、逻辑运算符

1、概览

  • 逻辑运算符是用来做逻辑运算的,也就是我们数学中常说的 “与或非”。
  • C语言中的逻辑运算符主要有三个,如下:

2、表示方式

  • C语言中的逻辑运算符和数学中的含义类似,但是表示方法截然不同,对应关系如下:
逻辑运算符释义操作数个数C语言表示数学表示
二元操作符&&

\land

∧ |
| 或 | 二元操作符 | || |

\lor

∨ |
| 非 | 一元操作符 | ! |

¬

\lnot

¬ |

  • 二元操作符的操作数是跟在符号两边的,而一元操作符的操作数则是跟在符号右边的。
  • 逻辑运算符的操作数可以是变量、数值 或 表达式。例如:
1)变量
  • a && b
  • a || b
  • !a
2)数值
  • a && 520
  • 1314 || 520
  • !0
3)表达式
  • a + b && c + d
  • a + b || c + d
  • !(a + b)

二、逻辑运算符的应用

1、运算结果

1)与运算(&&)

对于与运算,参与运算的操作数都为 “真” 时,结果才为 “真”,否则为 “假”。

#include <stdio.h>
int main() {
    printf("%d\n", 0 && 0);  // 0
    printf("%d\n", 5 && 0);  // 0
    printf("%d\n", 0 && 5);  // 0
    printf("%d\n", 5 && 9);  // 1
    return 0;
}

  • 注释中的内容,就是实际输出的内容。
  • 我们发现,无论操作数原本是什么,程序只关心它是 “零” 还是 “非零”。然后根据&&运算符自身的运算规则进行运算。

2)或运算(||)

对于或运算,参与运算的操作数都为“假”时,结果才为“假”,否则为“真”。

#include <stdio.h>
int main() {
    printf("%d\n", 0 || 0);  // 0
    printf("%d\n", 5 || 0);  // 1
    printf("%d\n", 0 || 5);  // 1
    printf("%d\n", 5 || 9);  // 1
    return 0;
}

  • 注释中的内容,就是实际输出的内容。
  • 我们同样发现,无论操作数原本是什么,程序只关心它是 “零” 还是 “非零”。然后根据||运算符自身的运算规则进行运算。

3)非运算(!)

对于非运算,操作数为 “真”,运算结果为 “假”;操作数为 “假”,运算结果为 “真”;

#include <stdio.h>
int main() {
    printf("%d\n", !0);  // 1
    printf("%d\n", !5);  // 0
    return 0;
}

  • 注释中的内容,就是实际输出的内容。
  • 八个字概括:非真即假,非假即真。

2、运算符嵌套

  • 和 关系运算符 一样,逻辑运算符也是可以支持嵌套的,即运算结果可以继续作为逻辑运算符的操作数,例如如下代码:
#include <stdio.h>
int main() {
    int a = !( (5 > 4) && (7 - 8) && (0 - 1) );
    printf("%d\n", a);
    return 0;
}

  • (5 > 4)(7 - 8)这两个表达式进行与运算,等价于:1 && 1,结果为1
  • 1(0 - 1)继续进行与运算,等价于1 && 1,结果为1
  • 1进行非运算,得到结果为 0
  • 所以这段代码最后输出的结果为:
0

3、运算符优先级

  • 接下来,我们看下三个运算符混合运用的情况,对于如下代码:
#include <stdio.h>
int main() {
    int a = !( 1 || 1 && 0 );
    printf("%d\n", a);
    return 0;
}

  • 这个问题的答案是:
0

  • 我们再来看个例子,区别只是在1 || 1的两边加上一个括号。
#include <stdio.h>
int main() {
    int a = !( (1 || 1) && 0 );
    printf("%d\n", a);
    return 0;
}

  • 现在输出的答案变成了:
1

  • 这是为什么呢?
  • 因为&&的优先级是比||要高的,所以在没有任何括号的情况下,&&会优先计算,简而言之,对于刚才的( 1 || 1 && 0 ),我们把它等价成( 1 || (1 && 0) ),这样是不是就好理解了。
  • 用类似的方法,我们可以得到 !的优先级是最高的,所以这三个符号的优先级排序如下:

<

&

&

<

!

|| \ < \ && \ < \ !

∣∣ < && < !

  • 当然,后面的章节,我们会对 算术运算符、关系运算符、逻辑运算符 等等所有的运算符的优先级 和 结合性 进行一个梳理,尽情期待 ~~

通过这一章,我们学会了:
  1)与运算:有假必假;
  2)或运算:有真必真;
  3)非运算:非真即假,非假即真;

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

课后习题


(12)- 类型转换

一、概念

  • 类型转换 就是将数据(即 变量、数值、表达式 等的结果)从一种类型转换成另一种类型。
  • 今天的章节主要围绕以下内容展开:

二、类型转换

1、自动类型转换

  • 这个过程不需要写代码的人干预,会自动发生。
  • 自动类型转换主要发生在两个时机:赋值 和 运算。
1)赋值
  • 将一种类型的数据赋值给另外一种类型的变量时会发生自动类型转换,如下代码所示:
#include <stdio.h>
int main() {
    float love = 520;
    return 0;
}

  • 这里的520原本是int类型的数据,为了赋值给love,他需要转换成float类型。
  • 再来看另一个例子:
#include <stdio.h>
int main() {
    int loveyou = 11 / 9;
    return 0;
}

  • love / 9的值明显不是一个整数,但是它需要赋值给int,所以需要先转换为int类型以后,才能赋值给变量loveyou
  • 由于在赋值运算中,赋值号两边的数据类型不同时,需要把右边数据的类型转换为左边变量的类型,这可能会导致数据失真,或者精度降低(例如上面例子中所说的浮点数转整数,就会截掉小数部分)。
2)运算
  • 在不同类型的混合运算中,编译器也会自动地转换数据类型,将参与运算的所有数据先转换为同一种类型,然后再进行计算。转换的规则如下:

转换原则如下:
  1)数据长度短的向输出长度长的进行转换;
  2)精度低的向精度高的进行转换;

  • 注意,所有的浮点运算都是以双精度进行的,即使运算中只有float类型,也要先转换为double类型,才能进行运算。
  • charshort参与运算时,必须先转换成int类型。
  • 来看一个计算圆周长的例子:
#include <stdio.h>
#include <math.h>
const float PI = acos(-1.0);  // 3.1415926535...
int main(){
    int c1, r = 10;
    double c2;
    c1 = 2 \* PI \* r;
    c2 = 2 \* PI \* r;
    printf("c1=%d, c2=%lf\n", c1, c2);
    return 0;
}

  • 输出结果为:
c1=62, c2=62.831855

  • 上述例子中,c1int类型,c2double类型,赋值号右边的内容是计算圆的周长,完全相同,但是就是由于被赋值的变量类型不同,从而导致运算结果截然不同。
  • 虽然表达式的结果都是double类型。但由于c1int类型,所以赋值运算的结果仍为int类型,舍去了小数部分,导致数据失真。

二、强制类型转换

  • 自动类型转换是编译器根据代码的上下文环境自行判断的,有时候并不是那么智能,不能满足所有的需求。所以有时候需要写代码的人,也就是程序员能够自己在代码中明确地提出要进行类型转换,这就是强制类型转换。

1)强制类型转换的格式
(type_name) expression

  • 其中 type_name 为新类型名称,expression为需要进行强制类型转换的表达式。
2)64位整数强转
  • 让我们来看个非常容易犯错的例子:
#include <stdio.h>
int main(){
    long long x = 1 << 32;
    printf("%lld\n", x); 
    return 0;
}

  • 我们想要干的事情,就是计算

2

32

2^{32}

232 并且存到变量x中。

  • 然而,这个程序的输出结果为:
0

2

32

1

2^{32}-1

232−1,所以这里显然是超了。

  • 更加具体的原因,这里的1int类型,所以进行左移32位时,产生了溢出,所以变成了0,这里涉及到补码相关的知识,我会在后续章节详细进行讲解。
  • 所以,我们需要先把 1 强制转换成long long再进行左移运算,如下:
#include <stdio.h>
int main(){
    long long x = (long long)1 << 32;
    printf("%lld\n", x); 
    return 0;
}

  • 得到的结果为:
4294967296

  • 是我们期望的结果,即

2

32

2^{32}

232。

3)浮点数强转
  • 另一个比较经典的例子,就是我们计算除法的时候,如下:
#include <stdio.h>
int main(){
    int a = 10;
    int b = 3; 
    double c = a / b;
    printf("%lf\n", c); 
    return 0;
}

  • 得到的结果为:
3.000000

  • 原因是因为ab都是int类型,如果不进行干预,那么a / b的运算结果也是int类型,小数部分将被丢弃;虽然是cdouble类型,可以接收小数部分,但是在赋值之前,小数部分提前就被舍弃了,它只能接收到整数部分,这就导致除法运算的结果失真。
  • 修改方案如下:
#include <stdio.h>
int main(){
    int a = 10;
    int b = 3; 
    double c = (double) a / b;
    printf("%lf\n", c); 
    return 0;
}

  • 我们只需要将ab其中之一转换成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转换成指针,当然,有些强制转换可能直接导致程序崩溃。

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

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

课后习题


(13)- 位运算概览

一、再谈二进制

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

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,也就是十进制下的 “十”。

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

  • 而我们今天要讲的位运算正是基于二进制展开的。

二、位运算简介

  • 位运算可以理解成对二进制数字上的每一个位进行操作的运算。
  • 位运算分为 布尔位运算符 和 移位位运算符。
  • 布尔位运算符又分为 位与(&)、位或(|)、异或(^)、按位取反(~);移位位运算符分为 左移(<<) 和 右移(>>)。
  • 如图所示:

三、位运算概览

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

1、布尔位运算

  • 对于布尔位运算,总共有四个,如下表所示:
C语言运算符表示含义示例
&位与x & y
``位或
^异或x ^ y
~按位取反x ~ y
1)位与
  • 位与就是对操作数的每一位按照如下表格进行运算,对于每一位只有 0 或 1 两种情况,所以组合出来总共

2

2

=

4

2^2 = 4

22=4 种情况。

左操作数右操作数结果
000
010
100
111

#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)位或
  • 位或的运算结果如下:
左操作数右操作数结果
000
011
101
111

  • 我们来看以下这段程序:
#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)异或
  • 异或的运算结果如下:
左操作数右操作数结果
000
011
101
110

  • 我们来看以下这段程序:
#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;
}

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


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

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

课后习题


(14)- 位运算 & 的应用

一、位与运算符

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

2

2

=

4

2^2 = 4

22=4 种情况。

左操作数右操作数结果
000
010
100
111
  • 通过这个表,我们得出一些结论:
  • 1)无论是 0 或 1,只要位与上 1,还是它本身;
  • 2)无论是 0 或 1,只要位与上 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​ 的每一位做表格中的&运算。

  • 所以最后输出结果为:
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】给定一个数,求它的二进制表示的末五位,以十进制输出即可。

  • 这个问题的核心就是:我们只需要末五位,剩下的位我们是不需要的,所以可以将给定的数 位与上0b11111,这样一来就直接得到末五位的值了。
  • 代码实现如下:
#include <stdio.h>
int main() {
    int x;
    scanf("%d", &x);
    printf("%d\n", (x & 0b11111) );
    return 0;
} 

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

4、消除末尾连续1

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


通过这一章,我们学会了:
  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

还有兄弟不知道网络安全面试可以提前刷题吗?费时一周整理的160+网络安全面试题,金九银十,做网络安全面试里的显眼包!

王岚嵚工程师面试题(附答案),只能帮兄弟们到这儿了!如果你能答对70%,找一个安全工作,问题不大。

对于有1-3年工作经验,想要跳槽的朋友来说,也是很好的温习资料!

【完整版领取方式在文末!!】

93道网络安全面试题

内容实在太多,不一一截图了

黑客学习资源推荐

最后给大家分享一份全套的网络安全学习资料,给那些想学习 网络安全的小伙伴们一点帮助!

对于从来没有接触过网络安全的同学,我们帮你准备了详细的学习成长路线图。可以说是最科学最系统的学习路线,大家跟着这个大的方向学习准没问题。

😝朋友们如果有需要的话,可以联系领取~

1️⃣零基础入门
① 学习路线

对于从来没有接触过网络安全的同学,我们帮你准备了详细的学习成长路线图。可以说是最科学最系统的学习路线,大家跟着这个大的方向学习准没问题。

image

② 路线对应学习视频

同时每个成长路线对应的板块都有配套的视频提供:

image-20231025112050764

2️⃣视频配套工具&国内外网安书籍、文档
① 工具

② 视频

image1

③ 书籍

image2

资源较为敏感,未展示全面,需要的最下面获取

在这里插入图片描述在这里插入图片描述

② 简历模板

在这里插入图片描述

因篇幅有限,资料较为敏感仅展示部分资料,添加上方即可获取👆

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

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

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

  • 19
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值