-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathassignment_and_operation.tex
More file actions
442 lines (385 loc) · 41.2 KB
/
assignment_and_operation.tex
File metadata and controls
442 lines (385 loc) · 41.2 KB
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
\chapter{赋值与运算}
\section{赋值}\label{fortran_assignment}
Fortran 中的赋值 (assignment), 其一般含义可总结为把数据拷贝到一个数据实体中, 分为四种: (普通的) 固有赋值 (intrinsic assignment), 掩码数组赋值 (masked array assignment), 指针赋值 (pointer assignment), 和超载赋值 (defined assignment). 本节只讲固有赋值的最简单情形, 固有赋值的其他情形一和掩码数组赋值一起放在 \ref{fortran_array_assignment} 节讲, 固有赋值的其他情形二和指针赋值一起放在 \ref{fortran_pointer_assignment} 节讲, 超载赋值放在 \ref{fortran_defined_assignment} 节讲.
固有赋值的形式是 \ttt{[var] = [expr]}. \ttt{[var]} 可以是很多东东, 最常见的情况是 \ttt{[var]} 是一个变量, 其他情况安安将一一列举. \ttt{[expr]} 是一个表达式 (expression), 放在 \ref{fortran_opration} 节详细介绍. 运行 \ttt{[var] = [expr]} 时, 程序会先对 \ttt{[expr]} 进行求值 (evaluation) 并得到一个结果 (result), 此结果是一个数据实体, 然后将得到的结果中的数据拷贝到 \ttt{[var]} 里去. 下面的例子中, 第4行对表达式 \ttt{10.0} 进行求值, 得到的结果中的数据 $10.0$ 拷贝到 \ttt{a} 里去, 第5行对表达式 \ttt{a + a*a - a/a} 进行求值, 得到的结果中的数据 $109.0$ 还拷贝到 \ttt{a} 里去, 所以最后 \ttt{a} 里的数据是 $109.0$.
\begin{lstlisting}
program main
implicit none
real :: a
a = 10.0
a = a + a*a - a/a
print *, a
end program main
\end{lstlisting}
为避免后面讲解时啰里巴嗦的, 安安需要先啰嗦几句. 如果数据实体中有 Fortran 程序拷贝到其中存储的数据, 安安就称数据实体中存储的数据为数据实体的值 (value), 例如上面的程序第4行运行后 \ttt{a} 的值是 $10.0$. 如果数据实体的值和一个字面常量的值相同, 安安也称数据实体的值为那个字面常量, 例如上面的程序第4行运行后 \ttt{a} 的值是 \ttt{10.0}. 安安还把对表达式进行求值得到的结果的值简称为表达式的值, 例如上面的程序第5行运行时表达式 \ttt{a + a*a - a/a} 的值是 \ttt{109.0}.
因为赋值会修改数据实体的值, 所以字面常量是不能被赋值的, 例如下面这个程序是不成的.
\begin{lstlisting}
program main
implicit none
10.0 = 10.0
end program main
\end{lstlisting}
而具名常量也是不能被赋值的, 这就出现了具名常量的值如何确定的问题. 具名常量的值需要且必须由初始化 (initialization) 确定, 所谓初始化实际上就是在声明的同时赋值\footnote{这话儿其实不太对, 因为按\href{https://j3-fortran.org/doc/year/24/24-007.pdf}{标准解释文档}的定义, 初始化和赋值是不同的东东, 但通常人们都会觉得初始化就是在声明的同时赋值, 同学们就这么认为拉倒吧!}, 方法是在声明时在具名常量名 \ttt{[name]} 后加 \ttt{ = [value]}, 其中 \ttt{[value]} 是常量. 下面这个程序声明了具名常量 \ttt{ten}, 并令其值为 \ttt{10.0}
\begin{lstlisting}
program main
implicit none
real, parameter :: ten = 10.0
end program main
\end{lstlisting}
如果是声明字符型具名常量, 那么初始化时还有个长度匹配的问题. 我们可以在声明时把长度写成 \ttt{*}, 这样字符型具名常量的长度直接由字面常量 \ttt{[value]} 确定为 \ttt{[value]} 的长度, 非常方便. 下面这个程序声明了具名常量 \ttt{author}, 它的长度直接由字面常量 \ttt{'GasinAn'} 确定为 $7$.
\begin{lstlisting}
program main
implicit none
character(*), parameter :: author = 'GasinAn'
print *, author, len(author)
end program main
\end{lstlisting}
另外, 如果一个变量在程序运行的时候第一次被赋值, 我们也称这个变量被初始化, 而如果一个变量在程序运行的时候没被赋值, 我们则称这个变量是未定义的. 在下面这个程序中, \ttt{a} 有被初始化, 而 \ttt{b} 没被初始化, 从始至终一直是未定义的.
\begin{lstlisting}
program main
implicit none
real :: a, b
a = 0.0
print *, a, b
end program main
\end{lstlisting}
但同学们会发现上面这个程序运行到最后, \ttt{a} 和 \ttt{b} 里头都有数据. 其中, \ttt{a} 里头的数据是我们用程序清清楚楚地指挥电脑赋给 \ttt{a} 的有意义的数据, 而 \ttt{b} 里头的数据是我们用程序指挥电脑创建 \ttt{b} 的时候 \ttt{b} 会随机拥有的无意义的数据. 如果一个变量是未定义的, 存在那个变量中的数据是无意义的, 那么取用那个变量中的数据也是无意义的, 程序必然有毛病. 然而 Fortran 和 C 这样古早的语言, 它们的编译器不一定会检查变量在取用时是否未定义, 程序因此容易出现检查不出的毛病, 同学们只好自己小心. Python 吸取教训, 不存在这个问题, 例如在 Python 3 中, 如果 \ttt{b} 未定义, 那么 \ttt{print(b)} 会直接报错.
当我们要用延迟长度字符型变量时, 我们本可能需要经常使用 allocate 语句和 deallocate 语句, 像下面的程序那样, 非常麻烦.
\begin{lstlisting}
program main
implicit none
character(:), allocatable :: char
allocate(character(len('allocated')) :: char)
char = 'allocated'
print *, char, len(char), len('allocated')
deallocate(char)
allocate(character(len('reallocated')) :: char)
char = 'reallocated'
print *, char, len(char), len('reallocated')
end program main
\end{lstlisting}
Fortran 为我们简化了语法. 我们可以直接对延迟长度字符型变量进行赋值. 如果延迟长度字符型变量 \ttt{[var]} 的长度未定, 那么 \ttt{[var] = [expr]} 时电脑会自动用 allocate 语句把 \ttt{[var]} 的长度定成 \ttt{[expr]} 的长度. 如果延迟长度字符型变量 \ttt{[var]} 的长度已定但不等于 \ttt{[expr]} 的长度, 那么 \ttt{[var] = [expr]} 时电脑会自动用 deallocate 语句让 \ttt{[var]} 的长度未定, 然后再用 allocate 语句把 \ttt{[var]} 的长度定成 \ttt{[expr]} 的长度. 所以上面的程序可简化成下面的程序, 非常方便.
\begin{lstlisting}
program main
implicit none
character(:), allocatable :: char
char = 'allocated'
print *, char, len(char), len('allocated')
char = 'reallocated'
print *, char, len(char), len('reallocated')
end program main
\end{lstlisting}
Fortran 是强类型语言 (strongly typed language), 因为 Fortran 程序赋值时用于赋值的和被赋值的数据实体的类型要一致, 数字型数据实体只能赋值给数字型数据实体, 字符型数据实体只能赋值给字符型数据实体, 逻辑型数据实体只能赋值给逻辑型数据实体. 不过官规虽是这么说, Ifx 和 Gfortran 却会在某些情况下允许数字型数据实体和逻辑型数据实体相互赋值, 根据规范 \ref{use_standard_only}, 同学们不许这么做.
数字型数据实体赋值给数字型数据实体的时候, ``\ttt{=}'' 两边的类型或种别可能不一样, 这时自然有个类型和种别转化的问题. 因为 Fortran 是静态类型语言, 所以赋值时 ``\ttt{=}'' 左边数据实体的类型和种别不会变, 而右边数据实体的类型和种别会\uline{临时}转化成左边数据实体的类型和种别. 安安努力查阅\href{https://j3-fortran.org/doc/year/24/24-007.pdf}{标准解释文档}后, 把 Fortran 数字型数据实体类型和种别转化的规则总结成表 \ref{numeric_type_convert}, 其中的术语解释如下:
\begin{itemize}
\item 值相等: 转化后的值和转化前的值相等.
\item 值近似: 转化后的值和转化前的值近似相等, 如何近似则由编译器自己决定.
\item 实部近似虚部0: 转化后的值的实部和转化前的值的实部近似相等, 转化后的值的虚部为 $0$, 如何近似则由编译器自己决定.
\item 向0取整: 转化后的值由转化前的值向 $0$ 取整而成. 即转化后的值的绝对值是不大于转化前的值的绝对值的最大的整数, 转化后的值的符号和转化前的值的符号相同.
\item 二次转化: 先转化成实型, 再转化成整型.
\end{itemize}
\begin{table}[htbp]
\centering
\begin{tabular}{|c|c|c|c|}
\hline
\diagbox{转化前}{转化后} & 整型 & 实型 & 复型 \\
\hline
整型 & 值相等 & 值近似 & 实部近似虚部0 \\
\hline
实型 & 向0取整 & 值近似 & 实部近似虚部0 \\
\hline
复型 & 二次转化 & 实部近似虚部0 & 值近似 \\
\hline
\end{tabular}
\caption{Fortran 数字型数据实体转化表}
\label{numeric_type_convert}
\end{table}
同学们莫要被这表吓破了胆, 我们可以简化记忆. 首先, 编译器们在自己决定 ``如何近似''的时候, 总是尽可能地近似 (敢不如此我们就卸载), 以至于引入的计算误差没人考虑, 因此我们可以把所有的 ``近似相等'' 直接当成 ``相等''. 这样, 表 \ref{numeric_type_convert} 就简化成表 \ref{simplified_numeric_type_convert}.
\begin{table}[htbp]
\centering
\begin{tabular}{|c|c|c|c|}
\hline
\diagbox{转化前}{转化后} & 整型 & 实型 & 复型 \\
\hline
整型 & 值相等 & 值相等 & 值相等 \\
\hline
实型 & 向0取整 & 值相等 & 值相等 \\
\hline
复型 & 二次转化 & 实部近似虚部0 & 值相等 \\
\hline
\end{tabular}
\caption{简化 Fortran 数字型数据实体转化表}
\label{simplified_numeric_type_convert}
\end{table}
最后同学们会发现只需记下面的内容即可:
\begin{itemize}
\item 实型转整型: 向0取整.
\item 复型转实型: 取实部.
\item 复型转整型: 先转实型, 再转整型.
\item 其他: 直接令值相等.
\end{itemize}
我们用一个示例来强化一下认识. 在下面的程序中, 第7行应使 \ttt{b} 的值为 $-5.5$, 然后第8行需要类型转化. 看 ``\ttt{=}'' 右边 \ttt{b} 是实型, 左边 \ttt{a} 是整型, 实型转整型向0取整, $-5.5$ 向0取整是 $-5$, 所以 \ttt{a} 的值为 $-5$. 但这里同学们常犯一个错误, 就是认为第8行后 \ttt{b} 的值也变为 $-5$ 了, 而实际上右边的类型和种别是\uline{临时}转化成左边的类型和种别, 所以第8行后 \ttt{b} 的类型和种别不会变, 值也不会变, 还是 $-5.5$.
\begin{lstlisting}
program main
implicit none
integer :: a
real :: b
b = -5.5
a = b
print *, a, b
end program main
\end{lstlisting}
默认情形下 Fortran 和 C 的的类型和种别转化是 ``隐式'' 的转化, 意思是, 在类型和种别转化的时候 Fortran 程序和 C 程序里没有一个明确的标记来说明, 需要仔细分析各个数据实体的类型和种别然后判断出有转化. 于是乎老师就可以借机整出一堆怪题来疯狂折磨同学们了, 并且实际人们用 Fortran 和 C 干活的时候, 也很容易没看出程序某某地方有类型和种别转化. Python 没有这个问题, 因为 Python 不是静态类型语言.\footnote{详细说来, Fortran 和 C 因为是静态类型语言, ``\ttt{=}'' 左边的类型和种别不能变, 所以赋值时是 ``右配合左'', 把 ``\ttt{=}'' 右边的类型和种别 (临时) 变成左边的类型和种别, 这样在实型转整型, 复型转实型, 复型转整型的时候, ``\ttt{=}'' 左右两边的值可能是不一样的. 而 Python 因为不是静态类型语言, ``\ttt{=}'' 左边的类型和种别能变, 所以赋值时是 ``左配合右'', 把 ``\ttt{=}'' 左边的类型和种别变成右边的类型和种别, 这样 ``\ttt{=}'' 左右两边的值永远是一样的, 不会折磨人, 所以 Python 大家都爱用. 然而这并不意味着 Python 无敌了, 正因为 Python ``\ttt{=}'' 左边的类型和种别能变, 所以电脑在运行 Python 程序的时候老是要查看变量的类型和种别到底是什么, 这样运行 Python 程序的时候就快不起来了\dots{}} 这样就有必要在 Fortran 程序和 C 程序里使用 ``显式'' 的转化. 对 Fortran 而言, 在实型转整型, 复型转实型, 复型转整型的时候, 转化前后的值可能会不一样, 我们应当用 \ttt{int([entity])}/\ttt{real([entity])} 来说明数据实体 \ttt{[entity]} 被转化成整型/实型. 其他情形下, 转化前后的值一样, 就没必要啰嗦了. 例如上面的程序应该改写成下面的程序, 这样看到明确的标记 \ttt{int} 就可以直接判断出 \ttt{b} 被转化成整型了. 仍要注意类型和种别转化只是\uline{临时}的, 第8行后 \ttt{b} 的值还是 $-5.5$.
\begin{lstlisting}
program main
implicit none
integer :: a
real :: b
b = -5.5
a = int(b)
print *, a, b
end program main
\end{lstlisting}
\begin{convention}
在实型转整型, 复型转实型, 复型转整型的时候, 用 \ttt{\emph{int([entity])}} /\ttt{\emph{real([entity])}} 来说明数据实体 \ttt{\emph{[entity]}} 被转化成整型/实型.
\end{convention}
字符型数据实体赋值给字符型数据实体的时候, ``\ttt{=}'' 两边的长度可能不一样, 这时自然有个长度转化的问题. 如果 ``\ttt{=}'' 左边字符串的长度小于右边字符串的长度, 则\uline{临时}把右边字符串尾巴多出的部分砍掉, 然后赋值. 如果 ``\ttt{=}'' 左边字符串的长度大于右边字符串的长度, 则\uline{临时}在右边字符串尾巴处补上空格让长度一样, 然后赋值. 示例如下. 仍要注意长度转化只是\uline{临时}的.
\begin{lstlisting}
program main
implicit none
character(4) :: sc
character(5) :: nc
character(6) :: lc
sc = 'hello'
print *, '"', sc, '"'
nc = 'hello'
print *, '"', nc, '"'
lc = 'hello'
print *, '"', lc, '"'
end program main
\end{lstlisting}
最后介绍 ``对变量的一部分赋值''. 对任意复型变量 \ttt{[z]}, 我们可以直接对 \ttt{[z]\%{}re}/\ttt{[z]\%{}im} 赋值以改变 \ttt{[z]} 的实部/虚部 而不改变 \ttt{[z]} 的虚部/实部. 示例如下.
\begin{lstlisting}
program main
implicit none
complex :: i
i%re = 0.0
i%im = 1.0
print *, i
end program main
\end{lstlisting}
在 \ref{fortran_complex} 节我们介绍过可以用 \ttt{real([z])}/\ttt{aimag([z])} 获取 \ttt{[z]} 的实部/虚部, 但 \ttt{real([z])}/\ttt{aimag([z])} 不能用在 ``\ttt{=}'' 左边. \ttt{[z]\%{}re}/\ttt{[z]\%{}im} 虽然可能可以用在 ``\ttt{=}'' 右边, 但同学们去用的时候, 程序就经常会报错, 这是由于 Fortran 的语法有缺陷, 而在 ``\ttt{=}'' 右边用 \ttt{real([z])}/\ttt{aimag([z])} 就永远不会出问题. Python 的语法简洁优美, 类似 \ttt{[z]\%{}re}/\ttt{[z]\%{}im} 的语法用在 ``\ttt{=}'' 左右两边都没问题, 而类似 \ttt{real([z])}/\ttt{aimag([z])} 的语法只能用在 ``\ttt{=}'' 右边. C 就麻烦了, 复型都不是基本类型, 所以用复型的时候老费劲儿了\dots{}
对任意字符型变量 \ttt{[c]}, 我们可以直接对 \ttt{[c](nl:nu)} 赋值以改变 \ttt{[c]} 的第 \ttt{nl} 到第 \ttt{nu} 个字符而不改变其他字符. 这个语法和字符串切片的语法在形式上是一样的. \ttt{nl} 和 \ttt{nu} 必须是整型数据实体. \ttt{nl} 和 \ttt{nu} 都可以不写. 如果 \ttt{nl} 不写, \ttt{nl} 就等于 \ttt{1}. 如果 \ttt{nu} 不写, \ttt{nu} 就等于 \ttt{[c]} 的长度. 示例如下. 这个示例需要解释一下, 第5行我们把 \ttt{'HELLO, WORLD!'} 赋给 \ttt{hello\_{}world} 的第 \ttt{1} 到第 \ttt{1} 个字符, 但 \ttt{hello\_{}world} 的第 \ttt{1} 到第 \ttt{1} 个字符长度只为 $1$, 所以根据字符串赋值规则, \ttt{'HELLO, WORLD!'} 的尾巴被砍掉, 只剩最前面的 \ttt{'H'} 赋给\ttt{hello\_{}world} 的第 \ttt{1} 到第 \ttt{1} 个字符.
\begin{lstlisting}
program main
implicit none
character(13) :: hello_world
hello_world = 'hello, world!'
hello_world(1:1) = 'HELLO, WORLD!'
print *, hello_world
end program main
\end{lstlisting}
Python 的语法简洁优美, 但 Python 不允许直接修改字符串的一部分, 同学们需另寻他法. C 就可怕了, 详细说来, C 的字符串有两种定义法, 方法一定义的字符串能直接修改整个字符串但不能直接修改字符串中的单个字符, 方法二定义的字符串能直接修改字符串中的单个字符但不能直接修改整个字符串\dots{}
但是, 这里有个小坑. 来看下面这个程序. 在这个程序中, 我们对延迟长度字符型变量 \ttt{char} 的一部分 \ttt{char(:)} 赋值. 我们会期待电脑将自动用 allocate 语句和 deallocate 语句来帮我们定好 \ttt{char} 的长度, 然而, 因为语法上 ``\ttt{=}'' 左边不是 \ttt{char}, 而是 \ttt{char} 的一部分 \ttt{char(:)}, 所以电脑\uline{不会}自动用 allocate 语句和 deallocate 语句来帮我们定好 \ttt{char} 的长度 (即使 \ttt{char(:)} 代表 \ttt{char} ``从头到尾的一部分'', 和 \ttt{char} 本身是相当的). 这个程序 Gfortran 运行时会报错, Ifx 不会报错, 但运行后屏幕上出现的结果怪怪的, 原因应该是 Ifx 在 \ttt{char} 长度未被确定过的情况下, 会自己认定 \ttt{char} 的长度是 $0$, 然后在赋值时又根据赋值规则, 把 \ttt{'allocated'} 和 \ttt{'deallocated'} 的尾巴砍掉, 但 \ttt{char} 的长度是 $0$, 所以砍完就变成 \ttt{''} 了\dots{}
\begin{lstlisting}
program main
implicit none
character(:), allocatable :: char
char(:) = 'allocated'
print *, char, len(char), len('allocated')
char(:) = 'reallocated'
print *, char, len(char), len('reallocated')
end program main
\end{lstlisting}
再来两个例子让同学们把这个问题弄清楚. 在下面这个例子中, 第4行 \ttt{char} 的长度被定了, 然后第6行和第8行 ``\ttt{=}'' 左边是 \ttt{char}, 这时遵循简化赋值语法, ``\ttt{=}'' 左边字符串的长度被重定, 来和 ``\ttt{=}'' 右边字符串的长度一致.
\begin{lstlisting}
program main
implicit none
character(:), allocatable :: char
char = '******'
print *, char, len(char), len('******')
char = 'allocated'
print *, char, len(char), len('allocated')
char = 'reallocated'
print *, char, len(char), len('reallocated')
end program main
\end{lstlisting}
而在下面这个例子中, 第4行 \ttt{char} 的长度被定了, 然后第6行和第8行 ``\ttt{=}'' 左边是 \ttt{char(:)}, 这时不遵循简化赋值语法, ``\ttt{=}'' 左边字符串的长度不被重定, 而``\ttt{=}'' 右边字符串被砍尾巴/补空格来和 ``\ttt{=}'' 左边字符串的长度一致.
\begin{lstlisting}
program main
implicit none
character(:), allocatable :: char
char = '******'
print *, char, len(char), len('******')
char(:) = 'allocated'
print *, char, len(char), len('allocated')
char(:) = 'reallocated'
print *, char, len(char), len('reallocated')
end program main
\end{lstlisting}
\section{运算}\label{fortran_opration}
表达式由操作数 (operand), 运算符 (operator), 和左小括号 ``\ttt{(}'' 与右小括号 ``\ttt{)}'' 组成. 同学们其实平常就经常见到表达式, 最常见的就是代数式了, 例如代数式 $(a+b)\times(a-b)$, 其中 $a$ 和 $b$ 就是操作数, 而 $+$ 和 $\times$ 则是运算符. Fortran 中所有操作数都是数据实体\footnote{这话儿其实不太对, 因为按\href{https://j3-fortran.org/doc/year/24/24-007.pdf}{标准解释文档}的定义, 有些操作数不被定义成数据实体, 但这些操作数和数据实体也没什么区别, 同学们就直接认为是数据实体拉倒吧!}, 所有数据实体都可以是操作数, 而表达式本身也是数据实体, 可以作为操作数出现在更大的表达式中. 同学们其实平常也经常见到这种情况, 例如在 $(a+b)\times(a-b)$, $a+b$ 和 $a-b$ 就是作为表达式的操作数.
运算符总是代表运算 (operation), 例如代数中运算符 $+$ 代表加法运算, 而运算符 $\times$ 代表乘法运算. Fortran 中的运算分为固有运算 (intrinsic operation) 和超载运算 (defined operation), 本节只讲固有运算, 超载运算放在 \ref{fortran_defined_operation} 节讲. 固有运算又分为四种: 算数运算(numeric operation), 字符运算(character operation), 逻辑运算(logical operation), 关系运算(relational operation).
如果运算符 \ttt{[op]} 把一个数据实体 \ttt{[q]} 变成另一个数据实体 \ttt{[op][q]}, 我们就称运算符 \ttt{[op]} 为一个一元运算符 (unary operator). 如果运算符 \ttt{[op]} 把两个数据实体 \ttt{[q1]} 和 \ttt{[q2]} 变成另一个数据实体\ttt{[q1][op][q2]}, 我们就称运算符 \ttt{[op]} 为一个二元运算符 (binary operator). 同学们平常也经常见到一元运算符和二元运算符, 例如在 $a-(-b)$ 中, 括号里的 $-$ 把 $b$ 变成 $-b$, 所以是一元运算符, 而括号外的 $-$ 把 $a$ 和 $(-b)$变成 $a-(-b)$, 所以是二元运算符.
在代数式中运算符有优先级, 例如在 $a+b\times c$ 中, $\times$ 比 $+$ 优先, 所以结果是 $a+(b\times c)$ 而不是 $(a+b)\times c$. Fortran 中的运算符也是如此, 在形如 \ttt{[q1][op12][q2][op23][q3]} 的表达式中, 如果 \ttt{[op12]} 和 \ttt{[op23]} 有优先级高低之分, 则优先级高的先算优先级低的后算, 如果 \ttt{[op12]} 和 \ttt{[op23]} 没有优先级高低之分, 则一般是和平常一样, 左边的 \ttt{[op12]} 先算右边的 \ttt{[op23]} 后算, 但有一个反例后面会讲.
在代数式中可以加括号, 来强行让括号里的表达式先算, 例如在 $(a+b)\times c$ 中是 $a+b$ 先算. Fortran 中也是如此, 但要注意, Fortran 中 (和几乎所有编程语言中) 都只能加小括号来强行让括号里的表达式先算, 代数式里的中括号和大括号在 Fortran 中都得用小括号替代.
大致说来, 如果一个表达式里只有常量, 这个表达式就是一个常量表达式 (constant expression). 之所以说 ``大致说来'', 是因为\href{https://j3-fortran.org/doc/year/24/24-007.pdf}{标准解释文档}里常量表达式的定义复杂至极, 但同学们不用管它. 在需要使用常量的地方改用常量表达式看来都是可以的 (安安想不出反例), 例如下面两个程序都是没有问题的.
\begin{lstlisting}
program main
use iso_fortran_env, only: l => int64
implicit none
character(1000000000000_l) :: very_long_char
end program main
\end{lstlisting}
\begin{lstlisting}
program main
use iso_fortran_env, only: l => int64
implicit none
character(10_l**12_l) :: very_long_char
end program main
\end{lstlisting}
\subsection{算数运算}\label{fortran_numeric_operator}
一元算数运算符有两个: \ttt{+}, \ttt{-}, 二元算数运算符有五个: \ttt{+}, \ttt{-}, \ttt{*}, \ttt{/}, \ttt{**}, 它们的左边右边必须是数字型数据实体. 如果数据实体 \ttt{[a]} 的值为 $a$, 数据实体 \ttt{[b]} 的值为 $b$, 那么 \ttt{+[a]}, \ttt{-[a]}, \ttt{[a]+[b]}, \ttt{[a]-[b]}, \ttt{[a]*[b]}, \ttt{[a]/[b]}, \ttt{[a]**[b]} 的值 (在不考虑计算误差的情况下) 一般是 $+a$, $-a$, $a+b$, $a-b$, $a\times b$, $a\div b$, $a^b$, 但有重要的例外后面会讲. 因为 $\sqrt[b]{a}=a^{1/b}$, 所以就没必要给开方准备一个运算符了.
使用 \ttt{**} 的时候请注意, 如果 \ttt{[a]**[b]} 中的 \ttt{[a]} 和 \ttt{[b]} 都是实型的, 且 $a$ 是负的, 那么高中数学课告诉我们 $a^b$ ill defined, 所以程序会报错. 如果 \ttt{[a]**[b]} 中的 \ttt{[a]} 和 \ttt{[b]} 都是复型的, 那么复变函数课\footnote{哎呀, 好像同学们还没上到复变函数课!?\dots{}}告诉我们 $a^b$ 是多值的, 所以程序会算 $a^b$ 的主值.
另外 \ttt{**} 的运算顺序和其他运算符相反, 在形如 \ttt{[a]**[b]**[c]} 的表达式中, 是先算 \ttt{[b]**[c]} 而不是 \ttt{[a]**[b]}. 可以这么记: \ttt{([a]**[b])**[c]} 其实等于 \ttt{[a]**([b]*[c])}, 如果 \ttt{[a]**[b]**[c]} 是 \ttt{([a]**[b])**[c]}, 因为乘法比较简单, 电脑算乘法应该比算乘方快, 所以写 \ttt{[a]**[b]**[c]} 显得太傻了, 还不如直接写 \ttt{[a]**([b]*[c])}, 所以 \ttt{[a]**[b]**[c]} 应该是 \ttt{[a]**([b]**[c])}. 但 \ttt{**} 的运算顺序和其他运算符相反这件事很容易忘了, 所以请同学们不写 \ttt{[a]**[b]**[c]} 而写 \ttt{[a]**([b]**[c])}.
\begin{convention}\label{use_parentheses}
在表达式中运算符的运算顺序容易被误解的情况下, 用括号指明运算顺序.
\end{convention}
代数式中应该只出现类似 $a-(-b)$ 之类的东东, 不出现类似 $a--b$ 之类的东东. 类似地, Fortran 表达式里不允许连着出现两个算数运算符, 因此只允许写 \ttt{[a] - (-[b])} 之类的东东, 不允许写 \ttt{[a] - -[b]} 之类的东东 (虽然 Ifx 和 Gfortran 其实不完全禁止这么写).
算数运算符的运算顺序和代数一致, 首先是 \ttt{**}, 然后是 \ttt{*} 和 \ttt{/}, 然后是一元 \ttt{+} 和 一元 \ttt{-}, 最后是二元 \ttt{+} 和 二元 \ttt{-}, 好记得很. 但同学们先别高兴得太早, 请回答 \ttt{-2.0 ** 2.0} 是多少? 如果同学们觉得是 \ttt{4.0} 就上当啦, 答案是 \ttt{-4.0}, 因为根据算数运算符的运算顺序, 是 \ttt{**} 先算 \ttt{-} 后算, 所以即使 \ttt{-2.0 ** 2.0} 里 \ttt{-} 和 \ttt{2.0} 粘在一起, 好像 \ttt{-2.0 ** 2.0} 是 \ttt{(-2.0) ** 2.0} 一样, \ttt{-2.0 ** 2.0} 也还是 \ttt{-(2.0 ** 2.0)}. 特别说明, Fortran 规定 \ttt{**} 先算 \ttt{-} 后算, 这不是在坑大家, 因为形如 $-a^b$ 的代数式本来就是 $-(a^b)$ 而不是 $(-a)^b$. 根据规范 \ref{fortran_blank}, 我们需要 ``在程序中适当地加入空格'', 在表达式中有些地方加空格好, 有些地方不加空格好, 上面的 \ttt{-2.0 ** 2.0} 就是一个典型反例 (应该写成 \ttt{- 2.0**2.0} 或 \ttt{-(2.0**2.0)}, 最不济也应该写成 \ttt{-2.0**2.0}). 但是到底应不应该加空格, 判断的原则很复杂, 而且不同的人意见不一. 同学们可以参考本笔记, Fortran \href{https://fortran-lang.org/}{官网}上的代码示例和 Python 的 \href{https://peps.python.org/pep-0008/}{PEP 8 规范}来决定是否加空格.
现在我们又要讨论烦人的类型和种别转化的问题了. 我们需要先定义类型和种别的高低. 我们定义类型从低到高依次为整型, 实型, 复型, 并定义整型种别是低种别, 实型种别是高种别. 对整型种别, 我们定义能表示的数的十进制表示的位数更少/多的种别为低/高种别, 这意味着 \ttt{intn} 的 \ttt{n} 更小/大的种别为低/高种别. 对实型种别, 我们定义能表示的数的十进制表示的精度更低/高的种别为低/高种别, 这意味着 \ttt{realn} 的 \ttt{n} 更小/大的种别为低/高种别. 以下 \ttt{[op]} 代表算符, \ttt{[q]}, \ttt{[q1]}, \ttt{[q2]} 代表数据实体.
\begin{itemize}
\item \ttt{[op][q]} 的类型/种别是 \ttt{[q]} 的类型/种别.
\item \ttt{[q1][op][q2]} 的类型/种别是 \ttt{[q1]} 和 \ttt{[q2]} 的类型/种别中更高的类型/种别; 如果 \ttt{[q1]} 和 \ttt{[q2]} 的类型/种别一样高, 那么编译器自己决定\ttt{[q1][op][q2]} 的类型/种别是 \ttt{[q1]} 和 \ttt{[q2]} 的类型/种别中的哪一个.
\item 在计算 \ttt{[q1][op][q2]} 前, \ttt{[q1]} 和 \ttt{[q2]} 的类型/种别总是被临时转化成 \ttt{[q1][op][q2]} 的类型/种别.
\end{itemize}
一言以蔽之, 运算时类型和种别转化遵循着 ``就高不就低'' 的规则.
我们已经把运算时类型和种别转化的规则简洁凝练地总结出来了, 但同学们莫要高兴, 马上同学们就会遇到一个非常传统, 非常典型, 非常折磨人的大坑, 巨坑, 奆坑, 这个坑就是两个整型数据实体相除. 来看下面这个例子.
\begin{lstlisting}
program main
implicit none
print *, 2 ** (1/4)
end program main
\end{lstlisting}
我猜同学们看到这个程序的第一眼肯定会觉得结果是 $2^{1/4}$, 然而同学们跑一跑就知道结果不是, 这是咋肥事哩? 同学们请看 \ttt{1/4}, 它的值看起来应该是 $1/4$, 然而根据类型与种别转化的规则, \ttt{1} 和 \ttt{4} 都是整型的, \ttt{1/4} 只能是整型的, 所以 \ttt{1/4} 的值不可能是 $1/4$, 好奇怪呢.
这是因为 Fortran 特别规定, 如果 \ttt{[a]} 和 \ttt{[b]} 都是整型数据实体, 那么 \ttt{[a]/[b]} 不是代表 \ttt{[a]} 除以 \ttt{[b]}, 而是代表 \ttt{[a]} \uline{整除}以 \ttt{[b]}!!! 具体说来, 如果 \ttt{[a]} 和 \ttt{[b]} 都是整型数据实体, 值分别是 $a$ 和 $b$, 那么 \ttt{[a]/[b]} 的值不是 $a\div b$ 而是 $a\div b$ 的向0取整. 所以 \ttt{1/4} 的值不是 $1/4$ , 而是 $1/4$ 的向0取整, 是 $0$, 那 \ttt{2 ** (1/4)} 当然是 \ttt{1} 了!
再来看下面这个例子.
\begin{lstlisting}
program main
implicit none
integer :: p, q
real :: r
p = 1.0
q = 4.0
r = p / q
print *, 2.0 ** r
end program main
\end{lstlisting}
首先给 \ttt{p} 和 \ttt{q} 分别赋上 \ttt{1.0} 和 \ttt{4.0}. 然而, \ttt{p} 和 \ttt{q} 都是整型的. 根据赋值时的类型转化规则, \ttt{p} 和 \ttt{q} 分别是 \ttt{1} 和 \ttt{4}.
然后计算 \ttt{p / q} 并赋给 \ttt{r}. \ttt{p} 是 \ttt{1}, \ttt{q} 是 \ttt{4}, $1\div 4=1/4$. 然而, \ttt{p} 和 \ttt{q} 都是整型的, 因此不是要除而是要\uline{整除}, $1/4$ 要向0取整, 所以 \ttt{p / q} 是 \ttt{0}. 虽然 \ttt{r} 是实型的, 但是\uline{并没有什么卵用}. 因为是先计算, 再赋值. 先计算 \ttt{p / q} 得到 \ttt{0}, 然后赋给 \ttt{r}, 再类型转化, 所以 \ttt{r} 是 \ttt{0.0}!!!
最后计算 \ttt{2.0 ** r}, 结果自然是 \ttt{1.0}!
最后来看下面这个例子.
\begin{lstlisting}
program main
implicit none
print *, 2 ** (-4)
end program main
\end{lstlisting}
我猜同学们看到这个程序肯定不敢觉得结果是 $2^{-4}$ 了吧. \ttt{2} 和 \ttt{4} 都是整型的, 那么结果肯定不是 $2^{-4}$ 喽. Fortran 还特别规定, 如果 \ttt{[a]} 和 \ttt{[b]} 都是整型数据实体, 值分别是 $a$ 和 $b$, 且 $b$ 是负的, 那么 \ttt{[a]**[b]} 的值不是 $a^b$ 而是 $a^b$ 的向0取整!!! 所以 \ttt{2 ** (-4)} 当然是 \ttt{0} 了!
我们把这么个坑死人不偿命的规则总结如下: 如果数据实体 \ttt{[a]} 是整型的且值为 $a$, 数据实体 \ttt{[b]} 是整型的且值为 $b$, \ttt{[op]} 是二元算数运算符且代表运算 $\text{OP}$, 那么 $a\,\text{OP}\,b$ 不一定是整数, 但运算时类型和种别转化的规则又需遵守, 因此 \ttt{[a][op][b]} 的值不规定为 $a\,\text{OP}\,b$ 而规定为 $a\,\text{OP}\,b$ 的向0取整. 这么规定也不是为了折磨大家, 凭安安拥有的知识大致可以推断, 这么规定后电脑处理数据和人们造编译器都会比较方便, 因此不仅 Fortran 是这么规定的, C 和 Python 2 也是这么规定的. 但实践表明, 这么规定对写程序的人来说实在是太不友好了, 很容易会有诸如运算符 \ttt{/} 的两边一不小心都是整型数据实体, 然后又忘了不是除而是整除, 结果计算出错的情况发生, 所以到 Python 3 那儿规则终于改了, 规定如果一种二元运算能用整型数据实体算出不是整数的值, 那么结果会自动变成实型数据实体而不会向0取整, 大家终于不会被折磨了!
Fortran 语法我们没法儿改, 我们只好自己想办法避坑. 我们发现, 坑会出现在用整型数据实体的时候, 那我们尽量不用整型数据实体就完事儿喽, 但有些地方 (比如字符串的长度) 只能用整型数据实体, 那就没办法了, 不过这些地方非常少, 以安安的经验, 我们是不会因这些整型数据实体而掉坑里去的.
\begin{convention}
尽可能避免使用整型数据实体; 如果一定要使用, 在表达式中先将其转化成实型数据实体.
\end{convention}
如果有一个实型变量 \ttt{a}, 要算 \ttt{a} 的 \ttt{2} 次方, 安安会写 \ttt{a**2.0} 而不会写 \ttt{a**2}. 如果有一个实型变量 \ttt{b}, 一个整型变量 \ttt{n}, 要算 \ttt{b} 的 \ttt{n} 次方, 安安会写 \ttt{b**real(n)} 而不会写 \ttt{b**n}. 同学们可能会觉得这么写实在是繁琐, 因为即使 \ttt{a**2.0} 写成 \ttt{a**2}, \ttt{b**real(n)} 写成 \ttt{b**n}, 由于 \ttt{a} 和 \ttt{b} 不是整型的, 这式子没可能算错的. 而且用整型数据实体还有些好处, 例如因为电脑算乘法应该比算乘方快, 所以聪明的编译器在看到 \ttt{a**2} 的时候, 很可能会先把它变成等价的式子 \ttt{a*a}, 然后再编译运行 (这也是 Fortran 官规明确允许的), 但如果写成 \ttt{a**2.0}, 编译器就要先判断出实型的 \ttt{2.0} 的值是整数, 然后再把它变成 \ttt{a*a}, 这对编译器和造编译器的人来说太难了, 老师也会因这个原因而倡导大家尽量用整型数据实体. 但不管同学们有没有被 Fortran 整怕 (希望不会), 反正俺是被整怕了, 所以宁愿写表达式的时候麻烦得要死也要全力避免踩坑\dots{}
\subsection{字符运算}\label{fortran_char_operator}
字符运算符只有一个: \ttt{//}, 其作用是把左右两边的字符串连起来得到一个新字符串. 示例如下.
\begin{lstlisting}
program main
implicit none
print *, 'Hello'//','//' '//'world'//'!'
end program main
\end{lstlisting}
如果整型数据实体 \ttt{[n]} 的值为 $n$, 那么 \ttt{achar([n])} 是 ASCII 码为 $n$ 的字符. 英文缩写 ``CR'' 的回车符 ASCII 码是 $13$, 英文缩写 ``LF'' 的换行符 ASCII 码是 $10$, 所以 Windows 的换行字符是 \ttt{achar(13)//achar(10)}, Linux 的换行字符是 \ttt{achar(10)}, 我们再用 \ttt{//} 就可以在字符串中加入换行字符了. 例如在 Windows 中给 \ttt{'Hello, world!'} 后面加 Windows 换行字符可写成下面这样.
\begin{lstlisting}
program main
implicit none
print *, 'Hello, world!'//achar(13)//achar(10)
end program main
\end{lstlisting}
但这么写让人晕乎, 因为不熟悉 ASCII 的人 (比如俺) 得查表才知道 ASCII 码 $13$ 和 $10$ 的字符是什么. 我们可以 ``用字面常量当注释'' 来提示 ASCII 码 $13$ 和 $10$ 的字符是什么, 写成下面这样子.
\begin{lstlisting}
program main
implicit none
character(*), parameter :: &
cr = achar(13), lf = achar(10)
print *, 'Hello, world!'//cr//lf
end program main
\end{lstlisting}
或者更简单地, 写成下面这样子.
\begin{lstlisting}
program main
implicit none
character(*), parameter :: &
crlf = achar(13)//achar(10)
print *, 'Hello, world!'//crlf
end program main
\end{lstlisting}
\subsection{逻辑运算}
逻辑运算符一共有五个, 左右需跟逻辑型数据实体. 逻辑运算符中间不能有空格, 例如 \ttt{.not.} 不能写成 \ttt{. not .}. 常用的有三个: \ttt{.not.}, \ttt{.and.}, \ttt{.or.}, 分别代表同学们高中学过的 ``非'', ``与'', ``或''. 不常用的有两个: \ttt{.eqv.} 和 \ttt{.neqv.}, 分别代表同学们高中没学过的 ``同或'' 和 ``异或''. 逻辑运算符详细解释如下:
\begin{itemize}
\item \ttt{.not. [b]}: \ttt{[b]} 为假则为真, 否则为假.
\item \ttt{[a] .and. [b]}: \ttt{[a]} 和 \ttt{[b]} 都为真则为真, 否则为假.
\item \ttt{[a] .or. [b]}: \ttt{[a]} 和 \ttt{[b]} 中有一个为真则为真, 否则为假.
\item \ttt{[a] .eqv. [b]}: \ttt{[a]} 和 \ttt{[b]} 都为真或都为假则为真, 否则为假.
\item \ttt{[a] .neqv. [b]}: \ttt{[a]} 和 \ttt{[b]} 中有且只有一个为真则为真, 否则为假.
\end{itemize}
示例俺懒得写了, 请同学们自己写例子来练习.
用逻辑运算符时要注意逻辑运算符是分优先级的, 但优先级俺不讲. 请同学们根据规范 \ref{use_parentheses}, 加括号来指明逻辑运算符的先后顺序, 例如同学们可以写 \ttt{(a .and. b) .or. c} 或 \ttt{a .and. (b .or. c)}, 但不可以写 \ttt{a .and. b .or. c}.
\subsection{关系运算}
关系运算符有六个: \ttt{<}, \ttt{<=}, \ttt{>}, \ttt{>=}, \ttt{==}, \ttt{/=}, 它们分别有等价的写法 \ttt{.lt.}, \ttt{.le.}, \ttt{.gt.}, \ttt{.ge.}, \ttt{.eq.}, \ttt{.ne.}, 但这些写法安安不喜欢用, 因为显得老气而且要多打几个字符. 关系运算符中间不能有空格, 例如 \ttt{==} 不能写成 \ttt{= =}, \ttt{.ge.} 不能写成 \ttt{. ge .}.
如果运算符左右两边是数, 则六个运算符分别代表$<$, $\leqslant$, $>$, $\geqslant$, $=$, $\neq$. 这种情况下关系运算符经常和其他运算符混用, 请同学们根据规范 \ref{use_parentheses}, 加上括号来指明运算符的先后顺序, 例如同学们可以写 \ttt{((2 - 1) < 0) .and. ((3 - 2) < 0)}, 但不可以写 \ttt{2 - 1 < 0 .and. 3 - 2 < 0}. 示例俺懒得写了, 请同学们自己写例子来练习.
严格意义上说, 关系运算符是不能连用的. 比如下面这个程序, Gfortran是运行不了的. 然而 Ifx 是可以的\dots{}
\begin{lstlisting}
program main
implicit none
print *, 1 < 2 < 3
end program main
\end{lstlisting}
要做 ``连续的关系运算'', 标准做法是把 ``连续的关系运算'' 拆分成 ``单独的关系运算'', 然后用 \ttt{.and.} 连起来, 像下面这样.
\begin{lstlisting}
program main
implicit none
print *, (1 < 2) .and. (2 < 3)
end program main
\end{lstlisting}
这里有一个经典的小坑, 像下面这样的程序可能得出 ``错误'' 的结果 \ttt{.false.}.
\begin{lstlisting}
program main
implicit none
print *, (0.1 + 0.2) == 0.3
end program main
\end{lstlisting}
因为电脑是二进制的, 十进制小数不一定能准确存储, 结果严格上说, \ttt{0.1}, \ttt{0.2}, \ttt{0.3} 的值只是非常接近于 $0.1$, $0.2$, $0.3$, 那 \ttt{(0.1 + 0.2) == 0.3} 如果是 \ttt{.false.} 其实也是不奇怪的. 这个 ``问题'' 广泛存在于各种编程语言中, 连 Python 3 默认情形下都是如此, 然而上面的程序 Ifx 和 Gfortran 竟都能得出正确的结果, 似乎非常高级\dots{}
因为电脑存储数据和计算的时候不免有误差, 所以如果两个表达式的结果是实型或复型的, 那么即使这两个表达式数学上是等价的, 电脑计算后的结果也不一定相等, 因此比较这两个表达式的结果是否相等其实是没有意义的. 不过有些时候, 比较两个表达式的结果是否相等还是有意义的, 这些情况下, 表达式中只有 ``整数到整数的运算'', 例如整数的加, 减, 乘, \uline{整}除之类的, 这类表达式不会引入存储数据和计算的误差.
\begin{convention}
如果两个表达式中所包含的所有表达式的结果不都是整型的, 那么不比较这两个表达式是否相等.
\end{convention}
还有一个小坑, 像下面这样的程序 \ttt{==} 左右两边一样, 结果算出来竟是 \ttt{.false.}
\begin{lstlisting}
program main
implicit none
real :: zero
zero = 0.0
print *, (zero / zero) == (zero / zero)
end program main
\end{lstlisting}
这是因为 \ttt{zero / zero} 算出来是 $\text{NaN}$, 而 $\text{NaN}$ 被规定成和任何数, 甚至它自己, 都不相等. 所以任意一个数据实体 \ttt{[r]}, 我们不能直接把它和 $\text{NaN}$ 放在 \ttt{==} 两边来判断 \ttt{[r]} 是不是 $\text{NaN}$ (不论是不是, 结果都是 \ttt{.false.}). 但因为只有 $\text{NaN}$ 被规定成和自己不相等, 所以当且仅当 \ttt{[r]} 是 $\text{NaN}$ 时, \ttt{[r] /= [r]} 为真, 我们可以利用这点巧妙地判断 \ttt{[r]} 是不是 $\text{NaN}$.
如果运算符左右两边是字符串, 则比较运算是比较字符串的先后顺序. 首先, 编译器自己定义了一套字符的先后顺序. 然后比较字符串的先后顺序的时候, 编译器先把两个字符串暂时补成一样长 (短的那个后面补空格).
\begin{itemize}
\item 如果两边的字符串前 $n$ 个字符都相同, 且左边字符串第 $(n+1)$ 个字符先于右边字符串第 $(n+1)$ 个字符, 则左边字符串 ``\ttt{<}'' 右边字符串.
\item 如果两边的字符串前 $n$ 个字符都相同, 且左边字符串第 $(n+1)$ 个字符后于右边字符串第 $(n+1)$ 个字符, 则左边字符串 ``\ttt{>}'' 右边字符串.
\item 如果两边的字符串完全相同, 则左边字符串 ``\ttt{==}'' 右边字符串.
\end{itemize}
自然, ``\ttt{>=}'' 就是不 ``\ttt{<}'', ``\ttt{<=}'' 就是不 ``\ttt{>}'', ``\ttt{/=}'' 就是不 ``\ttt{==}''.
至于编译器要怎么定义字符的先后顺序, Fortran 又挖了一坑, 在标准里给了一些规定, 使得 ASCII 的顺序是符合规定的顺序之一, 但又没规定死, 也就是说不同编译器进行字符串比较可能得出不同的结果, 所以根据规范 \ref{use_standard_only}, 我们不应该比较字符串的先后顺序. 不过 Fortran 填坑还是积极的, 如果 \ttt{[a]} 和 \ttt{[b]} 是字符串, 那么我们可以分别用 \ttt{lge([a], [b])}, \ttt{lgt([a], [b])}, \ttt{lle([a], [b])}, \ttt{llt([a], [b])} 代替 \ttt{[a] >= [b]}, \ttt{[a] > [b]}, \ttt{[a] <= [b]}, \ttt{[a] < [b]}, 这样编译器就不会用自己定的顺序, 而会用 ASCII 的顺序来比较字符串的先后顺序了. 至于 \ttt{[a] == [b]} 和 \ttt{[a] /= [b]}, 它们是比较字符串是否相同的, 结果和字符的先后顺序没关系, 就不用另搞一套来填坑了. 示例俺懒得写了, 请同学们自己写例子来练习.