這段代碼為何能輸出"Hello World"?

來自brainfuck語言:

++++++++++[&>+++++++&>++++++++++&>+++&>+&<&<&<&<-] &>++.&>+.+++++++..+++.&>++.&<&<+++++++++++++++. &>.+++.------.--------.&>+.&>.

參考鏈接:https://zh.wikipedia.org/wiki/Hello_World%E7%A8%8B%E5%BA%8F%E6%A0%B7%E4%BE%8B


作為一個用這個語言寫過不少有趣東西的程(shen)序(jing)員(bing)

我們來粗略地理解一下。

首先談到這個語言的定義和運行原理

該語言定義在這樣一個環境之上:

你有一列無限長的小火車,每個車廂里裝了一個數字,初始為0。

還有一個列車員,初始在最頭上那節車廂上。

好了,你把你寫的BrainFK程序交給列車員,列車員會做如下的事情:

  1. 從左向右、由上自下一個字元一個字元地讀取你的程序

  2. 當讀到`+`的時候,將所在車廂里的數字加一
  3. 當讀到`-`的時候,將所在的車廂里的數字減一
  4. 當讀到`&>`的時候,跑到後一個車廂去
  5. 當讀到`&<`的時候,跑到前一個車廂去
  6. 當讀到`[`的時候,如果該車廂裡面的數字為0,則跳去執行下一個`]`之後的程序內容
  7. 當讀到`]`的時候,如果該車想裡面的數字不為0,則跳去執行上一個`[`之後的程序內容
  8. 當讀到`.`的時候,將所在車廂裡面的數字翻譯成ASCII字元,顯示在你的屏幕上
  9. 當讀到`,`的時候,從等待使用者輸入一個ASCII字元,轉碼成數字寫進所在車廂里

接下來讓我們仔細看這段程序:

++++++++++[&>+++++++&>++++++++++&>+++&>+&<&<&<&<-] &>++.&>+.+++++++..+++.&>++.&<&<+++++++++++++++. &>.+++.------.--------.&>+.&>.

首先我們可以整理一下代碼,將其變為:

++++++++++
[
&>+++++++
&>++++++++++
&>+++
&>+
&<&<&<&<- ] &>++.
&>+.
+++++++..
+++.
&>++.
&<&<+++++++++++++++. &>.
+++.
------.
--------.
&>+.
&>.

為了提高可讀性,我在此(狂妄地)引入一個宏:

讓我們用 `oX` 來代替連續的X個o號吧!(比如 `+5` &<=&> `+++++`)

為了進一步提高可讀性,我在此(更加狂妄地)引入一個宏:

讓我們用 `//` 在後方形成注釋吧!

於是上面的代碼變成

+10 //火車頭車廂置為10
[
&> +7 //下一個車廂值加 7
&> +10 //下一個車廂值加 10
&> +3 //下一個車廂值加 3
&> +1 //下一個車廂值加 1
&<4 - //前移四個車廂,並將其值減 1 ] //此時列車員在第0車廂,如果該車廂內數字不為0,則跳轉到前面的`[`之後 //稍有常識的人都能看出,只要我們的程序執行到這裡之後 //前五個車廂的最終值就分別為 0, 70, 100, 30, 10 //此時列車員在第 0 車廂 &> +2 . //向後一車廂並+2然後輸出到屏幕,此時列車員在第1車廂,ASCII(72)="H"

&> +1 . //向後一車廂並+1然後輸出到屏幕,此時列車員在第2車廂,ASCII(101)="e"
+7 .. //列車員不動,將所在(第2)車廂內容繼續加7之後輸出兩次,ASCII(108)="l"
+3 . //列車員不動,將所在(第2)車廂內容繼續加3之後輸出,ASCII(111)="o"

&> +2 . //向後一車廂並+2然後輸出,此時列車員在第3車廂,ASCII(32)="&<空格&>"

&<2 +15 . //向前兩車廂並+15然後輸出,此時列車員在第1車廂,ASCII(87)="W" &> . //向後一車廂並直接輸出,此時列車員在第2車廂,該車廂保留原值111,ASCII(111)="o"
+3 . //不動,+3而後輸出,ASCII(114)="r"
-6 . //ASCII(108)="l"
-8 . //ASCII(100)="e"

&> +1 . //再向後一車廂,此時列車員在第3車廂,+1,ASCII(33)="!"

&> . //再向後一車廂,此時列車員在第4車廂,直接輸出,ASCII(10)="&<換行符&>"

所以事實上只用一個車廂就能寫就這個helloword,但是那樣寫程序就太長太沒有可讀性了。

所以我們需要在前面用一個循環,來初始化70,100,30,10四個車廂(並且預先用第0個車廂來標記循環次數),這四個車廂分別提供了對大寫字母、小寫字母、特殊文字元號、特殊控制符號的方便訪問。

說起來,這個語言簡單優雅易學,一旦熟悉之後,如果能藉助上面定義的數字宏,寫起來方便快捷讀起來行雲流水,簡直就是數組處理界的Regex(知乎為什麼不能給字加刪除線。。)

用這個語言可以有很多趣題,比如說我以前曾經想過一個問題:

能不能實現一個動態指針,即列車員讀取某車廂的數值,然後向後走該數值所表述數量的車廂,以操作目標車廂的數字。

這個問題後來是這樣解決的(摘自我之前的某個博客),下面這段文字里,`[X]`代表第X節車廂,指針即是列車員:

經過這樣的討論,之前的「是否可以實現指針的動態定位」問題變成了下面的問題:

是否可以找到一種方法,讓指針查找的時候,每一次移動都能夠找回原來的位置。

在這裡解釋一下,比如[4]用來存儲另一個地址,或者說[4]中存儲著一個自定義的指針,如果*[4] = 8,那麼當程序執行到[4]的時候應該將主指針定位到[12] // (12 = 8+4)

我們用BF語言試寫一下:

我們先初始化主指針到[4]:

&>&>&>&>

接下來我們將指針自增 *[4] = 8 次,問題就來了

如果我們只要自增八次,我們只要寫下&>&>&>&>&>&>&>&>即可

但是我們在編寫程序的時候可能並不知道[4]的值,或者說,[4]裡面是動態的

最早想到的一個辦法是指針每自增一次,*[4] - 1,但是事實上 *[4] - 1 這個語句在BF語言中就是一個 "-" ,但是主指針必須指向[4]。

如果我們寫 [&>&<-] 那麼到最後指針還是停留在[4]上

如果我們把過程拆開,就是[4]值先自減,指針自增,然後自減,值自減,然後自增兩次,再自減兩次,值自減,自增三次,自減三次,值自減,以此類推,直到*[4] = 0,然後指針自增8次

如果看到這你還沒笑,那你看了下面的代碼,一定就笑了:

- [

&>&<-

&>&>&<&<-

&>&>&>&<&<&<-

&>&>&>&>&<&<&<&<-

&>&>&>&>&>&<&<&<&<&<-

&>&>&>&>&>&>&<&<&<&<&<&<-

&>&>&>&>&>&>&>&<&<&<&<&<&<&<-

] &>&>&>&>&>&>&>&>

真是笑死了,這和&>&>&>&>&>&>&>&>沒有任何區別,[]循環形同虛設,還浪費資源,到頭來編寫者還是要在寫程序時得知*[4]

上面的代碼唯一值得讚揚的就是畫面美感

當然如果你寫成

-[&>&<-&>&>&<&<-&>&>&>&<&<&<-&>&>&>&>&<&<&<&<-&>&>&>&>&>&<&<&<&<&<-&>&>&>&>&>&>&<&<&<&<&<&<-&>&>&>&>&>&>&>&<&<&<&<&<&<&<-] &>&>&>&>&>&>&>&>

那就連畫面美感也沒了

那麼到底有沒有一種辦法可以在指針自增的同時,讓其還可以每一次自增之後再找回原來的[4]呢。

我腦袋比較笨,在討論的當天並沒有想出一個可行的演算法來。

想了很多很多辦法,包括用五個連續的地址存儲一個變數,[n]存儲值,[n+1],[n-1],[n+2][n-2]分別存儲變數的左右信息等等,但最後都被證明不可行。

然而當天清晨,我鬼使神差地夢到了自己用冒泡排序演算法解題。

醒來一拍大腿,哎呀我C,成了。

我所想到的演算法類似冒泡排序中冒泡的過程,如下:

指針每自增一次,[n]與[n-1]的值便交換,然後*[n]自減,重複這一過程直到 *[n] = 0

[n]與[n-1]的值交換,可以有兩種方法實現,一種是對*[n] 和 *[n-1] 進行異或,還有一種就是利用第三方[m]

鄙人不才,尚未找到在BF中實現抑或的辦法,所以決定使用後者。

那麼初步的演算法如下:

規定當*[n]表示一個地址時,*[n-1]必須為零,作為臨時區域,比如上面的例子,就必須*[4] = 8, *[3] = 0,該指針變數由[3]和[4]構成,長度為2,頭地址為[3]。

1 指針指向[n+2],將當前指針位記為[n]

2 [n-2] = [n]; [n] = [n-1]; [n-1] = [0]

3 [n]--

4 如果[n+1] 非零,跳轉到1

5 指針自減2

這個演算法用BF語言書寫如下:

[

&>&>

[&<&<+&>&>-]

&<[&>+&<-]

&>-&<

]&<&<

當然,如果你嫌行數太多看著難受,也可以寫成這樣

[&>&>[&<&<+&>&>-]&<[&>+&<-]&>-&<]&<&<

接下來把後面的所有內容向前移動兩格,填充那個多出來的部分就行了

當然,這段文字提出的方法是有缺陷的:

當列車員被正確定位之後,定位列車員所使用的那個車廂就被置0而無法再次使用(變數被破壞)了。

這個缺陷也是有解決方案,可以自己試試看哈哈哈哈


很顯然它試圖用循環省代碼。

暴力版大概長這樣:

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++.&>

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++.&>

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++.&>

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++.&>

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++.&>

++++++++++++++++++++++++++++++++.&>

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++.&>

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++.&>

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++.&>

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++.&>

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++.&>

+++++++++++++++++++++++++++++++++.&>

帥不帥氣?

這代碼太長,所以作者在輸出後面字母時復用了前面的值,其實可以拿一張ANSII碼錶對著數一數,就會發現每一個『.』之前數字剛好是『Hello World』各個字母的值(『.』是bf的輸出符,所以剛好可以在這裡用來對控制輸出每一個字元的代碼做分割)。

很有趣的是bf中字元串輸出比數學運算簡單多了。(畢竟bf是用的ANSII碼。。。)


雖然我不懂這種語言,但如果能輸出hello world,它明顯是在拼Ascii


推薦閱讀:

如何高效自學編程?
視頻網站的彈幕是如何保存的?
象棋和國際象棋的電腦程序是如何設計的?
為什麼在目前開發工資這麼高的情況下還是在知乎上看到很多程序員想轉行?
單片機編程最早是彙編,然後從彙編轉為c語言,那麼,c++會不會替代c語言來進行單片機編程 ?

TAG:編程 | 計算機 | 程序 | BrainFuck |