计算机是怎样跑起来的

第1章:计算机的三大原则

1.1 计算机的三个根本性基础

  1. 计算机是执行输入运算输出的机器
  2. 程序是指令数据的集合
  3. 计算机的处理方式有时与人们的思维习惯不同

1.2 输入、运算、输出是硬件的基础

IC:Integrated Circuit「集成电路」
Pasted image 20250920104114

输入、 运算、 输出
Pasted image 20250920104220

1.3 软件是指令和数据的集合

代码清单 1.1 C 语言的程序示例片段

int a, b ,c;
a = 10;
b = 20;
c = Average(a, b);

将代码清单 1.1 的内容保存为 MyProg.c,编译后会生成可执行文件 MyProg.exe。
用文件查看器打开 MyProg.exe,会看到类似代码清单 1.2 的十六进制数值。

代码清单 1.2 机器语言的程序示例

C7 45 FC 01 00 00 00 C7 45 F8 02 00 00 00 8B 45
F8 50 8B 4D FC 51 E8 82 FF FF FF 83 C4 08 89 45
F4 8B 55 F4 52 68 1C 30 42 00 E8 B9 03 00 00 83

无论是哪个程序,其内容都是数值的罗列,每个数值要么是指令,要么是数据

1.4 对计算机来说什么都是数字

计算机术语,如“文件句柄”、“公钥”和“私钥”,本质上都是数字
尽管这与人类的思维习惯不同,但将所有信息都视为数字,能让计算机更高效地处理它们。

1.5 只要理解了三大原则,即使遇到难懂的最新技术,也能轻松应对

计算机是执行程序的机器。 程序是指令数据的集合。
为了使互 联网上相互连接的计算机能通过程序协同工作,微软公司采用了 SOAP 以及 XML 规范。
SOAP 是关于调用指令的规范,XML 则是定义数据 格式的规范。

1.6 为了贴近人类,计算机在不断地进化

Windows XP 和 Office XP 末尾的 XP, 代表的就是 Experience(体验)。
面向组件编程:Component BasedProgramming
面向对象编程:Object Oriented Programming
Pasted image 20250920180356

1.7 稍微预习一下第2章

Pasted image 20250920105612
一台基本的计算机硬件,只需将 CPU内存I/O 用电路连接起来,并提供电源时钟信号 即可。
时钟信号「内含晶振发出电信号」 由时钟发生器发出,其频率(如Pentium CPU,几百MHz到2GHz)决定了CPU的工作速度。

晶振:一种利用石英晶体(又称水晶)的压电效应产生高精度振荡频率的电子元 件。

第2章:试着制造一台计算机吧

2.0 计算机术语和概念

术语

CPU 的位数

CPU 的位数(或称位宽字长)是一个系统性的概念,它主要由 CPU 内部核心部件的宽度共同决定,是其指令集架构 (ISA) 在底层设计中的体现。
我们可以从以下三个核心维度来理解和定义 CPU 的位数:

1. 核心决定因素:通用寄存器的宽度

结论: 我们通常所说的 CPU 位数(如 32 位、64 位),最直接、最核心的定义就是其 通用寄存器(General-Purpose Registers) 的宽度。

如果把这些所有类型的寄存器都加起来,总的 “寄存器空间” 会远大于通用寄存器的128字节,可能达到几千字节的量级。

2. 性能支撑因素:ALU 与数据总线的宽度

这两个部件的宽度必须与寄存器宽度协同一致,才能确保 CPU 的高效运行。

A. 算术逻辑单元(ALU)的宽度
B. 数据总线的宽度

3. 限制性因素:地址总线的宽度

地址总线的宽度不直接定义 CPU 的位数,但它决定了 CPU 的寻址能力,即能访问的最大内存容量

总结比喻:CPU 的位数就像一个工厂
部件 作用 对应比喻
通用寄存器 定义位数;一次处理的数据量 加工台的大小(最直接的标准)
ALU 宽度 支撑运算能力 机器手臂的力量(一次能处理多大的原材料)
数据总线 决定数据吞吐效率 进出工厂的传送带宽度
地址总线 决定可访问的内存范围 仓库的地址编号范围(能管理多大的仓库)
因此,一个 CPU 被定义为 N 位,是因为它的通用寄存器N 位宽的,并有 N 位宽的 ALU 和指令集来支持其高效运行。

CPU存储层次结构

一、 核心图示:存储金字塔

速度由快至慢,容量由小至大

      / \
     /   \   速度最快,成本最高
    /寄存器\   (CPU核心内部工作区)
   /-------\
  /  L1缓存 \   (每个核心的私人工作台)
 /-----------\
/    L2缓存   \  (每个核心的私人仓库)
------------- \
\    L3缓存    /  (所有核心共享的中央仓库)
 \-----------/
  \  内存   /    (主存,所有程序的工作区域)
   \------/
    \硬盘/       (永久存储,速度最慢)
     \ /

二、 详细对比表格

特性 寄存器 L1 缓存 L2 缓存 L3 缓存 内存 硬盘/SSD
位置 CPU核心内部 每个CPU核心内部 每个CPU核心内部/外部 CPU芯片上,所有核心共享 主板插槽上 机箱内,通过线缆连接
功能 直接参与运算
(存放指令、数据、地址)
核心专用高速缓存
分指令缓存和数据缓存
核心专用后备缓存
弥补L1容量不足
所有核心共享缓存
作为CPU与内存间的最后缓冲
程序运行时的临时工作区 永久存储操作系统、程序、文件
容量 极小
(几百字节 ~ 几KB)
很小
(几十KB ~ 几百KB/核心)
较小
(几百KB ~ 几MB/核心)

(几MB ~ 几百MB,所有核心共享
很大
(几GB ~ 几TB)
极大
(几百GB ~ 几十TB)
速度 最快
(与CPU时钟同步,0延迟)
极快
(1-4个时钟周期)
很快
(10-20个时钟周期)

(20-50个时钟周期)

(200+个时钟周期)
极慢
(数百万个时钟周期)
管理方 编译器/程序员
(通过汇编指令)
CPU硬件自动管理 CPU硬件自动管理 CPU硬件自动管理 操作系统 操作系统 & 文件系统

三、 关键知识点总结

  1. 设计哲学:这个分层结构是基于局部性原理(时间和空间局部性),用较小的成本,在速度和容量之间取得最佳平衡。
  2. 数据流
    • 当CPU需要数据时,它按照 寄存器 → L1 → L2 → L3 → 内存 → 硬盘 的顺序逐级查找。
    • 找到数据后,会将其复制到上一级更快的存储中,以便后续快速访问。这个过程对程序是透明的。
  3. 寄存器与缓存的本质区别
    • 寄存器运算部件,是CPU指令直接操作的对象。
    • 缓存存储部件/缓冲区,目的是加速对内存的访问。
  4. 缓存层级分工
    • L1:追求极致速度,分为指令缓存和数据缓存,专核心专用。
    • L2:平衡速度和容量,作为L1的后备,专核心专用。
    • L3:追求大容量和共享,解决多核心数据同步与冲突问题,是内存前的最后一道屏障
  5. 性能影响
    • 缓存命中率的高低直接决定了CPU的“实际工作效率”。缓存越大、越快,CPU“等待”数据的时间就越少,性能就越强。这就是为什么同架构的CPU,缓存大小是关键的性能指标。

计算机时钟频率

核心逻辑:同步与性能

  1. 核心作用:系统的“统一节拍” CPU内部数十亿晶体管必须步调一致。确保所有部件在每一个“滴答”瞬间同步完成取指、计算或存取,防止逻辑出错。
  2. 同步基准:协调多样频率 尽管 CPU、内存等组件工作速度不同,但它们都源于同一个基准时钟(BCLK)。这种同步机制确保了不同组件间数据交换的准确性。
  3. 运行逻辑:频率不等于速度 主频(如 5.0GHz)代表每秒的周期数。但受限于架构,“一个时钟周期”不等于“执行一条指令”
  4. 效率关键:IPC(每周期指令数) 现代 CPU 利用流水线超标量技术,能在一个周期内处理多条指令。IPC 是衡量架构先进程度的核心指标。
  5. 性能判定公式:
CPU 性能主频×IPC(每周期指令数)

性能由“节奏快慢”与“架构效率”共同决定。主频稍低但 IPC 极高的现代处理器,实际性能往往优于高主频的陈旧处理器。

逻辑流程图
graph LR
    A[基准时钟] --> B[统一节拍/同步]
    B --> C{CPU 最终性能}
    D[主频: 频率快慢] --> C
    E[IPC: 架构效率] --> C

硬件架构:从“中央集权”到“分布式”

阶段 组件 物理位置 作用与原理
1. 物理源头 石英晶振 (Crystal) 主板 (金属胶囊) 产生脉冲:利用压电效应产生极其稳定的 25MHz 原始物理频率(系统的“心脏”)。
2. 总指挥 时钟发生器芯片 (Clock Generator) 主板 (CPU/PCH 旁) 时钟合成 (Synthesis):通过内部 PLL 接收原始频率,消除抖动并加工成标准 100MHz BCLK 差分信号
3. 分布式执行 零件内部 PLL (锁相环) CPU / 内存内部 自主倍频:接收 100MHz 基准,各自按需翻倍。如 CPU 将其乘以 50 得到 5.0GHz 工作主频。

逻辑链路图 (从物理震动到 C 语言信号)

graph TD
    A[晶振 25MHz] -->|原始信号| B[独立时钟发生器芯片]
    B -->|100MHz BCLK| C[CPU 内部]
    B -->|100MHz| D[PCIe/外设]
    B -->|100MHz| E[PCH 芯片组]

    subgraph CPU 核心
    C -->|PLL 倍频| F[工作主频 5GHz]
    C -->|分频计时| G[Local APIC Timer]
    C -->|计数器| H[TSC 时间戳]
    end
    
    G -->|硬件中断| I[OS 内核]
    I -->|软件信号| J[C 语言 SIGALRM]

术语演进:FSB (外频) vs BCLK (基频)

特征 历史上的“外频” (FSB) 时代 (如 Pentium 4) 现代的“基准频率” (BCLK) 时代 (2008 年后)
术语 外频 (Front-Side Bus Frequency) 基准频率 (Base Clock)
硬件架构 中央总线:CPU、内存、芯片组共享一根总线 (FSB)。 点对点连接:使用 QPI / DMI / Infinity Fabric 等高速串行总线取代 FSB。
关键变革 内存控制器在主板芯片组内。 内存控制器集成到 CPU 内部 (IMC)。
时钟关系 同步锁定:所有主要部件(核心、内存、PCI)的频率都必须基于 FSB 同步运行 时钟分离:CPU 核心、内存、I/O 运行在各自独立的频率上。
物理含义 总线的实际工作频率:是 CPU 与主板交换数据的物理速度 基础时钟参考点:是一个低速 (100MHz) 的参考信号,本身不代表任何数据总线的实际运行速度
核心公式 CPU 核心频率 = 外频 × 倍频 CPU 核心频率 = 基准频率 × 倍频
  1. 外频 在现代硬件中被称为 BCLK (Base Clock),是所有设备的基础同步时钟,标准值是 100 MHz
  2. 基准频率”和“外频率”指的其实是同一个概念,即 BCLK (Base Clock),但“外频”这个术语在过去更为常见
  3. 现代计算机系统通过将内存控制器集成到 CPU 内部并使用点对点总线,实现了时钟域的解耦。因此,“外频” 这个描述总线速度的旧术语已经过时。“基准频率” (BCLK) 是更准确的名称,因为它代表了一个统一的时钟源,用于通过不同的倍率和分频器,生成 CPU 核心、内存和 I/O 等所有部件所需的工作频率。

CPU与内存频率

CPU 和内存的频率不同,但两者都以时钟发生器生成的基准频率为基础,通过各自的倍频/分频器工作。

1. CPU 核心频率
特点 说明
作用 决定 CPU 内部执行指令处理数据的速度
计算方式 通过倍频系数从基准频率衍生: $$\text{CPU 核心频率} = \text{基准频率} \times \text{倍频系数}$$
频率高低 系统中最高的频率之一(目前通常为数 GHz),追求单线程处理速度
2. 内存工作频率
特点 说明
作用 决定内存数据传输的物理速度和带宽,与 CPU 的内存控制器同步。
计算方式 通过分频/倍频比例(Memory Ratio)从基准频率衍生出内存时钟频率
频率高低 适中。它的带宽吞吐量更重要,受数据总线宽度DDR 技术影响。

带宽计算中的「频率」与 DDR 技术

在计算内存带宽时,使用的频率必须有效数据传输速率 (Data Rate)

1. 带宽公式
带宽 (MB/s)=数据总线宽度 (bit)×频率 (MHz/MT/s)8 (bit/Byte)
2. 频率的实际意义(DDR 技术)

核心区别:时钟频率 vs. 有效数据速率
对于 DDR (Double Data Rate) 内存技术,我们需要区分两个关键数值:

1. 内存时钟频率(Clock Frequency)
2. 有效数据速率(Effective Data Rate / Transfer Rate)
术语的简化和约定

在市场营销和口语交流中,人们常常用 MT/s 的数值来命名和指代内存,并习惯性地将其单位简化为 MHz,以方便计算带宽:

频率数值 含义 如何在公式中使用
内存时钟频率 实际的物理振荡频率(例如 1600 MHz)。 必须乘以 2(因为 DDR)。$$\text{带宽} = \frac{\text{宽度} \times (\text{时钟频率} \times 2)}{8}$$
有效数据速率 每秒实际传输次数(例如 3200 MT/s)。 直接代入公式,无需再乘以 2。$$\text{带宽} = \frac{\text{宽度} \times \text{数据速率}}{8}$$

计算机频率进阶与超频要点

1. 超频(Overclocking)基础
2. CPU 超频策略(现代架构)
方式 调整项 影响范围 稳定性 推荐度
倍频超频 CPU 倍频(如 ×40 设为 ×50 仅影响 CPU 核心和缓存 :由于 BCLK 不变,内存、PCIe 等时钟稳定。 首选:简单、安全。
基频超频 基准频率 (BCLK)(如 100MHz 设为 103MHz 波及整个平台:同时影响 CPU 核心、内存、PCIeSATA 等所有依赖 BCLK 的时钟域。 :牵一发动全身,极易造成系统不稳定。 不推荐:除非追求极限性能。
3. 内存与频率异步
4. 显卡频率的独立性

超频的核心限制因素

超频受到硬件本身的物理极限和系统支持能力的严格限制,主要集中在以下三个方面:

1. 硬件体质(芯片的物理极限)

这是决定超频幅度的最根本因素

2. 散热能力(温度限制)

散热是超频最实际的障碍,因为更高的频率和电压必然产生更多热量。

3. 供电与主板设计(VRM 限制)

主板的质量决定了能否为超频后的 CPU 提供稳定、充足的电力。

简而言之: 芯片体质决定了你的理论上限;散热能力决定了你的实际运行上限;而主板供电则决定了你的稳定运行下限

总线的定义

总线 是计算机内部多个部件之间共享的公共通信通道。它是一组信号线的集合,定义了各部件间传输信息的标准。

总线技术是计算机的“血液循环系统”

一、按总线所处的位置功能分类(最核心的分类方式)

类别 英文 功能描述 连接对象举例 特点
片内总线 Chip Internal Bus CPU内部的总线,连接CPU内部各部件(如寄存器、ALU、控制器)。 连接CPU内部的运算器、控制器、寄存器组 速度极快,与CPU工艺和内部结构紧密相关,对用户不可见。
系统总线 System Bus 计算机主板上的总线,连接CPU、内存、I/O通道等核心部件。这是最重要的一类总线。 CPU ↔ 内存、CPU ↔ I/O接口 是计算机系统的主干道,其性能直接影响整机性能。
通常又细分为:数据总线、地址总线、控制总线
外部总线 External Bus 计算机内部用于连接外部设备的总线,又称I/O总线 主板 ↔ 硬盘、显卡、网卡等外部设备 注重通用性、可扩展性和兼容性。速度通常低于系统总线。如PCIe, SATA, USB等。

二、系统总线的细分(三总线结构)

类型 英文 传输方向 功能 特点
数据总线 Data Bus 双向(可在CPU、存储器、I/O设备之间来回传输) 在系统部件之间传输实际数据
指令数据)。
宽度(位数) 决定了一次能传输多少的数据,是字长的关键指标(如64位CPU)。
一次能处理多少数据(MB/s)=线(bit)×(MHz)8(bit/Byte)
地址总线 Address Bus 单向(从CPU到存储器/I/O设备) 指定存储器I/O设备数据的位置(地址)。 宽度(位数) 决定了CPU的寻址能力
能管理多大内存=2线
控制总线 Control Bus 部分入、部分出 传输各种控制信号,协调各部件的工作。 信号种类多(读、写、中断、时钟、复位等),
决定了总线的控制和协调能力
我们通常说的“数据总线、地址总线、控制总线”指的是系统总线的三个功能组成部分。

三、常见外部总线(I/O总线)对比

总线标准 中文名 类型 特点 主要应用
PCIe PCI-Express 并行串行化 当前主流。采用点对点串行连接,带宽高、扩展性强。版本迭代快(PCIe 3.0/4.0/5.0)。 独立显卡、高速固态硬盘(SSD)、高端网卡
SATA 串行ATA 串行 取代了旧的PATA(IDE)。结构简单,线缆小巧。 机械硬盘、SATA接口固态硬盘、光驱
USB 通用串行总线 串行 应用最广泛的外部总线。支持热插拔、即插即用、可对外供电。 键盘、鼠标、U盘、移动硬盘、手机等几乎所有外设
PCI 外围组件互联 并行 上一代主流总线标准,已被PCIe取代。 老式的声卡、网卡等扩展卡
M.2 - 接口规范 不是总线,是一种物理接口形式。它可以使用PCIeSATA总线。 现代NVMe固态硬盘(走PCIe通道)
笔记点睛:外部总线的发展趋势是 从并行转向串行。虽然串行一次只传输1位数据,但可以通过提高时钟频率和采用多通道(Lanes,如PCIe x4, x16)来轻松实现超高带宽,而并行总线高频率信号同步干扰问题严重。

四、按数据传输方式分类

类型 原理 优点 缺点 举例
串行总线 数据位逐位依次传输 线数少,成本低,抗干扰能力强,适合长距离传输 理论上同一时钟下速度低于并行 USB, SATA, PCIe, 网线
并行总线 数据位多位(如8位、16位)同时通过多根线传输 理论上在同一时钟周期内数据传输率更高 线数多,成本高,各信号线间易产生干扰(串扰),频率难以提升 早期的PCI、PATA(IDE)、LPT打印口

总结笔记表格

分类维度 总线类型 核心特点 典型代表
按位置功能 片内总线 CPU内部,速度最快 (CPU内部设计,无通用标准)
系统总线 主板主干道,三结构 数据/地址/控制总线
外部总线(I/O) 连接外设,通用性强 PCIe, SATA, USB
按传输方式 串行总线 逐位传输,抗干扰,主流 USB, SATA, PCIe
并行总线 多位同时传输,已渐淘汰 PATA(IDE), PCI
系统总线组成 数据总线 双向,传输数据,决定字长 64位, 32位
地址总线 单向,传输地址,决定寻址空间 32位(4GB), 64位
控制总线 双向,传输控制信号 读/写/中断信号

存储容量:CPU 寻址能力 vs 内存芯片容量

存储容量的计算核心原则是:地址线决定单元数量,数据线决定单元位宽。 但这套原则应用在 CPU 和内存 IC 上,代表着不同的容量概念。

1. 核心概念对比

总线类型 作用 决定因素 公式
地址线 寻址,指定数据位置 可寻址的单元数量 单元数=2地址线数量
数据线 传输数据 每个存储单元的位数(位宽) N/A

2. 场景一:CPU 的最大寻址能力(系统内存上限)

这是从系统宏观的角度,衡量 CPU 能控制的最大内存空间

计算对象 CPU 的最大寻址能力
决定因素 仅由 CPU 地址总线宽度决定。
数据线影响 CPU 数据总线宽度与系统最大内存容量无关。它只影响数据传输速度,不影响寻址范围。
计算公式(字节) $$\text{系统最大内存 (Byte)} = 2^{\text{CPU地址总线宽度}}$$
实例 32 位地址总线:232 Byte=4 GB
关键意义 设定了系统能“看到”和使用的理论内存上限

3. 场景二:内存芯片(IC)的物理容量

这是从芯片微观的角度,衡量单个内存 IC实际存储量

计算对象 单个内存芯片的物理容量
决定因素 芯片自身的地址引脚数据引脚数量共同决定。
单元数 2芯片地址引脚(如 A0A10 共 11 根线,则有 211 个单元)
位宽 芯片数据引脚(如 D0D7 共 8 根线,则位宽为 8 bit)
容量计算公式(字节) $$\text{芯片总容量 (Byte)} = \frac{2^{\text{芯片地址引脚}} \times \text{芯片数据引脚}}{8}$$
实例 $$\frac{2^{20} \times 4}{8} = \frac{1048576 \times 4}{8} = 524288 \text{ Byte} = 512 \text{ KB}$$
关键意义 决定了该芯片实际能存储的数据总量

存储器容量与引脚关系

1. 存储矩阵容量的物理限制

核心原理: 单个存储芯片(如 SRAM/DRAM)的存储矩阵容量不可能大于由其地址引脚和数据引脚决定的理论容量

限制类型 限制引脚 限制公式/能力 结果
寻址深度 N 根地址引脚 (Ai) 最大存储单元数=2N 如果内部单元 >2N,多余部分将成为不可访问的“孤岛”
数据宽度 M 根数据引脚 (Di) 每个单元宽度必须是 M 如果内部单元宽度 >M,读写时多余的位将丢失或无法同步传输。

2. TC5517 示例(2K × 8)

芯片容量: 2048 个单元 × 8 位/单元 = 2KB。

3. 系统级容量扩展(突破单芯片限制)

在更大的计算机系统中,通过使用多片芯片并结合外部译码电路可以扩展总容量。

深度扩展(字扩展/级联)4K × 8(增加寻址单元数)
目标 方法 关键组件 示例
增加可寻址的单元总数 多片级联 1. 额外地址引脚(由 CPU 提供)。
2. 地址译码器(用 CPU 地址高位控制 CS)。
2 块 2K×8 芯片 4K×8 系统
graph TD
    A11[地址线 A11] --> Decoder[地址译码器]
    Decoder --> CS1[片选 CS1]
    Decoder --> CS2[片选 CS2]
    
    A10_A0[地址线 A10-A0] --> Chip1[芯片1 2K×8]
    A10_A0 --> Chip2[芯片2 2K×8]
    
    CS1 --> Chip1
    CS2 --> Chip2
    
    Chip1 --> DB[数据总线 D7-D0]
    Chip2 --> DB
    
    subgraph "深度扩展结果"
        Result[4K × 8 内存]
    end

核心原理: 芯片分时工作,利用CPU提供的额外地址线选择不同的芯片。

宽度扩展(位扩展/并联)2K × 16(增加数据总线宽度)
目标 方法 关键组件 示例
增加数据总线宽度 多片并联 1. 地址引脚并联(接到 CPU 地址总线)。
2. 数据引脚连接到 CPU 数据总线的不同部分
2 块 2K×8 芯片 2K×16 系统
graph TD
    Addr[地址线 A10-A0] --> Chip1[芯片1 2K×8]
    Addr --> Chip2[芯片2 2K×8]
    
    CS[片选 CS] --> Chip1
    CS --> Chip2
    
    Chip1 --> DB_Low[数据总线 D7-D0]
    Chip2 --> DB_High[数据总线 D15-D8]
    
    subgraph "宽度扩展结果"
        Result[2K × 16 内存]
    end

核心原理: 芯片同时工作,每块芯片负责数据总线的不同位段

核心区别总结
扩展方式 深度扩展(级联) 宽度扩展(并联)
目的 增加地址空间(增加字数 N) 增加数据位宽(增加位数 M)
结果容量 N1​×M + N2​×M ⟹ (N1​+N2​)×M N×M1​ + N×M2​ ⟹ N×(M1​+M2​)
芯片工作 芯片分时工作(某一时刻只有一块芯片工作) 芯片同时工作(所有芯片同时工作)
地址连接 低位地址并联用于片内寻址,高位地址用于选片。 全部地址完全并联。
片选逻辑 需要地址译码,由地址高位控制 CS。 共享片选信号,所有芯片 CS 信号并联。
数据连接 数据总线共享。 各自连接到数据总线的不同位段。

2.1 制作微型计算机所必需的元件

制作微型计算机需要三种基本元件:

Z80 微型计算机的电路图

Z80 微型计算机的电路图

微型计算机元件

为了构建一台简单的微型计算机,我们使用了以下核心组件:

辅助元件

为了驱动 CPU 运转,还需要一个时钟发生器。该元件通过晶振产生时钟信号,这种“滴答”的电信号决定了CPU的运行速度。我们使用的时钟发生器频率为2.5MHz。
注意:这台微型计算机仅作为学习模型,不具备实用价值。
Pasted image 20250920114336

用于输入程序的装置也是必不可少的。

2.2 电路图的读法

在电路图中,连接元件符号的直线代表导线。导线交叉但没有黑点,表示没有连接。只有在交叉处画上一个黑点,才代表此处是连接点。
Pasted image 20250920140404

为了简化电路图,+5V 和 0V 的连接会用专门的符号表示(如图 2.4),而不是直接画出导线。
IC 的引脚是从 1 开始递增逆时针顺序编号的。数引脚时,需将 IC 上的正方向标志(如半圆形缺口)朝向左边
举例来说, 带有 14 个引脚的 7404,其引脚序号就如图 2.5 所示。
Pasted image 20250920140722

电路图中,IC 的引脚排列方式不受物理顺序的限制,而是根据布线方便来布局。
通常会在引脚旁边标注序号,并在IC的矩形符号中用缩写代号(例如:RD代表读取,WR代表写入)来表示引脚的功能。具体代号的含义会在布线时详细解释。

有的电路图会按照引脚的实际物理顺序来绘制,这种图被称为实物布线图
为了便于理解,本书对部分引脚的功能代号进行了修改。例如,厂商资料中 TC5517 芯片的 OE(Output Enable,输出使能)引脚在这里被改为了 RD(Read,读取)。

2.3 连接电源、数据总线和地址总线

开始布线

请用红铅笔在电路图上完成以下连接:

  1. 电源连接:将 +5V 连接到 各个IC「Z80 CPU、TC5517、Z80 PIO、时钟发生器...」 的 Vcc 引脚,并将 0V 连接到它们的 GND 引脚。
  2. 连接数据总线地址总线
    • Z80 CPUD0~D7 引脚连接到 TC5517D0~D7 引脚。
    • Z80 CPUA0~A7 引脚连接到 TC5517A0~A7 引脚。
    • TC5517A8~A10 引脚连接到 0V

数字IC

给 IC 供电的 VccGND 引脚电压是固定的(+5V 和 0V),而其他引脚电压则在 +5V 和 0V 之间变化,以此传输信息。

CPU 的引脚作用

Z80 CPU 地址总线与寻址容量

核心结论

Z80 CPU 地址引脚的单位是位 (bit),而 CPU 寻址的最小单位是字节 (Byte)。这两者共同决定了系统的最大内存容量。

1. Z80 地址总线规格
特性 数值 解释
地址总线宽度 16 位 (A0A15) Z80 处理器拥有 16 根地址引脚,每根引脚传输 1 位信号。
可表示的地址数 216 16 位可以组合出 65,536 种不同的地址编号。
2. 寻址单位:为什么是 Byte?
3. 最大寻址容量计算

地址总线决定了最多能寻址多少个单元,而寻址单位决定了每个单元有多大

最大寻址容量=2地址总线宽度×寻址单位

代入 Z80 的数值:

\text{容量} = 2^{16} \times 1 \text{ Byte}$$$$\text{容量} = 65536 \text{ Bytes}$$$$\text{容量} = 64 \text{ KB}$$**准确表述:** 16 根地址总线赋予 Z80 **$64 \text{KB}$** 的最大内存寻址容量。 理解了地址引脚的“位”和寻址单位的“字节”的区别,有助于准确计算任何处理器的最大内存上限。 ### TC5517 内存芯片 TC5517 共有 A0~A10 **地址引脚和** D0~D7 **数据引脚**。 虽然它有 2^11=2048 个存储单元,但本次制作中,我们只使用其 A0~A7 引脚,因此只能用到 2^8=256 个存储单元。 ![Pasted image 20250920140738](https://weichengjun2.dpdns.org/i/2025/09/20/68ce459885940.png) #### 内存芯片容量的计算 内存芯片的容量是其**可存储的总位数(bit)** 或 **总字节数(Byte)**。计算公式如下: $$\text{容量} = 2^{\text{地址引脚数}} \times \text{数据引脚数}
1. 确定地址引脚数

地址引脚(Address Pins,通常标记为 A0,A1,A2,)决定了芯片内部有多少个存储单元(或称,Word)。

存储单元数=2地址引脚数
2. 确定数据引脚数

数据引脚(Data Pins,通常标记为 D0,D1,D2,)决定了每个存储单元可以存储的位数

3. 计算容量

将两者相乘,得出总容量(以 bit 为单位)。再除以 8,即可得出常用的字节(Byte)容量。

以 TC5517 芯片为例

我们以 TC5517(如 TC5517APL-10)为例,它是一款典型的 2K×8 bit SRAM 芯片。

  1. 确定数据结构:
    • 芯片规格通常表示为: 2K×8 bit
    • 存储单元数 (字数)=2K=2×1024=2048 个。
    • 每个单元的位数=8 bit
  2. 计算地址引脚数 (N):
    • 要表示 2048 个不同的地址,需要的地址引脚数为 N 满足 2N=2048
    • 210=1024
    • 211=2048
    • 所以,地址引脚数=11 个(即 A0A10)。
  3. 确定数据引脚数 (M):
    • 芯片可以直接存取 8 位数据,所以 数据引脚数=8 个(即 D0D7)。
  4. 计算容量:
    • 总位数容量 (bit): $$\text{容量}_{\text{bit}} = 2048 \times 8 = 16384 \text{ bit}$$
    • 总字节容量 (Byte): $$\text{容量}_{\text{Byte}} = \frac{16384}{8} = 2048 \text{ Byte} \quad \text{或} \quad 2\text{KB}$$
      在实际的计算机系统中,主内存(DRAM)通常由多片这样的芯片并行组合起来,以满足CPU地址总线和数据总线的宽度要求(例如,将多片 ×8 bit 的芯片并联,组成一个 ×64 bit 的内存条)。
技术 核心思想 等效数据总线宽度 效果
单通道 单条 64 位通道 64 位 基础带宽
双通道 两条 64 位通道并行 128 位 带宽几乎翻倍
四通道 四条 64 位通道并行 256 位 带宽是单通道的四倍

内存芯片速率的计算

计算理论上的数据传输带宽数据总线吞吐量

带宽 (MB/s)=数据总线宽度 (bit)×频率 (MHz)8 (bit/Byte)
含义 作用
数据总线宽度 (bit) 总线可以一次并行传输的位数(如 32, 64)。 决定了每次传输的数据量。
频率 (MHz) 数据总线每秒传输数据的次数 决定了每秒能进行多少次传输操作。
8 (bit/Byte) 转换因子,将 bit 转换为 Byte 确保最终单位为 MB/s
关于「时钟频率」的精确性

在这个带宽计算公式中的频率,指的必须是数据总线在单位时间内传输数据的次数

因此,公式应该为:

带宽 (MB/s)=数据总线宽度 (bit)×(频率 (MHz)=时钟频率×数据传输倍数)8 (bit/Byte)

例如,如果一个 DDR4 内存时钟频率1600 MHz,那么它的数据传输速率(等效频率)是 1600 MHz×2=3200 MT/s(Million Transfers per second,百万次传输/秒)。计算带宽时,应使用 3200 MHz

示例计算(以 DDR4 内存条为例)

假设有一个现代内存模块(如 DDR4 3200):

  1. 数据总线宽度: 内存控制器到内存模块通常是 64 bit
  2. 数据传输速率: 3200 MT/s(相当于 3200 MHz 频率)。
带宽 (MB/s)=64 bit×3200 MHz8 bit/Byte带宽 (MB/s)=8×3200带宽=25600 MB/s25.6 GB/s

这个结果 25.6 GB/s 正是该内存模块的理论峰值带宽

2.4 连接I/O

PIO 芯片概述

Z80 PIO 包含 4 个 CPU 可寻址的 8 位寄存器,分为 控制数据 两类:

划有横线的引脚代号表示其在电压为 0V有效;没有横线的则在电压为 +5V 时有效。

内部 4 个寄存器及其作用

寄存器名称 作用类别 主要功能
端口 A 控制 (Control) 控制 存储配置信息,用于设定 A 端口的工作模式(输入/输出/双向)、中断方式、握手信号等。
端口 A 数据 (Data) 数据 存储与 A 端口连接的外部设备(如指拨开关)交换的 8 位数据
端口 B 控制 (Control) 控制 存储配置信息,用于设定 B 端口的工作模式(输入/输出/双向)和相关控制逻辑。
端口 B 数据 (Data) 数据 存储与 B 端口连接的外部设备(如 LED)交换的 8 位数据

Z80 CPU 寻址机制(选中寄存器)

CPU 地址 A1 A0 选中端口(B/A 选中模式(C/D 被选中的 Z80 PIO 寄存器
00 0 0 选中 A (B/A 为 0) 选中 D (Data) (C/D 为 0) 端口 A 数据
01 0 1 选中 A (B/A 为 0) 选中 C (Control) (C/D 为 1) 端口 A 控制
10 1 0 选中 B (B/A 为 1) 选中 D (Data) (C/D 为 0) 端口 B 数据
11 1 1 选中 B (B/A 为 1) 选中 C (Control) (C/D 为 1) 端口 B 控制
CPU 通过操作 Z80 地址总线的 A0 和 A1 引脚来选择这 4 个寄存器。这两个引脚分别连接到 PIO 的 B/AC/D 控制引脚上:

Z80 PIO 与 CPU 连接方法

  1. 数据总线对接:将 Z80 PIO 的 D0~D7 数据引脚与 Z80 CPU 同名引脚直接相连,实现数据交换
  2. 地址线选择
    • PIO 的 B/A 引脚接 CPU A0A0=0 选端口A,A0=1 选端口B。
    • PIO 的 C/D 引脚接 CPU A1A1=0 选数据寄存器,A1=1 选控制寄存器。
  3. 地址空间:CPU 通过 A0~A1 的4种状态(00/01/10/11)选择PIO的4个寄存器。
  4. 未用引脚处理
    • CPU 的 A8~A15 地址线未使用,标记为 NC(No Connection,未连接)。
    • 输出引脚可悬空(NC),输入或双向引脚必须固定接 +5V 或 0V 避免悬空。

重点概念示例:控制 LED (输出)

以配置端口 B 为输出并控制 LED 为例:

  1. 配置端口 B (控制)
    • CPU 设置 A1A0=11(选中端口 B 控制寄存器)。
    • CPU 通过数据总线 写入 配置值(例如 C3H​),设定端口 B 为输出模式
  2. 输出数据 (数据)
    • CPU 设置 A1A0=10(选中端口 B 数据寄存器)。
    • CPU 通过数据总线 写入 要输出的数据(例如 0FH​),该数据立即从端口 B 的 PB0∼PB7 引脚输出,控制 LED 状态。

Z80 PIO 与 CPU 连接简图

PIO 引脚 连接 CPU 引脚 作用
D0∼D7 D0∼D7 数据交换(双向)
B/A A0 选择端口 (A 或 B)
C/D A1 选择模式 (数据 或 控制)

注: I/O 操作时,CPU 还会使用 IORQ​(I/O 请求)和 WR/RD(写/读)信号来完成总线操作时序。

PIO 连接到 RAM (TC5517/TC55617) 的原因:中断和数据处理

PIO 连接到 RAM(存储器芯片)并非是为了直接读写数据(PIO 不直接操作内存),而是为了实现 中断处理 机制和数据暂存

1. 中断向量(重要)

Z80 PIO 是一个支持中断的设备。当外部设备(如指拨开关)的状态发生变化时,PIO 可以向 CPU 发出中断请求 (INT)。

2. 数据暂存和程序运行

虽然 PIO 不会主动直接操作 RAM,但 CPU 从 PIO 读取的输入数据最终要被 CPU 拿到内存中进行处理、比较或暂存,而 CPU 输出给 PIO 的数据也是从内存或内部寄存器中取出的。因此,作为整个数据总线上的设备,PIO 间接地参与了与 RAM 的交互过程。

所有设备间的数据流转(PIO→CPU→RAM 或 RAM→CPU→PIO)都依赖于这条总线

总结: PIO 连接到 CPU 是为了直接的 I/O 控制,而它与 RAM 的联系主要是通过共享数据总线来实现中断向量的传递程序运行所需的数据流

2.5 连接时钟信号

Z80 CPU 和 Z80 PIO 需共享时钟信号以同步工作。

2.6 连接用于区分读写对象是内存还是I/O的引脚

连接内存(TC5517)和I/O(Z80 PIO)时,地址总线A0-A1冲突,需通过控制信号区分

解决方案:使用CPU的专用请求引脚

具体连接:

  1. 内存(TC5517)片选:将CPU的 MREQ 引脚连接至TC5517的 CE 引脚
    • MREQ=0激活内存芯片MREQ=1 时隔离(高阻态)
  2. I/O(Z80 PIO)片选:将CPU的 IORQ 引脚连接至Z80 PIO的 CEIORQ 引脚
    • IORQ=0激活PIO芯片IORQ=1 时隔离(高阻态)(两个引脚是因为这样更适用于多I/O场景)

数据方向控制:

Pasted image 20250920140824

2.7 连接剩余的控制引脚

CPU、内存和I/O上除地址/数据总线外的引脚统称为控制引脚,用于控制IC功能。

剩余控制引脚连接:

总结:
通过连接 M1 实现同步,连接 INT 使I/O能中断CPU流程。其他未提及控制引脚需根据具体电路需求处理。

RESET(重置)引脚操作:

  1. 功能:将Z80 CPU的 RESET 引脚先置 0 再恢复 1,可使CPU重置并从内存0号地址重新开始执行指令。
  2. 实现方式:通过按键开关电路实现:
    • 默认状态RESET 引脚通过电阻上拉至 +5V(逻辑1)
    • 按下按键:开关闭合,RESET 引脚被短接至 0V(逻辑0)
    • 释放按键:开关断开,RESET 引脚恢复 +5V(逻辑1)
  3. 电阻作用:串联在+5V与开关之间,防止短路(避免开关按下时+5V与0V直接相连导致电源短路)。
  4. 常见设计:这种“上拉电阻+开关接地”的结构是数字电路中实现复位、控制等功能的典型设计,广泛用于电路图中。
    Pasted image 20250920140905

上拉电阻+开关接地详细解析

产生电流的条件

  1. 有电压:就像水流需要有水压差一样,电流需要有电压差来驱动。
  2. 有闭合回路:电流需要一个完整、不间断的路径来流动。
闭合回路的作用

闭合回路(Closed Circuit) 是电流得以流动的通路
在闭合回路中,电子可以从电源负极出发,流经所有元件,最终回到电源正极,形成一个连续的循环。只有在这样的回路中,元件才能正常工作,例如灯泡发光、电机转动。

断开回路的作用

断开回路(Open Circuit) 是电流流动的中断
断开回路通常由开关控制,它的主要作用是停止电流流动。当回路断开时,即使有电压存在,电子也无法形成完整的通路,因此电流为零。这使得我们可以安全地控制和关闭电路中的设备,避免不必要的能耗或危险。

电压降

电压降是指电流流经电路中的某个元件时,这个元件两端的电压会降低的现象。
当电流流过一个电阻时,电阻会消耗一部分电能,导致其两端的电压产生差异,这部分被消耗的电压就是电压降(Voltage Drop)

要计算电阻的电压降,你不仅需要知道电阻值,还需要知道流经它的电流仅仅提供电源电压和电阻值,我们无法直接计算电压降

电压降的计算遵循欧姆定律
V=I×R

简单来说:电阻只有在有电流流过时才会产生电压降,如果电流是0,那么电阻就相当于一根普通的导线,不消耗任何电压。

左图电路闭合时「有电流流过」:输入0

Pasted image 20250920140905
这幅图展示了如何将一个 逻辑0(0V) 输入到IC的引脚。

右图电路断开无电流流过」:输入1

Pasted image 20250920140905
此时,电流无法形成回路,所以流过 4.7kΩ 电阻的电流为 0A
这幅图展示了如何将一个 逻辑1(+5V) 输入到IC的引脚。

欧姆定律 (I=V/R),
V (Voltage):电压,单位是伏特(V)。可以理解为驱动电流流动的“电压力”或“势能差”。
I (Current):电流,单位是安培(A)。表示单位时间内通过导体横截面的电荷量,即电荷流动的速率
R (Resistance):电阻,单位是欧姆(Ω)。表示导体对电流流动的阻碍能力
当电阻 R 趋近于 0 时,通过的电流 I 将会急剧增大。这股巨大的、不受控制的电流会瞬间从电源流向地线,导致电源过载、发热,甚至烧毁电路板或电源本身。这就是我们所说的短路

RESET 电路与自动上电复位:

电容在通电瞬间充电,使 RESET 引脚电压缓慢上升(暂保持低电平,不会立刻上 升到 +5V),充电完成后恢复高电平 +5V,实现自动复位。

(注:74367 为总线驱动器,用于增强信号或扩展连接,具体功能后续说明。)
Pasted image 20250920180843
Pasted image 20250920180912

2.8 连接外部设备,通过DMA输入程序

元件连接与数据写入

  1. 开关连接
    • 数据开关:第一个指拨开关连接到内存 TC5517 的数据总线引脚 D0~D7
    • 地址开关:第二个指拨开关连接到 TC5517 的地址总线引脚 A0~A7
    • 写入控制:按键开关连接到 TC5517 的 WE(写使能)引脚。
    • 内存状态:TC5517 的 RD(读)引脚上拉到 +5VCE(片选)引脚连接到 0V
  2. 写入操作:拨动指拨开关设定地址和数据,按下按键开关即可将数据写入 TC5517。

隔离与 DMA 控制

  1. 隔离需求:为防止程序执行时开关状态干扰电路,需要使用 74367 三态总线缓冲器进行隔离。
  2. 74367 特性
    • 导通:当控制引脚 G1G2 同时为 0 时,信号通过。
    • 隔离:当 G1G2 同时为 1 时,74367 与电路隔离。
  3. DMA 启用
    • 打开 Z80 CPU 的 BUSRQ 开关,CPU 进入 DMA 状态。
    • CPU 通过 BUSAK(总线应答)引脚输出 0
  4. 控制布线:将 Z80 CPU 的 BUSAK 引脚连接到所有 74367 的 G1G2 引脚上。这使得 CPU 进入 DMA 状态后,74367 导通,开关信号才能通过总线,实现通过 DMA 向内存写入数据。

2.9 连接用于输入输出的外部设备

Z80 PIO 输入/输出布线

数据输入(指拨开关)

用于从外部输入数据的指拨开关连接到 Z80 PIO 的 PA0~PA7 引脚。布线采用直接连接方式。

注意:不使用 74367目的是确保微型计算机在程序运行时能够实时地从指拨开关获取输入数据

数据输出(LED)

用于向外部显示数据的 LED 连接到 Z80 PIO 的 PB0~PB7 引脚。

  1. 连接方式: LED 必须通过电阻连接到 +5V。根据惯例,这个电路设计是:输入 0V 时点亮 LED输入 +5V 时关闭 LED(如图 2.9 所示)。
  2. 驱动 IC: LED 必须通过 7404 反相器 连接到 Z80 PIO。
  3. 最终逻辑: 7404 的作用是反转电信号(0 变 1,1 变 0)。
    • 当 Z80 PIO 输出 0V (逻辑 0) 时,7404 将其反转为 +5V,LED 熄灭。
    • 当 Z80 PIO 输出 +5V (逻辑 1) 时,7404 将其反转为 0V,LED 点亮。
    • 这种设计实现了:Z80 PIO 输出 1LED 点亮,输出 0LED 熄灭

IC 供电与未用引脚

所有集成电路(如 74367 和 7404)的 Vcc 引脚都必须连接到 +5V,而 GND 引脚连接到 0V。对于这些 IC 上未使用的引脚(标有 NC 或未连接的),可以选择空着不接,或将它们连接到 GND 上。
Pasted image 20250920141029

Z80 CPU、Z80 PIO、7404、LED 之间的数据输入输出

CPU 与外部设备交换数据的过程分为两个阶段:初始化配置数据交换

1. 初始化配置(设置端口工作模式)

在进行任何数据读写之前,CPU 必须通过 控制寄存器 来配置 PIO 端口的工作模式。

目标寄存器 CPU 地址 CPU 操作 目的
端口 A 控制 01 (A0=0,A1=1) IN (输入) 写入 A 端口配置数据,设定 A 为输入模式
端口 B 控制 11 (A0=1,A1=1) OUT (输出) 写入 B 端口配置数据,设定 B 为输出模式

2. 数据交换(实际读写数据)

数据输入(从指拨开关到 CPU)
  1. 外部输入: 指拨开关的状态(开/关)以电平形式直接反映在 Z80 PIO 的 PA0~PA7 引脚上。
  2. CPU 寻址: Z80 CPU 将地址信号设置为 A1=0A0=0(即 I/O 地址 00),选中 端口 A 数据 寄存器。
  3. CPU 读取: CPU 执行 IN(输入)指令。Z80 PIO 将当前 PA0-PA7 引脚上的电平状态读入端口 A 数据寄存器,然后通过 D0D7 数据总线发送给 CPU。
  4. CPU 接收: CPU 接收这 8 位数据,存入其内部的通用寄存器中进行处理。
数据输出(从 CPU 到 PIO)
  1. CPU 准备数据: CPU 在内部完成数据处理,生成一个 8 位输出值。
  2. CPU 寻址: Z80 CPU 将地址信号设置为 A1=1A0=0(即 I/O 地址 10),选中 端口 B 数据 寄存器。
  3. CPU 写入: CPU 执行 OUT(输出)指令。CPU 将其 8 位输出数据通过 D0D7 数据总线写入 Z80 PIO 的端口 B 数据寄存器。
  4. PIO 输出: Z80 PIO 立即将端口 B 数据寄存器的值输出到 PB0~PB7 引脚上。
输出到 LED(从 PIO 到 LED)

数据从 Z80 PIO 的 PB 端口出来后,必须经过 7404 反相器才能驱动 LED,以实现正确的逻辑。

  1. Z80 PIO 输出数据: PB 端口上的 8 位信号(+5V0V)代表了 CPU 希望 LED 呈现的状态(1 = 亮, 0 = 灭)。
  2. 7404 反相:
    • 每个 PB 引脚连接到 7404 反相器的一个输入端。
    • 7404 将接收到的信号反转
      • PB = 1 (+5V) 7404 输出 0V
      • PB = 0 (0V) 7404 输出 +5V
  3. 驱动 LED:
    • 7404 的输出端连接到 LED 的负极(经过电阻)。
    • 由于 LED 是连接到 +5V 上的(正极接 +5V),只有当负极电压拉低到 0V 时,电流才能流过 LED 并点亮它。
  4. 最终结果: 7404 成功地将 CPU 的逻辑 1 转换成了 LED 所需的 0V 驱动信号,从而使得 CPU 发出 1 时 LED 点亮,发出 0 时 LED 熄灭。

2.10 输入测试程序并进行调试

好的,我将您提供的机器语言测试程序(代码清单 2.1)整理成表格形式。为了清晰展示,我将二进制地址程序代码转换为更常用的十六进制表示,并保留了原始的二进制值作为参考。

代码清单 2.1:Z80 PIO 测试程序(机器语言)

内存地址 (二进制) 内存地址 (十六进制) 程序代码 (二进制) 程序代码 (十六进制) 备注 (字节数)
00000000 00 00111110 3E 1
00000001 01 11001111 CF 2
00000010 02 11010011 D3 3
00000011 03 00000010 02 4
00000100 04 00111110 3E 5
00000101 05 11111111 FF 6
00000110 06 11010011 D3 7
00000111 07 00000010 02 8
00001000 08 00111110 3E 9
00001001 09 11001111 CF 10
00001010 0A 11010011 D3 11
00001011 0B 00000011 03 12
00001100 0C 00111110 3E 13
00001101 0D 00000000 00 14
00001110 0E 11010011 D3 15
00001111 0F 00000011 03 16
00010000 10 11011011 DB 17
00010001 11 00000000 00 18
00010010 12 11010011 D3 19
00010011 13 00000001 01 20
00010100 14 11000011 C3 21
00010101 15 00010000 10 22
00010110 16 00000000 00 23
该程序的功能是读取指拨开关状态(输入),然后将该状态实时输出到 LED(输出)

1. 硬件组装后的启动状态

2. 编程语言的限制

3. 程序输入(人工加载机器码)流程

这里描述了将代码清单 2.1 所示的机器语言程序手动输入到内存中的过程:

步骤 操作对象 动作描述 目的
1. 准备 DMA 开关 按下 Z80 CPU 上的 DMA 请求开关(进入输入模式)。 允许人工控制总线,向内存写入数据。
2. 寻址 地址指拨开关 拨动开关,设定代码行对应的内存地址(如 00000000)。 确定程序指令的存储位置。
3. 输入 程序指拨开关 拨动开关,设定代码行对应的 8 位程序代码(如 00111110)。 准备要写入内存的数据。
4. 写入 写入按键开关 按下按键开关。 将当前设定的代码写入到当前设定的内存地址。
5. 重复 全流程 反复执行步骤 2、3、4。 直到代码清单 2.1所有行全部输入完毕。

4. 程序运行与功能

既然你提供了 TC5517(这是一个经典的 2K x 8 位 SRAM 芯片)的电路符号图,我们就以此为物理蓝图,还原“世”字存储的微观全过程。

存储实例:将“中”字写入 TC5517 (SRAM)

Pasted image 20260228134247

1. 编码转换:从“中”到电平信号

在存储开始前,CPU 已将“中”字转换为二进制序列(UTF-8 编码)。

2. 寻址定位:物理映射逻辑

TC5517 拥有 16,384 个存储位元,划分为 2,048 个字节。为了降低功耗,该芯片采用高位定行的非对称架构:

地址引脚 逻辑功能 物理实现 映射结果 (以地址 0x001 为例)
A10 - A3 行地址 (Row) 选通 256 条 Wordline 之一 00000000第 0 行
A2 - A0 列地址 (Col) 选通 8 组 Bitlines 之一 001第 1 列
物理场景结论

当 CPU 指定地址为 1 时,内部译码器激活 Wordline 0。这根字线横穿整个阵列,物理上接通了该行所有 6T 单元的晶体管“闸门”。

3. 位宽并联与物理布线细节

选中“第 1 列”在物理层面并非单一动作,而是 8 个子阵列(Sub-arrays) 的同步协作:

4. 存储过程:6T SRAM 的微观动作

SRAM 内部没有电容,数据依靠由 6 个晶体管 (6T) 组成的双稳态触发器锁存。
以写入 D7 位的“1”状态为例:

  1. 行选通 (Row Activation):Wordline 0 变为高电平,开启访问晶体管。
  2. 强制翻转 (State Overwrite)
    • 外部驱动电路将 Bitline7 拉高,同时将 Bitline7 拉低。
    • 强大的电位差压过单元内部维持电流,像“跷跷板”一样将内部电路顶向“1”端。
  3. 静态锁存 (Static Latching)
    • 当写使能 (WE) 瞬间拉低再拉高,Wordline 0 随之关闭。
    • 依靠内部反馈电路,只要 Vcc (24脚) 有电,该状态就永久锁存,无需刷新。

5. 控制信号时序总结

  1. CE (Chip Enable):拉低,激活芯片。
  2. WE (Write Enable):脉冲拉低,锁定数据。
  3. RD (Read):保持高电平,关闭读取路径。

📍 存储轨迹总结表 (地址 0x01 / 数据 0xE4)

步骤 动作主体 物理表现 结果
寻址 A10-A3 / A2-A0 Wordline 0 激活;第 1 列 8 对位线通电 定位到 8 个特定的 6T 单元
写入 D7-D0 引脚 差分电压注入 BL / BL 强行改变触发器平衡状态
保持 Vcc 电源环路 交叉耦合反相器持续反馈 完成“中”字首字节的物理锁存
为什么 A3-A10 是行? 低位地址 (A0-A2) 切换最频繁。将高位作为行地址能显著减少 Wordline 的翻转次数,这是 TC5517 作为经典低功耗 CMOS 芯片的标志性设计。

第3章:体验一次手工汇编

3.1 从程序员的角度看硬件

在进行手工汇编之前,需要了解以下 7 种核心硬件信息:
CPU 种类、时钟频率、内存地址空间、内存存储大小、I/O 种类、I/O 地址空间、连接的周边设备。

一、中央处理器 (CPU) 信息

  1. CPU 种类: Z80 CPU。
  2. 机器语言: 适用于 Z80 的机器语言(原生代码),由 0 和 1 构成,CPU 可直接理解和执行。
  3. 时钟频率: 2.5 MHz (兆赫兹)。
    • 用于估算指令执行时间,一条指令的时钟周期数取决于其类型。

二、内存信息

  1. 地址空间: 0255(共 256 个地址)。
  2. 存储单位: 每个地址可存储 8 比特(1 字节)的指令或数据。
  3. 特性: 内存中所有地址的功能相同,均可用于存储指令和数据。

三、I/O (输入/输出) 信息

  1. I/O 芯片: 仅安装了一个 Z80 PIO 芯片。
  2. 内部结构: PIO 内部带有 4 个 8 比特寄存器
  3. I/O 地址空间: 03(共 4 个地址)。
  4. I/O 特性: I/O 中不同地址(寄存器)对应不同功能,用于控制 PIO 功能或进行数据传输。
  5. 寄存器分配与连接:
    • 0 号地址: 端口 A 数据寄存器(连接指拨开关 数据输入)。
    • 1 号地址: 端口 B 数据寄存器(连接 LED 数据输出)。
    • 2 号地址: 端口 A 控制寄存器(用于设定 PIO 功能)。
    • 3 号地址: 端口 B 控制寄存器(用于设定 PIO 功能)。

3.2 机器语言和汇编语言

代码清单 3.1,这段机器语言程序的功能可以精简地总结为:
程序读取指拨开关的输入数据,并将其直接、原封不动地输出给 LED。
简单来说,它的作用就是让拨动指拨开关能立即控制对应 LED 的亮或灭,实现输入和输出的直接映射。

代码清单 3.1 点亮 LED 的机器语言程序

地址 (二进制) 机器语言 (二进制)
00000000 00111110
00000001 11001111
00000010 11010011
00000011 00000010
00000100 00111110
00000101 11111111
00000110 11010011
00000111 00000010
00001000 00111110
00001001 11001111
00001010 11010011
00001011 00000011
00001100 00111110
00001101 00000000
00001110 11010011
00001111 00000011
00010000 11011011
00010001 00000000
00010010 11010011
00010011 00000001
00010100 11000011
00010101 00010000
00010110 00000000
这段23 字节的机器语言程序被写入内存地址 00000000 到 00010110
CPU 重置后,将从0 号地址开始,按顺序执行这段程序。

机器语言到汇编语言的过渡

这段文字描述了从难以理解的机器语言(纯粹的 0 和 1)到更具可读性的汇编语言的转变。

汇编语言是将机器语言(0 和 1)符号化的编程方式。它使用类似英文的助记符(操作码)。
其语法极为简单,每行只包含三部分:标签操作码(指令)操作数(指令对象)

代码清单 3.2 汇编语言代码
标签 操作码 (指令) 操作数 (指令对象)
LD A, 207
OUT (2), A
LD A, 255
OUT (2), A
LD A, 207
OUT (3), A
LD A, 0
OUT (3), A
LOOP: IN A, (0)
OUT (1), A
JP LOOP

汇编语言核心概念

1. 汇编语言的三个要素

2. 语法和数据

3. I/O 访问机制

3.3 Z80 CPU 的寄存器结构

Pasted image 20250930124638

CPU、内存与 I/O

Z80 CPU 寄存器

汇编语言基础

Z80 PIO 编程示例

这个示例程序分为两部分:设定 Z80 PIO与 Z80 PIO 进行输入输出

1. 设定 Z80 PIO

2. 与 Z80 PIO 进行输入输出 (死循环)

3.4 追踪程序的运行过程

代码清单 3.3 汇编语言与机器语言的对应关系

地址 (Address) 机器语言 (Machine Code) 标签 (Label) 操作码 (Opcode) 操作数 (Operand)
00000000 00111110 11001111 LD A, 207
00000010 11010011 00000010 OUT (2), A
00000100 00111110 11111111 LD A, 255
00000110 11010011 00000010 OUT (2), A
00001000 00111110 11001111 LD A, 207
00001010 11010011 00000011 OUT (3), A
00010100 00111110 00000000 LD A, 0
00010110 11010011 00000011 OUT (3), A
00010000 11011011 00000000 LOOP: IN A, (0)
00010010 11010011 00000001 OUT (1), A
00010100 11000011 00010000 00000000 JP LOOP

汇编语言与机器语言

程序执行流程 (PC 寄存器的作用)

程序运行的核心在于 PC (程序指针) 寄存器,它控制了程序的流程。

  1. CPU 重置:CPU 自动将程序的起始地址(如 00000000)存储到 PC 寄存器中。
  2. 读取指令:CPU 从 PC 寄存器指示的内存地址读出机器语言(指令)。
  3. 判断长度并读取操作数:CPU 根据读取的第一个字节判断该指令的完整长度(可能是一个字节,也可能是多个字节),并从后续地址读出所有构成该指令的数据(如操作数)。
  4. 解释和执行:CPU 汇集完整的机器语言数据,并执行对应的操作(例如:将数值 207 写入寄存器 A)。
  5. 更新 PC 寄存器:PC 寄存器的值自动增加读取指令所占用的内存地址数(字节数)。例如,如果指令是 2 字节,PC 值就 +2,指向下一条指令的起始地址。
  6. 循环执行:CPU 不断重复“读取指令” “解释、执行指令” “更新 PC 寄存器的值”这三个操作,程序便持续运行。
  7. 跳转指令 (JP):当执行到跳转指令(如 JP LOOP)时,PC 寄存器的值不会自动增加,而是被直接设置为标签(LOOP)对应的目标地址(例如 00010000),从而实现循环或流程控制。

3.5 尝试手工汇编

手工汇编就是逐行对照 CPU 资料(包含助记符及其对应的机器语言数值),将汇编语言程序手工翻译成机器语言程序的过程。

表 3.2 从助记符到机器语言的转换方法

助记符 机器语言 时钟周期数
LD A, num 00111110 num 7
OUT (num), A 11010011 num 11
IN A, (num) 11011011 num 11
JP num 11000011 num 10

关键概念:数据存储顺序 (Endianness)

代码清单3.3 手工汇编过程总结

核心步骤

  1. 指令匹配与替换: 将汇编指令匹配到对应的机器语言模式(如 LD A, num 匹配 00111110 num)。
  2. 数值转换: 将十进制的操作数(如 207)转换为 8 比特二进制数(如 11001111)替换模式中的 num
  3. 地址确定:
    • 每条指令的机器码占用多少字节内存地址就相应增加多少。
    • JP (跳转) 指令的地址操作数需要用 16 比特表示(高 8 位补 0)。
  4. 小端序存储(Z80 特有): 在存储多字节数据(如 16 比特地址)时,采用小端序低 8 位在前,高 8 位在后)的逆序存储方式。

关键示例

简而言之: 手工汇编通过查表、将十进制操作数转为二进制并替换到机器码模式中,然后按指令长度递增地址,最后按照 CPU 特有的大小端序规则(此处为小端序)存储多字节数据。

3.6 尝试估算程序的执行时间

利用时钟周期数CPU 时钟频率来估算汇编程序代码的执行时间。

核心步骤与概念

  1. 统计时钟周期数:
    • 查阅指令集资料(如表 3.2),找出每条指令执行所需的时钟周期数
    • 将程序中所有指令的周期数累加,得到程序段的总周期数。
    • 示例:
      • LOOP 标签前 8 条指令:7+11+7+11+7+11+7+11=72 个周期。
      • LOOP 标签后 3 条指令:11+11+10=32 个周期。
  2. 计算单个周期时间:
    • 时钟频率: 微型计算机使用 2.5MHz 晶振,即每秒 2,500,000 个周期。
    • 单个周期时间: 1 秒÷2,500,000 次/秒=0.0000004 秒=0.4 微秒
  3. 计算程序执行时间:
    • 总周期数 × 单个周期时间 = 执行时间。
    • 示例:
      • 72 个周期×0.4 微秒/周期=28.8 微秒
      • 32 个周期×0.4 微秒/周期=12.8 微秒 (LOOP 段)。

循环执行频率估算

结论: 通过累加指令的时钟周期数并结合 CPU 时钟频率,可以精确估算出程序的执行时间,凸显了计算机惊人的运算速度。

第4章:程序像河水一样流动着

程序是“流动着的”

4.1 程序的流程分为三种

Pasted image 20251005175958

顺序执行 (Sequential Execution)

CPU 的执行流程

顺序执行机制:PC 寄存器

Pasted image 20251005175442

条件分支 (Conditional Branch)

循环 (Loop)

4.2 用流程图表示程序的流程

1. 流程图的定义与价值

2. 流程图的基本构成和图示方法

流程图仅需少数基础符号就能表达所有程序逻辑。
Pasted image 20251005180625

符号(形状) 名称 用途
矩形 处理框 表示处理/执行的步骤(如:初始化变量、增加计数器)。
菱形 判断框 表示条件分支循环条件的判断(如:是否平局、是否进行了 5 轮)。
圆角矩形/椭圆 终端符号 表示程序的开始结束
箭头/直线 流程线 连接符号,表示程序的流向
程序的三种基本流程都可由上述基础符号组合表示:
  1. 顺序执行:直线矩形框依次连接起来。
  2. 条件分支: 使用菱形(判断框),根据判断结果引出两条或多条分支流程。
  3. 循环 (Loop): 通过使用菱形(条件判断),将流程指引回前面的处理步骤,形成回路。
    Pasted image 20251005180657

4. 结论

4.3 表示循环程序块的“帽子”和“短裤”

帽子和短裤

在某些流程图标准(可能常见于信息技术水平考试)中,表示循环结构的一种特殊符号表示法。
Pasted image 20251005180802
Pasted image 20251005180856
Pasted image 20251005181124

1. 循环的特殊符号

2. 区分嵌套循环的规则

核心总结: 某些流程图使用成对的“帽子”和“短裤”符号来界定循环体。为了处理循环嵌套问题,必须在每对符号上标记不同的名称以明确匹配关系。

「程序块」和流程图的「帽子与短裤」符号

1. 硬件层面的循环实现(机器语言/汇编语言)

2. 高级语言的循环实现(程序块)

代码清单 4.2 用高级语言表示循环

' 进行 5 轮比试
For i = 1 To 5  ————— 相当于 “帽子”
    ' 处理步骤
    ...
Next            ————— 相当于 “短裤”

Pasted image 20251005182042

核心总结:

条件分支与循环在软硬件中的实现总结

1. 硬件层面的实现(汇编/机器语言)

在直接操作硬件的低级语言中,条件分支循环都通过同一个基本机制实现:跳转指令

2. 高级语言的实现(程序块)

在高级语言中,这些结构被抽象为程序块,提高了可读性和编程效率。

Pasted image 20251005182449
代码片段 4.3 用高级语言表示的条件分支

' 判定胜负,显示结果
If use = computer Then
    MsgBox s & "...平局!"    ' 区域 (1) - 平局
ElseIf computer = (user + 1) Mod 3 Then
    MsgBox s & "...玩家获胜!" ' 区域 (2) - 玩家获胜
    wins = wins + 1
Else
    MsgBox s & "...计算机获胜!" ' 区域 (3) - 计算机获胜
End If

核心总结

无论是循环还是条件分支,其在硬件上的本质都是通过跳转指令改变程序流程;但在高级语言中,它们被抽象为易于理解和编写的 For/NextIf/ElseIf/End If程序块结构。

4.4 结构化程序设计

结构化程序设计与“GoTo 有害论”

1. 结构化程序设计的核心思想

2. 为什么弃用跳转指令 (GoTo 有害)

3. 结构化流程与程序块

程序块是结构化程序设计的基础:

流程结构 高级语言程序块示例 作用
条件分支 If...ElseIf...End If 代替了依赖于跳转的复杂分支。
循环 For...Next 代替了依赖于跳转的循环回跳。
结构化异常处理 Try...Catch...End Try (Visual Basic.NET) 是一种现代的错误处理机制。它使用程序块来取代旧版本语言中“发生错误就跳转到错误处理代码”的非结构化方式。
(1) 旧版本的 Visual Basic 用跳转指令(GoTo 语句)实现错误处理
On Error GoTo ERR_HANDLER
' ...
' 做某些处理
' ...

' ---------------------------------
' , 处理错误的部分
ERR_HANDLER:
MsgBox "出错了!"
(2) Visual Basic .NET 用程序块实现错误处理
Try
' 做某些处理
' ...

Catch e As Exception
' , 处理错误的部分
MsgBox "出错了!"
End Try

4. 软硬件的最终关系

核心总结

结构化程序设计倡导仅使用顺序、分支、循环这三种程序块结构来编写程序,并废除 GoTo 等跳转指令,以避免代码陷入“意大利面条”式的混乱。尽管硬件最终仍用跳转指令,但高级语言的抽象让程序员能够专注于编写清晰、有条理的代码。

4.5 画流程图来思考算法

1. 算法的定义

2. 思考算法的要点(两步走)

  1. 从整体上考虑程序的粗略流程
  2. 再考虑程序各个部分的细节流程(将在后续章节介绍)。

3. 程序的整体流程(粗略流程图)

4. 示例(见图 4.12 & 4.13)

Pasted image 20251005210449
Pasted image 20251005210501

5. 建议

4.6 特殊的程序流程——中断处理

1. 定义

2. 中断流程与硬件机制 (以 Z80 CPU 为例)

  1. 数据输入 (1):用户在键盘上按下按键。
  2. 中断请求信号 (2):连接到键盘的 I/O 模块向 CPU 发出中断请求信号(例如通过 Z80 CPU 的 INT/NMI 引脚)。
  3. 流程跳转 (3):CPU 接收信号后,程序的流程突然跳转到特定的中断处理例程/程序 (Handler/Routine)
  4. 数据读入 (4):CPU 在中断处理程序中从 I/O 设备(键盘)读入数据

注解 A:INT 用于一般中断请求;NMI(非屏蔽中断)用于即使 CPU 屏蔽了中断也必须立即响应的情况。
Pasted image 20251005211344

3. 中断处理的性质

4. 程序员须知

4.7 特殊的程序流程——事件驱动

1. 定义与机制

2. 代码实现骨架 (以 C 语言/Windows 为例)

代码清单 4.5 用C语言编写的Windows 应用程序的骨架

/* 主例程 */
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInst, LPSTR lpCmdLine, int nCmdShow){
    ...
}
/* 窗口过程 */
LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam){
    ...
}

3. 性质

4. 流程的表示方法

由于事件驱动流程图会涉及大量条件分支(菱形符号),画起来复杂,因此推荐使用以下方式表示:

A. 状态转化图

B. 状态转化表

Pasted image 20251005212428

第5章:与算法成为好朋友的七个要点

5.1 算法是程序设计的“熟语”

1. 算法的定义与目的

2. 算法的层次

3. 算法的重要性

4. 消除对算法的误解

5.2 要点 1:算法中解决问题的步骤是明确有限

JIS(日本工业标准)定义:被明确定义的有限个规则的集合, 用于根据有限的步骤解决问题。 例如在既定的精度下,把求解 sin x 的计算步骤无一遗漏地记录下来的文字

求出 12 和 42 的 最大公约数

Pasted image 20251015140330

  1. 最大公约数的求解方法(中学方法):
  1. 对该方法作为“算法”的质疑:

5.3 要点 2:计算机不靠直觉而是机械地解决问题

辗转相除法(求解最大公约数的方法)

Pasted image 20250814094756

辗转相除法

$a = 12;
$b = 42;
while ($a != $b) {
    if ($a > $b) {
        $a = $a - $b;
    } elseif ($b > $a) {
        $b = $b - $a;
    }
}
echo '最大公约数为 ' . $a . PHP_EOL; // 最大公约数为 6

5.4 要点 3:了解并应用典型算法

典型算法

名称 用途
辗转相除法 求解最大公约数
埃拉托斯特尼筛法 判定素数
顺序查找 检索数据
二分查找 检索数据
哈希查找 检索数据
冒泡排序 数据排序
快速排序 数据排序

因数

在数学中,因数(Factor)也叫1;;约数,是指一个能够1;;整除给定1;;正整数的数。

核心定义

如果整数 a 除以整数 bb0),除得的1;;商正好是1;;整数而没有1;;余数,我们就说 ba1;;因数

公式表示:a÷b=c(其中 a,b,c 均为整数),则 bc 都是 a 的因数。

因数的关键特性

  1. 成对出现: 因数通常是成对产生的。例如,因为 3×4=12,所以 3 和 4 都是 12 的因数。
  2. 范围固定: 一个数的因数总是==1;;<===它本身。
  3. 最小与最大: 任何正整数最小因数1;;1最大因数1;;它本身
  4. 有限性: 一个数的因数个数是有限的。

举例说明:求 18 的所有因数

我们可以通过寻找乘法对来列出 18 的所有因数:

因数与质数、合数的关系

实用技巧:如何快速找因数

从 1 开始依次尝试除以每个自然数,如果能整除,就把除数都记下来。
当你尝试的数字开始重复(比如已经找到了 3×6,下一个尝试的是 6×3)时,就说明你已经找全了

求解最小公倍数的算法

LCM(最小公倍数)Least Common Multiple
GCD(最大公约数)Greatest Common Divisor

最小公倍数的定义和常规求解方法:

适用于计算机的最小公倍数算法:

LCM(a,b)=a×bGCD(a,b)

(两个整数的乘积除以这两个整数的最大公约数)

LCM(12,42)=12×426=84

(其中 6 是上文求得的 GCD(12,42)

对该方法的评价:

学习建议:

公式分析

理解公式法 LCM(a,b)=a×bGCD(a,b) 的核心在于:避免重复计算公共的部分

1. 核心逻辑:消除“重复”

想象每一个数都是由若干个 “质因数” 组成的。

2. 质因数分解的视角(最透彻的理解)

假设我们要找 12 和 18 的最小公倍数:

(2×2×3)×(2×3×3)

你会发现其中的 (2×3) 出现了两次。
而真正的 LCM 只需要把 所有的因数“集齐”不重复

LCM=2×2×3×3=36

所以公式就变成了:

LCM=12×186=2166=36
3. 集合论视角(韦恩图模型)

如果你把数 a 的因数看作集合 A,数 b 的因数看作集合 B

4. 为什么这个方法最快?

在处理大数字时,分解质因数可能非常耗时,但求 GCD(最大公因数) 有一个极其高效的算法——辗转相除法(Euclidean Algorithm)

  1. 用辗转相除法快速算出 GCD。
  2. 代入公式即可瞬间得到 LCM。

小技巧: 计算时,先用其中一个大数除以 GCD,再去乘以另一个数。例如计算 120×4515,先算 120÷15=8,再算 8×45=360,这样可以避免处理巨大的中间乘积。

5.5 要点 4:利用计算机的处理速度

埃拉托斯特尼筛法

是一种用于把某个范围内的所有素数都筛选出来的算法, 比如筛选 100 以内的所有素数, 其基本思路就是用待判定的数除以比它小的所有正整数。

判定是否是素数的程序

$a = 91;
$s = " 是素数。";
// 循环从 2 到 $a-1
for ($i = 2; $i < $a; $i++) {
    // 检查 $a 是否能被 $i 整除
    if ($a % $i === 0) {
        $s = " 不是素数。";
        // 如果找到一个因数,就退出循环
        break;
    }
}
// 组合并输出结果
echo $a . $s;

求解鸡兔同笼问题的程序

Pasted image 20250814095441
因为鸡和兔子的只数应该都在 0~10 这个范围内, 所以就试着把 0~10 中的每个数依次代入 x 和 y,只要能够找到使这两个方程同时成 立的数值也就求出了答案。 利用计算机的处理速度, 答案一瞬间就出 来了。

// 外层循环遍历鸡的数量
for ($x = 0; $x <= 10; $x++) {
    // 内层循环遍历兔子的数量
    for ($y = 0; $y <= 10; $y++) {
        // 计算鸡和兔子的总数
        $total_animals = $x + $y;

        // 计算鸡和兔子的总脚数
        $total_legs = 2 * $x + 4 * $y;

        // 如果满足两个条件:总数等于 10,总脚数等于 32
        if (($total_animals === 10) && ($total_legs === 32)) {
            // 输出结果
            echo "鸡的数量 = " . $x . ",兔子的数量 = " . $y; // 鸡的数量 = 4,兔子的数量 = 6
        }
    }
}

5.6 要点 5:使用编程技巧提升程序执行速度

素数/质数(Prime Number)

素数和质数是同一个概念,基础教育偏向质数,高等教育偏向素数

概念

素数:是指一个大于 1 的自然数,除了 1 和它本身以外,不能被其他任何自然数整除
自然数:是 0,1,2,3,… 这些数。它们是整数(没有小数或分数部分)中所有非负的数

核心观点

为什么 N2 的优化是正确的?

如果 N 是合数(非素数),它一定可以被分解为 N=A×B

更优算法:N

如果 N 是合数(非素数),它一定可以被分解为 N=A×B,其中 AB 都是大于 1 的整数。
原理如下:
在乘积 N=A×B 中,AB 不可能同时大于 N

  1. 如果 A>NB>N,那么 A×B>N×N=N,这与前提矛盾。
  2. 因此,任何合数 N 必有一个因数小于或等于 N

效果对比

检查范围 约需执行的除法次数 效果提升(与检查到 N1 相比)
原算法2N1 N 基准
文中技巧2N2 N2 减少约 12
标准技巧2N N 极大地减少
对于 N=999999937

哨兵

指的是一种含有特殊值的数据,可用于标识数据的结尾等。

字符串的末尾用 0 表示,链表的末尾用-1 表示,像这种特殊的数据就是哨兵。 在本章中, 我们将展示如何在“线性搜索”算法中灵活地应用哨兵。
 
首先看看不使用哨兵的方法。 从第一个箱子开始依次检查每个箱 子中的纸条。 每检查完一个纸条, 还要再检查箱子的编号(用变量 N 表示),并进一步确认编号是否已超过最后一个编号了。这个过程用流 程图表示后如图 5.6 所示。
Pasted image 20250814095542
图 5.6 所示的过程,虽然看起来似乎没什么问题,但是实际上含有 不必要的处理——每回都要检查箱子的编号有没有到 100。

通过放入哨兵, 就一定能找到要找的数据了。 找到要找的数据后, 如果该箱子的编号还没有到 101 就意味着找到了实际的数据;如果该箱子的编号是 101,则意味着找到的是哨兵,而没有找到实际的数据。
Pasted image 20250814095630

5.7 要点 6:找出数字间的规律

所有的信息都可以用数字表示——这是计算机的特性之一。 因此 为了构造算法, 经常会利用到存在于数字间的规律。
Pasted image 20250814100000

$a = 1; // 假设玩家A出布
$b = 0; // 假设玩家B出石头

if ($a === $b) {
    echo "平局";
} elseif ($a === ($b + 1) % 3) {
    echo "玩家 A 获胜";
} else {
    echo "玩家 B 获胜";
}

5.8 要点 7:先在纸上考虑算法

在纸上画完或写完流程以后, 再把具体的数据代入以跟踪流程的 处理, 确认是否能得到预期的结果。

曾经有一本被誉为凡是立志成为程序员的人都应该去读的名著, 那就是 Niklaus Wirth 的 Algorithms + Data Structures = Programs

第6章:与数据结构成为好朋友的七个要点

6.1 要点1:了解內存和变量的关系

Pasted image 20250826095313

变量和地址的基础概念

计算机所处理的数据都存储在了被称为内存的IC(IntegratedCircuit,集成电路)中。在单片机或现代计算机体系结构中,内存存储的基本单位(寻址最小单位)通常被设计为 8 bit(1 byte)
为了区分各个单元,每个单元都被分配了一个编号,这个编号被称为“地址”。如果一台个人计算机装配有64M字节的内存,那么就会有从0到64M(M=100万)这么多个地址

变量名与值的内存归宿

1. 物理层 (Memory IC)
2. 逻辑层 (Compiler/OS)

多字节变量的存储方式

软件层面:数据类型溢出 (Overflow)

如果你在程序定义时强行将一个大于 255(281)的数值赋给一个 charuint8_t 类型的变量,会发生截断

硬件与逻辑层:多字节存储 (Multi-byte Storage)

当一个变量(如 int, long, float, double 或结构体等)占据超过一个字节的空间时(例如,一个 int 在多数系统中是 4 个字节):

  1. 连续存储:这个变量的所有字节会连续地存放在内存中相邻的地址上。
  2. 首地址(Lowest Address):该变量的地址(即指针所存储的值)是它所占据的第一个字节的地址(即地址值最小的那个字节)。
示例:一个 4 字节的 int 变量

假设在一个 32 位系统中,一个 int 变量 i 占据 4 个字节,它的值为 0x12345678(十六进制表示),并且它的起始a地址是 0x1000

因为 24=16,所以 1 位 16 进制数正好对应 4 位 2 进制数

地址 字节中的数据
0x1000 字节 1
0x1001 字节 2
0x1002 字节 3
0x1003 字节 4
变量 i 的地址就是 0x1000

字节序(Endianness)- 存储顺序的关键

对于多字节变量,还有一个非常重要的概念决定了它的字节是如何排列在内存中的,那就是字节序(Endianness)。字节序分为两种:

a. 大端序 (Big-Endian)
地址 字节中的数据
0x1000 0x12 (MSB)
0x1001 0x34
0x1002 0x56
0x1003 0x78 (LSB)
b. 小端序 (Little-Endian)
地址 字节中的数据
0x1000 0x78 (LSB)
0x1001 0x56
0x1002 0x34
0x1003 0x12 (MSB)

针对 TC5517 芯片的微观处理

回到你提供的 TC5517 芯片图,它的数据引脚只有 D0 - D7,这意味着它的物理“门口”一次只能通过 8 位。
当 CPU 要存一个 16 位数据到这个芯片时,会触发两个写周期

  1. 周期 1:CPU 将地址 A 发送给引脚,将数据低 8 位压入 D0-D7,拉低 WE
  2. 周期 2:CPU 自动将地址 A+1 发送给引脚,将数据高 8 位压入 D0-D7,再次拉低 WE

总结

尽管一个地址只能容纳一个字节,但是一个多字节变量通过占据连续的多个地址来存储它的完整数据,并且使用首地址来代表整个变量的内存位置。变量内部的字节顺序则由系统的字节序决定。

6.2 要点2:了解作为数据结构基础的数组

数组实际上是为了存储多个数据而在内存上集中分配出的一块内存空间,并且为这块空间整体赋予了一个名字
数组数据结构的基础,之所以这么说是因为数组反映了内存的物理结构本身
在内存中存储数据的空间是连续分布的。而在程序中,往往要从内存整体中分配出一块连续的空间以供使用。如果用程序中的语句表示这种分配使用方式的话,就要用到数组(如图6.2所示)。
Pasted image 20250826095424

数组变量名是否占用空间

1. 核心结论

2. 对比总结

概念 内存占用 物理表现
单个变量名 0 bit 仅作为特定地址(如 0x100)的代号。
数组变量名 0 bit 仅作为首元素起始地址(如 0x200)的代号。
数组内容 N 字节 真实锁存在内存 IC 矩阵中的电荷或逻辑状态。
指针变量 8 字节 真实占用空间,用于存放另一个目标的地址(64位系统)。

3. 深度辨析

数组寻址的微观原理:基址 + 偏移

1. 核心公式:逻辑坐标到物理地址

CPU 准确定位数组中的某个元素(如 x[i])不需要额外的存储空间,而是通过实时计算

2. 硬件实现:寻址指令的扭转

在 CPU 执行层面,这一计算通过专门的寻址模式指令完成:

3. 64 位系统下的性能黑科技

4. 总结

为什么数组快?

  1. 计算零成本:寻址是硬件级加法,不占内存空间。
  2. 空间局部性:物理地址连续性完美契合 CPU 缓存的预取逻辑。
  3. 0 字节开销:无需像链表那样额外存储“下一个节点”的指针。

6.3 要点3:了解数组的应用—作为典型算法的数据结构

数组是数据结构的基础,只要使用数组就能通过程序实现各种各样的算法以处理大量的数据。

线性搜索

通常把像变量i这样的用于记录循环次数的变量称为循环计数器(LoopCounter)。数组之所以方便,就是因为可以把循环计数器的值与数组的索引对应起来使用(如图6.3所示)。
Pasted image 20250826095711

冒泡排序

⚠ Switch to EXCALIDRAW VIEW in the MORE OPTIONS menu of this document. ⚠ You can decompress Drawing data with the command palette: 'Decompress current Excalidraw file'. For more info check in plugin settings under 'Saving'

Excalidraw Data

Text Elements

1
4
2
3
5
6
7
8
9
1
4
2
3
5
6
7
8
9
0
0

for (i = 999; i > 0; i--){
	for (j = 0; j < i; j++){
		if (x[i] > x[j]){
			tmp = x[i];
			x[i] = x[j];
			x[j] = tmp;
		}
	}
}

6.4 要点4:了解并掌握典型数据结构的类型和概念

数组是一种直接利用内存物理结构(计算机的特性)的最基本数据结构

主要的典型数据结构

名称 数据结构的特征
把数据像小山一样堆积起来
队列 把数据排成一队
链表 可以任意地改变数据的排列顺序
二叉树 把数据分为两路排列

“栈”(Stack)

本意是干草堆(如图 6.4 所示)。
LIFO(LastInFirstOut,后进先出)
Pasted image 20250826100215

“队列”(Queue)

就是等待做某事而排成的队。
FIFO(FirstInFirstOut,先进先出)
Pasted image 20250826100239

“链表”

就相当于几个人手拉着手排成一排(如图 6.6 所示)。
某个人只要松开拉住的那只手,再去拉住另一只手,这一排人(相当于数据)的排列顺序就改变了。而只要先松开拉住的手,再让一个新人加入进来并拉住他的手,就相当于完成了数据的插入操作。
Pasted image 20250826100252

“二叉树”

就相当于一棵树。
不过这棵树与自然界中的树稍有些不同,二叉树从树干开始分杈,树枝上又有分杈,但每次都只会分为两杈,在每个分杈点上有一片叶子(相当于数据)(如图6.7所示)。
稍后诸位就会了解到二叉树其实是链表的特殊形态。
Pasted image 20250826100303

6.5 要点5:了解栈和队列的实现方法

实现栈的元素

  1. 数组
  2. 栈顶指针
  3. 入栈函数
  4. 出栈函数
    Pasted image 20250826101802
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

int Stack[5];          // 作为栈本质的数组
int StackPointer = -1; // 栈顶指针
// 入栈函数
void Push(int Data)
{
    // 更新栈顶指针的值
    StackPointer++;
    // 把数据存储到栈顶指针所指的位置上
    Stack[StackPointer] = Data;
    printf("Pushed: %d\n", Data);
}
// 出栈函数
int Pop()
{
    int Data = Stack[StackPointer];
    if (StackPointer <= -1)
    {
        // 实际应用中,可能返回一个错误码或抛出异常
        printf("无数据可出栈!\n");
        exit(EXIT_FAILURE);
    }
    // 更新栈顶指针的值
    StackPointer--;
    printf("Popped: %d\n", Data);
    // 把数据从栈顶指针所指的位置中取出来
    return Data;
}
// 测试程序
int main()
{
    for (int i = 1; i <= 7; i++)
    {
        Push(i);
    }
    printf("-----------------------\n");
    while (true)
    {
        Pop();
    }
    return 0;
}

输出

Pushed: 1
Pushed: 2
Pushed: 3
Pushed: 4
Pushed: 5
Pushed: 6
-----------------------
Popped: 6
Popped: 5
Popped: 4
Popped: 3
Popped: 2
Popped: 1
无数据可出栈!

实现队列的元素

  1. 数组
  2. 队头指针
  3. 队尾指针
  4. 存入函数
  5. 取出函数
    如果数据一直存放到了数组的末尾,那么下一个存储位置就会折回到数组的开头
    这样就相当于数组的末尾就和它的开头连接上了,于是虽然数组的物理结构“直线”,但是其逻辑结构已经变成 “圆环” 了。
    Pasted image 20250826104943
#include <stdio.h>

int Queue[100];   /* 作为队列本质的数组 */
int SetIndex = 0; /* 标识数据存储位置的索引 */
int GetIndex = 0; /* 标识数据读取位置的索引 */
/* 存储数据的函数 */
void Set(int Data)
{
    /* 存入数据 */
    Queue[SetIndex] = Data;
    /* 更新标识数据存储位置的索引 */
    SetIndex++;
    /* 如果已到达数组的末尾则折回到开头 */
    if (SetIndex >= 100)
    {
        SetIndex = 0;
    }
}
/* 读取数据的函数 */
int Get()
{
    int Data;
    /* 读出数据 */
    Data = Queue[GetIndex];
    /* 更新标识数据读取位置的索引 */
    GetIndex++;
    /* 如果已到达数组的末尾则折回到开头 */
    if (GetIndex >= 100)
    {
        GetIndex = 0;
    }
    /* 返回读出的数据 */
    return Data;
}

int main()
{
    for (int i = 0; i < 3; i++)
    {
        Set(i);
        printf("Pushed: %d\n", i);
    }
    printf("Popping from the queue...\n");
    while (1)
    {
        if (GetIndex >= SetIndex)
        {
            break;
        }
        printf("Popped: %d\n", Get());
    }
    return 0;
}

输出

Pushed: 0 GetIndex 0 SetIndex 0 
Pushed: 1 GetIndex 0 SetIndex 1 
Pushed: 2 GetIndex 0 SetIndex 2 
Pushed: 3 GetIndex 0 SetIndex 3 
Pushed: 4 GetIndex 0 SetIndex 4 
Pushed: 5 GetIndex 0 SetIndex 0 
---------------------
Popped: 5
Popped: 1
Popped: 2
Popped: 3
Popped: 4
队列已空: GetIndex 0 SetIndex 1

6.6 要点6:了解结构体的组成

结构体

把若干个数据项汇集到一处并赋予其名字后所形成的一个整体。例如,可以把学生的语文、数学、英语的考试成绩汇集起来,做成一个叫作TestResult的结构体。
Pasted image 20250826105953

#include <stdio.h> // 需要包含这个头文件用于 printf
#include <string.h> // 需要包含这个头文件用于 strcpy
// 定义结构体
struct Student
{
    char name[6]; // 注意:数组大小要考虑字符串结束符'\0'
    int age;
    float score;
};
// 定义结构体数组
struct Student StudentArr[3];
int main(void)
{
    // 声明3个字符串数组并填充字符串
    char names[][6] = {"riven", "loren", "gamer"};
    for (int i = 0; i < 3; i++)
    {
        // 使用 strcpy 复制字符串到字符数组
        strcpy(StudentArr[i].name, names[i]);
        StudentArr[i].age = i + 1;
        StudentArr[i].score = i + 2;
    }
    // 打印结构体数组中的内容
    int j = 0;
    while (j < 3)
    {
        printf("学生姓名:%s\n", StudentArr[j].name);
        printf("学生年龄:%d\n", StudentArr[j].age);
        printf("学生分数:%f\n", StudentArr[j].score);
        printf("\n");
        j++;
    }
}

输出结果

学生姓名:riven
学生年龄:1
学生分数:2.000000

学生姓名:loren
学生年龄:2
学生分数:3.000000

学生姓名:gamer
学生年龄:3
学生分数:4.000000

6.7 要点7:了解链表和二叉树的实现方法

C语言: “拷贝值”(Value Copy)和“拷贝指针”(Pointer Copy)

1. 拷贝值(Value Copy)

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

int main(void)
{
    int a = 10;
    // 拷贝值。b 得到值 10。
    int b = a;
    printf("a = %d, b = %d\n", a, b); // a = 10, b = 10
    // 此时 a 和 b 是两个独立的变量,存储在不同的内存地址中。
    b = 20;
    printf("a = %d, b = %d\n", a, b); // a = 10, b = 20
}

2. 拷贝指针(Pointer Copy)

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

int main(void)
{
    int a = 10;
    int *p1 = &a; // p1 存储 a 的地址
    int *p2 = p1; // 拷贝指针(地址)。p2 存储的也是 a 的地址。
    /**
     * 通过 p2 修改它指向的数据 (a)
     * 此时 p1 和 p2 仍然指向同一块内存, a 是 20,*p1 也是 20。
     */
    *p2 = 20;
    printf("a = %d, *p1 = %d, *p2 = %d\n", a, *p1, *p2); // a = 20, *p1 = 20, *p2 = 20
    int b = 30;
    /**
     * 改变指针变量 p2 本身,让它指向 b
     * 此时 p1 仍然指向 a (值为 20),p2 指向 b (值为 30)。
     * p1 和 p2 自身是不同的变量,但一开始指向了同一个目标。
     */
    p2 = &b;
    printf("a = %d, *p1 = %d, *p2 = %d\n", a, *p1, *p2); // a = 20, *p1 = 20, *p2 = 30
}
代码行 *int p1 = &a; *int p2 = p1;
& 符号 需要 & 不需要 &
操作数 普通变量 a 指针变量 p1
操作目的 获取变量 a地址 复制指针变量 p1 存储的地址值
本质 普通数据的地址赋值给指针 一个指针的值(地址)赋值给另一个指针

总结比较

特点 拷贝值(Value Copy) 拷贝指针(Pointer Copy)
复制对象 变量的实际数据/内容 变量的内存地址
副本独立性 完全独立 两个指针指向同一个目标
修改副本 影响原数据 通过指针解引用修改会影响原数据
使用场景 确保数据独立、传递小型数据 共享/修改同一个数据、传递大型结构体/数组(更高效)
关键点:

在处理大型数据结构时,拷贝指针(传递指针)通常更高效,因为它只需要复制一个固定大小的地址(通常是4字节或8字节),而不是复制整个结构体。但是,这也要求程序员注意,任何通过指针进行的修改都会影响原始数据。

链表

链表是一种通过指针实现元素逻辑关联线性数据结构。其核心在于将数据块(节点)离散地分布在内存中,并依靠地址指向维持顺序。

使用结构体实现链表:让数组元素“手拉手”

在传统的数组中,元素的顺序是由它们在内存中的物理位置决定的(下标 0 紧跟着下标 1)。
链表(Linked List) 则打破了这种物理限制,它通过让每个元素“记住”下一个元素的位置,实现逻辑上的连接

如图6.11所示,Ptr中存储着本元素接下来该与哪一个元素相连的信息,即下一个元素的地址
Pasted image 20250826114423

核心成员:自我引用结构体

要让结构体元素能“手拉手”,我们需要在结构体中增加一个特殊的成员——指针(Pointer)

struct Student {
    char name[10];
    struct Student *next; // 指向另一个同类型结构体的地址
};

链表的逻辑构造

连接:地址映射

链表的建立并非依靠物理相邻,而是依靠地址赋值

访问:指针追溯 (Dereferencing)

链表不支持随机访问(Random Access),必须从头节点(Head)开始,通过连续解引用指针成员来顺序移动。

链表与普通数组的区别

特性 数组 链表
内存分布 必须是连续的一块空间 散落在内存各处,通过地址连接
顺序确定 靠下标(物理位置) 靠指针(逻辑连接)
灵活性 大小固定,插入删除困难 动态增减,插入删除只需改动指针

自我引用结构体

#include <stdio.h>  // 需要包含这个头文件用于 printf
#include <string.h> // 需要包含这个头文件用于 strcpy
// 定义结构体
struct Student
{
    char name[6];         // 注意:数组大小要考虑字符串结束符'\0'
    struct Student *tail; // 指向下一个Student结构体的指针
};
// 从头指针出发,通过每个节点的指针域(*tail)顺次访问后续节点,直至指向空。
void printLinkList(struct Student *student)
{
    struct Student *current = student;
    while (current != NULL)
    {
        printf("Student Name: %s\n", current->name);
        current = current->tail;
    }
}
int main(void)
{
    // 创建结构体变量
    struct Student student1, student2, student3;
    strcpy(student1.name, "riven");
    strcpy(student2.name, "loren");
    strcpy(student3.name, "gamer");
    student1.tail = &student2; // student1的tail指向student2
    student2.tail = &student3; // student2的tail指向student3
    student3.tail = NULL;      // student3的tail指向NULL,表示链表结束
    printLinkList(&student1);  // 从student1开始打印链表
}

运行结果

Student Name: riven
Student Name: loren
Student Name: gamer

链表改变顺序的优势

链表之所以方便,是因为它通过改变指针的指向来排序、插入和删除数据,避免了在内存中移动大量元素,从而提高了处理效率。
逻辑上的顺序变更:原有的顺序A→B→C就变成了A→C→B(如图6.12所示)。
Pasted image 20250826114607

二叉树

二叉树多用于实现那些用于搜索数据的算法,比如“二分查找法”。比起只使用链表,使用二叉树能够更快地找到数据。
因为搜索数据时并不是像在简单数组中那样沿一条线搜索,而是寻着二叉树不断生长出来的两根树杈中的某一枝搜索,这样就能更快地找到目标数据了(如图6.13所示)。

二叉搜索树(BST)的插入规则

BST 的核心规则是:

二叉树「堆」

#include <stdio.h>  // 包含标准输入输出库,用于 printf
#include <string.h> // 包含字符串处理库,用于 strcpy
#include <stdlib.h> // 包含标准库,用于 malloc
// 定义二叉树节点结构体
struct Element
{
    char name[6];
    struct Element *left;
    struct Element *right;
};
// 创建新节点
struct Element *create(char name[6])
{
    struct Element *node = (struct Element *)malloc(sizeof(struct Element));
    // 内存分配失败,返回NULL
    if (node == NULL)
    {
        return NULL;
    }
    strcpy(node->name, name);
    node->left = NULL;
    node->right = NULL;
    return node;
};
// 前序遍历二叉树
void traverse(struct Element *node)
{
    if (node == NULL)
    {
        return;
    }
    printf("%s\n", node->name);
    traverse(node->left);
    traverse(node->right);
}
// 释放二叉树内存
void freeTree(struct Element *node)
{
    if (node == NULL)
    {
        return;
    }
    freeTree(node->left);
    freeTree(node->right);
    free(node);
}
int main(void)
{
    /* 建立二叉树结构
             A
           /   \
          B     C
         / \   / \
        D   E F   G
    */
    struct Element *nodeA = create("A");
    nodeA->left = create("B");
    nodeA->right = create("C");
    nodeA->left->left = create("D");
    nodeA->left->right = create("E");
    nodeA->right->left = create("F");
    nodeA->right->right = create("G");
    // 前序遍历二叉树
    traverse(nodeA);
    // 释放二叉树内存
    freeTree(nodeA);
    return 0;
}

输出结果

A
B
D
E
C
F
G

二叉树「栈」

#include <stdio.h>  // 包含标准输入输出库,用于 printf
#include <string.h> // 包含字符串处理库,用于 strcpy

// 定义结构体:表示树中的一个节点
struct Student
{
    char name[6];          // 存储学生姓名,大小6可以存储最长5个字符的姓名 + 字符串结束符'\0'
    struct Student *left;  // 指向左子节点的指针
    struct Student *right; // 指向右子节点的指针
};

/**
 * @brief 递归地遍历并打印二叉树(先序遍历:根->左->右)
 * * @param student 指向当前节点的指针
 */
void traverseTree(struct Student *student) // 优化函数命名,使其更具描述性
{
    // 递归终止条件:如果当前节点为空,则返回
    if (student == NULL)
    {
        return;
    }

    // 1. 访问根节点 (打印当前学生姓名) - '先序'遍历的关键
    printf("Student Name: %s\n", student->name);

    // 2. 递归访问左子树
    traverseTree(student->left);

    // 3. 递归访问右子树
    traverseTree(student->right);
}

int main(void)
{
    // =========================================================================
    // 1. 节点创建与初始化:在栈上创建 7 个 Student 结构体变量。
    //    使用指定初始化器确保所有字段被正确初始化。
    // =========================================================================
    struct Student student1 = {.left = NULL, .right = NULL}; // 根节点 A
    struct Student student2 = {.left = NULL, .right = NULL}; // 节点 B
    struct Student student3 = {.left = NULL, .right = NULL}; // 节点 C
    struct Student student4 = {.left = NULL, .right = NULL}; // 节点 D
    struct Student student5 = {.left = NULL, .right = NULL}; // 节点 E
    struct Student student6 = {.left = NULL, .right = NULL}; // 节点 F
    struct Student student7 = {.left = NULL, .right = NULL}; // 节点 G

    // =========================================================================
    // 2. 节点数据填充:使用 strcpy 复制姓名字符串到结构体成员中。
    // =========================================================================
    // 树结构:
    //         A
    //       /   \
    //      B     C
    //     / \   / \
    //    D   E F   G

    strcpy(student1.name, "A");
    strcpy(student2.name, "B");
    strcpy(student3.name, "C");
    strcpy(student4.name, "D");
    strcpy(student5.name, "E");
    strcpy(student6.name, "F");
    strcpy(student7.name, "G");

    // =========================================================================
    // 3. 建立连接:设置指针,构建二叉树结构。
    // =========================================================================
    // 第一层 (A 的子节点)
    student1.left = &student2;
    student1.right = &student3;

    // 第二层 (B 和 C 的子节点)
    student2.left = &student4;
    student2.right = &student5;
    student3.left = &student6;
    student3.right = &student7;

    // =========================================================================
    // 4. 调用遍历函数:从根节点开始遍历并打印。
    // =========================================================================
    // 预期输出顺序 (先序遍历): A, B, D, E, C, F, G
    printf("--- 开始二叉树先序遍历 ---\n");
    traverseTree(&student1); // 从根节点 student1 的地址开始遍历
    printf("--- 遍历结束 ---\n");

    return 0; // main 函数返回 0 表示程序成功执行
}

输出

--- 开始二叉树先序遍历 ---
Student Name: A
Student Name: B
Student Name: D
Student Name: E
Student Name: C
Student Name: F
Student Name: G
--- 遍历结束 ---

二叉树「堆」vs 二叉树「栈」

1. 结构与内存分配方式的区别
特性 二叉树「栈」 (使用 struct Student) 二叉树「堆」 (使用 struct TestResultmalloc)
内存分配 栈上内存分配 (Stack Allocation) 堆上内存分配 (Heap Allocation)
初始化 直接定义结构体变量并初始化 (struct Student student1 = ...) 使用 createNode 函数,内部调用 malloc 动态分配内存。
指针连接 使用变量的地址 (&student2, &student3) 进行连接。 使用 malloc 返回的堆内存地址进行连接。
生命周期 仅在 main 函数执行期间有效。main 函数结束,内存自动释放。 内存直到显式调用 freeTree(root) 释放,否则会造成内存泄漏
数据类型 使用 char name[6] 存储姓名(字符串)。 使用 char NodeName 存储单个字符作为节点名。
代码复杂性 简单直接,适用于小型、固定结构的测试。 需要编写内存分配 (malloc) 和释放 (free) 的辅助函数,更复杂但更灵活。
2. 关于 malloc 函数的必要性

可以不用 malloc,但仅限于特定的场景。 在实际应用中,构建大型可变规模的树结构malloc 是必须的。

struct Student student1 = { ... };
struct Student student2 = { ... };
// ...

为什么可以?

  1. 节点数量固定且已知: 您在编译时就知道需要 7 个节点,并且在 main 函数中逐一定义了这 7 个变量。
  2. 生命周期限制: 这些变量(节点)的生命周期被限制在 main 函数的作用域内。当 main 函数返回时,这些栈上的内存会自动被操作系统清理和回收。
    缺点:
  3. 缺乏灵活性: 你无法在运行时根据输入动态地增加或删除节点。如果你需要 100 个节点,你必须手动写 100 行定义。
  4. 栈溢出风险: 栈内存空间有限(通常只有几 MB)。如果树结构非常大(例如有数万个节点),在栈上分配可能会导致栈溢出 (Stack Overflow)
struct TestResult* root = createNode('A', 90, 85, 92); // 内部调用 malloc

为什么必须使用 malloc

  1. 动态性: malloc 从堆 (Heap) 上分配内存,堆空间远大于栈空间。这允许程序在运行时根据需要动态地创建、扩展或收缩数据结构(如二叉搜索树、平衡树等)。这是构建复杂、真实应用数据结构的标准方法
  2. 持久性: 堆内存的生命周期独立于创建它的函数。即使 createNode 函数返回了,它分配的内存仍然存在,直到你显式调用 free() 释放它。这使得你可以构建一个数据结构,并在程序的整个运行过程中持续使用它。

总结:

第7章:成为会使用面向对象编程的程序员吧

7.1 面向对象编程

1. OOP 的定义与目标

2. 管理层的青睐(成本优势)

3. 程序员的顾虑与挑战

4. 未来趋势与学习的必要性

5. 学习建议

7.2 对 OOP 的多种理解方法

1. 术语定义 (正式描述)

2. 理解的复杂性与多样性

3. 学习目标

7.3 观点 1:面向对象编程通过把组件拼装到一起构建程序

1. 类的核心概念

2. 传统编程(非 OOP)的构成

代码清单 7.1 程序是函数和变量的集合(C语言)

int Variable1;
int Variable2;
int Variable3;
...

void Function1() { 处理过程 }
void Function2() { 处理过程 }
void Function3() { 处理过程 }
...

3. 类的发明与作用

4. 类的结构与成员

代码清单 7.2 定义类 MyClass,将函数和变量组织到一起(C++)

class MyClass // 类名
{
    int Variable1;
    int Variable2;
    ...

    void Function1() { 处理过程 }
    void Function2() { 处理过程 }
    ...
};

5. OOP 语言的发展

7.4 观点 2:面向对象编程能够提升程序的开发效率和可维护性

Pasted image 20251005215450

1. 类库与开发效率

2. 类与可维护性

3. OOP 开发中的角色分工

4. 创造类的原则(设计规范)

5. 接口(Interface)

7.5 观点 3:面向对象编程是适用于大型程序的开发方法

1. OOP 适用于大型程序的优势

2. 核心价值(顺应人类思维)

3. 学习提示

7.6 观点 4:面向对象编程就是在为现实世界建模

1. 建模的目的与定义

2. 建模过程的关键步骤

建模过程主要包含两个步骤:组件化省略化

A. 组件化(抽象归类)

B. 省略化(忽略细节)

7.7 观点 5:面向对象编程可以借助 UML 设计程序

1. 建模与 UML

2. UML 的九种图与用途

UML 规定了九种图,用于从不同角度表示建模结果:

名称 主要用途 视角
用例图 (Use Case Diagram) 表示用户使用程序的方式 用户视角
类图 (Class Diagram) 表示及多个类之间的关系 程序视角
对象图 (Object Diagram) 表示对象
时序图 (Sequence Diagram) 时间上关注并表示多个对象间的交互
协作图 (Collaboration Diagram) 协作关系上关注并表示多个对象间的交互
状态图 (Statechart Diagram) 表示对象状态的变化
活动图 (Activity Diagram) 表示处理的流程
组件图 (Component Diagram) 表示文件以及多个文件之间的关系
配置图 (Deployment Diagram) 表示计算机或程序的部署配置方法

3. UML 类图示例 (见图 7.4)

4. OOP 设计的思维方式

7.8 观点 6:面向对象编程通过在对象间传递消息驱动程序

1. OOP 与传统编程(C++ vs C)的差异

特性 传统编程语言 (C) 面向对象编程语言 (C++)
函数关系 函数是独立存在的 (GetHand())。 函数隶属于某个类/对象
函数调用 直接调用函数。 使用对象名.函数名() 的语法,例如 PlayerA.GetHand()

代码清单 7.3 未使用面向对象编程语言的情况 (C 语言)

/* 玩家 A 确定手势 */
a = GetHand();
/* 玩家 B 确定手势 */
b = GetHand();
/* 判定胜负 */
winner = GetWinner(a, b);

代码清单 7.4 使用了面向对象编程语言的情况 (C++)

// 玩家 A 确定手势
a = PlayerA.GetHand();
// 玩家 B 确定手势
b = PlayerB.GetHand();
// 由裁判判定胜负
winner = Judge.GetWinner(a, b);

2. 消息传递(Message Passing)

3. 编程语言的类型

4. 程序运行过程的表示方法

编程范式 表示工具 特点 思维习惯
面向过程 流程图 表示处理过程的流程。 习惯用流程图思考。
面向对象 时序图 / 协作图 (UML) 时序图:横向排列对象,从上往下表示时间流逝,用箭头表示对象间的消息传递(函数调用) 需要改用时序图来思考程序的运行过程。
图 7.5 示例(时序图)

7.9 观点 7:在面向对象编程中使用继承、封装和多态

1. OOP 三大基本特性

继承 (Inheritance)封装 (Encapsulation)多态 (Polymorphism) 被称为面向对象编程的三个基本特性。

2. 特性的定义与优势

特性 定义 带来的好处(效率与维护性)
继承 通过继承已存在的类所拥有的成员(变量和函数)来生成新的类 高效地生成新类。如果修正了被继承的父类,则所有子类都会同步修正
封装 隐藏掉类中没有必要展现给调用者的成员 使类成为黑盒,易于使用且便于维护。由于隐藏成员不可访问,可以放心地修改这些成员而不影响外部。
多态 针对同一种消息(函数调用),不同的对象可以进行不同的操作 减少使用这组类的程序员需要记忆的东西,简化代码。

3. 实现细节与学习建议

4. 总结

无论是继承、封装还是多态,它们带来的好处都旨在实现开发效率可维护性的提升。

7.10 类和对象的区别

1. 类与对象的定义

2. 编程中的实现与使用

代码清单 7.5 (C++)

MyClass obj;            // 创建对象
obj.Variable = 123;     // 使用对象所持有的变量
obj.Function();         // 使用对象所持有的函数

3. 为什么必须创建对象(现实世界的映射)

7.11 类有三种使用方法

1. 程序员的分工

2. 使用类的三种方法

方法 描述 示例 (C# 代码) 关系类型
1. 仅调用个别成员 直接调用类所持有的函数和变量 Int32.Parse(...)MessageBox.Show(...) 关联/调用 (Association)
2. 组合 (Composition) 在类的定义中包含其他的类作为其成员变量,即类中引用了其他的类。 Form1 包含 textBox1 (类型为 System.Windows.Forms.TextBox) 和 button1 (类型为 System.Windows.Forms.Button)。 组合/聚合 (Has-A)
3. 继承 (Inheritance) 通过继承已存在的类来定义出新的类 Form1 继承了类库中的类 System.Windows.Forms.Form (用 : 表示继承)。 继承 (Is-A)

3. C# 示例程序 (代码清单 7.6)

7.12 在 Java 和 .NET 中有关 OOP 的知识不能少

1. 框架 (Framework) 的角色

2. OOP 的不可避免性

3. 主流开发语言的现状

4. 结论与建议

第8章:一用就会的数据库

SQL: Structured Query Language,结构化查询语言
Transaction:事务

8.1 数据库是数据的基地

1. 数据库的定义与目的

2. 数据库类型对比

A. 卡片型数据库 (Card-Type Database)

B. 关系型数据库 (Relational Database)

8.2 数据文件、DBMS 和数据库应用程序

1. DBMS 的作用与定位

2. 数据库系统的三种主要形式 (见图 8.4)

数据库系统主要由数据文件、DBMS 和应用程序三部分构成。根据这三部分部署的位置,系统形式分为以下几种:

A. 独立型系统 (Standalone System)

B. 文件共享型系统 (File Sharing System)

C. 客户端/服务器型系统 (Client/Server System)

D. Web 系统 (Web System)

8.3 设计数据库

1. 数据库设计的第一步:确定所需数据

数据库设计始于回答一个核心问题:“你想要了解什么?” 无论是为自己还是为客户设计,都必须找出需要存储的数据。

2. 数据的属性(模式/内模式)

确定必要数据后,下一步是考虑各种数据的属性(也称作模式)。

3. 数据库术语回顾

在关系型数据库中,有一些基本术语需要记住:

常用术语 同义术语 描述
记录 行 (Row)、元组 (Tuple) 表中每一行数据,代表一个完整的实体信息。
字段 列 (Column)、属性 (Attribute) 构成一条记录的各个数据项(如商品名称、单价)所在的列。属性(数据类型、长度等)就是设置在字段上的。
- 具有特定名称,由字段和记录构成的数据集合。

如图 8.5 所示,通过设置字段名称和属性,如“商品名称”设置为“文本”,“单价”设置为“数字”,就完成了表的定义。之后录入数据即可使用。
Pasted image 20251006104827

8.4 通过拆表和整理数据实现规范化

1. 单一表结构产生的问题 (如图 8.6 所示)

如果仅使用一张表(像卡片型数据库那样),在实际录入数据时会产生与卡片型数据库相似的问题:

2. 规范化的目的与方法

为了解决上述问题,在设计关系型数据库时需要进行规范化(Normalization)

3. 规范化示例:酒铺数据库 (如图 8.7 & 8.8 所示)

通过规范化,酒铺数据库被拆分成三个表,并在它们之间建立了关系:

  1. 商品表:存储商品名称、单价等信息。
  2. 顾客表:存储顾客姓名、住址、电话号码等信息。
  3. 销售记录表:记录了具体的销售行为,通过来连接商品表顾客表

8.5 用主键和外键在表间建立关系

1. 键(Key)的定义与作用

键是添加到表中、能够反映表与表之间关系的新字段。

A. 主键 (Primary Key)

B. 外键 (Foreign Key)

2. 表之间的关系

记录之间的关系在逻辑上分为三种,但在关系型数据库中无法直接表示多对多关系。

3. 参照完整性与 DBMS 的安全保障

8.6 索引能够提升数据的检索速度

1. 索引的定义与作用

2. 索引的实现机制

3. 使用索引的代价与原则

8.7 设计用户界面

1. 系统设计顺序

2. 数据库操作:CRUD

对数据库进行的操作种类通常称为 CRUD,是所有数据库应用程序的核心功能:

3. 用户界面 (UI) 设计原则

8.8 向 DBMS 发送 CRUD 操作的 SQL 语句

1. SQL 语言的定义与作用

2. SQL 语句与 CRUD 操作的对应关系

数据库的核心操作是 CRUD(创建、获取、更新、删除)。

CRUD 操作 SQL 语句 作用
Read / Refer (获取) SELECT 从表中获取数据。
Create (插入) INSERT 插入记录。
Update (更新) UPDATE 更新记录。
Delete (删除) DELETE 删除记录。

3. SELECT 语句示例解析

SELECT 语句是 SQL 中用于 获取数据 (R 操作) 的命令。
示例语句:

SELECT 顾客姓名, 住址, 电话号码, 商品名称, 单价, 销售量
FROM 顾客表, 商品表, 销售记录表
WHERE 顾客表.顾客姓名 = "日经次郎"
AND 销售记录表.顾客 ID = 顾客表.顾客 ID
AND 销售记录表.商品 ID = 商品表.商品 ID;

4. SQL 语句的执行

8.9 使用数据对象向 DBMS 发送 SQL 语句

在 Windows 应用程序中,程序不是直接发送 SQL 语句,而是通过被称为数据对象(Data Object) 的软件组件(即面向对象编程中的)来与 DBMS 交互。

1. 数据对象:ADO 的构成与作用

在 Visual Basic 6.0 中,使用的数据对象被称为 ADO (ActiveX Data Object)

  1. Connection 类:用于建立和 DBMS(数据库管理系统)的连接
  2. Command 类:用于向 DBMS 发送 SQL 语句
  3. Recordset 类:用于存储 DBMS 返回的结果(即数据集合)。

数据对象(如 ADO)为应用程序提供了一个面向对象的接口简化复杂的数据库操作,让程序员能够通过调用类方法(如 AddNew, Delete)而不是手动编写所有 SQL 语句来完成 CRUD 功能。

2. CRUD 操作与 ADO 方法

数据库应用程序通过 ADO 进行 CRUD(创建、获取、更新、删除)操作:

ADO 机制:AddNewUpdateDelete 这些方法在内部会自动生成相应的 SQL 语句(INSERT, UPDATE, DELETE)并发送给 DBMS。

3. 程序启动与退出流程

ADO 访问数据库的典型流程:

  1. 程序启动 (Form_Load)
    • 设置连接字符串 (con.ConnectionString) 指定 Provider 和数据源。
    • 调用 con.Open 建立与 DBMS 的连接。
  2. 程序结束 (Form_Unload)
    • 调用 con.Close 关闭与 DBMS 的连接。

4. 数据浏览与操作示例 (代码清单 8.1 关键点)

代码清单 8.1 使用 ADO 访问数据库的示例程序(VB 6.0)

' 实例化 ADO 提供的类
Dim con As New ADODB.Connection
Dim cmd As New ADODB.Command
Dim rst As New ADODB.Recordset

' 处理程序启动事件
Private Sub Form_Load()
    con.ConnectionString = _
    "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=liquor_store.mdb"
    con.Open
End Sub

' 处理“录入”按钮单击事件
Private Sub cmdCreate_Click()
    rst.AddNew
    SetRecordset
    rst.Update
End Sub

' 处理“获取”按钮单击事件
Private Sub cmdRetrieve_Click()
    If rst.State = adStateOpen Then
        rst.Close
    End If
    rst.Open "SELECT 顾客姓名 , 住址 , 电话号码 , 商品名称 , 单价 , 销售量 " & _
    "FROM 顾客表 , 商品表 , 销售记录表 " & _
    "WHERE 顾客表 . 顾客姓名 = """ & txtCustomer.Text & """" & _
    "AND 销售记录表 . 顾客 ID = 顾客表 . 顾客 ID " & _
    "AND 销售记录表 . 商品 ID = 商品表 . 商品 ID", _
    con, adOpenKeyset, adLockOptimistic
    
    If rst.RecordCount > 0 Then
        rst.MoveFirst
        ShowRecordset
    Else
        MsgBox " 找不到符合条件的数据! ", vbInformation, ""
    End If
End Sub

' 处理“更新”按钮单击事件
Private Sub cmdUpdate_Click()
    SetRecordset
    rst.Update
End Sub

' 处理“删除”按钮单击事件
Private Sub cmdDelete_Click()
    rst.Delete
    rst.Update
End Sub

' 处理“上一条”按钮单击事件
Private Sub cmdPrev_Click()
    rst.MovePrevious
    If rst.BOF Then
        rst.MoveFirst
    End If
    ShowRecordset
End Sub

' 处理“下一条”按钮单击事件
Private Sub cmdNext_Click()
    rst.MoveNext
    If rst.EOF Then
        rst.MoveLast
    End If
    ShowRecordset
End Sub

' 处理程序退出事件
Private Sub Form_Unload(Cancel As Integer)
    con.Close
End Sub

' Recordset 显示 Recordset 中的内容
Private Sub ShowRecordset()
    txtCustomer.Text = rst.Fields(0)
    txtAddress.Text = rst.Fields(1)
    txtPhone.Text = rst.Fields(2)
    txtItems.Text = rst.Fields(3)
    txtUnitPrice.Text = rst.Fields(4)
    txtSales.Text = rst.Fields(5)
End Sub

' Recordset 设置 Recordset 中的数据
Private Sub SetRecordset()
    rst.Fields(0) = txtCustomer.Text
    rst.Fields(1) = txtAddress.Text
    rst.Fields(2) = txtPhone.Text
    rst.Fields(3) = txtItem.Text
    rst.Fields(4) = txtUnitPrice.Text
    rst.Fields(5) = txtSales.Text
End Sub

8.10 事务控制也可以交给 DBMS 处理

1. 事务的定义与必要性

2. 事务控制的 SQL 语句与流程 (如图 8.16 所示)

Pasted image 20251007152941

为了防止数据不一致,SQL 语言设计了以下三条语句来控制事务的完整性:

SQL 语句 ADO 对应方法 (Connection Class) 作用
BEGIN TRANSACTION BeginTrans 通知 DBMS 开启事务
COMMIT CommitTrans 通知 DBMS 提交事务。只有在所有操作都成功后,数据才会被永久保存。
ROLLBACK RollbackTrans 在事务进行中发生问题时,用于将数据库中的数据恢复到事务开始前的状态(回滚)。

3. 结论

第9章:通过七个简单的实验理解 TCP/IP网络

IPC:InterProcess Communication 「进程间通信」

9.1 实现环境

Pasted image 20250723102216

9.2 实验1:查看网卡的MAC地址

NIC: Network Interface Card 「网卡」
CSMA/CD: Carrier Sense Multiple Access with Collision Detection 「带冲突检测的载波监听多路访问」
CS: Carrier Sense 「载波监听」
ROM: Read Only Memory「只读存储器」

以太网中传输电信号特点

  1. 传输电信号是抢占式的,也就是说要先确保没有人在占用网络,然后才能发送自己想传输的电信号
  2. 发送给一台计算机的电信号也可以被其他所有的计算机收到。 一台计算机收到了电信号以后会先做判断, 如果是发送给 自己的则选择接收, 反之则选择忽略。

这套机制叫作 CSMA/CD(Carrier Sense Multiple Access with Collision Detection,带冲突检测的载波监听多路访问)。
所谓载波监听(Carrier Sense),指的是这套机制会去监听(Sense)表示网络是否正在使用的电信号(Carrier)。
而多路复用(Multiple Access)指的是多个(Multiple)设备可以同时访问(Access)传输介质。
带冲突检测(with Collision Detection)则表示这套机制会去检测(Detection)因同一时刻的传输而导致的电信号冲突(Collision)。
在小规模的 LAN 中,像这样 略显粗躁的 CSMA/CD 机制是可以正常运转的。 因为 CSMA/CD 归根 结底也只是一种适用于 LAN 的机制。
Pasted image 20250723103220

9.3 实验 2:查看计算机的 IP 地址

IP

IP 地址是一个 32 比特的整数, 每 8 比特为一组, 组间用“.”分隔, 分成 4 段 表示。
8 比特所表示的整数换算成十进制后范围是 0~255, 因此可用作 IP 地址的整数是 0.0.0.0~255.255.255.255, 共计 4294967296 个「约42亿」。

Subnet Mask

子网掩码的作用是标识出在 32 比特的 IP 地址中,从哪一位到哪一位是网络地址,从哪一位到哪一位是主机地址

255.255.255.240 用二进制表示的话,结果如下所示

 11111111.11111111.11111111.11110000

子网掩码中,值为 1 的那些位对应着 IP 地址中的网络地址,后面 值为 0 的那些位则对应着主机地址。
 因此 255.255.255.240 这个子网掩 码就表示,其所对应的 IP 地址前 28 比特网络地址后 4 比特是主机地址

9.4 实验 3:了解 DHCP 服务器的作用

DHCP: Dynamic Host Configuration Protocol 「动态主机设置协议」
Gateway:默认网关
Pasted image 20250723111541

9.5 实验 4:路由器是数据传输过程中的指路人

路由器的工作原理:

查看附加到数据上的 IP 地址中的网络地址部分, 只要发现这个数据 不是发送给 LAN 内计算机的, 就把它发送到 LAN 外, 即互联网的世界中
Pasted image 20250723111528

路由表由 5 列构成。

Network DestinationNetmaskGatewayInterface 这四列记录着数据发送的目的地路由器的 IP 地址等信息
Metric 这一列记录着路径的权重,这个值由某种算法决定,比如数据传输过程中经过的路由器的数量

如果遇到有多条候选路径都可以通往目的地的情况,路由器就会选择 Metric 值较小的那条路径。

路由追踪

traceroute www.baidu.com
traceroute to www.baidu.com (198.18.1.17), 30 hops max, 46 byte packets
 1  172.128.0.1 (172.128.0.1)  0.007 ms  0.008 ms  0.005 ms
 2  192.168.64.1 (192.168.64.1)  0.449 ms  0.181 ms  0.130 ms

9.7 实验 6:DNS 服务器可以把主机名解析成 IP 地址

DNS: Domain Name System「域名系统」
FQDN: Fully Qualified Domain Name「完整限定域名,主机名和域名组合起来形成的名字」
ARP: Address Resolution Protocol「地址解析协议」

# 查询本机名
hostname
Mac-mini.local

# dns查询工具
nslookup www.baidu.com
Server:		192.168.65.7
Address:	192.168.65.7:53

Non-authoritative answer:
Name:	www.baidu.com
Address: 198.18.0.143

Non-authoritative answer:

9.8 实验 7:查看 IP 地址和 MAC 地址的对应关系

ARP

在互联网的世界中,到处传输的都是附带了 IP 地址的数据。但是 能够标识作为数据最终接收者的网卡的,还是 MAC 地址。
于是在计算 机中就加入了一种程序, 用于实现由 IP 地址到 MAC 地址的转换, 这 种功能被称作 ARP(Address Resolution Protocol,地址解析协议)。

工作方式

它会对 LAN 中的所有计算机提问: “有谁的 IP 地址是 210.160.205.80 吗?有的话请把你的 MAC 地址告诉我。
”通常把这种同时向所有 LAN 内的计算机发送数据的过程称作“广播”(Broadcast)。
通过广播询问, 如果有某台计算机回复了 MAC 地 址, 那么这台计算机的 IP 地址和 MAC 地址的对应关系也就明确了。

广播后会缓存结果,通过以下命令查询

# ARP 缓存表
arp -a
? (192.168.2.1) at 70:2a:d7:1:c1:c3 on en1 ifscope [ethernet]
? (192.168.2.6) at 4a:c9:37:c:a8:f3 on en1 ifscope [ethernet]
? (192.168.2.8) at a4:cf:99:60:1d:58 on en1 ifscope [ethernet]
? (192.168.2.9) at a0:78:17:81:85:20 on en1 ifscope [ethernet]
? (192.168.2.10) at ca:5d:be:2d:d:26 on en1 ifscope [ethernet]
? (192.168.2.12) at 7e:e1:46:d2:13:57 on en1 ifscope [ethernet]
? (192.168.2.18) at 7e:9b:2:91:8f:c0 on en1 ifscope [ethernet]
? (192.168.2.20) at f8:4d:89:95:94:6 on en1 ifscope [ethernet]
? (192.168.2.22) at e:48:13:81:de:3f on en1 ifscope [ethernet]
...

9.9 TCP 的作用及 TCP/IP 网络的层级模型

Handshake

而 TCP 协议则用于通过数据发送者接收者相互回应对方发来的确认信号, 可靠地传输数据。 通常把像这样的数据传送方式称作“握手”(Handshake)。
Pasted image 20250723113416

Packet

TCP 协议中还规定, 发送者要先把原始的大数据分割成以“包”(Packet)为单位的数据单元,然后再发送, 而接收者要把收到的包拼装在一起还原出原始数据。
Pasted image 20250723113429

实现了TCP/IP网络的程序的层级

硬件上发送数据的是网卡。
在网卡之上是设备驱动程序(用于控制 网卡这类硬件的程序),设备驱动程序之上是实现了 IP协议的程序,IP 程序之上则是实现了 TCP 协议的程序而再往上才是应用程序
比如 Web 或电子邮件。
Pasted image 20250723113436

第10章:试着加密数据吧

10.1 先来明确一下什么是加密

文本数据可以由各种各样的字符构成。 其中每个字符都被分配了一个数字,我们称之为“字符编码”。
定义了应该把哪个编码分配给哪 个字符的字符编码体系叫作字符集
字符集分为:

在表 10.1 中,以十进制数字列出了大写拉丁字母(A 至 Z)的 ASCII 编码。
计算机会把文本数据处理成数字序列, 例如在使用了 ASCII 编码的计算机中,就会把 NIKKEI 处理成“78 73 75 75 69 73”。
可是只要把这一串数字转换为对应的字符显示在屏幕上,就又变成了人们 所认识的 NIKKEI 了。
通常把这种未经加密的文本数据称为“明文”。
Pasted image 20250822113455
数据一旦以明文的方式在网络中传输,就会有被盗取滥用的危险, 因此要对明文进行加密,将它转换成为“密文”。

虽然存在各种各样的加密技术, 但是其中的基本手段无外乎还是字符编码的变换, 即将构成明文的每个字符的编码分别变换成其他的 数值。
通过反转这种变换过程, 加密后的文本数据就可以还原。
通常 把密文还原成明文的过程(即解读密码的过程)称为“解密”。

10.2 错开字符编码的加密方式

通过字符编码加减3进行加密和解密

function encryptDecrypt(string $plaintext, string $type = 'bcadd', int $secretKey = 3): string
{
    $tmp = '';
    // 遍历明文中的每一个字符
    for ($i = 0; $i < mb_strlen($plaintext, 'UTF-8'); $i++) {
        // 获取当前字符
        $char = mb_substr($plaintext, $i, 1, 'UTF-8');
        // 将字符转换为其 ASCII 值,然后加 3
        $asciiShifted = $type(mb_ord($char, 'UTF-8'), $secretKey);
        // 将新的 ASCII 值转回字符并拼接到密文
        $tmp .= mb_chr($asciiShifted, 'UTF-8');
    }
    return $tmp;
}
// 加密
echo encryptDecrypt('这里是明文'), PHP_EOL; // 远量昲昑斊
// 解密
echo encryptDecrypt('远量昲昑斊', 'bcsub'), PHP_EOL; // 这里是明文

也就是说, 加上 3 就是加密, 减去 3 就是解密。 因此通常把像 3 这样用于加密和解密的数字称为“密钥”。
如果事先就把 3 这个密钥作 为只有数据的发送者和接受者才知道的秘密, 那么不知道这个密钥的 人,就无法对加密过的数据进行解密。

通过 XOR 运算进行加密和解密

通过把每一个字符的编码与密钥做XOR运算(eXclusiveOR,逻辑异或运算),将明文转换成密文。
XOR运算的有趣之处在于,用XOR运算加密后的密文,可以通过相同的XOR运算解密。也就是说,一个程序既可加密又可解密,很方便。

function encryptDecrypt(string $plaintext, int $secretKey = 8): string
{
    $tmp = '';
    // 遍历明文中的每一个字符
    for ($i = 0; $i < mb_strlen($plaintext, 'UTF-8'); $i++) {
        // 获取当前字符
        $char = mb_substr($plaintext, $i, 1, 'UTF-8');
        // 将字符转换为其 ASCII 值,
        $ascii_shifted = mb_ord($char, 'UTF-8') ^ $secretKey;
        // 将新的 ASCII 值转回字符并拼接到密文
        $tmp .= mb_chr($ascii_shifted, 'UTF-8');
    }
    return $tmp;
}
// 加密
echo encryptDecrypt("这里是明文"), PHP_EOL; // 近釄昧昆斏
// 解密
echo encryptDecrypt("近釄昧昆斏"), PHP_EOL; // 这里是明文

XOR运算的法则是把两个数据先分别用二进制表示,然后当一个数据中的某一位与另一个数据中的1相对时,就将这一位反转(若这一位是0就变成1,是1就变成0
因为是靠翻转数字实现的加密,所以只要再翻转一次就可以解密。
Pasted image 20250822141040

10.3 密钥越长,解密越困难

一般情况下,会将所使用的加密方式公开,而只对密钥的值保密。

暴力破解

利用计算机强大的计算能力,用密钥所有可能的取值去试着破解。
例如,要想破解用XOR运算加密得到的密文MJHHFJ,程序只要把0到9这几个值分别作为密钥都尝试一遍就能做到

function encryptDecrypt(string $plaintext, int $secretKey = 8): string
{
    $tmp = '';
    // 遍历明文中的每一个字符
    for ($i = 0; $i < mb_strlen($plaintext, 'UTF-8'); $i++) {
        // 获取当前字符
        $char = mb_substr($plaintext, $i, 1, 'UTF-8');
        // 将字符转换为其 ASCII 值,然后加 3
        $ascii_shifted = mb_ord($char, 'UTF-8') ^ $secretKey;
        // 将新的 ASCII 值转回字符并拼接到密文
        $tmp .= mb_chr($ascii_shifted, 'UTF-8');
    }
    return $tmp;
}
// 暴力解密, 第9次执行破解成功
for ($i = 0; $i < 9; $i++) {
    echo encryptDecrypt("近釄昧昆斏", $i), PHP_EOL;
}
/*
近釄昧昆斏
运釅昦昇斎
迓釆春昄斍
迒采昤昅斌
迕釀昣昂斋
返釁昢昃斊
迗釂昡昀斉
迖釃映昁斈
这里是明文
*/

通过与三位数的密钥进行 XOR 运算实现加密和解密

将明文中的第一个字符与3做XOR运算、第二个字符与4做XOR运算、第三个字符与5做XOR运算。
从第四个字母开始,还是以三个字母为一组依次与3、4、5做XOR运算,依此类推。
这种方法也常被称为“流密码”或“循环密钥异或加密”。

加密过程

function xorEncrypt(string $text, string $key = '345'): string
{
    $result    = '';
    $keyLength = strlen($key);
    for ($i = 0; $i < mb_strlen($text, 'UTF-8'); $i++) {
        // 获取当前字符
        $char = mb_substr($text, $i, 1, 'UTF-8');
        // 获取当前字符的 Unicode 码点
        $ord = mb_ord($char, 'UTF-8');
        // 计算 XOR 键
        $currentKey = $key[$i % $keyLength];
        // 应用 XOR 运算并获取新字符
        $xorResult = $ord ^ $currentKey;
        $result    .= mb_chr($xorResult, 'UTF-8');
    }
    return $result;
}
echo xorEncrypt('这里是明文') . PHP_EOL; // 迚釈昪昍斃
echo xorEncrypt('迚釈昪昍斃') . PHP_EOL; // 这里是明文

这仅适用于密钥是随机且一次一密(one-time pad)的情况

10.4 适用于互联网的公开密钥加密技术

公钥私钥

公钥:在公开密钥加密技术中,用于加密的密钥可以公开给全世界
私钥:用于解密的密钥是只有自己才知道的秘密
Pasted image 20250822162803

RSA 算法

RSA这个名字是由三位发明者RonaldRivest、AdiShamir和LeonardAdleman姓氏的首字母拼在一起组成的

RSA 创建公钥和私钥的步骤

无论是公钥还是私钥都包含着两个数值,两个数值组成的数对儿才是一个完整的密钥。

素数(Prime number)

大于 1 的自然数,并且除了 1 和它本身之外,不能被其他任何自然数整除

公约数(Common Divisor)

能同时整除两个或多个整数的数。
例如,对于整数 12 和 18:

公约数的概念是数学中的基础,在约分、求最大公约数和最小公倍数等运算中非常重要。
Pasted image 20250822163123
由图10.8的步骤可以得出:
323和11是公钥,323和131是私钥,的确是两个值都不相同的密钥。

在使用这对儿密钥进行加密和解密时,需要对每个字符执行如图10.9所示的运算。
这里参与运算的对象是字母N(字符编码为78)。用公钥对N进行加密得到224,用私钥对224进行解密可使其还原为78。
Pasted image 20250822172916
乍一看会以为只要了解了RSA算法,就可以通过公钥c=323、e=11推算出私钥c=323,f=131了。
但是为了求解私钥中的f,就不得不对c进行因子分解,分解为两个素数a、b
在本例中c的位数很短,而在实际应用公开密钥加密时,建议将c的位数(用二进制数表示 时)扩充为1024位(相当于128字节「截止2024年推荐2048位256字节以上」
要把这样的天文数字分解为两个素数,就算计算机的速度再快,也还是要花费不可估量的时间,时间可能长到不得不放弃解密的程度。
通常推荐使用2048位或更长的密钥

1. 为什么“天文数字”很难分解?

这个说法基于一个核心的数学难题:大整数分解

目前最好的分解算法(如通用数域筛法,GNFS)对于分解1024位整数的时间复杂度是亚指数的,但仍然需要巨大的计算资源

10.5 数字签名可以证明数据的发送者是谁

公开密钥加密技术的实际应用——数字签名。
在通过网络传输的文件中,数字签名可以发挥出与印章和签名同样的证明效果。

步骤中所提及的“信息摘要”(MessageDigest)可以理解为就是一个数值,通过对构成明文的所有字符的编码进行某种运算就能得出该数值

生成数据签名步骤

文本数据的发送者

  1. 选取一段明文;例:NIKKEI
  2. 计算出明文内容的信息摘要;例:(78 + 73 +75 +75 +69 + 73)÷100 的余数 = 43「实际中会使用更复杂的算法(如 SHA-256),例如 NIKKEI 的 SHA-256 哈希值为 3a7d4f...(64 位十六进制字符串)。」
  3. 用私钥对计算出的信息摘要进行加密;例:43 → 66(字母 B 的编码)「66 是 加密后的结果,也就是“数字签名”,这个步骤是对整个“用私钥加密摘要”这一复杂操作的极其简化的象征 RSA ECDSA」
  4. 把步骤(3)得出的值附加到明文后面再发送给接收者;例:NIKKEIB

文本数据的接收者

  1. 用发送者的公钥对信息摘要进行解密;例:B = 66 → 43
  2. 计算出明文部分的信息摘要;例:(78+73+75+75+69+73)÷100 的余数 = 43
  3. 比较在步骤(1)和(2)中求得的值,二者相同则证明接收的 信息有效;例:因为两边都是 43,所以信息有效

请诸位注意,这里是使用私钥进行加密、使用公钥进行解密,这与之前的用法刚好相反(如图10.10所示)。
而且这里所使用的是信息发送者(图10.10中的A小姐)的密钥对儿,而之前所使用的则是信息接收者(B先生)的密钥对儿。

信息摘要的算法

本例中信息摘要的算法是把明文中所有字母的编码加起来,然后取总和的最后两位。
而在实际中计算数字签名时,使用的是通过以下更加复杂的公式计算得出的的信息摘要。

Pasted image 20250822180521
也许诸位会认为把文件发送者的名字,比如“矢泽久雄”这个字符串用私钥加密,然后让对方用公钥解密也能代替印章或签字。但是如果这样做就不算是数字签名了,因为印章或签字有两层约束。其一是发送者承认文件的内容是完整有效的;其二是文件确实是由发送者本人发送的。发送者用构成文件的所有字符的编码生成了信息摘要,就证明发送者从头到尾检查了文件并承认其内容完整有效。如果接收者重新算出的信息摘要和经过发送者加密的信息摘要匹配,就证明文件在传输过程中没有被篡改,并且的确是发送者本人发送的。正因为数据是用发送者的私钥加密的,接收者才能用发送者的公钥进行解密。

数字签名的核心要点是

  1. 使用加密哈希函数生成唯一的消息摘要
  2. 使用发送方私钥加密摘要生成签名
  3. 接收方使用发送方公钥验证签名
  4. 通过比较哈希值验证消息完整性

绝对无法破解的加密技术也是存在的

其实,绝对无法破解的加密技术也是存在的。首先密钥的位数要与文件数据中的字符个数相同,其次每次发送文件时都需要先更换密钥,最后为了防止密钥被盗,发送者还要亲手把密钥交给接收者。
诸位明白为什么说这样做就绝对无法破解了吗?原因在于这样做等同于发送完全随机并且没有任何意义的数据。可是这种加密技术是不切实际的。
合理的密钥应该满足如下条件:

第11章:XML究竟是什么

11.1 XML 是标记语言

1. XML 的含义

2. 标记语言(Markup Language)

3. HTML 作为标记语言的示例

11.2 XML 是可扩展的语言

1. XML 文件的特征

2. “可扩展”的含义

3. XML 与 HTML 的区别

11.3 XML 是元语言

1. XML 作为元语言

2. XML 约束与格式良好文档

3. XML 解析与验证

11.4 XML 可以为信息赋予意义

1. HTML 的局限性

HTML 诞生之初的主要目的是供人浏览。然而,在商业应用中,如果网站只提供 HTML,程序几乎不可能进行自动化操作。

2. XML 的核心用途:赋予信息意义

为了解决 HTML 的局限性,XML 应运而生。

3. XML 与 Web 标准化

11.5 XML 是通用的数据交换格式

1. XML 作为通用数据交换格式

2. CSV(逗号分隔值)的对比

CSV (Comma Separated Value) 是一种长期以来在计算机行业被作为通用数据交换格式沿用的文件格式。

特点 XML (可扩展标记语言) CSV (逗号分隔值)
数据格式 仅由字符构成的纯文本文件 仅由字符构成的纯文本文件
结构 使用标签(如 <productID>)来组织信息 记录是经过 ","(半角逗号)分割后的信息。字符串要用 " 括起来,数字直接书写。
信息含义 为各个信息赋予了意义,分析起来更方便 只记录了信息本身,并没有为各个信息赋予意义
文件大小 文件尺寸更大(可能比 CSV 大 5 倍以上) 文件尺寸较小
劣势 文件尺寸增大,意味着会占用更多的存储空间、需要更长的传输及处理时间 缺乏对信息的意义描述

3. 未来趋势

11.6 可以为 XML 标签设定命名空间

1. 产生的问题:标签的同形异义

2. 解决方案:XML 命名空间 (XML Namespace)

<cat xmlns="http://www.grapecity.com/yazawa">小玉</cat>

11.7 可以严格地定义 XML 的文档结构

除了“格式良好的 XML 文档”(Well-formed XML Document)之外,还有一个概念叫做 “有效的 XML 文档”(Valid XML Document)

1. XML 文档的组成部分

完整的 XML 文档通常包括三个部分:XML 声明XML 实例DTD

部分 作用 示例/说明
XML 声明 写在 XML 文档开头,表明 XML 版本和字符编码 形如 <?xml version="1.0" encoding="Shift_JIS"?>
XML 实例 文档中通过标签被标记的部分 <mydata>...</mydata> 及其内部的标签数据
DTD 定义 XML 实例的结构 <!DOCTYPE mydata [...]> 部分> 括起来的部分就是 DTD]

2. DTD 的作用 (文档类型描述)

3. XML Schema 与发展趋势

11.8 用于解析 XML 的组件

虽然 XML 文档是纯文本格式,原则上可以用任何编程语言从零开始编写读写文件的程序,但这会非常麻烦。因此,人们开发了现成的程序组件来处理 XML 文档。

1. XML 文档处理规范

存在着用于处理 XML 文档的程序组件规范,其中主要的有:

规范名称 全称 制定机构
DOM Document Object Model(文档对象模型) W3C 标准
SAX Simple API for XML XML-dev 社区

2. 使用 DOM 组件的示例

以 Windows 系统为例,微软提供了一个遵循 DOM 规范的组件(文件名为 msxml3.dll)。

代码清单 11.1 使用了 DOM 的程序 (VBScript)

Set obj = CreateObject("Microsoft.XMLDOM")
obj.async = False
obj.Load "MyPet.xml"
s = ""
For i = 1 To obj.documentElement.childNodes.length
    ' 获取节点名称
    s = s & obj.documentElement.childNodes.Item(i - 1).nodeName
    s = s & "..."
    ' 获取节点内容
    s = s & obj.documentElement.childNodes.Item(i - 1).Text
    s = s & vbCrLf
Next
MsgBox s

11.9 XML 可用于各种各样的领域

本节总结了 XML 作为元语言所衍生的应用,特别强调了其作为通用数据交换格式在互联网和分布式计算中的重要性。

1. XML 定义的标记语言示例

通过 XML 这种元语言,诞生了各种各样的标记语言,用于处理不同类型的数据(如表 11.2 所示)。XML 格式的标记语言正在成为主流,取代以往厂商私有的数据格式。

表 11.2 用 XML 定义的标记语言示例

名称 用途 有关的企业或组织
XSL 为 XML 中的信息提供显示格式 W3C
MathML (Mathematical Markup Language) 描述数学算式 W3C
SMIL 把多媒体数据嵌入到网页中 W3C
MML 描述电子病历 电子病历研究会
SVG 用向量表示图形数据 W3C
JepaX 表示电子书 日本电子出版协会等
WML 表示移动终端上的内容 WAP Forum
CHTML 表示手机上的内容 Acces 等 6 家公司
XHTML (Extensible Hypertext Markup Language) 用 XML 定义 HTML 4.0 W3C
SOAP (Simple Object Access Protocol) 实现分布式计算 W3C
MathML 描述数学算式的示例

MathML (Mathematical Markup Language) 是一种用 XML 定义的标记语言,其用途是描述数学算式。它通过定义专门的标签来表示数学元素,例如根号、乘方或分数。
算式:aX2+bX+c=0
这个一元二次方程用 MathML 描述后,其 XML 结构(XML 实例)如下所示:

<math xmlns="http://www.w3.org/1998/Math/MathML">
 <mrow>
  <mi>a</mi>
  <msup>
   <mi>X</mi>
   <mn>2</mn>
  </msup>
  <mo>+</mo>
  <mi>b</mi>
  <mi>X</mi>
  <mo>+</mo>
  <mi>c</mi>
  <mo>=</mo>
  <mn>0</mn>
 </mrow>
</math>
标签解释 (部分)
标签 含义/用途 示例(在 aX2 中)
<math> 顶层标签,包含整个数学算式 <math xmlns="..."> ... </math>
<mrow> 将表达式组合成水平行 (Row) <mrow> ... </mrow>
<mi> 标识符 (Identifier),表示变量或函数名 <mi>a</mi><mi>X</mi>
<mn> 数字 (Number) <mn>2</mn><mn>0</mn>
<mo> 运算符 (Operator),表示数学符号 <mo>+</mo><mo>=</mo>
<msup> 上标 (Superscript) 结构 <msup> <mi>X</mi> <mn>2</mn> </msup> 表示 X2

2. SOAP 与分布式计算

3. XML 的价值与局限性

第12章:SE负责监管计算机系统的构建

12.1 SE 是自始至终参与系统开发过程的工程师

基本定位

主要工作内容

职业 工作内容 所需技能
SE 调查、分析客户的业务内容
计算机系统的基本设计
确定计算机系统的规格
估算开发费用和开发周期
项目管理
软件开发管理
计算机系统的维护管理
倾听需求
书写策划案
硬件
软件
网络
数据库
管理能力
程序员 制作软件(编程) 编程语言
算法和数据结构
关于开发工具和程序组件的知识
SE 负责业务信息化的整个生命周期:
  1. 最初阶段 (调查与设计)
    • 业务分析: 调查、分析客户的业务内容(手工作业流程)。
    • 系统规划: 确定计算机系统的基础设计详细规格
    • 估算: 估算开发费用和开发周期。
  2. 开发阶段 (项目与软件管理)
    • 项目管理: 负责系统开发项目的整体管理。
    • 软件开发管理: 负责软件的开发管理(将编程工作交由程序员完成)。
  3. 最后阶段 (运维)
    • 维护管理: 负责计算机系统的维护管理工作。

所需技能与知识

12.2 SE 未必担任过程序员

1. 角色定位与本质区别

2. 职业发展路线

3. 日本企业现状(造成职场关系变化的原因)

12.3 系统开发过程的规范

1. 瀑布模型的定义与作用

2. 核心流程与审核机制

3. 瀑布模型的特征与要求

12.4 各个阶段的工作内容及文档

阶段 文档
需求分析 系统策划文档、系统功能需求规格文档
外部设计 外部设计文档
内部设计 内部设计文档
程序设计 程序设计文档
编码实现 模块设计文档、测试计划文档
测试 测试报告
部署、维护 部署手册、维护手册

1. 需求分析

2. 系统设计阶段(外部、内部、程序设计)

阶段 视角/目的 工作内容 文档成果
外部设计 用户视角(用户看得到的部分) 设计系统处理的数据、用户界面(画面)、打印样式等外部表现 外部设计文档
内部设计 开发者视角(用户看不到的部分) 将外部设计的内容具体化 内部设计文档
程序设计 实现设计 为用程序实现内部设计内容而做出的更详细设计 程序设计文档

3. 编码实现

4. 测试

5. 部署、维护

12.5 所谓设计,就是拆解

1. 瀑布模型中的两大核心工作

瀑布模型(从需求分析到部署/维护)的工作可以分为两个主要阶段:

  1. 拆解(从需求分析到程序设计):
    • 工作内容: 将要被计算机系统替代的手工业务拆解为细小的要素
  2. 集成(从编码实现到部署、维护):
    • 工作内容: 将拆解后的细小要素转换成程序模块,并将其拼装组合在一起构成大型计算机系统。

2. 系统构建原理:化繁为简

3. 程序设计方法(拆解的关注点)

可以说,计算机系统的设计就是拆解。各种设计方法的差异在于拆解时的关注点不同。

表 12.3 具有代表性的程序设计方法

设计方法 拆解时所关注的事物
通用功能分割法 在整个计算机系统中通用的功能
STS 法 (Source, Transform, Sink) 数据流(输入、变换、输出)
TR 法 (Transaction) 事务(数据的处理单位)
Jackson 法 / Warner 法 输入数据输出数据 / 输入数据
面向对象法 构成计算机系统的事物(对象)

4. 设计方法与计算机三大原则

12.6 面向对象法简化了系统维护工作

1. 面向对象的概念

2. 核心优势:易维护与易改造

采用面向对象方法设计的系统,其关键优势在于面对现实世界业务变化时的响应能力:

12.7 技术能力和沟通能力

1. SE 必须具备的两大核心能力

能力类型 定义 涵盖范围
技术能力 (Technical Skill) 灵活运用 IT 技术的能力。 硬件、软件、网络、数据库等技术。
沟通能力 (Communication Skill) 和他人进行双向信息交换的能力。 1. 倾听需求(客户到 SE)
2. 传达信息(SE 到客户)

2. 沟通能力的基础知识与 SE 的定位

3. SE 沟通的正确切入点

12.8 IT 不等于引进计算机

1. IT(信息化)的本质

2. 计算机系统的切入点

3. SE 的关键职责

12.9 计算机系统的成功与失败

1. SE 的成就感与特权

2. 成功计算机系统的标准

3. 案例实践:名片管理系统 IT 解决方案

SE 在提出解决方案时,必须考虑客户预算,避免品质过剩。

解决方案 核心构成 成本/优势
IT 解决方案 1 台个人计算机 + 1 台打印机 + Windows + 市场上出售的通信录软件 低成本: 总费用可控制在 20 万日元以内。
核心认知 市面产品组合成的计算机系统,一样能提供完美的 IT 解决方案。 可行性: 低预算方案更易于客户接受并引进尝试。

4. 确保系统成功的维护与劝说艺术

12.10 大幅提升设备利用率的多机备份

1. 设备利用率的计算

设备利用率=正常运转的时间正常运转的时间+出现故障处于维修状态的时间

2. 系统架构对利用率的影响

A. 串联系统(单机)

B. 并联系统(多机备份)

3. SE 的技术说服力

计算机技术人员的职业发展与意识

1. 职业目标与定位

2. 成为行业专家的要求

3. 工程师的核心意识