字数16,932 字,阅读时间 90 分
第一章:对于程序员来说CPU是什么
CPU的内部结构解析和CPU是寄存器的集合体
(1)CPU是英文Cental Prcocessing Unit(中央处理器)的缩写,相当于计算机的大脑。
CPU的内部由控制器、寄存器、运算器和时钟四个部分组成;
寄存器用来暂存数据和指令等对象,不同CPU内部有20-几百个
运算器:运算从内存读入寄存器的数据
时钟:发出CPU开始计时的时钟信号,1GHz=10亿次每秒
对于程序员来说,上面三个无需过多关注。
数据启动后:根据时钟信号,控制器会从内存中读取指令和数据。运算器会对数据进行运算;控制器根据运算结果来控制计算机。
控制器:把内存上的指令丶数据等读入寄存器,并且根据指令的执行结果拉控制整个计算机
(2)CPU是寄存器的集合体
程序是把寄存器作为对象来描述的
程序员眼中的CPU是寄存器的集合体
汇编语言采用助记符(memonic)来编写程序,与机器语言基本一一对应
汇编:汇编代码到机器代码
反汇编:机器码到汇编代码
决定程序流程的程序计数器和条件分支和循环机制
(1)CPU每执行一个指令,程序计数器的值就会自动加1。列如,CPU执行0100地址的指令后,程序计数器的值就变成了0101(当执行的指令占据多个内存地址时,增加与指令长度相应的数值)。然后,CPU的控制器就会按照程序计数器的数值,从内存中读取命令并执行。也就是说,程序计数器决定着程序的流程。
(2)程序执行(程序计数器中存放下一条需要执行的指令和地址):
- 顺序执行(程序计数器每次+1)
- 条件分支(跳转指令)
- 循环(反复执行跳转指令)
函数的调用和通过地址与索引实现数组和
(1)函数调用(单纯的跳转指令无法实现函数的调用):
机器语言通过call和return指令来解决函数调用的跳转问题,在将函数的入口地址赋值到程序计数器之前,call指令会将调用函数后要执行的指令地址存储在栈的主存内,函数处理完毕后,通过函数出口执行return命令,其功能就是把保存在栈中的地址赋值给程序计数器。
(2)通过地址和索引实现数组
通过基质寄存器和变址寄存器,可以对主内存的特定内存区域进行划分,从而实现类似于数组的操作。
实际地址=基质寄存器的值+变址寄存器的值
变址寄存器的值相当于高级语言中的数组索引
- 程序运行的基本流程是什么?
①编写高级语言代码 → ②编译为机器语言的EXE文件 → ③运行时生成内存副本 → ④CPU解释执行3。 - CPU的核心功能是什么?
CPU负责解释和运行转换为机器语言的程序,由寄存器、控制器、运算器和时钟组成。寄存器暂存指令和数据,控制器协调内存与寄存器间的数据流动
第二章:二进制与数据
用二进制数表示计算机信息的原因,什么是二进制数,位移运算和乘除运算的关系,便于计算机处理的”补数“
计算机处理信息的最小单位是位(bit),因为计算机内部使用IC(集成电路)构成的。IC所有的引脚只有直流电压0v或5v俩个状态。
字节(byte)是信息的基本单位 1个字节=8位
计算机内部所有信息都用二进制数处理
位移运算指的是将二进制数值的各位数进行左右位移(shift=移位)的运算,移位有左移(向高位方向)和右移(向低位)两种。
- 二进制表示负数值时,一般会把最高位作为符号来使用,因此把这个最高位称为符号位。符号位是0时表示正数,符号位是1时表示负数。
- 但“1的二进制数是00000001,因此-1就是10000001”通常是错误的,正确答案是11111111(补码)
因为计算机在做减法运算时,实际上内部是在做加法运算。
- 计算机使用补码表示负数
- 补数求解的变换方式就是:“取反”+1
补数的计算方式:取反加1
- 5:
0000000000101
(正值) - -6:
1111111111010
(取反) - -5:
1111111111011
(补码)取反+1
逻辑右移和算数右移的区别,掌握逻辑运算的窍门
左移时逻辑左移和算数左移都只需要在空出来的低位补0即可;右移时算数右移和逻辑右移有区别。
逻辑右移:移位后,需要在空出来的高位补0。
算数右移:移位后,需要在空出来的高位补符号位的值(0或者1),正数补0,负数补1。
- 逻辑非(NOT)
- 逻辑与(AND)
- 逻辑或(OR)
- 逻辑异或(XOR)
逻辑的值,不要当数值,要当开关。
- 32位是几个字节?
- 答:4
- 二进制数01011100转换成十进制数是多少?
- 答:92
- 二进制数00001111左移两位后,会变成原数的几倍?
- 答:4。
- 解释:二进制左移一位就是两倍,十进制左移一位是10倍
- 反转部分图形模式时,使用的是什么逻辑运算?
- 答:XOR运算
- 补码形式表示8位二进制数10101010,用16位的二进制数表示的话是多少?
- 答:11111111 10101010。
- 解释:前面用符号位填充。
第三章:计算机进行小数运算时出错的原因
计算机将0.1累加100次也得不到10.原因是计算机计算十进制0.1会先转换成二进制,会变成0.00011001100…(1100循环),就像十进制无法表示1/3一样,只能是无限接近。
什么是浮点数,浮点数计算
浮点数是指用符号丶尾数丶基数和指数这四部分来表示的小数。
float:单精度浮点数(32位)
double:双精度浮点数(64位)
尾数部分用得是“将小数点前面的值固定位1的正则表达式”,而指数部分用的则是“EXCESS系统表现”
基数的表达式
指数的表达式
不同的形式表达同一个十进制或者二进制数值
0.75 = 0.75 × 10⁰
0.75 = 75 × 10⁻²
0.75 = 0.075 × 10¹
为了方便计算机处理,需要制定一个统一的规则:“将小数点前面的值固定为1的正则表达式”
±m×2e
十进制0.75用二进制表示
+1.5X2的负一次方
如何避免计算机出错,二进制数和十六进制数
回避策略,无视这些错误。即误差在可接受范围之内。
另一个策略是把小数转换成整数来计算。可以避免误差的积累。
BCD(Binary Coded Decimal):就是用4位二进制来表示0~9的1位数字的处理方法。
进制转换
- 二进制到十六进制的转换:
- 32位二进制数:
0011 1101 1100 1100 1100 1100 1101
- 对应的8位十六进制数:
3DCCCCCD
- 32位二进制数:
- 二进制小数到十六进制的转换:
- 二进制小数:
1011.011
- 转换为:
1011.0110
(小数点后补足4位,相当于十六进制的1位) - 对应的十六进制小数:
B.6
- 二进制小数:
- 总结:
- 通过使用十六进制,可以使得表示数字的位数变短,从而简化数据的表示和处理
- 为何0.1无法精确表示为二进制小数?
0.1转换为二进制是无限循环小数(0.00011001100…),导致精度丢失7。 - 浮点数如何表示?
包含符号、尾数、基数和指数四部分,尾数采用“小数点前固定为1”的正则表达式,指数用EXCESS系统避免负数符号
第四章:熟悉使用有棱有角的内存
内存的物理机制,逻辑,指针
内存实际上是一种名为内存 IC 的电子元件。
内存 IC 中有电源、地址信号、数据信号、控制信号等用于输入输出的大量引脚,通过为其指定地址(address),来进行数据的读写。
VCC、GND: | 电源 |
---|---|
A0 ~ A9: | 地址信号 |
D0 ~ D7: | 数据信号 |
RD、WR: | 控制信号 |
地址个数:210=1024
每个地址:8位(1字节)
内存容量:1024 × 1 = 1k
ROM(Read Only Memory)是一种只能用来读取的内存。
RAM分为需要经常刷新(refresh)以保存数据的DRAM,以及不需要刷新电路即能保存数据的SRAM(Static RAM)。
指针:存放地址的变量。 指针的数据类型表示一次可以读写的长度。
变量的数据类型不一样,所占用的内存大小也不同
数组,栈丶队列以及环形缓冲区
数组:
- 数组的结构与内存的物理构造一致。(高效:不需要太多指令就可以操作)
- 多个同样数据类型的数据在内存中连续排列的形式。
- 作为数组元素的各个数据会通过连续的编号被区分开来,这个编号称为索引(index)。
CPU是通过基址寄存器和变址寄存器来制定内存地址。
栈和队列,都可以不通过指定地址和索引来对数组的元素进行读取。
栈:LIFO(Last In First Out,后进先出)
队列:排队,FIFO(First In First Out,先进先出) 队列一般是以环状缓冲区(ring buffer)的方式来实现的。
链表或二叉查找树追加元素和删除
链表和二叉查找树,都不用考虑索引的顺序就可以进行读写:
- 二叉查找树:高效的对容器中的元素进行检索。
- 链表:高效的对容器中的元素进行删除、插入。
单向链表和双向链表
二叉查找树是指在链表的基础上,在数组中追加元素时,考虑到数据的大小关系,将其分成左右两个方向的表现形式。
树(tree)构造指的是数据像树一样分叉连接的方式。二叉查找树也是树构造的一种。
- 10位地址引脚的内存IC寻址范围是多少?
0~1023(即1KB容量)1。 - 指针变量在32位系统中的长度是多少?
4字节(32位),与地址总线位数一致1。 - LIFO结构指什么?
栈(Stack),后进先出,用于函数调用时的地址存储1。
第五章:内存和磁盘的亲密关系
不读入内存就无法运行,磁盘缓存加快了磁盘访问速度,虚拟内存把磁盘作为部分内存来使用
计算机中主要的存储部件是内存和磁盘
– 内存:高速价高
– 磁盘:低速廉价
– CPU需要通过程序计数器来指定内存地址,然后才能读出程序。
– 即使CPU可以直接访问磁盘,程序的运行速度也会降低(磁盘读取速度慢)
磁盘缓存(disk cache)
– 把从磁盘读出的数据存储到内存中的方式。
– 再次访问时,直接从内存(磁盘缓存部分)获取,提高访问速度
– 随着硬盘访问速度的大幅改善,现在作用已经不明显了。
把内存当磁盘,提高访问速度
虚拟内存(virtual memory)
– 把一部分磁盘作为内存的一部分
– 实际运行的程序部分,在运行时刻必须在内存中
– 需要通过物理内存和虚拟内存的置换(swap)来实现
把磁盘当内存,运行更大的程序
Windows 虚拟内存采用分页式方法,即在不考虑程序构造的情况下,把运行的程序按照一定大小的页(page)进行分割,并以页为单位在内存和磁盘间进行置换。
Windows页大小一般是4KB
节约内存的编程方法,
磁盘的物理结构
为了从根本上解决内存不足的问题,需要增加内存的容量,或者尽量把运行的应用文件变小。
通过 DLL 文件实现函数共有
静态链接(Static Link):
– 编译时把函数打包到 exe
动态链接(Dynamic Link):
– 编译时把函数打包成 DLL
– exe 执行时动态加载 DLL
– 多个应用程序可以共有同一个 DLL
– 在不变更 exe 的情况下,只通过升级 DLL 就可以更新程序。
静态链接导致内存利用率下降,动态链接可节约内存
磁盘通过把其物理表面划分成多个空间来使用的。
- 划分方式有扇区方式、可变方式
- 扇区方式:划分成固定长度的空间
- 扇区是对磁盘进行物理读写的最小单位
- 一般一个扇区是512个字节
- 簇(cu):扇区的倍数,磁盘的容量越大,簇的容量就越大。
- 不同的文件不能存储在同一个簇中。
- 所有文件都占1簇的整数倍的磁盘空间。
- 可变方式:划分成可变长度的空间
- 扇区方式中,把磁盘表面分成若干个同心圆的空间就是磁道。
扇区和簇的大小,是由处理速度和存储容量的平衡来决定的
– 以簇为单位进行读写时,1簇中没有填满的区域会保持不被使用的状态。
– 如果减少簇的容量,磁盘访问次数就会增加,就会导致读写文件的时间变长。
– 由于在磁盘表面上,表示扇区区分的领域是必要的,因此,如果簇的容量过小,磁盘的整体容量也会减少
- 如何通过内存提高磁盘访问速度?
使用磁盘缓存机制,将频繁访问的磁盘数据暂存至内存7。 - 虚拟内存的作用是什么?
将磁盘空间模拟为内存使用,扩展可用内存容量
第六章:亲自尝试压缩数据
文件就是字节数据的集合体
RLE算法的机制,RLE算法的缺点
压缩后同压缩前的文件大小的比率,称为压缩比率或压缩比。
虽然针对相同数据经常连续出现的图像、文件等,RLE算法可以发挥不错的效果。在实际的文本文件中,同样字符多次重复出现的情况并不多见。
通过莫尔斯编码来看哈夫曼算法的基础,二叉树实现哈夫曼编码,可逆压缩和非可逆压缩
哈夫曼算法
实施难度会比较大,因为只能按bit,而不能按byte处理
莫尔斯编码和哈夫曼编码都利用字符的出现频率来分配编码长度。
莫尔斯编码是根据日常文本中字符出现的频率来决定字符长度的,压缩比例不高
哈夫曼编码是根据压缩对象的出现频率来决定字符长度的,压缩比例高
两者都遵循“高频字符用短码,低频字符用长码”的原则。
二叉树实现哈夫曼编码
哈夫曼编码是一种广泛使用的无损数据压缩算法,它通过构建一棵二叉树(哈夫曼树)来实现字符的编码。这种编码方式基于字符出现的频率:出现频率越高的字符,其编码越短;出现频率越低的字符,其编码越长。
哈夫曼编码表AAAAAABBCDDEEEEF,结果为0000000000001001001101011010101010101111,40位=5字节(这里为不包含哈夫曼编码信息的情况)。
哈夫曼编码表AAAAAABBCDDEEE 00000000000010010011010101040位=5字节(这里为不包含哈夫曼编码信息的情况)。
没有经过压缩的图片格式:
BMP(Bitmap,点映射)
经过压缩的图片格式:
GIF(Graphics Interchange Format)这种格式要求色数不超过 256 色。
JPEG(Joint Photographic Experts Group)
TIFF(Tag Image File Format)
- RLE与哈夫曼算法的区别?
RLE用“数据×重复次数”压缩(如AAABB→A3B2),适用于简单重复数据;哈夫曼算法通过二叉树优化编码,适用于高频数据压缩311。 - 可逆压缩与非可逆压缩的区别?
可逆压缩(如ZIP)可完全还原数据,非可逆压缩(如JPEG)会丢失部分信息以减小体积11。
第七章:程序是在何种环境中运行的
运行环境,windows操作系统,源代码,API,Porting
运行环境=操作系统+硬件
CPU如何处理机器语言,以及源代码如何通过编译过程转换为本地代码
- CPU与机器语言:
- CPU只能解释自身固有的机器语言。
- 不同的CPU能解释的机器语言种类不同,例如x86、MIPS、SPARC、PowerPC等,它们的机器语言是完全不同的。
- 本地代码(Native Code):
- 机器语言的程序称为本地代码。
- 程序员用C语言等高级语言编写的程序,在编写阶段是文本文件,可以显示和编辑,称之为源代码。
- 通过对源代码进行编译,可以得到本地代码。
- 在市面上出售的用于Windows的应用软件包CD-ROM中,收录的是本地代码,而不是源代码。
- API(应用程序编程接口):
- 应用程序向操作系统传递指令的途径称为 API。
- Windows 和 Unix 系列操作系统的 API 提供了应用程序可以利用的函数组合。
- 不同操作系统的 API 存在差异,因此将应用程序移植到其他操作系统时,需要重写应用中利用到 API 的部分,如键盘输入、鼠标输入、显示器输出、文件输入输出等。
- 跨平台兼容性:
- 在同类型操作系统下,API 基本上没有差别,因此针对某特定操作系统的 API 编写的程序可以在任何硬件上运行。
- 由于 CPU 种类不同,机器语言也不相同,因此本地代码也不同,需要利用编译器生成各 CPU 专用的本地代码。
移植(Porting)是指将软件从一种硬件或软件环境迁移到另一种环境的过程。这个过程可能涉及到修改软件的源代码,以便它能够在新的环境下正确运行。移植的目的是使软件能够在不同的操作系统、硬件平台或编程语言环境中工作,而不需要从头开始重新编写整个程序。
虚拟机获得其他操作系统,BIOS和引导
即使不通过移植,也可以利用虚拟机软件在一台计算机上运行其他操作系统的应用。
从操作系统方面看,java虚拟机是一个应用,从java应用方面来看,java虚拟机就是运行环境
- Java虚拟机(Java VM):
- Java虚拟机提供了一种不依赖于特定硬件及操作系统的程序运行环境。
- Java源代码编译后生成的是字节代码,而不是特定CPU使用的本地代码。
- Java虚拟机在运行时将字节代码转换成本地代码,并由本地CPU处理。
- Java虚拟机的工作原理:
- 编译器将源代码(如 sample.java)转换成字节代码(sample.class)。
- Java虚拟机(java.exe)将字节代码转换成x86系列CPU适用的本地代码。
- Java虚拟机的优势:
- 同样的字节代码可以在不同的环境下运行,只要结合各种类型的操作系统和硬件作成Java虚拟机。
- Java虚拟机的局限性:
- 不同的Java虚拟机之间无法进行完整互换,因为想让所有字节代码在任意Java虚拟机上都能运行是比较困难的。
- 当使用只适用于某些特定硬件的功能时,可能会出现在其他Java虚拟机上无法运行或功能受限的情况。
BIOS(基本输入输出系统)是存储在 ROM 中的系统,预先内置在计算机主机内部。它负责基本的硬件控制,如键盘、磁盘、显卡等。BIOS 还具有启动“引导程序”的功能。
引导程序:引导程序是存储在启动驱动器起始区域的小程序操作系统的启动驱动器一般是硬盘,但也可以是 CD-ROM 或软盘。引导程序的功能是将硬盘等记录的操作系统加载到内存中运行。
- 运行环境的定义是什么?
指操作系统与硬件组合,不同环境(如Windows与MacOS)需特定编译信息,导致应用无法跨平台直接运行911。 - Java虚拟机的作用与局限?
解释字节码实现跨平台,但依赖虚拟机版本且转换本地代码可能导致性能损耗59。
第八章:从源文件到可执行文件
计算机代码,编译
源代码是文本文件,可用记事本等编辑器编写,但 CPU 无法直接运行。CPU 仅能解析并执行本地代码(即机器语言)。不同编程语言(C、C++、BASIC、Pascal 等)编写的源代码,最终均需转换成本地代码,否则 CPU 无法理解。
- 本地(Native):指 CPU 的“母语”机器语言,本地代码即机器语言程序。
- 跨语言一致性:无论源语言为何,转换后的本地代码均以同一机器语言表示。
- 源代码(Source Code):原始代码,扩展名通常与语言相关(如 C 语言为
.c
)。 - 编译器作用:将高级语言代码翻译为本地代码,例如 C++ 编译器可兼容 C 语言源文件。
编译器的特性:
- 与 CPU 架构相关:不同 CPU 类型(如 x86、PowerPC)需要不同的编译器。例如,x86 编译器生成的本地代码无法直接在 PowerPC CPU 上运行。
- 跨平台兼容性:同一份源代码可通过不同编译器生成适用于不同 CPU 的本地代码(图 8-5)。
1. 编译器的运行环境
- 编译器本身是程序,需在特定环境中运行(如 Windows 或 Linux)。
- 交叉编译器:允许在一种环境中生成另一种 CPU 架构的本地代码。例如:
- 在 Windows 环境下生成 SH(SuperH)或 MIPS 架构的本地代码,用于嵌入式设备(如 PDA、车载 GPS)。
- 通过交叉编译器为 Windows CE 系统开发程序。
2. 可执行文件的生成
- 编译后的本地代码仍需经过链接(Link)等步骤,整合库文件与资源,最终生成可执行文件。
- 本地代码的可视化:
- 用记事本打开
.exe
文件会显示乱码(图 8-3)。 - 通过 Dump 工具 可将文件内容转换为十六进制格式,每个字节对应一条指令或数据(图 8-4)。
- 用记事本打开
仅靠编译无法得到可执行文件
编译与链接的关系
链接:整合目标文件、启动文件及库文件,生成可执行文件(.exe
)
编译:将源代码转换为目标文件(.obj
),但目标文件是未完成的本地代码,无法直接运行。
示例命令:bash复制bcc32 -W -c Sample1.c # 生成 Sample1.obj
DLL 文件及导入库,程序加载时会生成栈和堆
DLL(Dynamic Link Library)
- 特点:动态链接库,程序运行时加载。
- 优势:节省内存,支持多程序共享同一函数库(如
user32.dll
)。
导入库的作用
示例:import32.lib
告知链接器 MessageBox()
位于 user32.dll
。
存储动态链接信息(如函数位置和 DLL 路径),不包含实际代码。
静态链接 vs. 动态链接
类型 | 静态链接库(.lib) | 动态链接库(DLL) |
---|---|---|
代码整合 | 直接嵌入 EXE 文件 | 运行时动态加载 |
更新维护 | 需重新编译 | 替换 DLL 即可生效 |
示例文件 | cw32.lib (含 sprintf ) | user32.dll (含 MessageBox ) |
当 EXE 文件加载到内存后,程序的内存布局由以下四部分构成:
- 变量组:存储全局变量(如
title
)。 - 函数组:存储函数逻辑(如
WinMain
)。 - 栈(Stack):存储局部变量(如
ave
、buff
)和函数参数。 - 堆(Heap):存储程序运行时动态分配的数据或对象。
栈与堆的对比
特性 | 栈(Stack) | 堆(Heap) |
---|---|---|
内存管理 | 编译器自动分配/释放(函数调用时处理) | 程序员显式分配/释放(需手动管理) |
生命周期 | 随函数调用开始和结束 | 从分配持续到显式释放 |
典型操作 | 自动处理,无需代码干预 | C: malloc() /free() ;C++: new /delete |
风险 | 栈溢出(如递归过深) | 内存泄漏(未释放导致内存耗尽) |
内存泄漏(Memory Leak)
- 定义:未释放堆中不再使用的内存空间。
- 后果:持续累积可能导致内存不足,引发程序或系统崩溃。
栈和堆的分配机制
- EXE 文件与内存结构的差异
- EXE 文件中仅包含变量组、函数组及再配置信息,不包含栈和堆。
- 栈和堆的内存空间在程序运行时动态分配。
- 栈的分配特点
- 自动管理:函数调用时分配局部变量,函数返回时自动释放。
- 高效但受限:栈空间大小有限(默认约 1-8 MB),超出会导致栈溢出错误。
- 堆的分配特点
- 灵活但需谨慎:可申请大块内存(受系统可用内存限制),但需手动释放。
- 跨函数持久性:堆中数据生命周期不受函数调用限制。
- 栈与堆的性能权衡:
- 栈:速度快,适合小型临时数据。
- 堆:速度较慢(涉及系统调用),适合大型或生命周期复杂的数据。
Q1:编译器和解释器有什么不同?
- 编译器:在程序运行前一次性将所有源代码转换为本地代码(如生成
.exe
文件)。- 示例:C/C++ 编译器(如 GCC、Clang)。
- 解释器:在程序运行时逐行解释并执行源代码,不生成独立可执行文件。
- 示例:Python、JavaScript 的解释器。
Q2:“分割编译”指的是什么?
- 定义:将大型程序拆分为多个源文件(如
module1.c
、module2.c
),分别编译为独立的目标文件(.obj
),最终通过链接器合并为单一可执行文件。 - 优势:
- 便于协作开发(不同程序员负责不同模块)。
- 减少编译时间(仅需重新编译修改过的文件)。
Q3:“Build”指的是什么?
- 定义:集成开发环境(IDE)中的一个流程,自动执行编译和链接,生成最终可执行文件。
- 示例:在 Visual Studio 中选择“生成解决方案”(Build Solution)。
Q4:使用 DLL 文件的好处是什么?
- 核心优势:
- 资源共享:多个程序可共用同一 DLL 中的函数(如
user32.dll
的MessageBox()
)。 - 节省内存与磁盘空间:DLL 在内存中仅加载一次,避免重复占用。
- 维护便捷:更新 DLL 时无需重新编译依赖它的程序。
- 资源共享:多个程序可共用同一 DLL 中的函数(如
Q5:不链接导入库的语法无法调用 DLL 文件中的函数吗?
- 答案:可以,但需手动操作:
- 使用
LoadLibrary()
加载 DLL 文件。 - 使用
GetProcAddress()
获取函数地址。
- 缺点:代码复杂度高,需显式管理函数指针。
- 推荐方式:链接导入库(如
import32.lib
),编译器自动处理符号绑定。
- 使用
Q6:“叠加链接”这个术语指的是什么?
- 定义:一种内存优化技术,将不会同时执行的函数交替加载到同一内存地址中运行。
- 历史背景:在 MS-DOS 时代内存有限(如 640KB),通过叠加链接减少内存占用。
- 现代应用:基本被虚拟内存机制取代。
Q7:和内存管理相关的“垃圾回收机制”指的是什么?
- 定义:自动回收堆中不再使用的内存空间,防止内存泄漏。
- 实现方式:语言管理方式示例操作C/C++手动管理
free()
(C)、delete
(C++)Java/C#自动垃圾回收(GC)由运行时环境自动追踪并释放 - 优势:避免程序员疏忽导致的内存泄漏问题。
第九章:操作系统和应用的关系
应用(Word/Excel)➡️依赖 操作系统(Windows) ➡️管理 硬件(CPU、内存等)
windows操作系统
监控程序可以说是操作系统的原型。监控程序具有加载和运行程序的功能。
操作系统本身并不是一个单独的程序,而是由多个程序组成的集合体。
Windows操作系统是大多数用户常用的操作系统,具有以下主要特征:
- 32位操作系统(也有64位版本):
- 32位操作系统表示处理效率最高的数据大小为32位。
- Windows处理数据的基本单位是32位。
- 通过API函数集来提供系统调用:
- Windows操作系统通过API(应用程序编程接口)函数集来提供系统调用,方便程序员进行系统级操作。
- 提供采用了图形用户界面的用户界面:
- Windows操作系统提供图形用户界面(GUI),使用户能够通过图形化的方式与系统交互。
- 通过WYSIWYG实现打印输出:
- WYSIWYG(What You See Is What You Get)即所见即所得,Windows操作系统支持这种打印输出方式。
- 提供多任务功能:
- Windows操作系统支持多任务处理,允许用户同时运行多个应用程序。
- 提供网络功能及数据库功能:
- Windows操作系统提供网络连接和数据库管理功能,方便用户进行网络通信和数据存储。
- 通过即插即用实现设备驱动的自动设定:
- Windows操作系统支持即插即用技术,能够自动检测和配置新连接的硬件设备。
windows操作系统和编程
- 系统调用:
- 操作系统的硬件控制功能通常通过一些小的函数集合体来提供,这些函数及调用函数的行为统称为系统调用(system call)。
- 系统调用是应用对操作系统(system)的功能进行调用(call)的意思。
- 高级编程语言的移植性:
- 高级编程语言并不依赖于特定的操作系统。这是因为人们希望不管是Windows还是Linux,都能使用几乎相同的源代码。
- 高级编程语言的机制是,使用独自的函数名,然后再在编译时将其转换成相应操作系统的系统调用(也有可能是多个系统调用的组合)。
通过操作系统和高级编程语言的抽象化,程序员可以更方便地进行文件操作,而无需直接处理硬件细节。这种抽象化不仅简化了编程,还提高了程序的可移植性和可维护性。
1. 监控程序的主要功能是什么?
答案:程序的加载和运行
解析:
监控程序(Monitor)是早期操作系统的原型,主要负责管理程序的生命周期,包括加载、执行和终止程序。它类似于现代操作系统的内核功能,但功能较为基础。
2. 在操作系统上运行的程序称为什么?
答案:应用(或应用程序)
解析:
在操作系统上运行的各类功能软件(如文字处理软件、游戏、开发工具等)统称为“应用程序”。操作系统负责管理这些程序的资源分配和执行。
3. 调用操作系统功能的接口称为什么?
答案:系统调用(system call)
解析:
应用程序通过“系统调用”间接使用硬件资源(如读写文件、网络通信)。系统调用是操作系统对上层应用提供的核心接口。
4. Windows Vista 是多少位的操作系统?
答案:32 位(也有 64 位的版本)
解析:
Windows Vista 是微软推出的多版本操作系统,支持 32 位和 64 位 CPU。用户需根据硬件选择对应版本。
5. GUI 是什么的缩写?
答案:Graphical User Interface(图形用户界面)
解析:
GUI 通过窗口、图标、按钮等图形元素与用户交互,取代了传统的命令行操作。它是现代操作系统(如 Windows、macOS)的核心特征。
6. WYSIWYG 是什么的缩写?
答案:What You See Is What You Get(所见即所得)
解析:
WYSIWYG 表示用户在屏幕上看到的内容(如文档排版、图片位置)与最终输出(如打印结果)完全一致。这是图形界面操作系统(如 Windows)的重要特性。
第十章:通过汇编语言了解程序的实际构成
汇编语言和本地代码是对应的,编译器,伪指令,
在计算机系统中,CPU 只能直接解释和运行本地代码(机器语言)。用高级语言(如 C 语言)编写的源代码需要通过编译器编译转换成本地代码才能被 CPU 执行。
本地代码的可读性
直接查看本地代码通常只能看到数值的罗列,这对于理解程序的实际运行方式并不直观。因此,产生了一种需求,即在本地代码中附加上表示其功能的助记符,以提高代码的可读性。
汇编语言的作用
汇编语言通过使用助记符(如 add
表示加法,cmp
表示比较)来表示本地代码的功能。这样,通过查看汇编语言编写的代码,可以更容易地理解程序的本质。汇编语言和本地代码之间是一一对应的关系。
汇编和反汇编
反汇编:将本地代码转换成汇编语言源代码的过程称为反汇编,负责这一工作的程序称为反汇编器。
汇编:将汇编语言编写的源代码转换成本地代码的过程称为汇编,负责这一工作的程序称为汇编器。
高级语言与本地代码
即使是用高级语言编写的源代码,编译后也会转换成特定 CPU 用的本地代码。通过反汇编,可以得到汇编语言的源代码。然而,将本地代码反编译成高级语言源代码则更为困难,因为高级语言的源代码与本地代码之间并不是一一对应的关系
总结
汇编语言提供了一种方式,使得程序员能够更直观地理解和分析程序的本地代码。通过汇编和反汇编,可以在不同层次上对程序进行调试和优化。尽管高级语言与本地代码之间的转换更为复杂,但汇编语言仍然是理解程序底层机制的重要工具。
- 获取汇编语言源代码的方法:
- 除了反汇编本地代码外,还可以通过编译器将C语言源代码转换成汇编语言源代码。
- 大部分C语言编译器都支持这一功能,可以将C语言源代码转换成汇编语言源代码而不是本地代码。
通过编译器将C语言源代码转换成汇编语言源代码,可以更好地理解程序的实际构成和运行机制。汇编语言源代码与C语言源代码交叉显示,便于比较和学习。通过这种方式,程序员可以深入理解程序的底层实现,从而优化代码和提高性能。
汇编语言通过段定义和过程划分程序结构,伪指令管理代码组织,Borland C++ 编译器按固定规则处理函数名和内存布局。
- 过程(函数)定义
- 使用
proc
和endp
伪指令定义过程(类似C语言的函数)。 - Borland C++ 编译器会在函数名前加下划线(如
AddNum
→_AddNum
)。
- 使用
- 段定义(Segment)
- 由
segment
和ends
伪指令包围,用于组织程序结构和内存布局。 - 主要段类型:
_TEXT
:存放指令(代码)。DATA
:存放已初始化数据。BSS
:存放未初始化数据。
- Borland C++ 按
_TEXT
→DATA
→BSS
顺序分配内存,确保连续性。 group
伪指令可将多个段合并为组(如DGROUP = _BSS + _DATA
)。
- 由
- 伪指令的作用
- 不转换成本地代码,仅指导汇编器组织代码和数据(如
segment
、ends
、group
)。
- 不转换成本地代码,仅指导汇编器组织代码和数据(如
- 源代码结构
end
伪指令表示源代码结束。- 函数和指令需放置在对应段内(如
_AddNum
和_MyFunc
属于_TEXT
段)。
- 编译规则(Borland C++)
- 栈和堆在程序运行时动态分配。
- 汇编源代码中指令与数据混杂,编译后按段定义整理为本地代码。
汇编语言的语法,常用的mov指令
汇编指令(操作码+操作数)
例:mov eax, ebx
→ 将ebx
的值赋给eax
。
语法结构:操作码 操作数
(部分指令仅操作码)。
操作码:指令动作(如mov
, add
)。
操作数:指令对象(如寄存器、内存地址、常数)。
- 常用指令及功能:
指令 | 操作数 | 功能 |
---|---|---|
mov | A, B | 将B 的值赋给A |
add | A, B | A = A + B |
push | A | 将A 的值压入栈 |
pop | A | 从栈顶弹出值并赋给A |
call | 函数名 | 调用指定函数 |
ret | 无 | 返回到函数调用位置 |
栈与堆:栈由esp
管理,运行时动态分配;堆同理。
- 程序通过寄存器与内存交互,指令由操作码定义动作,操作数定义对象。
- 代码、数据按段划分,伪指令管理程序结构,CPU按指令流执行。
mov
是汇编中最基础的指令,用于数据传递。关键点在于:
- 区分直接值和内存地址(方括号)。
- 明确数据大小(如
dword ptr
)。 - 支持灵活的内存寻址方式(基址+偏移)。
- 遵循操作数类型限制(如内存到内存需中转)。
对栈进行push和pop,函数的调用和内部处理
基本概念
- 定义:栈是内存中用于存储临时数据的区域,遵循 LIFO(Last In First Out,后进先出)规则。
- 操作指令:
push
:数据入栈(存储)。pop
:数据出栈(读取并删除)。
栈的物理结构
- 内存模型:
- 高位地址 → 栈底(初始位置)。
- 低位地址 → 栈顶(随
push
向下增长)。
栈通过 push
和 pop
实现临时数据的高效管理,由 esp
自动跟踪栈顶地址。关键点包括:
- 必须保持栈平衡,避免指针错误或崩溃。
- LIFO规则:后入栈的数据先出栈。
- 操作单位:32位数据(4字节),
esp
随操作自动更新。 - 典型用途:保存寄存器、传参、局部变量存储。
函数调用与栈帧建立
- 保存调用者的基址指针
push ebp
:将当前函数的基址指针(ebp
)压入栈中,以便函数返回时恢复。mov ebp, esp
:将栈顶指针(esp
)的值赋给ebp
,作为新函数的基址指针,用于访问参数和局部变量
- 参数传递参数逆序入栈
- 在C语言中,函数参数按从右到左的顺序压入栈。例如,
AddNum(123, 456)
会先压入456
(代码清单10-4的步骤3),再压入123
(步骤4)。- 栈内布局(从高地址到低地址):
ebp
原值- 返回地址(由
call
指令自动压入) - 参数1:
123
([ebp+8]
)
- 在C语言中,函数参数按从右到左的顺序压入栈。例如,
- 函数调用与返回
call
指令的作用call _AddNum
:跳转到AddNum
函数地址,并将下一条指令的地址(即add esp, 8
的地址)压入栈,作为返回地址。ret
指令的作用
函数结束时,ret
会从栈中弹出返回地址,跳转回调用位置(代码清单10-4的步骤6)。
- 栈清理
- 清理参数空间
add esp, 8
:将栈指针(esp
)增加8字节(2个4字节参数),相当于销毁参数占用的栈空间。 - 替代方案:两次
pop
操作,但add
效率更高(仅需1次操作)。
- 清理参数空间
- 函数内部处理
- 访问参数与返回值
mov eax, [ebp+8]
:通过基址指针ebp
访问第一个参数(123
)。add eax, [ebp+12]
:将第二个参数(456
)加到eax
。- 返回值规则:C语言规定返回值通过
eax
寄存器传递,无需恢复eax
的原始值。
- 访问参数与返回值
- 栈状态变化
- 调用前后对比(参考图10-4)
- 调用前:栈中包含
ebp
原值、返回地址、参数123
和456
。 - 调用后:通过
add esp, 8
清理参数,ebp
恢复为原值(通过pop ebp
),栈指针回到函数调用前的状态。
- 调用前:栈中包含
- 调用前后对比(参考图10-4)
- 编译器优化
- 未使用变量的处理
若函数返回值未被使用(如代码清单10-1中的变量c
),编译器可能省略相关汇编代码,仅保留必要的操作(如参数传递和计算),并生成警告(如“赋值未使用”)。- 参数2:
456
([ebp+12]
)
- 参数2:
- 未使用变量的处理
- 参数传递:通过栈逆序压入,由
ebp
固定偏移访问。 - 返回值:通过
eax
寄存器传递。 - 栈管理:
ebp
保存栈帧基址,esp
动态管理栈顶,call
/ret
自动处理返回地址。 - 优化影响:编译器可能省略冗余操作,需注意警告信息。
确保全局变量的内存空间,局部变量
在C语言中,函数外部定义的变量称为全局变量,可以在整个程序的任何部分访问。
函数内部定义的变量称为局部变量,只能在定义它们的函数内部访问。
特性 | 全局变量 | 局部变量 |
---|---|---|
存储位置 | _DATA (初始化)或_BSS (未初始化)段 | 栈(函数调用时动态分配) |
生命周期 | 程序运行期间始终存在 | 函数执行期间存在,函数返回后销毁 |
访问范围 | 全程序可见 | 仅在定义变量的函数内部可见 |
初始化方式 | _DATA 段显式初始化,_BSS 段默认置零 | 需手动赋值(如mov [ebp-4], 1 ) |
汇编指令 | dd (初始化)、db dup(?) (未初始化) | add esp, N 调整栈指针,mov 赋值 |
- 全局变量:
- 初始化变量占用
_DATA
段,编译时写入初始值。 - 未初始化变量占用
_BSS
段,程序启动时自动置零。
- 初始化变量占用
- 局部变量:
- 通过栈动态分配,生命周期限于函数内部。
- 需手动管理栈空间(如
add esp, N
调整指针)。
- 访问机制:
- 全局变量通过固定标签(如
_a1
)直接访问。 - 局部变量通过
ebp
偏移量(如[ebp-4]
)间接访问。
- 全局变量通过固定标签(如
局部变量
机制 | 实现方式 |
---|---|
寄存器分配 | 优先使用空闲寄存器(如 eax 、edx ),提高访问速度。 |
栈分配 | 寄存器不足时,通过 sub esp, N 预留栈空间,并通过 ebp 偏移访问变量。 |
生命周期 | 局部变量在函数执行期间存在,函数返回后栈空间自动释放。 |
编译器优化 | 旧编译器可能仅使用栈,现代编译器(如 Borland C++)优先利用寄存器。 |
循环,条件分支,程序运行方式的必要性
循环的实现
核心机制:汇编通过cmp
+jl
实现条件跳转,inc
更新计数器,call
执行循环体。
性能优化:优先使用寄存器、高效指令(如xor
)。
可读性:C语言的for
循环更符合人类直觉,而汇编直接映射CPU操作,适合底层优化。
汇编与C语言对应关系
C语言 | 汇编指令 | 功能 |
---|---|---|
i = 0 | xor ebx, ebx | 清零循环计数器 |
MySub() | call _MySub | 执行循环体函数 |
i++ | inc ebx | 递增循环计数器 |
if (i < 10) goto L4 | cmp ebx, 10 + jl @4 | 条件判断与跳转 |
条件分支的实现
在汇编语言中,条件分支通常使用条件跳转指令来实现,如JMP
(无条件跳转)、JE
(等于时跳转)、JNE
(不等于时跳转)等。
在C语言中,条件分支通常使用if
、else if
和else
语句来实现。这些语句提供了一种高级、易读的方式来表达条件逻辑。C语言编译器负责将这些高级语句转换为底层的机器指令。
通过理解汇编语言和程序的实际运行方式,可以更好地理解多线程编程中的问题,并找到合适的解决方案。虽然现在很少直接使用汇编语言编写程序,但了解汇编语言对于理解高级语言编写的程序的底层行为是非常有帮助的。
- 函数调用如何实现?
call
指令将返回地址压入栈,函数执行后通过ret
指令从栈恢复地址,参数通过栈传递,返回值存寄存器910。 - 全局变量与局部变量的内存分配差异?
全局变量在程序启动时分配固定内存,局部变量在栈中临时分配,函数结束即释放9。
第十一章:硬件控制方法
应用和硬件的关系
Windows 应用程序通过 系统调用(API) 间接控制硬件,而非直接操作硬件
- 1. 高级语言与硬件控制的关系
- 为何不直接操作硬件?
高级语言(如 C 语言)编写的应用程序运行在操作系统(如 Windows)之上,操作系统负责管理硬件资源。直接操作硬件会引发安全性和兼容性问题,因此硬件控制权由操作系统统一管理。 - API 的作用
Windows 提供 API(应用程序接口),例如TextOut
函数,允许应用程序通过调用这些函数间接控制硬件。API 的底层实现在 DLL 文件中,开发者无需关注硬件细节。
- 为何不直接操作硬件?
- 系统调用的底层机制:IN/OUT 指令
- IN 和 OUT 指令
这是 x86 系列 CPU 的汇编指令,用于 CPU 与 I/O 控制器 通信:- IN 指令:从指定端口号(Port)读取数据到 CPU 寄存器。
- OUT 指令:将 CPU 寄存器的数据写入指定端口号。
- 端口(Port)与 I/O 控制器
- I/O 控制器:连接外围设备(如显示器、键盘)的集成电路(IC),负责转换电信号(如数字信号与模拟信号)。
- 端口:I/O 控制器内部的内存(寄存器),用于临时存储输入输出数据。每个外围设备对应一个或多个端口号(如软驱的端口号可通过控制面板查看)。
- Windows 接收请求,通过系统调用将参数转换为硬件操作指令。
- Windows 使用 OUT 指令 向显示器对应的端口发送数据,最终由显示器 I/O 控制器处理并显示内容。
- IN 和 OUT 指令
- TextOut 函数的执行流程
- 应用程序调用
TextOut
API,传递参数(如显示坐标、字符串内容)。
- 应用程序调用
- 实际应用与调试
- 查看端口号
通过控制面板(如设备管理器)可查看外围设备的端口号,例如旧版 Windows 中软驱的 I/O 范围。 - 编写输入输出程序
调试时可通过汇编语言直接使用 IN/OUT 指令控制硬件,但需谨慎操作(现代操作系统通常禁止用户程序直接访问硬件端口)。
- 查看端口号
总结
硬件无关性:高级语言通过 API 屏蔽硬件细节,确保程序兼容性和安全性。
硬件控制流程:
应用程序 → 调用 API → Windows 系统调用 → IN/OUT 指令 → I/O 控制器 → 硬件操作。
这种分层设计平衡了开发效率与硬件控制能力,是操作系统核心功能之一。
中断请求
中断处理
- 中断请求:由连接外围设备的I/O控制器发出,用于通知CPU有事件发生,需要处理。
- 中断控制器:位于I/O控制器和CPU之间,用于缓冲和有序传递多个外围设备的中断请求给CPU。
- 中断处理程序:操作系统和BIOS提供,用于响应特定的中断编号。
- 实时处理:中断允许系统实时响应外围设备的输入输出,如键盘输入和打印机输出,而不需要主程序不断轮询设备状态。
DMA(直接内存访问)
- DMA机制:允许外围设备直接与主内存进行数据传输,无需CPU介入,从而节省CPU时间,提高数据传输速度。
- DMA通道:用于识别和控制哪些外围设备使用DMA机制,每个通道可以指定给特定的设备。
- 设备冲突:如果多个外围设备被错误地分配了相同的端口号、IRQ或DMA通道,可能会导致系统无法正常工作,出现设备冲突。
实时数据处理和大量数据传输
- 中断的优势:通过中断,系统可以实时处理外围设备的输入输出,而不需要主程序频繁检查设备状态,这对于需要快速响应的设备(如键盘、鼠标)尤为重要。
- DMA的优势:DMA允许大量数据快速传输,适用于需要快速读写大量数据的设备(如磁盘驱动器),从而提高系统的整体性能。
设备识别
I/O端口号、IRQ、DMA通道:这些是识别和配置外围设备的关键参数,确保每个设备都有唯一的标识,避免设备冲突。
显示器上显示的信息存储在一种特殊的内存中,称为VRAM。当程序向VRAM写入数据时,这些数据就会在显示器上显示出来。
第十二章:让计算机“思考”
计算机“思考”的本质
- 程序驱动逻辑:计算机的“思考”本质上是程序对数据的逻辑处理。它通过条件分支(if/else)、循环(for/while)和算法(如排序、搜索)模拟人类的判断过程。
- 规则化与符号化:计算机的“思考”依赖明确的规则和符号,无法像人类一样模糊联想或直觉判断(如理解情感、处理未定义的场景)。
输入数据 + 程序逻辑 + 算法 → 输出结果
计算机的“思考”是程序对数据的符号化处理,本质是逻辑与计算的结合。
- 计算机的“思考”是人类思维的延伸工具,而非替代。
- 程序设计的核心在于将问题分解为可计算的逻辑步骤。
各章补充
第1章:
- 什么是程序?
- 答:程序是计算机每一步动作的一组指令。
- 程序是由什么组成的?
- 答:指令和数据
- 什么是机器语言?
- 答:CPU可以直接识别并使用的语言。
- 正在运行的程序存储在什么位置
- 答:内存
- 什么是内存地址
- 答:内存中,用来表示命令和数据存储位置的数值
- 计算机中,负责程序的解释和运行的是哪个元件
- 答:CPU
第3章:
- 二进制数0.1,用十进制数表示的话是多少?
- 答:0.5。
- 解释:2−1=0.5。十进制里是 10−1=0.1。
- 用小数点后有3位二进制数,能表示十进制数0.625吗?
- 答:能。
- 解释:0.625 = 0.5 + 0.125;即二进制的:0.1 + 0.001 = 0.101。
- 将小数分为符号、尾数、基数、指数4部分进行表现的形式成为什么?
- 答:浮点数(浮点数形式)。
- 解释:浮点数:“符号尾数 × 基数的指数次幂”。
- 二进制数的基数是多少?
- 答:2。
- 解释:十进制的基数是10,XX进制的基数是XX。
- 通过把0作为数值范围的中间值,从而在不使用符号位的情况下来表示负数的表示方法称为什么?
- 答:EXCESS系统表现。
- 解释:EXCESS是“剩余的”意思。
- 10101100.01010011这个二进制数,用十六进制数表示的话是多少?
- 答:AC.53。
- 解释:二进制的4位相当于16进制的1位。
第4章::
- 有十个地址信号引脚的内存IC(集成电路)可以指定的地址范围是多少?
- 答:用二进制数来表示的话是0000000000
1111111111(十进制:01023)。 - 解释:十个引脚,一共可以表示 210=1024种状态
- 答:用二进制数来表示的话是0000000000
- 高级编程语言中的数据类型表示的是什么?
- 答:占据内存区域的大小和数据类型(编码规则)。
- 解释:例如C语言中的short表示占据2个字节,并且存储整数。
- 在32位内存地址的环境中,指针变量的长度是多少位?
- 答:32。
- 与物理内存有着相同构造的数组的数据类型长度是多少?
- 答:1字节。
- 解释:物理内存是以字节为单位进行数据存储的。
- 用LIFO方式进行数据读写的数据结构称为什么?
- 答:栈。
- 解释:Last In First Out(后进先出)。
- 根据数据的大小链表分叉为两个方向的数据结构称为什么?
- 答:二叉查找树(binary search tree)。
- 解释:从节点分成两个叉的树状数据结构。
第5章
- 存储程序方式指的是什么?
- 答:在存储装置中保存程序,并逐一运行的方式。
- 解释:现在计算机采用的是存储程序方式。
- 通过使用内存来提高磁盘访问速度的机制称为什么?
- 答:Disk Cache(磁盘缓存)。
- 解释:把从磁盘中读出的数据存储在内存中,当该数据再次被读取时,不是从磁盘而是直接从内存中高速读出。
- Windows中,在程序运行时,存储着可以动态加载调用的函数和数据的文件称为什么?
- 答:DLL。
- 解释:Dynamic Link Library。
- 在EXE程序文件中,静态加载函数的方式称为什么?
- 答:静态链接。
- 解释:函数的加载方式有静态链接和动态链接两种。
第6章::
- DOC、LZH和TXT这些扩展名中,哪一个是压缩文件的扩展名?
- 答:LZH。
- 解释:LZH是用LHA等压缩工具压缩过的文件的扩展名。
2.文件内容用“数据的值×循环次数”来表示的压缩方式是RLE算法还是哈夫曼算法?
- 答:RLE算法。
- 解释:例如,AAABB这个数据压缩后就是A3B2。
3.在Windows计算机中,1个半角英数用几个字节的数据来表示?
- 答:1字节(=8位)。
- 解释:半角英文数字是1个字节来表示的,汉字等全角字符是用2个字节来表示的。
4.可逆压缩和非可逆压缩的不同点是什么?
- 答:是否能够还原。
- 解释:JPEG就是不可逆压缩。
第八章
- 计算机如何处理小数运算?
浮点数用符号、尾数、基数和指数表示,双精度(64位)和单精度(32位)分别满足不同精度需求53。 - 逻辑运算与算术运算的差异?
算术运算由CPU运算器执行数学计算,逻辑运算(如AND/OR)由逻辑电路处理布尔值7。