第四章:文法中的递归以及消除方法
在介绍递归文法之前,首先介绍一下递归下降分析器及其原理,然后分析右递归是如何处理的,再来分析左递归和间接左递归。
递归下降分析器
自顶向下语法分析的目的是为输入串寻找最左推导,或者说,从根节点(文法开始符号)开始,自上而下,从左到右地为输入字符串建立一棵分析树,并以预先确定的顺序创建分析树的节点。这种自顶向下分析的一般形式,称之为递归下降分析法。“下降”表示自顶向下,“递归”表示可能会调用自身。
在递归下降分析器中,最简单的一种,就是 LL(1) 递归下降分析器。我们尝试来构造一个简单的满足 LL(1) 的递归下降语法分析器。语法结构如下
stat : assign_stat| ifstat| return_stat;
assign_stat : ID '=' expr ;
ifstat : IF expr THEN stat ;
return_stat : RETURN expr ;
expr : ID '<' NUM| NUM;
当我们解析 if x < 0 then x = 0
这句语句时,会构造成如图 4-1 所示的解析树
(图 4-1)
解析树里面含有语句的所有语法结构的信息。而解析就是将线性的词法单元序列组成带有结构的解析树。
语法解析器能检查句子的结构是否符合语法规范(语言实际上就是合法句子的集合)。为了验证句子是否合法,解析器必须识别句子的解析树。不过解析器实际上并不需要构造出形式上的树型结构,只要识别出各种句子结构和相关的词法单元即可。解析器不必构造出具体的解析树,只要为解析树中的指定子结构编写专用的函数,就能从解析函数的调用序列中隐式的得到解析树的信息。比如函数 f(),在匹配其子节点时会调用相应的函数,而需要匹配词法单元时会调用 match() 的辅助函数。顺着这条思路,可以为 return x+1
这条语句编写如下的识别函数
void stat() { returnstat(); }
void returnstat() { match("return"); expr();}
void expr() { match("x"); match("+"); match("1"); }
match() 函数将输入流里的词法单元与传入它的参数进行比较,然后将输入指针往前移动。但是对于 stat 语法分支,情况就会变得复杂,因为需要考虑到三种不同的情况,
void stat() {if (超前扫描词法单元是 return) returnstat();else if (超前扫描词法单元是 ID) assignstat();else if (超前扫描词法单元是 IF) ifstat();else 错误处理
}
这种自顶向下的语法解析器,从解析树的顶部开始,一直向下处理,直到叶子节点。
LL(1) 算法
LL(1)语法解析器,是一种相对来说最简单的自顶向下的语法解析器,两个 L 都表示 left-to-right,第一个 L 表示解析器按照从左到右的顺序解析输入内容,第二个 L 表示下降解析时也是按照从左到右的顺序遍历子节点。1 代表决定语法解析器的每一步动作时向前扫描一个词法单元。
正如上面构造出的简单的递归下降分析器,在 stat 中,如果是 ifstat 子句,我们会调用 ifstat() 函数,定义如下
void ifstat() {match("if");expr();match("then");stat();
}
在 ifstat 中,匹配到 then 后,后面还是一个 stat 子句,调用 stat 函数进行匹配。这里就是一个右递归。在 ifstat 匹配完成后,调用 stat 匹配 then 后面的部分。递归下降分析器通过调用自身,是可以处理右递归的。
左递归
对于形如
expr -> expr + term
这样的产生式,右部的最左符号与产生式左部的非终结符相同,这样的产生式即为左递归产生式。假定 expr 对应的过程要使用这个产生式,因为右部是由 expr 开始的,expr 过程会被递归调用,导致无限循环。只有右部终结符与超前扫描符号匹配时,超前扫描符号才会改变。这个产生式中,右部是以非终结符 expr 开始的,输入符号在递归调用期间没有机会改变,所以导致无限循环。
左递归如何消除
如果 文法 1 具有一个非终结符 A 使得对某个字符串 α \alpha α 存在推导
A ⟹ A α (文法1) A \implies A\alpha \tag {文法1} A⟹Aα(文法1)
则称 文法1 是左递归的。
考虑下面 文法2
A ⟹ B | a | C B D (1) A \implies \text{B | a | C B D} \tag 1 A⟹B | a | C B D(1)
B ⟹ C | b (2) B \implies \text{C | b} \tag 2 B⟹C | b(2)
C ⟹ A | c (3) C \implies \text{A | c} \tag 3 C⟹A | c(3)
D ⟹ d (4) D \implies d \tag 4 D⟹d(4)
由该文法能产生诸如 a, cbd 等的串。能够发现,该文法不是直接左递归的,因为并没有直接的类似于文法1一样的产生式。但是,文法 G 确实是左递归的,因为我们可以通过推导,获得
A ⟹ A | c | b | a | C B D A \implies \text{A | c | b | a | C B D} A⟹A | c | b | a | C B D
这就是左递归文法了。再来分析一下这个文法,计算一下文法的 FIRST 和 FOLLOW 集合。
在计算 FIRST 和 FOLLOW 集合之前,首先介绍一下这两个集合。
如果 α \alpha α 是任意的文法符号串,则我们定义 F I R S T ( α ) FIRST(\alpha) FIRST(α) 是从 α \alpha α 推导出的串的开始符号的终结符集合,即
F I R S T ( α ) = { a ∣ α ⟹ a ⋯ , a 是 终 结 符 } FIRST(\alpha) = \{a | \alpha \implies a \cdots,a 是终结符 \} FIRST(α)={a∣α⟹a⋯,a是终结符}
如果
α ⟹ ϵ \alpha \implies \epsilon α⟹ϵ
则 ϵ \epsilon ϵ 也属于 F I R S T ( α ) FIRST(\alpha) FIRST(α) 。运用如下规则,来计算文法符号 X 的 FIRST(X) 集合,直到没有终结符或者 ϵ \epsilon ϵ 可加到某个 FIRST 集合为止
- 如果 X 是终结符,则 FIRST(X) 是 {X}
- 如果 X ⟹ ϵ X \implies \epsilon X⟹ϵ 是一个产生式,则将 ϵ \epsilon ϵ 加入到 FIRST(X) 中
- 如果 X 是非终结符,且 $ X \implies Y_1Y_2Y_3…Y_n$ 是一个产生式,则
- F I R S T ( Y i ) FIRST(Y_i) FIRST(Yi) 中所有符号在 FIRST(X) 中
- 若对于某个 i,a 属于 F I R S T ( Y i ) FIRST(Y_i) FIRST(Yi) 且 ϵ \epsilon ϵ 属于 F I R S T ( Y 1 ) FIRST(Y_1) FIRST(Y1) ,……。 F I R S T ( Y i − 1 ) FIRST(Y_{i-1}) FIRST(Yi−1) ,即 Y 1 . . . Y i − 1 ⟹ ϵ Y_1...Y{i-1} \implies \epsilon Y1...Yi−1⟹ϵ ,则将 a 加入到 FIRST(X) 中
- 若对于所有的 j=1, 2, …, k, ϵ \epsilon ϵ 在 F I R S T ( Y i ) FIRST(Y_i) FIRST(Yi) 中,则将 ϵ \epsilon ϵ 加入到 FIRST(X) 中
FOLLOW(A) 是计算所有非终结符 A 的后继符号的集合。可以应用如下规则,直到每个 FOLLOW(A) 集合都不能在加入任何符号或者 $ 为止
- 将 $ 加入到 FOLLOW(S) 中,其中 S 是开始符号,$ 是输入串的结束符
- 如果存在产生式 A ⟹ α B β A \implies \alpha B \beta A⟹αBβ ,则将 F I R S T ( β ) FIRST(\beta) FIRST(β) 中除了 ϵ \epsilon ϵ 以外的符号都加入到 FOLLOW(B) 中
- 如果存在产生式 A ⟹ α B A \implies \alpha B A⟹αB ,或者 A ⟹ α B β A \implies \alpha B \beta A⟹αBβ ,其中 F I R S T ( β ) FIRST(\beta) FIRST(β) 中包含 ϵ \epsilon ϵ ,即 β ⟹ ϵ \beta \implies \epsilon β⟹ϵ ,则将 FOLLOW(A) 中的所有符号都加入到 FOLLOW(B) 中
FIRST 和 FOLLOW 集合分别表示了从文法推导出的串的开始符号的终结符集合和后继终结符号的集合,在语法解析时,通过超前扫描的方式,获取到的token,与产生式的 FIRST 和 FOLLOW 集合比较,能够判断出语法适配到的产生式或者分支,做到预测分析的能力。
我们再来看下文法2,
F I R S T ( A ) = F I R S T ( B ) ⋃ F I R S T ( C B D ) ⋃ a F I R S T ( B ) = F I R S T ( C ) ⋃ b F I R S T ( C ) = F I R S T ( A ) ⋃ c \begin{array}{ccc} FIRST(A) = FIRST(B) \bigcup FIRST(C B D) \bigcup \text{{a}} \\ FIRST(B) = FIRST(C) \bigcup \text{{b}} \\ FIRST(C) = FIRST(A) \bigcup \text{{c}} \\ \end{array} FIRST(A)=FIRST(B)⋃FIRST(CBD)⋃aFIRST(B)=FIRST(C)⋃bFIRST(C)=FIRST(A)⋃c
同时, F I R S T ( C B D ) = F I R S T ( C ) FIRST(CBD)= FIRST(C) FIRST(CBD)=FIRST(C),由 (1) 和 (3) 可得
F I R S T ( A ) = F I R S T ( B ) ⋃ F I R S T ( C ) ⋃ a ⟹ F I R S T ( A ) = F I R S T ( B ) ⋃ F I R S T ( A ) ⋃ c ⋃ a ⟹ F I R S T ( A ) = F I R S T ( C ) ⋃ b ⋃ a, c ⟹ F I R S T ( A ) = a, b, c \begin{array}{cccc} FIRST(A)=FIRST(B) \bigcup FIRST(C) \bigcup \text{{a}} \\ \implies FIRST(A) = FIRST(B) \bigcup FIRST(A) \bigcup \text{{c}} \bigcup \text{{a}} \\ \implies FIRST(A) = FIRST(C) \bigcup \text{{b}} \bigcup \text {{a, c}} \\ \implies FIRST(A) = \text{{a, b, c}} \end{array} FIRST(A)=FIRST(B)⋃FIRST(C)⋃a⟹FIRST(A)=FIRST(B)⋃FIRST(A)⋃c⋃a⟹FIRST(A)=FIRST(C)⋃b⋃a, c⟹FIRST(A)=a, b, c
同理,可计算出
F I R S T ( C ) = F I R S T ( A ) ⋃ c = a, b, c F I R S T ( B ) = F I R S T ( C ) ⋃ b = a, b, c F I R S T ( D ) = d FIRST(C) = FIRST(A) \bigcup \text{{c}} = \text{{a, b, c}} \\ FIRST(B) = FIRST(C) \bigcup \text{{b}} = \text{{a, b, c}} \\ FIRST(D) = \text{{d}} FIRST(C)=FIRST(A)⋃c=a, b, cFIRST(B)=FIRST(C)⋃b=a, b, cFIRST(D)=d
计算 FOLLOW(A) 集合,首先需要找出非终结符 A 出现在右边的所有产生式,在文法2 的产生式 (3) 中,A 出现在产生式右边,同时也是在结束位置,所以 FOLLOW(A) 是包含 FOLLOW( C )。
再来看非终结符 C,在产生式 (1) 中, A ⟹ C B D A \implies C B D A⟹CBD,右边 C 的 右边是 B,那么 FOLLOW( C ) 是包含 FIRST(B) 的,在产生式 (2) 中,右边出现 C 同时也是产生式的结束位置,所以 FOLLOW( C ) 是包含 FOLLOW(B) 的。
再看 B,在产生式 (1) 中,B 右边出现的是 D,那么$ FOLLOW(B)$ 包含 F I R S T ( D ) = d FIRST(D)=\text{{d}} FIRST(D)=d ,同时 B 也是产生式的结束位置,那么 FOLLOW(B) 包含 FOLLOW(A),由以上所得
F O L L O W ( C ) = F I R S T ( B ) ⋃ F O L L O W ( B ) = a, b, c, d F O L L O W ( A ) = F O L L O W ( C ) = F O L L O W ( B ) = a, b, c, d FOLLOW(C) = FIRST(B) \bigcup FOLLOW(B) = \text{{a, b, c, d}} \\ FOLLOW(A) = FOLLOW(C) = FOLLOW(B) = \text{{a, b, c, d}} FOLLOW(C)=FIRST(B)⋃FOLLOW(B)=a, b, c, dFOLLOW(A)=FOLLOW(C)=FOLLOW(B)=a, b, c, d
在产生式 (1) 中,D 出现在产生式结束位置,所以
F O L L O W ( D ) = F O L L O W ( A ) = a, b, c, d FOLLOW(D) = FOLLOW(A) = \text{{a, b, c, d}} FOLLOW(D)=FOLLOW(A)=a, b, c, d
根据 FIRST 集合和 FOLLOW 集合,就可以进行预测分析,但是文法2,由于左递归的性质,当超前扫描是字符 a 时,并不能直接推测出匹配的产生式是 1-3 中的哪一个,而在递归下降语法解析器中,左递归会导致无限循环,这就需要使用到左递归消除。
左递归消除方法
对于文法2中的产生式 (1) 我们要将产生式变换成直接左递归的形式,如下
A ⟹ B ∣ a ∣ C B D A \implies B | a | \text{C B D} \\ A⟹B∣a∣C B D
将产生式 (2) 放进去替换 B
A ⟹ C ∣ b ∣ a C B D A \implies C | b | a \text{C B D} A⟹C∣b∣aC B D
将产生式 (3) 放进去替换 B
A ⟹ A ∣ c ∣ b ∣ a ∣ A B D ∣ c B D ⟹ A ∣ A B D ∣ c | b | a | c B D A \implies A | c | b | a | \text{A B D} | \text{c B D} \implies A | \text{A B D} | \text{c | b | a | c B D} A⟹A∣c∣b∣a∣A B D∣c B D⟹A∣A B D∣c | b | a | c B D
产生式右部中出现的 A,对于 A ⟹ A A \implies A A⟹A 没什么实际意义,可以直接去掉,产生式也就变成了
A ⟹ A B D ∣ c | b | a | c B D A \implies \text{A B D} | \text{c | b | a | c B D} A⟹A B D∣c | b | a | c B D
这很明显是一个直接左递归的产生式,能够产生以 c 或者 b 或者 a 或者 cBD 交替出现的,以BDBDBD……结束的串。我们将 A 进行变形,按照如下方式进行变换
A ⟹ ( c | b | a | c B D ) A ′ A ′ ⟹ ϵ ∣ B D A ′ A \implies (\text{c | b | a | c B D})A^{\prime} \\ A^{\prime} \implies \epsilon | BDA^{\prime} A⟹(c | b | a | c B D)A′A′⟹ϵ∣BDA′
这样,就将直接左递归变成了右递归,而右递归,递归下降分析器是可以处理的,这样,语法分析器就能正常的处理文法规则了。
将上面左递归消除的方法一般化,对形如
P ⟹ P α ∣ β , 其 中 β 不 以 P 开 头 (文法3) P \implies P \alpha | \beta ,\ \ 其中\beta不以P开头 \tag {文法3} P⟹Pα∣β, 其中β不以P开头(文法3)
的文法,按照如下方式进行左递归消除(如果不是直接左递归的方式,先转换成直接左递归)
P ⟹ β P ′ P ′ ⟹ α P ′ ∣ ϵ P \implies \beta P^{\prime} \\ P^{\prime} \implies \alpha P^{\prime} | \epsilon P⟹βP′P′⟹αP′∣ϵ
可以证明一下,对于
P ⟹ P α ∣ β P \implies P \alpha | \beta P⟹Pα∣β
产生式能够产生 β α α α … \beta \alpha \alpha \alpha \ldots βααα… 的串,对于消除左递归之后的文法,做如下变换
P ′ ⟹ α α α … P ⟹ β P ′ ⟹ β α α α … P^{\prime} \implies \alpha \alpha \alpha \ldots \\ P \implies \beta P^{\prime} \implies \beta \alpha \alpha \alpha \ldots P′⟹ααα…P⟹βP′⟹βααα…
两个产生式产生的串是等价的,也就证明了消除后的右递归的文法与之前的直接左递归的文法是等价的。
对于更一般的情况,形如
P ⟹ P ( α 1 ∣ α 2 ∣ … ∣ α n ) ∣ ( β 1 ∣ β 2 ∣ … ∣ β n ) P \implies P(\alpha_1 | \alpha_2 | \ldots | \alpha_n) | (\beta_1 | \beta_2 | \ldots | \beta_n) P⟹P(α1∣α2∣…∣αn)∣(β1∣β2∣…∣βn)
这样的文法规则,如果不是上面这样的表示,可以通过提取公因子的方式,变换成上面所示的产生式的形式,然后,还是按照上面文法 3 所示的方法进行递归消除,只不过此处,我们可以将 α 1 ∣ α 2 ∣ … ∣ α n \alpha_1 | \alpha_2 | \ldots | \alpha_n α1∣α2∣…∣αn 记为 α \alpha α,将 β 1 ∣ β 2 ∣ … ∣ β n \beta_1 | \beta_2 | \ldots | \beta_n β1∣β2∣…∣βn 记为 β \beta β。
间接左递归
indirectly left recursive grammar is that where x calls y which calls x.
通俗的讲,就是类似 A ⟹ B , 和 B ⟹ A A\implies B,和 B \implies A A⟹B,和B⟹A 这种文法规则,而 hidden left recursive grammar 是产生是右边允许为空集的情况,比如 A ⟹ B A , 和 B ⟹ ϵ A \implies BA,和 B \implies \epsilon A⟹BA,和B⟹ϵ
比如 文法 4
S ⟹ Q c ∣ c (5) S \implies Q c | c \tag 5 S⟹Qc∣c(5)
Q ⟹ R b ∣ b (6) Q \implies R b | b \tag 6 Q⟹Rb∣b(6)
R ⟹ S a ∣ a (7) R \implies S a | a \tag 7 R⟹Sa∣a(7)
该文法就是一个间接左递归的文法。利用前面所讲的递归下降语法解析器来说,文法 4 也会产生无限循环,解析器不能从递归嵌套的无限循环中解放出来。
对文法 4 进行替换变形,将 (7) 代入 (6)
Q ⟹ S a b ∣ a b ∣ b (8) Q \implies S a b | a b | b \tag 8 Q⟹Sab∣ab∣b(8)
将 (8) 代入 (5)
S ⟹ S a b c ∣ a b c ∣ b c ∣ c (9) S \implies S a b c | a b c | b c | c \tag 9 S⟹Sabc∣abc∣bc∣c(9)
可见,通过替换变形,产生式 (5) 变成了产生式 (9),变成了一个直接左递归的产生式,在按照直接左递归的变化方法
S ⟹ ( a b c | b c | c ) S ′ (10) S \implies (\text{a b c | b c | c}) S^{\prime} \tag {10} S⟹(a b c | b c | c)S′(10)
S ′ ⟹ ϵ ∣ a b c S ′ (11) S^{\prime} \implies \epsilon | a b c S^{\prime} \tag {11} S′⟹ϵ∣abcS′(11)
此时,文法 S 可以直接由 S ′ S^{\prime} S′ 和 abc 推导,Q 和 R 已经不需要了,直接删除即可。那么,产生式 (10) 和 产生式 (11) 就是文法 4 由间接左递归消除后变成直接右递归的结果。
一般来说,左递归的消除,不管是左递归还是间接左递归,最终都是需要消解成右递归的方式,因为递归下降解析器最适合处理的就是右递归。面对间接左递归,先通过代换的方式变形成直接左递归,然后在将直接左递归消解成右递归。
消除全部左递归算法
消除全部左递归,要求文法中
- 不含以 ϵ \epsilon ϵ 为右部的产生式
- 不含回路
算法实现
- 把文法中所有的非终结符按任意顺序排列
P 1 , P 2 , ⋯ , P n P_1, P_2, \cdots, P_n P1,P2,⋯,Pn
for i=1 To n; DOBEGIN for j=1 To i-1; DO
把形如 P i → P j γ P_i \to P_j\gamma Pi→Pjγ 的规则写成 P i → δ 1 γ ∣ δ 2 γ ∣ ⋯ ∣ δ k γ P_i \to \delta_1 \gamma | \delta_2 \gamma | \cdots | \delta_k \gamma Pi→δ1γ∣δ2γ∣⋯∣δkγ ,其中 P j → δ 1 ∣ δ 2 ∣ ⋯ ∣ δ k P_j \to \delta_1 | \delta_2 | \cdots | \delta_k Pj→δ1∣δ2∣⋯∣δk , δ i \delta_i δi 就是关于 P i P_i Pi 的所有规则
消除关于 P i P_i Pi 的所有直接左递归
END
- 将从开始符号出发永远无法到达的规则删除
也就是说,对每个非终结符号,用排在它前面的其他非终结符号的产生式表示出来(代入),并消除产生式中的直接左递归
reference
- 编程语言实现模式,第2章
- 编译原理龙书,第4章
- CS164-left recursion elimination example
待填坑
- antlr4 是如何解决左递归的
第四章:文法中的递归以及消除方法相关推荐
- 计算机网络中的高层应用,第四章计算机网络中的高层应用-.ppt
第四章计算机网络中的高层应用- 第四章 计算机网络中的高层应用 掌握C/S模式和B/S模式 熟练掌握常用的网络高层服务: WWW.Telnet .FTP.EMAIL.DNS等 了解其它网络应用:BBS ...
- 计算机图形学 opengl版本 第三版------胡事民 第四章 图形学中的向量工具
计算机图形学 opengl版本 第三版------胡事民 第四章 图形学中的向量工具 一 基础 1:向量分析和变换 两个工具 可以设计出各种几何对象 点和向量基于坐标系定义 拇指指向z轴正 ...
- matlab 信号去直流,基于FIR滤波的ADC采样信号中直流信号的消除方法与流程
本发明属于卫星导航领域,介绍了ADC采样信号中的直流偏置消除方法. 背景技术: 卫星导航系统在军事和民用领域应用越来越广泛.以GPS卫星导航系统为例,其到地面的信号功率仅为-130dBm,这么微弱的信 ...
- 锋利的Jquery【读书笔记】 -- 第四章 jQuery中的事件和动画
锋利的Jquery读书笔记 第三章 jQuery中的DOM操作 jQuery中的事件 事件绑定 bind方法 合成事件 hover方法 toggle方法 事件冒泡 事件对象 停止事件冒泡 阻止默认行为 ...
- [算法竞赛]第四章_函数和递归
第4章 函数和递归 [教学内容相关章节] 4.1数学函数 4.2地址的指针 4.3递归 4.4本章小结 [教学目标] (1)掌握多参数.单返回值的数学函数的定义和使用方法: (2)学会用typedef ...
- 【C++ Primer】第十四章 C++中的代码重用
序:C++的一个主要目标是促进代码重用,其中包含公有继承.包含.使用私有或保护继承 一,包含对象成员的类 1)valarray类简介 #include <valarray> ...
- 《C++ Primer Plus(第六版)》(30)(第十四章 C++中的代码重用 编程题答案)
14.7 编程练习 1.Wine类有一个string类对象成员(参见第4章)和一个Pair对象(参见本章):其中前者用来存储葡萄酒的名称,而后者有2个valarry<int>对象(参见本章 ...
- 《UVM实战》学习笔记——第四章 UVM中的TLM1.0通信
文章目录 前言 一.TLM1.0 1.TLM的定义 2.数据流:数据流动的方向 3.控制流:动作发起者initiator.动作接收者target 4.各种端口的连接 5.transport 6.non ...
- 第四章 python中的循环结构
目录 一.python中的内置函数 1. range(stop) 2.range(start,stop) 3.range(start,stop,step) 4.range的实际应用 二.python中 ...
最新文章
- 十张图看懂未来大数据世界
- html制作滚动游戏,HTML标签marquee实现滚动效果的简单方法(必看)
- TQ2440平台上LCD驱动的移植
- Android 使用 ActivityResult 处理 Activity 之间的数据通信及调起拍照实例
- blob照片显示 oracle_保存图片到oracle的blob字段,报ora-01465
- 中国二氧化碳激光器行业现状研究与可行性分析报告2022-2028年版
- 实战dock安装和镜像的拉取
- python 消息机制_Python并发编程之线程消息通信机制任务协调(四)
- iOS项目之同时点击多个按钮解决方案
- 【蓝桥杯】 2018年国赛 矩阵求和
- 如何在AWS EC2实例上部署Spring Boot应用程序
- 面试常考题---交换变量
- g++编译c++11 thread报错问题 及c++多线程操作
- Android APK签名原理
- 深入理解JVM虚拟机(总结篇)
- golang幽灵蛛(pholcus)(一)
- 先直播平台后抖音——不要再问用户需要什么
- Quartz简介及初始化
- 【JavaScript】ES6 数组的扩展
- 51单片机(2):最小系统