-
Notifications
You must be signed in to change notification settings - Fork 154
/
Copy path1.5.html
1226 lines (705 loc) · 86.3 KB
/
1.5.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!DOCTYPE HTML>
<html lang="" >
<head>
<meta charset="UTF-8">
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
<title>表达式求值 · GitBook</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="description" content="">
<meta name="generator" content="GitBook 3.2.3">
<link rel="stylesheet" href="gitbook/style.css">
<link rel="stylesheet" href="gitbook/gitbook-plugin-intopic-toc/style.css">
<link rel="stylesheet" href="gitbook/gitbook-plugin-page-footer-ex/style/plugin.css">
<link rel="stylesheet" href="gitbook/gitbook-plugin-callouts/plugin.css">
<link rel="stylesheet" href="gitbook/gitbook-plugin-highlight/website.css">
<link rel="stylesheet" href="gitbook/gitbook-plugin-search/search.css">
<link rel="stylesheet" href="gitbook/gitbook-plugin-fontsettings/website.css">
<link rel="stylesheet" href="gitbook/gitbook-plugin-theme-comscore/test.css">
<link rel="stylesheet" href="styles.css">
<meta name="HandheldFriendly" content="true"/>
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<link rel="apple-touch-icon-precomposed" sizes="152x152" href="gitbook/images/apple-touch-icon-precomposed-152.png">
<link rel="shortcut icon" href="gitbook/images/favicon.ico" type="image/x-icon">
<link rel="next" href="1.6.html" />
<link rel="prev" href="1.4.html" />
</head>
<body>
<div class="book">
<div class="book-summary">
<div id="book-search-input" role="search">
<input type="text" placeholder="Type to search" />
</div>
<nav role="navigation">
<ul class="summary">
<li class="chapter " data-level="1.1" data-path="index.html">
<a href="index.html">
Introduction
</a>
</li>
<li class="chapter " data-level="1.2" data-path="PA0.html">
<a href="PA0.html">
PA0 - 世界诞生的前夜: 开发环境配置
</a>
<ul class="articles">
<li class="chapter " data-level="1.2.1" data-path="0.1.html">
<a href="0.1.html">
Installing GNU/Linux
</a>
</li>
<li class="chapter " data-level="1.2.2" data-path="0.2.html">
<a href="0.2.html">
First Exploration with GNU/Linux
</a>
</li>
<li class="chapter " data-level="1.2.3" data-path="0.3.html">
<a href="0.3.html">
Installing Tools
</a>
</li>
<li class="chapter " data-level="1.2.4" data-path="0.4.html">
<a href="0.4.html">
Configuring vim
</a>
</li>
<li class="chapter " data-level="1.2.5" data-path="0.5.html">
<a href="0.5.html">
More Exploration
</a>
</li>
<li class="chapter " data-level="1.2.6" data-path="0.6.html">
<a href="0.6.html">
Getting Source Code for PAs
</a>
</li>
</ul>
</li>
<li class="chapter " data-level="1.3" data-path="PA1.html">
<a href="PA1.html">
PA1 - 开天辟地的篇章: 最简单的计算机
</a>
<ul class="articles">
<li class="chapter " data-level="1.3.1" data-path="1.1.html">
<a href="1.1.html">
在开始愉快的PA之旅之前
</a>
</li>
<li class="chapter " data-level="1.3.2" data-path="1.2.html">
<a href="1.2.html">
开天辟地的篇章
</a>
</li>
<li class="chapter " data-level="1.3.3" data-path="1.3.html">
<a href="1.3.html">
RTFSC
</a>
</li>
<li class="chapter " data-level="1.3.4" data-path="1.4.html">
<a href="1.4.html">
基础设施
</a>
</li>
<li class="chapter active" data-level="1.3.5" data-path="1.5.html">
<a href="1.5.html">
表达式求值
</a>
</li>
<li class="chapter " data-level="1.3.6" data-path="1.6.html">
<a href="1.6.html">
监视点
</a>
</li>
<li class="chapter " data-level="1.3.7" data-path="1.7.html">
<a href="1.7.html">
如何阅读手册
</a>
</li>
</ul>
</li>
<li class="chapter " data-level="1.4" data-path="PA2.html">
<a href="PA2.html">
PA2 - 简单复杂的机器: 冯诺依曼计算机系统
</a>
<ul class="articles">
<li class="chapter " data-level="1.4.1" data-path="2.1.html">
<a href="2.1.html">
不停计算的机器
</a>
</li>
<li class="chapter " data-level="1.4.2" data-path="2.2.html">
<a href="2.2.html">
RTFSC(2)
</a>
</li>
<li class="chapter " data-level="1.4.3" data-path="2.3.html">
<a href="2.3.html">
程序, 运行时环境与AM
</a>
</li>
<li class="chapter " data-level="1.4.4" data-path="2.4.html">
<a href="2.4.html">
基础设施(2)
</a>
</li>
<li class="chapter " data-level="1.4.5" data-path="2.5.html">
<a href="2.5.html">
输入输出
</a>
</li>
</ul>
</li>
<li class="chapter " data-level="1.5" data-path="PA3.html">
<a href="PA3.html">
PA3 - 穿越时空的旅程: 批处理系统
</a>
<ul class="articles">
<li class="chapter " data-level="1.5.1" data-path="3.1.html">
<a href="3.1.html">
最简单的操作系统
</a>
</li>
<li class="chapter " data-level="1.5.2" data-path="3.2.html">
<a href="3.2.html">
穿越时空的旅程
</a>
</li>
<li class="chapter " data-level="1.5.3" data-path="3.3.html">
<a href="3.3.html">
用户程序和系统调用
</a>
</li>
<li class="chapter " data-level="1.5.4" data-path="3.4.html">
<a href="3.4.html">
文件系统
</a>
</li>
<li class="chapter " data-level="1.5.5" data-path="3.5.html">
<a href="3.5.html">
精彩纷呈的应用程序
</a>
</li>
</ul>
</li>
<li class="chapter " data-level="1.6" data-path="PA4.html">
<a href="PA4.html">
PA4 - 虚实交错的魔法: 分时多任务
</a>
<ul class="articles">
<li class="chapter " data-level="1.6.1" data-path="4.1.html">
<a href="4.1.html">
多道程序
</a>
</li>
<li class="chapter " data-level="1.6.2" data-path="4.2.html">
<a href="4.2.html">
虚实交错的魔法
</a>
</li>
<li class="chapter " data-level="1.6.3" data-path="4.3.html">
<a href="4.3.html">
超越容量的界限
</a>
</li>
<li class="chapter " data-level="1.6.4" data-path="4.4.html">
<a href="4.4.html">
来自外部的声音
</a>
</li>
<li class="chapter " data-level="1.6.5" data-path="4.5.html">
<a href="4.5.html">
编写不朽的传奇
</a>
</li>
</ul>
</li>
<li class="chapter " data-level="1.7" data-path="blank.html">
<a href="blank.html">
杂项
</a>
<ul class="articles">
<li class="chapter " data-level="1.7.1" data-path="FAQ.html">
<a href="FAQ.html">
常见问题(FAQ)
</a>
</li>
<li class="chapter " data-level="1.7.2" data-path="why.html">
<a href="why.html">
为什么要学习计算机系统基础
</a>
</li>
<li class="chapter " data-level="1.7.3" data-path="linux.html">
<a href="linux.html">
Linux入门教程
</a>
</li>
<li class="chapter " data-level="1.7.4" data-path="man.html">
<a href="man.html">
man入门教程
</a>
</li>
<li class="chapter " data-level="1.7.5" data-path="git.html">
<a href="git.html">
git入门教程
</a>
</li>
<li class="chapter " data-level="1.7.6" data-path="nemu-isa-api.html">
<a href="nemu-isa-api.html">
NEMU ISA相关API说明文档
</a>
</li>
<li class="chapter " data-level="1.7.7" data-path="changelog.html">
<a href="changelog.html">
更新日志
</a>
</li>
<li class="chapter " data-level="1.7.8" data-path="i386-intro.html">
<a href="i386-intro.html">
i386手册指令集阅读指南
</a>
</li>
<li class="chapter " data-level="1.7.9" data-path="exec.html">
<a href="exec.html">
指令执行例子
</a>
</li>
</ul>
</li>
<li class="divider"></li>
<li>
<a href="https://www.gitbook.com" target="blank" class="gitbook-link">
Published with GitBook
</a>
</li>
</ul>
</nav>
</div>
<div class="book-body">
<div class="body-inner">
<div class="book-header" role="navigation">
<!-- Title -->
<h1>
<i class="fa fa-circle-o-notch fa-spin"></i>
<a href="." >表达式求值</a>
</h1>
</div>
<div class="page-wrapper" tabindex="-1" role="main">
<div class="page-inner">
<div id="book-search-results">
<div class="search-noresults">
<section class="normal markdown-section">
<h2 id="表达式求值">表达式求值</h2>
<p>在TRM中, 寄存器(包括PC)和内存中的值唯一地确定了计算机的一个状态.
因此从道理上来讲, 打印寄存器和扫描内存这两个功能一定可以帮助我们调试出所有的问题.
但为了方便使用, 我们还希望简易调试器能帮我们计算一些带有寄存器和内存的表达式.
所以你需要在简易调试器中添加表达式求值的功能.
为了简单起见, 我们先来考虑数学表达式的求值实现.</p>
<h3 id="数学表达式求值">数学表达式求值</h3>
<p>给你一个表达式的字符串</p>
<pre><code>"5 + 4 * 3 / 2 - 1"
</code></pre><p>你如何求出它的值?
表达式求值是一个很经典的问题, 以至于有很多方法来解决它.
我们在所需知识和难度两方面做了权衡, 在这里使用如下方法来解决表达式求值的问题:</p>
<ol>
<li>首先识别出表达式中的单元</li>
<li>根据表达式的归纳定义进行递归求值</li>
</ol>
<h4 id="词法分析">词法分析</h4>
<p>"词法分析"这个词看上去很高端, 说白了就是做上面的第1件事情, "识别出表达式中的单元".
这里的"单元"是指有独立含义的子串, 它们正式的称呼叫token.
具体地说, 我们需要在上述表达式中识别出<code>5</code>, <code>+</code>, <code>4</code>, <code>*</code>, <code>3</code>, <code>/</code>, <code>2</code>, <code>-</code>, <code>1</code>这些token.
你可能会觉得这是一件很简单的事情, 但考虑以下的表达式:</p>
<pre><code>"0x80100000+ ($a0 +5)*4 - *( $t1 + 8) + number"
</code></pre><p>它包含更多的功能, 例如十六进制整数(<code>0x80100000</code>), 小括号,
访问寄存器(<code>$a0</code>), 指针解引用(第二个<code>*</code>), 访问变量(<code>number</code>).
事实上, 这种复杂的表达式在调试过程中经常用到,
而且你需要在空格数目不固定(0个或多个)的情况下仍然能正确识别出其中的token.
当然你仍然可以手动进行处理(如果你喜欢挑战性的工作的话), 一种更方便快捷的做法是使用正则表达式.
正则表达式可以很方便地匹配出一些复杂的pattern, 是程序员必须掌握的内容.
如果你从来没有接触过正则表达式, 请查阅相关资料.
在实验中, 你只需要了解正则表达式的一些基本知识就可以了(例如元字符).</p>
<p>学会使用简单的正则表达式之后, 你就可以开始考虑如何利用正则表达式来识别出token了.
我们先来处理一种简单的情况 -- 算术表达式, 即待求值表达式中只允许出现以下的token类型:</p>
<ul>
<li>十进制整数</li>
<li><code>+</code>, <code>-</code>, <code>*</code>, <code>/</code></li>
<li><code>(</code>, <code>)</code></li>
<li>空格串(一个或多个空格)</li>
</ul>
<p>首先我们需要使用正则表达式分别编写用于识别这些token类型的规则.
在框架代码中, 一条规则是由正则表达式和token类型组成的二元组.
框架代码中已经给出了<code>+</code>和空格串的规则, 其中空格串的token类型是<code>TK_NOTYPE</code>,
因为空格串并不参加求值过程, 识别出来之后就可以将它们丢弃了; <code>+</code>的token类型是<code>'+'</code>.
事实上token类型只是一个整数, 只要保证不同的类型的token被编码成不同的整数就可以了.
框架代码中还有一条用于识别双等号的规则, 不过我们现在可以暂时忽略它.</p>
<p>这些规则会在简易调试器初始化的时候通过<code>init_regex()</code>被编译成一些用于进行pattern匹配的内部信息,
这些内部信息是被库函数使用的, 而且它们会被反复使用, 但你不必关心它们如何组织.
但如果正则表达式的编译不通过, NEMU将会触发assertion fail, 此时你需要检查编写的规则是否符合正则表达式的语法.</p>
<p>给出一个待求值表达式, 我们首先要识别出其中的token, 进行这项工作的是<code>make_token()</code>函数.
<code>make_token()</code>函数的工作方式十分直接, 它用<code>position</code>变量来指示当前处理到的位置,
并且按顺序尝试用不同的规则来匹配当前位置的字符串.
当一条规则匹配成功, 并且匹配出的子串正好是<code>position</code>所在位置的时候,
我们就成功地识别出一个token, <code>Log()</code>宏会输出识别成功的信息.
你需要做的是将识别出的token信息记录下来(一个例外是空格串), 我们使用<code>Token</code>结构体来记录token的信息:</p>
<pre><code class="lang-c"><span class="hljs-keyword">typedef</span> <span class="hljs-keyword">struct</span> token {
<span class="hljs-keyword">int</span> type;
<span class="hljs-keyword">char</span> str[<span class="hljs-number">32</span>];
} Token;
</code></pre>
<p>其中<code>type</code>成员用于记录token的类型.
大部分token只要记录类型就可以了, 例如<code>+</code>, <code>-</code>, <code>*</code>, <code>/</code>, 但这对于有些token类型是不够的:
如果我们只记录了一个十进制整数token的类型, 在进行求值的时候我们还是不知道这个十进制整数是多少.
这时我们应该将token相应的子串也记录下来, <code>str</code>成员就是用来做这件事情的.
需要注意的是, <code>str</code>成员的长度是有限的, 当你发现缓冲区将要溢出的时候,
要进行相应的处理(思考一下, 你会如何进行处理?), 否则将会造成难以理解的bug.
<code>tokens</code>数组用于按顺序存放已经被识别出的token信息, <code>nr_token</code>指示已经被识别出的token数目.</p>
<p>如果尝试了所有的规则都无法在当前位置识别出token, 识别将会失败,
框架代码会输出当前token的位置(当表达式过长导致在终端里输出需要换行时,
<code>^</code>可能无法指示正确的位置, 此时建议通过输出的<code>position</code>值来定位token的位置).
这通常是待求值表达式并不合法造成的, <code>make_token()</code>函数将返回<code>false</code>, 表示词法分析失败.</p>
<div class="panel panel-warning"><div class="panel-heading"><h5 class="panel-title" id="实现算术表达式的词法分析"><i class="fa fa-edit"></i> 实现算术表达式的词法分析</h5></div><div class="panel-body"><p>你需要完成以下的内容:</p><ul>
<li>为算术表达式中的各种token类型添加规则, 你需要注意C语言字符串中转义字符的存在和正则表达式中元字符的功能.</li>
<li>在成功识别出token后, 将token的信息依次记录到<code>tokens</code>数组中.</li>
</ul></div></div>
<!-- -->
<div class="panel panel-danger"><div class="panel-heading"><h5 class="panel-title" id="调试公理"><i class="fa fa-bullhorn"></i> 调试公理</h5></div><div class="panel-body"><ul>
<li>The machine is always right. (机器永远是对的)<ul>
<li>Corollary: If the program does not produce the desired output, it is the programmer's fault.</li>
</ul>
</li>
<li>Every line of untested code is always wrong. (未测试代码永远是错的)<ul>
<li>Corollary: Mistakes are likely to appear in the "must-be-correct" code.</li>
</ul>
</li>
</ul><p>这两条公理的意思是: 抱怨是没有用的, 接受代码有bug的现实, 耐心调试.</p><p>jyy曾经将它们作为fact提出.
事实上无数程序员(包括你的学长学姐)在实践当中一次又一次验证了它们的正确性, 因此它们在这里作为公理出现.</p></div></div>
<!-- -->
<div class="panel panel-success"><div class="panel-heading"><h5 class="panel-title" id="如何调试"><i class="fa fa-lightbulb-o"></i> 如何调试</h5></div><div class="panel-body"><ul>
<li>不要使用"目光调试法", 要思考如何用正确的工具和方法帮助调试<ul>
<li>程序设计课上盯着几十行的程序, 你或许还能在大脑中像NEMU那样模拟程序的执行过程;
但程序规模大了之后, 很快你就会放弃的: 你的大脑不可能模拟得了一个巨大的状态机</li>
<li>我们学习计算机是为了学习计算机的工作原理, 而不是学习如何像计算机那样机械地工作</li>
</ul>
</li>
<li>使用<code>assert()</code>设置检查点, 拦截非预期情况<ul>
<li>例如<code>assert(p != NULL)</code>就可以拦截由空指针解引用引起的段错误</li>
</ul>
</li>
<li>结合对程序执行行为的理解, 使用<code>printf()</code>查看程序执行的情况(注意字符串要换行)<ul>
<li><code>printf()</code>输出任意信息可以检查代码可达性: 输出了相应信息, 当且仅当相应的代码块被执行</li>
<li><code>printf()</code>输出变量的值, 可以检查其变化过程与原因</li>
</ul>
</li>
<li>使用GDB观察程序的任意状态和行为<ul>
<li>打印变量, 断点, 监视点, 函数调用栈...</li>
</ul>
</li>
</ul><p>如果你突然觉得上述方法很有道理, 说明你在程序设计课上没有受到该有的训练.</p></div></div>
<!-- -->
<div class="panel panel-info"><div class="panel-heading"><h5 class="panel-title" id="为什么printf的输出要换行"><i class="fa fa-question-circle"></i> 为什么printf()的输出要换行?</h5></div><div class="panel-body"><p>如果不换行, 可能会发生什么?
你可以在代码中尝试一下, 并思考原因, 然后STFW对比你的想法.</p></div></div>
<!-- -->
<div class="panel panel-danger"><div class="panel-heading"><h5 class="panel-title" id="系统设计的黄金法则----kiss法则"><i class="fa fa-bullhorn"></i> 系统设计的黄金法则 -- KISS法则</h5></div><div class="panel-body"><p>这里的<code>KISS</code>是<code>Keep It Simple, Stupid</code>的缩写, 它的中文翻译是: 不要在一开始追求绝对的完美.</p><p>你已经学习过程序设计基础, 这意味着你已经学会写程序了, 但这并不意味着你可以顺利地完成PA,
因为在现实世界中, 我们需要的是可以运行的system, 而不是求阶乘的小程序.
NEMU作为一个麻雀虽小, 五脏俱全的小型系统, 其代码量达到3000多行(不包括空行).
随着PA的进行, 代码量会越来越多, 各个模块之间的交互也越来越复杂,
工程的维护变得越来越困难, 一个很弱智的bug可能需要调好几天.
在这种情况下, 系统能跑起来才是王道, 跑不起来什么都是浮云, 追求面面俱到只会增加代码维护的难度.</p><p>唯一可以把你从bug的混沌中拯救出来的就是KISS法则,
它的宗旨是<strong>从易到难, 逐步推进</strong>, 一次只做一件事, 少做无关的事.
如果你不知道这是什么意思, 我们以上文提到的<code>str</code>成员缓冲区溢出问题来作为例子.
KISS法则告诉你, 你应该使用<code>assert(0)</code>, 就算不"得体"地处理上述问题, 仍然不会影响表达式求值的核心功能的正确性.
如果你还记得调试公理, 你会发现两者之间是有联系的: 调试公理第二点告诉你, 未测试代码永远是错的.
与其一下子写那么多"错误"的代码, 倒不如使用<code>assert(0)</code>来有效帮助你减少这些"错误".</p><p>如果把KISS法则放在软件工程领域来解释, 它强调的就是多做<a href="http://en.wikipedia.org/wiki/Unit_testing" target="_blank">单元测试</a>:
写一个函数, 对它进行测试, 正确之后再写下一个函数, 再对它进行测试...
一种好的测试方式是使用assertion进行验证, <code>reg_test()</code>就是这样的例子.
学会使用assertion, 对程序的测试和调试都百利而无一害.</p><p>KISS法则不但广泛用在计算机领域, 就连其它很多领域也视其为黄金法则,
<a href="http://blog.sciencenet.cn/blog-414166-562616.html" target="_blank">这里</a>有一篇文章举出了很多的例子, 我们强烈建议你阅读它, 体会KISS法则的重要性.</p></div></div>
<h4 id="递归求值">递归求值</h4>
<p>把待求值表达式中的token都成功识别出来之后, 接下来我们就可以进行求值了.
需要注意的是, 我们现在是在对tokens数组进行处理, 为了方便叙述, 我们称它为"token表达式".
例如待求值表达式</p>
<pre><code>"4 +3*(2- 1)"
</code></pre><p>的token表达式为</p>
<pre><code>+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| NUM | '+' | NUM | '*' | '(' | NUM | '-' | NUM | ')' |
| "4" | | "3" | | | "2" | | "1" | |
+-----+-----+-----+-----+-----+-----+-----+-----+-----+
</code></pre><p>根据表达式的归纳定义特性, 我们可以很方便地使用递归来进行求值.
首先我们给出算术表达式的归纳定义:</p>
<pre><code><expr> ::= <number> # 一个数是表达式
| "(" <expr> ")" # 在表达式两边加个括号也是表达式
| <expr> "+" <expr> # 两个表达式相加也是表达式
| <expr> "-" <expr> # 接下来你全懂了
| <expr> "*" <expr>
| <expr> "/" <expr>
</code></pre><p>上面这种表示方法就是大名鼎鼎的<a href="http://en.wikipedia.org/wiki/Backus%E2%80%93Naur_Form" target="_blank">BNF</a>,
任何一本正规的程序设计语言教程都会使用BNF来给出这种程序设计语言的语法.</p>
<p>根据上述BNF定义, 一种解决方案已经逐渐成型了:
既然长表达式是由短表达式构成的, 我们就先对短表达式求值, 然后再对长表达式求值.
这种十分自然的解决方案就是<a href="http://en.wikipedia.org/wiki/Divide_and_conquer_algorithms" target="_blank">分治法</a>的应用,
就算你没听过这个高大上的名词, 也不难理解这种思路.
而要实现这种解决方案, 递归是你的不二选择.</p>
<p>为了在token表达式中指示一个子表达式, 我们可以使用两个整数<code>p</code>和<code>q</code>来指示这个子表达式的开始位置和结束位置.
这样我们就可以很容易把求值函数的框架写出来了:</p>
<pre><code class="lang-c">eval(p, q) {
<span class="hljs-keyword">if</span> (p > q) {
<span class="hljs-comment">/* Bad expression */</span>
}
<span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (p == q) {
<span class="hljs-comment">/* Single token.
* For now this token should be a number.
* Return the value of the number.
*/</span>
}
<span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (check_parentheses(p, q) == <span class="hljs-literal">true</span>) {
<span class="hljs-comment">/* The expression is surrounded by a matched pair of parentheses.
* If that is the case, just throw away the parentheses.
*/</span>
<span class="hljs-keyword">return</span> eval(p + <span class="hljs-number">1</span>, q - <span class="hljs-number">1</span>);
}
<span class="hljs-keyword">else</span> {
<span class="hljs-comment">/* We should do more things here. */</span>
}
}
</code></pre>
<p>其中<code>check_parentheses()</code>函数用于判断表达式是否被一对匹配的括号包围着,
同时检查表达式的左右括号是否匹配, 如果不匹配, 这个表达式肯定是不符合语法的, 也就不需要继续进行求值了.
我们举一些例子来说明<code>check_parentheses()</code>函数的功能:</p>
<pre><code>"(2 - 1)" // true
"(4 + 3 * (2 - 1))" // true
"4 + 3 * (2 - 1)" // false, the whole expression is not surrounded by a matched
// pair of parentheses
"(4 + 3)) * ((2 - 1)" // false, bad expression
"(4 + 3) * (2 - 1)" // false, the leftmost '(' and the rightmost ')' are not matched
</code></pre><p>至于怎么检查左右括号是否匹配, 就当作一个程序设计作业, 留给聪明的你来思考吧!</p>
<p>上面的框架已经考虑了BNF中算术表达式的开头两种定义,
接下来我们来考虑剩下的情况(即上述伪代码中最后一个<code>else</code>中的内容).
一个问题是, 给出一个最左边和最右边不同时是括号的长表达式, 我们要怎么正确地将它分裂成两个子表达式?
我们定义"主运算符"为表达式人工求值时, 最后一步进行运行的运算符,
它指示了表达式的类型(例如当一个表达式的最后一步是减法运算时, 它本质上是一个减法表达式).
要正确地对一个长表达式进行分裂, 就是要找到它的主运算符.
我们继续使用上面的例子来探讨这个问题:</p>
<pre><code>"4 + 3 * ( 2 - 1 )"
/*********************/
case 1:
"+"
/ \
"4" "3 * ( 2 - 1 )"
case 2:
"*"
/ \
"4 + 3" "( 2 - 1 )"
case 3:
"-"
/ \
"4 + 3 * ( 2" "1 )"
</code></pre><p>上面列出了3种可能的分裂, 注意到我们不可能在非运算符的token处进行分裂, 否则分裂得到的结果均不是合法的表达式.
根据主运算符的定义, 我们很容易发现, 只有第一种分裂才是正确的.
这其实也符合我们人工求值的过程: 先算<code>4</code>和<code>3 * ( 2 - 1 )</code>, 最后把它们的结果相加.
第二种分裂违反了算术运算的优先级, 它会导致加法比乘法更早进行.
第三种分裂破坏了括号的平衡, 分裂得到的结果均不是合法的表达式.</p>
<p>通过上面这个简单的例子, 我们就可以总结出如何在一个token表达式中寻找主运算符了:</p>
<ul>
<li>非运算符的token不是主运算符.</li>
<li>出现在一对括号中的token不是主运算符.
注意到这里不会出现有括号包围整个表达式的情况, 因为这种情况已经在<code>check_parentheses()</code>相应的<code>if</code>块中被处理了.</li>
<li>主运算符的优先级在表达式中是最低的.
这是因为主运算符是最后一步才进行的运算符.</li>
<li>当有多个运算符的优先级都是最低时, 根据结合性, 最后被结合的运算符才是主运算符.
一个例子是<code>1 + 2 + 3</code>, 它的主运算符应该是右边的<code>+</code>.</li>
</ul>
<p>要找出主运算符, 只需要将token表达式全部扫描一遍, 就可以按照上述方法唯一确定主运算符.</p>
<p>找到了正确的主运算符之后, 事情就变得很简单了:
先对分裂出来的两个子表达式进行递归求值, 然后再根据主运算符的类型对两个子表达式的值进行运算即可.
于是完整的求值函数如下:</p>
<pre><code class="lang-c">eval(p, q) {
<span class="hljs-keyword">if</span> (p > q) {
<span class="hljs-comment">/* Bad expression */</span>
}
<span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (p == q) {
<span class="hljs-comment">/* Single token.
* For now this token should be a number.
* Return the value of the number.
*/</span>
}
<span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (check_parentheses(p, q) == <span class="hljs-literal">true</span>) {
<span class="hljs-comment">/* The expression is surrounded by a matched pair of parentheses.
* If that is the case, just throw away the parentheses.
*/</span>
<span class="hljs-keyword">return</span> eval(p + <span class="hljs-number">1</span>, q - <span class="hljs-number">1</span>);
}
<span class="hljs-keyword">else</span> {
op = the position of 主运算符 in the token expression;