跳至內容

dc (程序)

本頁使用了標題或全文手工轉換
維基百科,自由的百科全書

dc
原作者Robert Morris英語Robert Morris (cryptographer)
(於AT&T貝爾實驗室
Lorinda Cherry英語Lorinda Cherry
開發者各種開源商業開發者
編程語言B, C[1]
操作系統Unix, 類Unix, Plan 9
平台跨平台
類型命令

dcdesk calculator:桌面計算器)是採用逆波蘭表示法跨平台計算器,它支持任意精度算術[2]。它是Robert Morris英語Robert Morris (cryptographer)貝爾實驗室工作期間書寫的[3],作為最老的Unix實用工具,先於C語言的發明。像那個年代的其他實用工具一樣,它有着一組強力的特徵和簡潔的語法[4][5]。傳統上,採用中綴表示法bc計算器程序是在dc之上實現的。

歷史

[編輯]

dc是倖存的最老的Unix語言[3]。在貝爾實驗室收到第一台PDP-11的時候,用B語言寫成的dc是在這個新機器上運行的第一個語言,甚至在匯編器之前[6]

基本運算

[編輯]

在dc中要做4和5的乘法:

$ dc
4 5 *
p
20
q

這可轉譯為「把4和5壓入棧頂,通過乘法算符,從棧中彈出兩個元素,將二者相乘並把結果壓回棧頂」。接着使用p命令打印棧頂的元素。使用q命令退出此次調用的dc實例。注意數值相互間必須以空白分隔,但某些算符可以不必如此。 還可以用如下命令得到這個結果:

$ dc -e '4 5 * p'
20
$ echo "4 5 * p" | dc
20
$ dc -
4 5 *pq
20
$ cat <<EOF > cal.dc
4 5 *
p 
EOF
$ dc cal.dc
20

使用命令k來變更算術精度,它設置算術運算的小數位數。因為缺省精度是0,例如:

$ dc -e '10946 6765 / p'
1

通過使用命令k調整精度,可以產生任意數目的小數數位,例如:

$ dc -e '7k 10946 6765 / p'
1.6180339

dc有科學計算器的基本運算功能,比如求黃金分割率的值:

$ dc -e '7k 5 v 1 + 2 / p'
1.6180339

dc將輸入數值的前導_識別為負號,命令^計算冪,命令v計算平方根。

d命令用於複製棧頂元素。r命令用於對棧頂和僅次棧頂的兩個元素進行對換。z命令用於壓入當前棧深度,即執行z命令前棧中元素的數目。

輸入/輸出

[編輯]

使用?命令,從stdin讀取一行並執行它。這允許從宏中向用戶要求輸入,故而此輸入必須是語法上正確的,並且這有潛在的安全問題,因為dc的!命令可以執行任意系統命令。

前面提及過,p命令打印棧頂元素,帶有隨後的一個換行。n命令彈出棧頂元素並輸出它,沒有尾隨換行。f命令打印整個,從棧頂到棧底並且一項一行。

dc還支持控制輸入和輸出的底數o命令設置輸出底數,輸出底數必須大於等於2。i命令彈出棧頂元素並將它用作輸入底數,輸入底數必須在2和16之間,十六進制數字必須大寫以避免和dc命令衝突。要記住輸入底數將影響對後面的所有數值的分析,所以通常建議在設置輸入底數之前先設置輸出底數。例如將二進制轉換成十六進制:

$ echo '16o2i 10011010101111001101111011110000 p' | dc
9ABCDEF0

要讀取設置的這些數值,KIO命令將壓入當前精度、輸入基數和輸出基數到棧頂。

語言特徵

[編輯]

除了上述的基本算術和操作,dc包括了對、條件和存儲結果用於以後檢索的支持。

寄存器

[編輯]

寄存器在dc中是有着單一字符名字的存貯位置,它可以分別通過命令sl來存儲和檢索,它是宏和條件的底層機制:sc彈出棧頂元素並將它存儲入寄存器c,而lc將寄存器c的值壓入棧頂。例如:

3 sc 4 lc * p

寄存器還被當作次要棧,可以使用SL命令在它們和主要棧之間壓入和彈出數值。存儲棧頂元素到寄存器中並把這個元素留在棧頂,需要聯合使用ds命令。

字符串

[編輯]

字符串是包圍在[]之中的字符,可以被壓入棧頂和存入寄存器。使用x命令從棧頂彈出字符串並執行它,使用P命令從棧頂彈出並打印字符串,無尾隨換行。a命令可以把數值的低位字節轉換成ASCII字符,或者在棧頂是字符串時把它替換為這個字符串的第一個字符。此外沒有方法去建造字符串或進行字符串操縱。

#字符開始一個注釋直到此行結束。

[編輯]

通過允許寄存器和棧項目像數值一樣存儲字符串,從而實現了。一個字符串可以被打印,也可以被執行,就是說作為dc命令的序列而傳遞。例如可以把一個宏「加1並接着乘以2」存儲到一個寄存器M中:

[1+ 2*]sM

下面的命令將一個數值3壓入棧頂,使用x命令執行存儲在寄存器M中的宏,並打印留在棧頂的結果:

3 lMx p

條件

[編輯]

最後提供了有條件執行宏的機制。命令=M將從棧頂彈出兩個值,如果二者相等,則執行存儲在寄存器M中的宏。如下命令序列將在原棧頂元素等於5的條件下打印字符串equal

[[equal]p]sM d5=M

這裡使用了d命令保留原棧頂元素。其他條件有>!><!<!=,如果棧頂元素分別大於、不大於(小於等於)、小於、不小於(大於等於)、不等於僅次於棧頂的元素,則執行指定的宏。注意不同於ForthPostScriptFactor,在不等式比較中的運算元的次序同在算術中的次序相反,5 3 - 等價於中綴表示法的5-3,然而5 3< R3<5時運行寄存器R的內容。下面是有條件執行宏的示例:

$ echo 5 | dc -e '? [[equal]p]sM d5=M'
equal
$ echo 4 | dc -e '? [[equal]p]sM d5=M'
$

這裡的命令在棧頂元素等於5之時有相應的輸出,在它不等於5之時沒有輸出。

如果要實現一般編程語言中表達有兩個分支控制流程條件運算符?:條件語句,則需要兩個宏構造,例如:

$ echo 5 | dc -e '? [[equal]p q]sM [d5=M [not equal]p]x'
equal
$ echo 4 | dc -e '? [[equal]p q]sM [d5=M [not equal]p]x'
not equal

這裡的命令在棧頂元素等於5和不等於5之時都有相應的輸出。

q命令退出2層宏,如果宏少於2層則退出dc。Q命令從棧頂彈出一個值作為退出宏的層數,比如2Q命令退出2層宏,它永不導致退出dc。

遞歸示例

[編輯]

通過定義進行有條件的遞歸調用的宏,可以實現遞歸過程和迭代運算。

階乘

[編輯]

下面是對棧頂元素計算階乘遞歸過程

# F(x):
#   x > 1 ? x * F(x-1) : x
# F()

本文中的偽代碼儘量採用條件運算符?:而少用條件語句。這裡的F(x):是過程定義,過程F消費在棧頂的命名為x的一個值;這裡的F(x-1)是過程調用,在調用過程F之前先將x-1的結果值壓入棧頂;這裡的F()也是過程調用,在調用過程F之前不需要向棧頂壓入一個值。

這個過程在x > 0時有效,它可直接轉寫為互遞歸形式:

# F(x):
#   x > 1 ? G(x) : x
# G(x):
#   x * F(x-1)
# F()

它可實現為:

[d 1- lFx *]sG [d1<G]dsFx

計算階乘還可以採用這個條件表達式的等價形式:

# F(x):
#   x <= 1 ? x : x * F(x-1)
# F()

它可實現為:

[q]sQ [d1!<Q d 1- lFx *]dsFx

這個遞歸過程可以進一步採用尾調用寫為:

# F(x):
#   x
#   x > 1 ? G(x) : x
# G(x):
#   F(x-1)
# H(x, acc):
#   x := x * acc
#   z() > 1 ? H() : x
# H(F())

這裡的偽代碼中的H(x, acc):是過程定義,過程H消費在棧頂的命名為acc的一個值,和緊鄰其下的命名為x的另一個值;這裡的H()是過程調用,在調用過程H之前不需要向棧頂壓入一個值。它可實現為:

[1- lFx]sG [d d1<G]dsFx [* z1<H]dsHx

這裡的運算由兩個步驟串接而成,首先將遞減的整數壓入堆棧形成整數集偏序關係區間,然後在這個數列上進行初始值為的應用乘法運算的右側歸約,最終得出一個結果值。這裡的命令z,將當前的堆棧深度即堆棧中元素數目壓入棧頂。還可以進一步增加針對初始的棧頂元素xx ≤ 0情況的預處理,即執行x := x-x+1來實現x := 1。下面是其執行示例:

$ echo 0 | dc -e '? [d-1+]sAd0!<A [1- lFx]sG [d d1<G]dsFx [* z1<H]dsHx p'
1
$ echo 9 | dc -e '? [d-1+]sAd0!<A [1- lFx]sG [d d1<G]dsFx [* z1<H]dsHx p'
362880

下面的例子採用迭代法打印出階乘的前n項的數列

# n := x
# i := 1
# F(x):
#   x := x * i 
#   p(x)
#   i := i + 1
#   i <= n ? F() : x
# F(1)

這裡n := x中的x指稱初始的棧頂元素。這裡的迭代過程F(x)不同於一般的遞歸過程之處,是在調用自身之時仍採用當前棧頂元素為參數而不向堆棧壓入新元素。這裡所迭代的是需要初始化的堆棧中的自動變量x,它是,其初始值1i既是迭代的遞增計數器,也是迭代二元運算英語Iterated binary operation運算元,其遞增形成整數集偏序關係區間,同步地在這個數列上進行輸出中間值的初始值為的應用乘法運算的左側歸約,逐項輸出中間結果形成數列,這裡的x所起到的作用也被稱為累加器(accumulator),其含義為「累計」或「累積」而不必然採用加法或乘法。它可實現為:

sn 1si 1 [lid1+si *p liln!<F]dsFx

下面是其執行示例:

$ echo 6 | dc -e '? sn 1si 1 [lid1+si *p liln!<F]dsFx'
1
2
6
24
120
720

可以將它改為計算單個的階乘:

$ echo 0 | dc -e '? sn 1si 1 [lid1+si * liln!<F]dsFx p'
1
$ echo 9 | dc -e '? sn 1si 1 [lid1+si * liln!<F]dsFx p'
362880

斐波那契數

[編輯]

下面是對棧頂元素計算斐波那契數遞歸過程:

# F(x):
#   x > 1 ? F(x-1) + F(x-2) : x
# F()

這個過程在x ≥ 0時有效,它可直接轉寫為互遞歸形式:

# F(x):
#   x > 1 ? G(x) : x
# G(x):
#   F(x-1) + F(x-2)
# F()

它可實現為:

[d 2- lFx r 1- lFx +]sG [d1<G]dsFx

這裡的r命令反轉(交換)棧頂元素和僅次於棧頂的元素二者的次序,此外dc還有循環移位運算英語Circular shiftR命令,其參數為正數時是循環左移一位,參數為負數時是循環右移一位,參數的絕對值是參與循環移位的堆棧中含棧頂的元素數目,比如後文中用到_3R命令會將堆棧頂部的三個元素a b c轉變為c a b

下面是其執行示例,並展示了通過給它加打印斷點來觀察其遞歸調用的規模:

$ echo 0 | dc -e '? [d 2- lFx r 1- lFx +]sG [d1<G]dsFx p'
0
$ echo 20 | dc -e '? [d 2- lFx r 1- lFx +]sG [d1<G]dsFx p'
6765
$ echo 20 | dc -e '? [p d 2- lFx r 1- lFx +]sG [d1<G]dsFx' | wc -l
10945
$ echo 21 | dc -e '? [d 2- lFx r 1- lFx +]sG [d1<G]dsFx p'
10946

這個過程可以進一步採用記憶化而寫為:

# i := 1
# F(x):
#   x > 1 ? M(x) : x
# M(x):
#   if x <= i
#     a[x]
#   else 
#     temp := G(x)
#     i := i + 1
#     a[i] := temp 
#     temp
# G(x):
#   F(x-1) + F(x-2)
# F()

基於數組的保存命令:和訪問命令;,它可實現為:

1si [d 2- lFx r 1- lFx +]sG [;a q]sN [dli!<N lGx dli1+dsi:a]sM [d1<M]dsFx

遞歸定義中,正在計算並要記憶其結果的是,即將它的結果存儲在這裡的a[n]中,其下標為n,而當前已存儲過的最大下標是n-1,將要存儲的下標是已經存儲的最大下標的增加1。這裡的i是數組a的已存儲過的最大下標,它的初始值是1,即假定a[0]a[1]已經存儲了數值。這裡的F(x)在被遇到01這兩個參數之時,直接在代碼中返回數值,而不會用它們作為下標去訪問數組。

這個過程可以更一般化的寫為:

# a[0] := 0
# a[1] := 1
# i := 1
# F(x):
#   if x <= i
#     a[x]
#   else
#     S(x, b)    
#     temp := G(x)
#     x := L(b)
#     i := x
#     a[x] := temp 
#     temp
# G(x):
#   F(x-1) + F(x-2)
# F()

這裡的偽代碼中的S(x, b)表示複製主棧的棧頂元素x並把它壓入b的棧頂,L(b)表示彈出b的棧頂元素並把它壓入主棧的棧頂。基於寄存器關聯的保存命令S和裝載命令L,它可實現為:

0 0:a 1 1:a 1si [d 2- lFx r 1- lFx +]sG [;a q]sN [dli!<N dSb lGxd Lbdsi:a]dsFx

遞歸定義遞歸調用中,隨着的索引n的遞減,而遞歸下降至被計算出來並保存的第一項,然後沿着調用鏈逐級返回。在遞歸的每個步驟中都要計算並保存,它所需要的這兩項之中:

  • 如果首先進行遞歸調用並從其返回,則也需要計算並保存,為此需要先後訪問已保存的
  • 如果首先進行遞歸調用並從其返回,則需要訪問已保存的

下面是其執行示例,展示了通過給它加打印斷點來觀察其遞歸調用的具體步驟,其中連續高亮標示出某個過程從開始被調用直到它結束返回:

$ echo 8 | dc -e '? 0 0:a 1 1:a 1si [d 2- lFx r 1- lFx +]sG [;a q]sN [[Level]nzn[ F]np dli!<N dSb lGxd Lbdsi:a]dsFx'
Level1 F8
Level2 F6
Level3 F4
Level4 F2
Level5 F0
Level5 F1
Level4 F3
Level5 F1
Level5 F2
Level3 F5
Level4 F3
Level4 F4
Level2 F7
Level3 F5
Level3 F6
$ echo 8 | dc -e '? 0 0:a 1 1:a 1si [d 1- lFx r 2- lFx +]sG [;a q]sN [[Level]nzn[ F]np dli!<N dSb lGxd Lbdsi:a]dsFx'
Level1 F8
Level2 F7
Level3 F6
Level4 F5
Level5 F4
Level6 F3
Level7 F2
Level8 F1
Level8 F0
Level7 F1
Level6 F2
Level5 F3
Level4 F4
Level3 F5
Level2 F6

遞歸過程的返回值之間形成了遞推關係,每個的值被用到一次或兩次,首次是從遞歸調用直接返回,再次是對其已保存的值進行訪問,每個步驟要麼訪問兩項並保存兩項,要麼訪問一項並保存一項。故而它可以進一步寫為:

# i := 1
# F(x):
#   x > 1 ? M(x) : x
# M(x):
#   if x <= i
#     a[x%2]
#   else 
#     temp := G(x)
#     i := i + 1
#     a[i%2] := temp 
#     temp
# G(x):
#   F(x-1) + F(x-2)
# F()

它可實現為:

1si [d 2- lFx r 1- lFx +]sG [2%;a q]sN [dli!<N lGx dli1+dsi2%:a]sM [d1<M]dsFx

遞歸定義的上述兩種求值次序中,首先進行遞歸調用的這種形式,其記憶化實現從一開始就持續進行遞歸調用直到首次遞歸返回,然後就逐級遞歸返回而不再進行遞歸調用,故而它可以進一步寫為:

# a := 0
# F(x):
#   x > 1 ? G(x) : x
# G(x):
#   temp := F(x-1)
#   prev := a 
#   a := temp
#   temp + prev
# F()

這裡的a初始化的值0x在步入遞歸調用之後,除了在被遞減至的值1之時作為調用結果來返回之外,沒有參與到後續的加法運算之中,而主要起到了遞減計數器的作用。它可實現為:

0sa [1- lFx la r dsa +]sG [d1<G]dsFx

下面的例子採用迭代法打印出斐波那契數列的不含第0項的前n項:

# i := x
# a := 1
# F(x):
#   prev := a
#   a := x
#   x := x + prev
#   p(x)
#   i := i - 1
#   i > 0 ? F() : x
# i > 0 ? F(0) : 0

這裡i := x中的x指稱初始的棧頂元素。這裡的迭代過程F(x)不同於一般的遞歸過程之處,是在調用自身之時仍採用當前棧頂元素為參數而不向堆棧壓入新元素。針對遞推關係,它總共進行n次迭代運算得出。這裡的i是進行迭代的遞減計數器,所迭代的是需要初始化的兩個變量xa:在堆棧中的自動變量x,先是被迭代的,再是迭代出來的,其初始值0,它所起到的作用也被稱為累加器;作為全局變量a,以所得的1作為初始值,在首次迭代之後它是始於。它可實現為:

si 1sa [lardsa +p li1-si li0<F]sF 0 li0<F

下面是其執行示例:

$ echo 6 | dc -e '? si 1sa [lardsa +p li1-si li0<F]sF 0 li0<F'
1
1
2
3
5
8

可以將它改為計算單個的斐波那契數:

$ echo 0 | dc -e '? si 1sa [lardsa + li1-si li0<F]sF 0 li0<F p'
0
$ echo 9 | dc -e '? si 1sa [lardsa + li1-si li0<F]sF 0 li0<F p'
34

男人抑或男孩測試

[編輯]

下面的例子通過男人抑或男孩測試展示dc的數組能支持遞歸運算的程度,這個測試用Python可以寫為:

import sys
sys.setrecursionlimit(1033) # 这里设置的递归限制在实测时刚好够用于k=10

def A(k, x1, x2, x3, x4, x5):
    def B():
        nonlocal k
        k -= 1
        return A(k, B, x1, x2, x3, x4)
    return x4() + x5() if k <= 0 else B()

print(A(10, lambda: 1, lambda: -1, lambda: -1, lambda: 1, lambda: 0))

通過手工設立並操縱簡易調用棧非局部引用英語Non-local variable頭等函數,這個測試可以用dc實現為:

# M[0] := S := T := K[0] := K := I[0] := 0
# P(): 
#   S := T + 1
#   n := z()
#   T := T + n + 1
#   M := R(n, n)
#   K := K + 1
#   K[K] := S
# R(x, n):
#   M[S+n] := x
#   n := n - 1
#   n >= 0 ? R(n) : n
# Q():
#   T := T - M[T] - 1
#   S := T - M[T]
#   K := K - 1
# U(x):
#   I := I + 1
#   W[I] := x
#   N[I] := K
#   I[K] := I
# V():
#   I := I[K] - 1
# W(i):
#   W[i](N[i])
0 0:M 0sS 0sT 0 0:K 0sK 0 0:I
[lT1+sS z d1+lT+sT dlRxsM lK1+sK lSlK:K]sP 
[d_3RlS+:M 1- d0!>R]sR
[lTd;M-1-sT lTd;M-sS lK1-sK]sQ
[lI1+sI lI:W lKlI:N lIlK:I]sU
[lK;I1-sI]sV [d;Nr;Wx]sW

# A(k, x1, x2, x3, x4, x5):
#   P(); F(); Q()
# F():
#   M[S] <= 0 ? G() : U(B), B(K), V()
# G():
#   A[K] := W(M[S+5])
#   W(M[S+4]) + A[K]
# B(x):
#   E := K(x)
#   J := I(x)
#   M[E] := M[E] - 1
#   A(M[E], J, M[E+1], M[E+2], M[E+3], M[E+4])
[lPx lFx lQx]sA [lS;M0!<G [lBx]lUx lKlBx lVx]sF
[lS5+;M lWx lK:A lS4+;M lWx lK;A + q]sG
[d;KsE ;IsJ lE;M1-lE:M lE;M lJ lE1+;M lE2+;M lE3+;M lE4+;M lAx]sB

# I := 4
# L(x): x
# W[0] := W[1] := W[2] := W[3] := W[4] := L
# N[0] := 1; N[1] := -1; N[2] := -1; N[3] := 1; N[4] := 0
# p(A(?(), 0, 1, 2, 3, 4))
4sI []sL
[lLx] d0:W d1:W d2:W d3:W 4:W
1 0:N _1 1:N _1 2:N 1 3:N 0 4:N
? 0 1 2 3 4 lAx p

對於k = 10,這個測試中函數A()執行了722次,這裡它在傳值調用形式參數小於等於零的情況下,先求值第5傳名調用形式參數再求值第4個傳名調用形式參數,其在調用棧中的棧幀數量的最大值即遞歸深度是323層;而為函數B()的創建了416閉包,其堆疊數量的最大值是258層;並為用作在最外層的函數A()的實際參數的5恆等函數,分別創建了等待應用於其上的實際參數。

將上述代碼保存入manorboy.dc文件中,下面是針對k010它的執行示例:

$ for i in $(seq 0 10); do echo $i | dc manorboy.dc; done
1
0
-2
0
1
0
1
-1
-10
-30
-67

可以將閉包恆等函數及其參數也存儲在調用棧之中:

# M[0] := S := T := K[0] := K := I[0] := 0
# P(): 
#   S := T + 1
#   n := z()
#   T := T + n + 1
#   M := R(n, n)
#   K := K + 1
#   K[K] := S
#   I[K] := T
# R(x, n):
#   M[S+n] := x
#   n := n - 1
#   n >= 0 ? R(n) : n
# Q():
#   T := T - M[T] - 1
#   S := T - M[T]
#   K := K - 1
# W(i):
#   M[I[i]-1](M[I[i]-2])
0 0:M 0sS 0sT 0 0:K 0sK 0 0:I
[lT1+sS z d1+lT+sT dlRxsM lK1+sK lSlK:K lTlK:I]sP 
[d_3RlS+:M 1- d0!>R]sR
[lTd;M-1-sT lTd;M-sS lK1-sK]sQ
[;Id2-;M r1-;Mx]sW

# A(k, x1, x2, x3, x4, x5):
#   P(); F(); Q()
# F():
#   M[S] <= 0 ? G() : P(K, B), B(K-1), Q()
# G():
#   A[K] := W(M[S+5])
#   W(M[S+4]) + A[K]
# B(x):
#   E := K[x]
#   J := x + 1
#   M[E] := M[E] - 1
#   A(M[E], J, M[E+1], M[E+2], M[E+3], M[E+4])
[lPx lFx lQx]sA [lS;M0!<G lK[lBx]lPx lK1-lBx lQx]sF
[lS5+;M lWx lK:A lS4+;M lWx lK;A + q]sG
[d;KsE 1+sJ lE;M1-lE:M lE;M lJ lE1+;M lE2+;M lE3+;M lE4+;M lAx]sB

# L(x): x
# P(1, L); P(-1, L); P(-1, L); P(1, L); P(0, L)
# p(A(?(), 1, 2, 3, 4, 5))
[]sL 1[lLx]lPx _1[lLx]lPx _1[lLx]lPx 1[lLx]lPx 0[lLx]lPx
? 1 2 3 4 5 lAx p

這裡在k = 10的情況下,調用棧的遞歸深度為586層,如果在函數A()中先求值第4個傳名調用形式參數再求值第5個傳名調用形式參數,則遞歸深度為901層。

參見

[編輯]

引用

[編輯]
  1. ^ dc.c. 1979-01-20. 
  2. ^ dc(1): an arbitrary precision calculator – Linux用戶命令(User Commands)手冊頁
  3. ^ 3.0 3.1 Brian Kernighan and Ken Thompson. A nerdy delight for any Vintage Computer Fest 2019 attendee: Kernighan interviewing Thompson about Unix. YouTube. 事件發生在 29m45s. [September 3, 2019]. (原始內容存檔於2022-02-01). 
  4. ^ The sources for the manual page for 7th Edition Unix dc. [2020-09-25]. (原始內容存檔於2019-09-24). 
  5. ^ Ritchie, Dennis M. The Evolution of the Unix Timesharing System. Sep 1979 [2019-05-31]. (原始內容存檔於2010-05-06). 
  6. ^ McIlroy, M. D. A Research Unix reader: annotated excerpts from the Programmer's Manual, 1971–1986 (PDF) (技術報告). CSTR. Bell Labs. 1987 [2019-05-31]. 139. (原始內容存檔 (PDF)於2019-11-30). 

外部連結

[編輯]