-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathbasic.tex
635 lines (554 loc) · 37.1 KB
/
basic.tex
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
\chapter{基础知识}
\section{程序}\label{fortran_program}
程序 (program) 是指挥电脑工作的一堆指令. 这堆指令要用一堆字符表示, 表示指令的所有字符合起来称为源代码 (source code). 源代码需要被保存在文件中, 保存源代码的所有文件合起来称为源代码文件 (source code file).
比如,在一个名为 \ttt{main.f90} 的文件内写入下面这些内容并保存.
\begin{lstlisting}
program main
implicit none
call helloworld()
end program main
\end{lstlisting}
在另一个名为 \ttt{helloworld.f90} 的文件内写入下面这些内容并保存.
\begin{lstlisting}
subroutine helloworld()
implicit none
print *, "Hello, world!"
end subroutine helloworld
\end{lstlisting}
那么 \ttt{main.f90} 和 \ttt{helloworld.f90} 就是源代码文件, 这俩文件里头一共有 8 行字符, 这些字符合起来就是源代码, 表示的指令如表 \ref{source_and_command_main} 和表 \ref{source_and_command_helloworld} 所示, 这一大堆指令合起来就是程序.
\begin{table}[!htbp]
\centering
\begin{tabular}{|p{0.42\textwidth}|p{0.48\textwidth}|}
\hline
\multicolumn{1}{|c|}{源代码}&\multicolumn{1}{|c|}{指令}\\
\hline
\ttt{program main}&开始运行主程序 \ttt{main}\\
\hline
\ttt{implicit none}&禁止隐式声明\\
\hline
\ttt{call helloworld()}&调用子程序 \ttt{helloworld}\\
\hline
\ttt{end program main}&结束运行主程序 \ttt{main}\\
\hline
\end{tabular}
\caption{\ttt{main.f90} 中源代码与指令的对照}\label{source_and_command_main}
\end{table}
\begin{table}[!htbp]
\centering
\begin{tabular}{|p{0.42\textwidth}|p{0.48\textwidth}|}
\hline
\multicolumn{1}{|c|}{源代码}&\multicolumn{1}{|c|}{指令}\\
\hline
\ttt{subroutine helloworld()}&开始运行子例行 \ttt{helloworld}\\
\hline
\ttt{implicit none}&禁止隐式声明\\
\hline
\ttt{print *, "Hello, world!"}&让电脑屏幕上出现 ``Hello, world!''\\
\hline
\ttt{end subroutine helloworld}&结束运行子例行 \ttt{helloworld}\\
\hline
\end{tabular}
\caption{\ttt{helloworld.f90} 中源代码与指令的对照}\label{source_and_command_helloworld}
\end{table}
\section{标准与规范}
任何编程\uline{语言}都有自己的句法/语法 (syntax), 比如 ``让电脑屏幕上出现 `A'{}'', Fortran 要写成 \ttt{print *, "A"}, C 要写成 \ttt{printf("A");}, Python 2 要写成 \ttt{print "A"}, Python 3 要写成 \ttt{print("A")}. 编程语言对语法的统一规定称为标准 (standard). Fortran 的现行标准是 Fortran 2023 (第\ref{introduction}章中已介绍), 旧标准有 FORTRAN 66, FORTRAN 77, Fortran 90, Fortran 95, Fortran 2003, Fortran 2008, Fortran 2018.
不太好玩儿的是, 虽然 Fortran 语法已由现行标准规定好了, Fortran 编译器们却偶尔不按 Fortran 标准来干活儿, 自己制定了一些规则, 还有些时候, Fortran 标准直接就把权利赋予编译器, 让他们自定一些规则. 这样, Fortran 编译器们时不时就有和 ``官规'' 不同的 ``器规''.
编程语言的标准是 ``硬规则'', 除此之外还有 ``软规则'', 称为规范 (convention). 所谓 ``规范'', 就是被代码毒打的前人总结的血泪教训. 不遵守规范, 也不一定会出什么问题, 但一旦出了问题, 就不知道问题出在哪儿啦. 请同学们尽量遵守本笔记列出的 Fortran 规范, 不然期末挂了不要怪 Fortran 哟! 不过有时不遵守规范也确实不会出什么问题, 安安会适时说明的.
编译器各有各的器规, 这可能会出事. 想象一下, 同学们期末考写好程序, Ifx 和 Gfortran 说程序没问题, 同学们兴冲冲地交卷了, 然后老师掏出 Visual Fortran (某老款编译器), Visual Fortran 说程序有问题, 同学们就挂科啦. 为防止这种悲惨情形发生, 我们需要避免使用Fortran 标准没规定而编译器自定的规则.
\begin{convention}\label{use_standard_only}
只使用 Fortran 现行标准自行规定的规则.
\end{convention}
这样, 我们的程序编译器要是不认, 安安唯一能想到的可能就是 Fortran 标准更新了, 而编译器没更新, 还用着旧标准. 不过同学们没必要担心这一情形, 本笔记里的东东绝大多数 Fortran 90 就有了 (我猜老师现在也只教到 Fortran 90), 即使是 Visual Fortran 也会认的, 要是某 Fortran 编译器竟连 Fortran 90 都不认, 我们就把它卸载了\dots{}
\section{程序单元}\label{program_unit}
每个 Fortran 程序都由若干程序单元 (program unit) 组成. 程序单元可能是主程序 (main program), 或外部子程序 (external subprogram), 或模块 (module), 或子模块 (submodule). 一个 Fortran 程序中必须有且只有一个主程序, 其他程序单元数目随便.
主程序长下面这样.
\begin{lstlisting}
program [program-name]
...
end program [program-name]
\end{lstlisting}
其中 \ttt{[program-name]} 处是一个名称 (name), 名称指的是由字母, 数字和下划线 ``\ttt{\_{}}''\footnote{
下划线是空格的替代字符,长得还是蛮像滴!
} 组成的, 开头是字母的字符串\footnote{后文中的 ``某某名'' 若不另加说明则都是名称, 不再赘述.}, ``\ttt{...}'' 处则是各种指令, 一行一条. 比如, 之前 \ref{fortran_program} 节里 \ttt{main.f90} 里的程序就是一个名称为 \ttt{main} 的主程序\footnote{安安习惯把主程序的名称取做 \ttt{main} 并放在 \ttt{main.f90} 里.} \\(\ttt{helloworld.f90} 里的程序则是一个外部子程序).
外部子程序在第\ref{fortran_procedure}章里讲, 模块在第\ref{fortran_module}章里讲, 子模块在第\ref{fortran_submodule}章里讲.\footnote{??\mbox{}的内容安安会在猴年马月写好的!}
每个 Fortran 源代码文件里都可以放若干完整的程序单元, 但习惯上一个源代码文件里只放一个程序单元, 并且文件名和文件里的程序单元名相同 (比如主程序 \ttt{main} 放在 \ttt{main.f90} 里).
\begin{convention}
一个源代码文件里只放一个程序单元, 并且文件名和文件里的程序单元名相同.\label{one_program_unit}
\end{convention}
当然, 安安偶尔也会在一个源代码文件里放多个程序单元, 比如安安在玩 SOFA 的时候, 习惯把除 \ttt{t\_{}sofa\_{}f.for} 外的所有文件里的东东通通复制粘贴到一个名为 \ttt{sofa.for} 的新文件里, 这个文件里就有一大堆外部子程序了.
今后程序若有多个程序单元, 我将把这多个程序单元写在一起 (虽然按规范 \ref{one_program_unit} 它们应该被分别保存). 比如 \ref{fortran_program} 节 \ttt{main.f90} 和 \ttt{helloworld.f90} 中的程序会合写成如下模样.
\begin{lstlisting}
program main
implicit none
call helloworld()
end program main
subroutine helloworld()
implicit none
print *, "Hello, world!"
end subroutine helloworld
\end{lstlisting}
\section{编译与运行}\label{run_fortran}
我们用 Fortran 语写好了程序, 命令电脑照此干活儿, 然而电脑会说干不了. 原因在于, 电脑不认得 Fortran 语, 电脑只懂得电脑语 (学名是机器语言), 甚至不同电脑的电脑语还不一样. 而电脑语身为正常人类的我们完全搞不懂\dots{}
这时我们就得请编译器 (compiler) 们出场干活儿了, 它们会在操作系统的帮助下, 把 Fortran 语翻译成电脑语, 这个过程称为编译\footnote{这里说的是广义的编译. 狭义的编译是广义的编译中的一个环节.} (compile). \ref{use_ifx} 小节中输入 \ttt{ifx *.f* /o a.exe} 回车就是请 Ifx 编译, \ref{use_gfortran} 小节中输入 \ttt{gfortran *.f*} 回车就是请 Gfortran 编译, 最后电脑语程序保存在称为可执行文件 (executable file) 的 \ttt{a.exe} 里.
把程序翻译成电脑语后, 电脑没借口不干活儿了. 我们输入 \ttt{a.exe} 回车, 命令电脑照电脑语程序干活儿, 这个过程称为运行/执行 (execute) 程序, 也俗称跑 (run) 程序. Fortran 程序中指令运行的基本顺序是从主程序第一行 \ttt{program [program-name]} 开始, 向下逐行依次运行, 到主程序最后第一行 \ttt{end program [program-name]} 结束. 本笔记里会介绍许多例外情况, 遇到例外时会称之为跳转 (jump). 例如 \ref{fortran_program} 节 \ttt{main.f90} 和 \ttt{helloworld.f90} 中的程序, 指令的运行顺序如下, 其中指令 3 跳转到指令 4, 指令 7 跳转到指令 8.
\begin{table}[!htbp]
\centering
\begin{tabular}{|p{0.42\textwidth}|p{0.48\textwidth}|}
\hline
\multicolumn{1}{|c|}{源代码}&\multicolumn{1}{|c|}{指令}\\
\hline
\ttt{program main}&1.开始运行主程序 \ttt{main}\\
\hline
\ttt{implicit none}&2.禁止隐式声明\\
\hline
\ttt{call helloworld()}&3.调用子程序 \ttt{helloworld}\\
\hline
\ttt{end program main}&8.结束运行主程序 \ttt{main}\\
\hline
\end{tabular}
\caption{\ttt{main.f90} 中指令运行顺序}
\end{table}
\begin{table}[!htbp]
\centering
\begin{tabular}{|p{0.42\textwidth}|p{0.48\textwidth}|}
\hline
\multicolumn{1}{|c|}{源代码}&\multicolumn{1}{|c|}{指令}\\
\hline
\ttt{subroutine helloworld()}&4.开始运行子例行 \ttt{helloworld}\\
\hline
\ttt{implicit none}&5.禁止隐式声明\\
\hline
\ttt{print *, 'Hello, world!'}&6.让电脑屏幕上出现``Hello, world!''\\
\hline
\ttt{end subroutine helloworld}&7.结束运行子例行 \ttt{helloworld}\\
\hline
\end{tabular}
\caption{\ttt{helloworld.f90} 中指令运行顺序}
\end{table}
当年编程语言刚刚出世的时候, 人们对程序设计的认识不足, 程序里的指令可以胡乱跳转, 总有人写出让人根本读不懂的鬼魅程序. 后来人们制定了结构化 (structured) 程序设计原则, 其基本特征是只保留尽可能少的, 有明确规则的指令跳转. Fortran 语法如今已基本符合结构化原则了, 但仍有少数不符合的. 本笔记里会介绍一些不符合结构化原则的语法, 但只是因为不用那些语法, 有些活儿干不了. 请同学们尽可能少用那些语法.
\begin{convention}
尽可能少用不符合结构化程序设计原则的语法.\label{no_no_structured}
\end{convention}
最后补充一下, 玩 Fortran 时我们是先用编译器把 Fortran 程序从头到尾全翻译了然后再运行. 但编程语言还有另一种运行方式, 是用解释器 (interpreter) 把程序一点点翻译成机器语言, 翻译一点运行一点, 好像电脑装了解释器后就能直接读懂编程语言一样, 这个过程称为解释 (interpret). 这两种方式还可以混用, 同学们以后玩 Python 的时候就会遇到混合方式.
\section{异常}\label{fortran_exception}
程序编译和运行时可能会有不妙的事情发生, 那就是抛出 (raise) 异常 (exception). 异常分为错误 (error) 和警告 (warning), 抛出错误和抛出警告也有些俗称, 比如报错和弹警告之类的.
\subsection{错误}\label{fortran_error}
抛出错误有两种情况. 最常见的是我们写的程序不符合 Fortran 语法, 编译器读不懂, 无法翻译, 就抛出错误然后罢工了. 比如, 编译这样一个保存在 \ttt{main.f90} 中的程序.
\begin{lstlisting}
program main
implicit none
print 'Hello, world!'
end program main
\end{lstlisting}
Ifx 给出的结果如下\footnote{
假定屏幕宽度是 60 字符宽, 且略去版权声明等无关紧要的内容, 下同.
}.
\begin{verbatim}
main.f90(3): error #6899: First non-blank character in a cha
racter type format specifier must be a left parenthesis. [
'Hello, world!']
print 'Hello, world!'
-----------^
compilation aborted for main.f90 (code 1)
\end{verbatim}
Gfortran 给出的结果如下.
\begin{verbatim}
main.f90:3:10:
print 'Hello, world!'
1
Error: Missing leading left parenthesis in format string at
(1)
\end{verbatim}
由于程序第三行不符合 Fortran 语法 (\ttt{print} 后少了 \ttt{*,} 之类的东东), 编译器不知怎么干活儿了, 只好报个错. 智能的 VS Code 会在编译前就请 Gfortran 来检查我们的程序是否有语法错误, 这将节约我们的时间.
因为有器规, 所以报错还和编译器有关. 比如运行下面这个程序.
\begin{lstlisting}
program main
implicit none
print *, 1.0/0.0
end program main
\end{lstlisting}
Gfortran 会报错说不能除以 $0$, 但 Ifx 不会报错并会算出正无穷.
另一种抛出错误的情况是我们写的程序符合 Fortran 语法, 编译器能翻译, 但之后运行的时候程序请操作系统帮忙干活儿, 操作系统发现没法儿干, 程序就抛出错误然后罢工了. 比如在 Windows 系统里运行这样一个程序.
\begin{lstlisting}
program main
implicit none
open(10, file='C:\*')
end program main
\end{lstlisting}
这回编译时 Ifx 和 Gfortran 都不认为有什么问题, 但运行时程序让 Windows 打开文件 \ttt{C:\bs{}*} (若无此文件则新建一个后打开), 而 Windows 文件名里不可以有 \ttt{*}, 程序只好报个错. 这个程序在同学们以后会学的 Linux 系统里就可以正常工作, 因为 Linux 文件名里可以有 \ttt{*}.
\subsection{警告}\label{fortran_warning}
有时候我们写的程序符合 Fortran 语法, 编译器读得懂能翻译, 但它觉得我们写的程序十分 ``危险'', 很有可能程序的实际执行过程并不是我们预想的那样, 这时编译器就会抛出警告. 什么时候应该给个错误, 不同编译器的观点还是比较一致的, 但什么时候给个警告, 那区别就相当大了.
比如, 编译这样一个保存在 \ttt{main.f90} 中的程序.
\begin{lstlisting}
program main
implicit none
integer :: i
i = 1000000000000
print *, i
end program main
\end{lstlisting}
Ifx 给出这样的结果.
\begin{verbatim}
main.f90(4): warning #8221: This integer constant is outside
the default integer range - using INTEGER(8) instead. [10
00000000000]
i = 1000000000000
--------^
main.f90(4): warning #6384: This value is out-of-range for a
n INTEGER(KIND=4) type. [1000000000000]
i = 1000000000000
--------^
\end{verbatim}
Ifx 认为 \ttt{1000000000000} 过大, 无法正确存储, 于是就给个警告, 但程序仍然可以运行. 正常想来, 程序运行后电脑屏幕上应该出现 \ttt{1000000000000}, 结果出现的却是 \ttt{-727379968}, 果然很奇怪! 至于 Gfortran 嘛, 它也认为 \ttt{1000000000000}过大, 但它不是给个警告, 而是直接报错.
再比如, 编译这样一个保存在 \ttt{main.f90} 中的程序.
\begin{lstlisting}
program main
implicit none
integer :: i
i = .true.
print *, i
end program main
\end{lstlisting}
Gfortran 给出这样的结果.
\begin{verbatim}
main.f90:4:8:
4 | i = .true.
| 1
Warning: Extension: Conversion from LOGICAL(4) to INTEGER(4)
at (1)
\end{verbatim}
Gfortran 觉得令整数 \ttt{i} 等于逻辑 ``真'' 的操作其实是标准不允许的, 是自己放水才能这么干的, 于是就给个警告 (VS Code 会提前请 Gfortran 来看是否会弹警告), 但程序仍然可以运行. 运行后电脑屏幕上出现 \ttt{1} . 至于 Ifx 嘛, 它倒是不报错也不给警告, 干脆利落地编译完了, 但因为器规不同, 运行后电脑屏幕上出现的是 \ttt{-1}\dots{}
\section{字符集}
Fortran 可以在程序中使用所有能直接用英语输入法打出的字符, 其他字符 (比如汉字, 汉语标点) 能否在程序中用则由器规决定. 注意, 英文标点和中文标点是\uline{完全不同}的\footnote{用 VS Code 的同学会发现英文标点和中文标点显示出来完全不一样.}, 英文标点是``半角标点'', 中文标点是``全角标点''哟!\footnote{本笔记里使用的全部是英文标点呢.}
经俺测试, Ifx 和 Gfortran 都是支持汉字和汉语标点的, 但直接让电脑屏幕上出现汉字或汉语标点可能会出现 ``穞堵臸猭畍\footnote{不认识这东东的同学请自行搜索.}'' 之类的乱码 (这可以解决).
为避免各种各样的问题, 还是只用正常字符为好. 另外源代码文件名最好也如此, 不然编译器可能会无法识别源代码文件呢.
\begin{convention}
在程序和源代码文件名中只使用能直接用英语输入法打出的字符.\label{english_character_convention}
\end{convention}
Fortran 是大小写不敏感的 (case-insensitive), 也就是说, 很多情况下程序里的字母大写小写效果是完全相同的 (效果不同的地方见 \ref{fortran_char} 节和 \ref{character_string_edit_descriptor} 小节). 比如, 下面这两个程序和 \ref{fortran_program} 节的 \ttt{main.f90} 内的程序是完全等价的.
\begin{lstlisting}
PROGRAM MAIN
IMPLICIT NONE
CALL HELLOWORLD()
END PROGRAM MAIN
\end{lstlisting}
Fortran 大小写不分是有历史原因的. 当年电脑太烂了, 为节省存储空间, 一开始 Fortran 只许用大写字母, 后来电脑不那么烂了, 能用小写字母了, Fortran 大概是因为想尽量保证老程序能用, 所以直接规定大小写不分, 结果挖了个如今没法儿填的史诗级奆坑, 学 Fortran 玩 Fortran 的历来都深受其害. 举个例子, 当年俺搞毕设, 需要解 Kepler 方程 $E=M+e\cos E$,在程序里对应地写成 \ttt{E = M + e*cos(E)}, 然而 Fortran 是大小写不分的, 所以 \ttt{E = M + e*cos(E)} 其实对应于 $e=M+e\cos e$ 或 $E=M+E\cos E$, 然后俺忘了这茬, 结果死活找不出自己哪里写错了\dots{}
被 Fortran 反复毒打后, 人们决定做些什么来避免被毒打. 既然 Fortran 大小写不分, 那避免混淆的最好办法就是在程序里统一只用小写或大写字母 (俺搞毕设时知道这事, 但俺偷懒了, 结果惨遭报应\dots{}). 因为单词写成小写字母比写成大写字母好认, 所以现在人们都统一只用小写字母.
\begin{convention}
只在程序中使用小写字母.\label{lower_case_convention}
\end{convention}
如上所述, 有时 Fortran 大小写是分的, 这时规范 \ref{lower_case_convention} 没法儿遵守, 当然就不遵守了. 除此之外, 规范 \ref{lower_case_convention} 还有其他一些可以不遵守的情形, 俺留着后面说. 这样, \ttt{E = M + e*cos(E)} 就得改写了. 因为 Kepler 方程里 $E$ 是偏近点角, 是个角, 所以 \ttt{E} 可以写成 \ttt{e\_{}angle} (也可以写成 \ttt{angle\_{}e}, 但绝不能写成 \ttt{E\_{}angle}!!!), $e$ 则是离心率, 虽然写成 \ttt{e} 也可以和 \ttt{e\_{}angle} 区别开, 但为了让区别更明显, 可以写成 \ttt{ecc} (eccentricity 的缩写).
\section{源代码格式}
Fortran 有两种源代码格式 (source form): 自由格式 (free form) 和固定格式 (fixed form). 固定格式是将被废弃的 (obsolescent) 老格式. 安安的笔记里只讲自由格式.
\begin{convention}
永远编写自由格式的程序.
\end{convention}
自由格式源代码文件拓展名一般是 \ttt{.f90}, 固定格式源代码文件拓展名一般是 \ttt{.for}/\ttt{.f}, 这是 Fortran 编译器的惯例. 查 \href{https://cdrdv2.intel.com/v1/dl/getContent/824360?fileName=fortran-compiler_developer-guide-reference_2024.2-767251-824360.pdf}{Ifx 文档} 和 \href{https://gcc.gnu.org/onlinedocs/gcc-13.2.0/gfortran.pdf}{Gfortran 文档} 可知, 它俩都遵从这个惯例. 请同学们把自由格式的 Fortran 程序保存在拓展名为 \ttt{.f90} 的文件里, 以保证它俩能认得. 要是某 Fortran 编译器竟不遵从惯例, 我们就把它卸载了\dots{}
\begin{convention}
保证自由格式源代码文件拓展名为 \ttt{\emph{.f90}}.
\end{convention}
这里需澄清, 有习惯说法是把自由格式程序说成 ``Fortran 90 程序'', 把固定格式程序说成``FORTRAN 77 程序''. 这种说法的根源是 Fortran 历史上有一场巨变: FORTRAN 77 只有固定格式, 而 Fortran 90 新引入了自由格式. 然而, 这并不意味着固定格式程序中不能使用 Fortran 90 及以后的标准新出的语法.
在 VS Code 中, 把鼠标移到右下角铃铛图标按键左边第一个, 鼠标移到后会显示 ``Select Language Mode'' 的按键上, 点击之, 然后弹出的菜单下拉选择 ``Fortran (FortranFreeForm)'', VS Code 便会自动检查程序是否符合 Fortran 自由格式.
\subsection{空格}
Fortran 代码里空格 (blank/space) 通常没有意义 (\ref{fortran_char} 节和 \ref{character_string_edit_descriptor} 小节中有例外), 可多可少可没有, 但有些时候空格是必要的, 否则会出现混淆, 比如 \ttt{program main} 就不能写成 \ttt{programmain} (没有编译器能智能到把它拆成两个词).
虽然空格没有意义, 但能让代码更清楚. 如果把 \ttt{print *, 'A'} 写成 \ttt{print*,'A'}, 挤成一团, 也不能说是错误, 但即便像 VS Code 里那样代码有颜色区分, 也让人看得晕乎, 更何况笔记里这样代码没颜色呢.
\begin{convention}
在程序中适当地加入空格.\label{fortran_blank}
\end{convention}
至于什么时候加入空格, 其实没什么具体原则, 不同的人意见不一. 同学们可以参考本笔记, Fortran \href{https://fortran-lang.org/}{官网}上的代码示例和 Python 的 \href{https://peps.python.org/pep-0008/}{PEP 8 规范}来决定是否加空格.
\subsection{缩进}\label{indent}
缩进 (indent) 是指源代码每行最前面的空格. 对 Fortran 而言, 缩进本身没有意义, 但适当的缩进能表示程序的某个结构嵌套在另一个结构里, 这能使程序更容易被理解. 来看下面这个程序.
\begin{lstlisting}
program main
implicit none
integer :: i
do i = 1, 1
if (.true.) then
print *, 'I'
end if
end do
end program main
\end{lstlisting}
在上面的程序中: 第 1 行到第 9 行是个主程序, 之间的代码缩进一层, 有 4 个空格的缩进; 第 4 行到第 8 行是个 \ref{do_construct} 小节会介绍的 \ttt{do} 结构, 嵌套在主程序里, 之间的代码又缩进一层, 有 8 个空格的缩进; 第 3 行到第 7 行是个 \ref{if_construct} 小节会介绍的 \ttt{if} 结构, 嵌套在 \ttt{do} 结构里, 之间的代码又又缩进一层, 有 12 个空格的缩进.
\begin{convention}
在程序中加入适当的缩进, 以 $4$ 个空格为单位.\label{fortran_indent}
\end{convention}
``以 4 个空格为单位'', 是指保持缩进为 4 个空格, 或 8 个空格, 或 12 个空格, 以此类推. 有些人可能更喜欢以 8 个空格为单位的缩进, 如果他们的电脑屏幕比较宽, 不怕代码跑到屏幕右侧以外的话. 比如, 上面的程序如果缩进改以 8 个空格为单位的话, 就会变成下面的程序.
\begin{lstlisting}
program main
implicit none
integer :: i
do i = 1, 1
if (.true.) then
print *, 'I'
end if
end do
end program main
\end{lstlisting}
请注意, 在文本编辑器中按 [Tab] 键, 也会出现一长串 ``空格'', 但实际上对电脑而言, 按 [Tab] 键和按一堆空格, 效果是不同的. 智能的文本编辑器, 比如 VS Code, 可能会自动把 [Tab] 转换为适当数目的空格, 但这不保险. 一些经典的优秀编辑器, 比如 Vim, 是会严格区分 [Tab] 和空格的\footnote{
这并不见得是坏事, 因为有些特殊文件是必须使用 [Tab] 的.
}, 除非另装外挂. 编译器可能会允许在程序中使用 [Tab] 键, 但这也不保险. 总而言之, 使用 [Tab] 键, 实在不清楚会不会出问题.
\begin{convention}
不要使用\emph{[Tab]}键.
\end{convention}
在 VS Code 中, 把鼠标移到右下角铃铛图标按键左边第四个, 鼠标移到后会显示 ``Select Indentation'' 的按键上, 点击之, 然后弹出的菜单下拉选择 ``Indent Using Spaces'', 再选择 ``4'', VS Code 便会智能添加以 4 个空格为单位的缩进了.
\subsection{空行}\label{empty_line}
为使得程序更容易被理解, 还要在程序中加入适当的空行, 以把程序划分成多个部分. 我们可以给上面 \ref{indent} 小节的程序加一个空行, 变成下面的程序.
\begin{lstlisting}
program main
implicit none
integer :: i
do i = 1, 1
if (.true.) then
print *, 'I'
end if
end do
end program main
\end{lstlisting}
学第\ref{fortran_intrinsic_type}章后同学们可知, 在上面的程序中: 空行以上的部分可以称为 ``说明部分''; 空行以下的部分可以称为 ``执行部分''. 还常用双空行和单空行来进一步划分. 比如下面这个程序.
\begin{lstlisting}
program main
implicit none
integer :: m, n
do m = 1, 1
if (.true.) then
print *, 'M'
end if
end do
do n = 1, 1
if (.true.) then
print *, 'N'
end if
end do
end program main
\end{lstlisting}
在上面的程序中: 双空行划分 ``说明部分''和 ``执行部分''; 单空行划分 ``执行部分''中的各部分.
\begin{convention}
在程序中加入适当的空行.
\end{convention}
至于什么时候加入一个或几个空行, 其实没什么具体原则, 完全看个人心情. 简单原则就是, 如果读程序长长的一部分, 感觉头昏脑涨, 或觉得以后肯定会头昏脑涨, 插入空行来把这部分划分成各自能完成一件事的几部分就是不错的选择.
\subsection{注释}
程序每一行中, \ttt{!} 及后面的所有内容都会被无视 (例外见 \ref{fortran_char} 节和 \ref{character_string_edit_descriptor} 小节), 也就相当于 \ttt{!} 及后面的内容不存在. 会被无视的 \ttt{!} 及后面的内容称为注释 (comment). 例如下面两个程序是完全一样的.
\begin{lstlisting}
program main
! _oo0oo_
! o8888888o
! 88" . "88
! (| -_- |)
! 0\ = /0
! ___/`---'\___
! .' \\| |// '.
! / \\||| : |||// \
! / _||||| -:- |||||- \
! | | \\\ - /// | |
! | \_| ''\---/'' |_/ |
! \ .-\__ '-' ___/-. /
! ___'. .' /--.--\ `. .'___
! ."" '< `.___\_<|>_/___.' >' "".
! | | : `- \`.;`\ _ /`;.`/ - ` : | |
! \ \ `_. \_ __\ /__ _/ .-` / /
! =====`-.____`.___ \_____/___.-`___.-'=====
! `=---='
implicit none
print *, 'No bugs forever!' ! The Buddha bless us!
end program main
\end{lstlisting}
\begin{lstlisting}
program main
implicit none
print *, 'No bugs forever!'
end program main
\end{lstlisting}
注释看起来没什么用, 其实用处大极了, 主要功能就是在代码中插入 ``笔记'', 方便读代码的人理解代码在干什么. 比如下面这个程序, 其实是算 $10$ 的阶乘, 但是代码本身只能表示 ``从 $1$ 乘到 $10$'' 的意思, 要理解是算 $10$ 的阶乘, 还要费点脑子, 而加上个注释, 就能一眼看出代码是在算 $10$ 的阶乘了 (除非英语看不懂\dots{}).
\begin{lstlisting}
program main
implicit none
integer :: i, f
! Calculate f == factorial(10).
f = 1
do i = 1, 10
f = f * i
end do
print *, f
end program main
\end{lstlisting}
因为经常有偷懒不加注释, 结果写代码的人自己过几天都看不懂自己之前写的代码的事情发生, 人们总是强调要写注释. 但这种说法有偏颇之处, 因为有些代码自己足以表达意思, 根本没必要加注释. 比如下面这个程序, 注释就是废话, 因为看代码本身就能一眼看出代码是在算 $\tan 0$.
\begin{lstlisting}
program main
use iso_fortran_env, only: dp => real64
implicit none
! Calculate tan(0).
print *, tan(0.0_dp)
end program main
\end{lstlisting}
当然, 如果代码自己不足以表达意思, 还是尽可能加上注释吧, 偷懒可是要遭报应的呀!
\begin{convention}
在代码本身不足以表明代码含义的时候尽可能加入注释以说明.
\end{convention}
因为注释会被忽略, 所以规范 \ref{lower_case_convention} 我们可以无视. 同学们乃华夏儿女, 看来都很希望能在注释里用中文, 然而用中文可能导致编译器不能干活, 还是用英文吧! (尤其是代码要写给外国人看的时候)
\subsection{续行}
写代码的时候, 我们不希望一行里有过多的字符, 因为代码会跑到屏幕右侧以外, 不方便阅读.
\begin{convention}
程序每一行不应超过 $80$ 个字符.\label{fortran_no_more_than_80}
\end{convention}
有些人可能会放宽到不超过 $120$ 个字符, 配合以 8 个空格为单位的缩进和宽屏电脑.
用 VS Code 的话, 可以在 \ttt{settings.json} 里加入下面这两个键值对.
\begin{verbatim}
"editor.wordWrap": "wordWrapColumn",
"editor.wordWrapColumn": 80
\end{verbatim}
这样如果一行超过 80 个字符, 就会分行显示. 注意, 只是分行显示, 这一行还是超过 80 个字符的 (可以看到 ``分行'' 后下面一 ``行'' 是没有行号的). 加入这两个键值对只是起提醒作用, 太长还是要分行写的 (照顾那些没有像 VS Code 这样智能的编辑器的可怜人).
但有时代码一行确实写不完, 这时我们可以使用续行 (continuation). 首先我们需要介绍注释行 (comment line). 注释行就是相当于空行的行, 包括空行, 只有空格的行和除空格外只有注释的行. 而续行就是在一行的末尾加上 \ttt{\&{}}, 表示把下面第一个不是注释行的行剪切下来粘贴到这行后 (并去掉用于表示续行的 \ttt{\&{}}). 比如下面两个程序是一样的.
\begin{lstlisting}
program main
implicit none
print *, 'All Phenomena, ' &
//'are illusions.'
end program main
\end{lstlisting}
\begin{lstlisting}
program main
implicit none
print *, 'All Phenomena, '//'are illusions.'
end program main
\end{lstlisting}
如果一个续行不够, 我们还可以连着用, 像下面的程序这样.
\begin{lstlisting}
program main
implicit none
print *, 'All the Relative, are like ' &
//'dreams, illusions, ' &
//'bubbles, shadows, ' &
//'dew drops and lightning flashes: ' &
//'This is what we must believe in.'
end program main
\end{lstlisting}
我猜同学们会觉得续行的定义非常繁复难懂, 非得先定义一个 ``注释行'', 然后再巴拉巴拉, 难道不能直接定义成 ``把下面第一行剪切下来粘贴到这行后''? 还真不行. 来看下面这个程序.
\begin{lstlisting}
program main
implicit none
print *, 'All Phenomena, ' &
! Important!
//'are illusions.'
end program main
\end{lstlisting}
如果把续行定义成 ``把下面第一行剪切下来粘贴到这行后'', 上面那个程序就等同于下面这个程序.
\begin{lstlisting}
program main
implicit none
print *, 'All Phenomena, ' ! Important!
//'are illusions.'
end program main
\end{lstlisting}
这个程序无法工作. 实际上按正确定义, 上面上面那个程序等同于下面这个程序 (注释被忽略), 没有问题.
\begin{lstlisting}
program main
implicit none
print *, 'All Phenomena, '//'are illusions.'
end program main
\end{lstlisting}
续行有许多需要补充说明的. 首先注释相比续行是优先的, 所以下面这个程序不行, 因为 \ttt{\&{}} 在 \ttt{!} 后, 被当成注释了 (笔记里显示成斜体 \emph{\ttt{\&{}}}).
\begin{lstlisting}
program main
implicit none
print *, 'All Phenomena, ' ! --- The Buddha &
//'are illusions.'
end program main
\end{lstlisting}
写成下面这样才行, 注释被删去后可以成功续行.
\begin{lstlisting}
program main
implicit none
print *, 'All Phenomena, ' & ! --- The Buddha
//'are illusions.'
end program main
\end{lstlisting}
\ref{fortran_char} 节和 \ref{character_string_edit_descriptor} 小节中还有其他续行失效的例子.
乱用续行搞幺蛾子可不行. 来看下面这个程序. 这个程序没有错, 但让人看得晕乎.
\begin{lstlisting}
program &
main
implicit &
none
print &
* &
, &
'All Phenomena, '//'are illusions.'
end &
program &
main
\end{lstlisting}
为了不晕乎, 我们首先要尽量少用续行 (把 \ttt{program main} 拆成两行简直是犯罪), 其次我们不应该在不应加空格的地方用续行 (比如在 \ttt{*,} 之间用续行拆成两半).
\begin{convention}
只在不用续行就无法遵守规范 \emph{\ref{fortran_no_more_than_80}} 的时候使用续行.
\end{convention}
\begin{convention}
不在不应加空格的地方使用续行.
\end{convention}
有同学可能会突然发现不对, 本章的许多程序不符合规范 \ref{fortran_indent}, 例如本章的第一个程序. 其实用续行的时候, 规范 \ref{fortran_indent} 可以不遵守. 本章的第一个程序采用的那种缩进方式, 是让下一行和上一行 ``内容对齐'', 这也是续行时常用的方式. 具体到程序, 就是让 \ttt{*,} 所在的行的下一行的第一个非空格字符, 和 \ttt{*,} 之后的第一个非空格字符对齐. 不过续行时的缩进还有其他方式. 同学们可以参考本笔记, Fortran \href{https://fortran-lang.org/}{官网}上的代码示例和 Python 的 \href{https://peps.python.org/pep-0008/}{PEP 8 规范}来决定续行时如何缩进.
续行还有一个冷门规则, 在程序的每一行中, 都不能除空格和注释外, 单单只有一个用于续行的 \ttt{\&{}}. 比如下面这个程序是不行的. 然而, Ifx 和 Gfortran 却认为这个程序没问题, 同学们遇到器规啦!
\begin{lstlisting}
program main
implicit none
print *, 'All Phenomena, '//'are illusions.'
& ! --- The Buddha
end program main
\end{lstlisting}
至于为什么有这么个怪怪规则, 我猜是由于其他我不想讲的续行规则, 如果不定这个规则, 代码就可能会出现歧义. 而为什么那些续行规则我不想讲, 是因为那些规则会让同学们不遵守规范搞幺蛾子, 写出读不懂的程序来.
\subsection{标号}
除后面会说的例外情况外, 在程序中的每一行的开头都可以加上标号 (label). 标号是用不超过五个数字表示的 1--99999 的整数, 开头可以是 0, 后面需要跟至少一个空格. 习惯上使用四位数的, 整千或整百的标号. 下面的程序第三行使用了标号.
\begin{lstlisting}
program main
implicit none
1000 print *, 'label'
end program main
\end{lstlisting}
显然在一个程序单元内不能有重复的标号, 比如下面这个程序不成 (两个标号都是 9999).
\begin{lstlisting}
program main
implicit none
09999 print *, '09999'
9999 print *, '9999'
end program main
\end{lstlisting}
注释行不被允许加标号, 原因可能和续行一样, 也是会有歧义 (但这回 Ifx 和 Gfortran 的器规也是不允许). 另外续行符 \ttt{\&{}} 所在的行的下面第一个不是注释行的行, 也就是 ``用于续行的行'', 也不被允许加标号, 因为 ``用于续行的行'' 其实只是续行符 \ttt{\&{}} 所在的行的后面部分, 它的开头其实不是一行的开头. 以上规则的反例如下.
\begin{lstlisting}
program main
implicit none
print *, 'All Phenomena, '//'are illusions.'
1000 ! --- The Buddha
end program main
\end{lstlisting}
\begin{lstlisting}
program main
implicit none
print *, 'All Phenomena, ' &
! Important!
1000 //'are illusions.'
end program main
\end{lstlisting}
标号是 Fortran 老得要死的语法, 不是个好东西. 首先, 标号只是个数字, 没法儿给所在的那行是干什么用的提供任何有效信息. 更要命的是, 使用标号写出来的程序极可能不符合结构化原则. 请同学们根据规范 \ref{no_no_structured} 少用标号.
\subsection{标签}
在程序中某些特殊的地方 (见 \ref{fortran_exit} 小节和 \ref{fortran_cycle} 小节) 可以加标签\footnote{\href{https://j3-fortran.org/doc/year/24/24-007.pdf}{标准解释文档}里没这个词, 但\href{https://fortran-lang.org/learn/quickstart/}{官网教程}里有.} (tag). 标签用其标签名表示. 下面的程序第四行和第六行使用了标签名为 \ttt{tag} 的标签.
\begin{lstlisting}
program main
implicit none
integer :: i
tag: do i = 1, 1
print *, 'tag'
end do tag
end program main
\end{lstlisting}
显然在一个程序单元内也不能有重复的标签, 比如下面这个程序不成.
\begin{lstlisting}
program main
implicit none
integer :: i, j
tag: do i = 1, 1
tag: do j = 1, 1
print *, 'tag'
end do tag
end do tag
end program main
\end{lstlisting}
标签不像标号, 是好东西. 同学们可以尽情使用.