列印自身的程式

2021-04-12 22:16:27 字數 3802 閱讀 2231

這篇文章發表於2023年第《csdn開發高手》第5期。本來是投稿給《程式設計師》雜誌的,但是給「調劑」到《csdn開發高手》上去了,是一大遺憾。《csdn開發高手》目前已經停刊。

#include

int main() "; printf( s, 10, 34, s, 34 ); return 0; }

「列印自身的程式」雜談

「寫乙個程式,其執行結果是把程式自身列印出來。」

這個論題我在網上看到過若干次,不過真正意義上的回答卻從來沒看見過。學過計算理論的高手們應該都知道答案,但是大多數程式設計師們可能沒有精力去啃理論書籍,就讓我對這個有趣的命題來做個介紹吧。

首先要澄清,什麼是「列印」,什麼是「自身」,以及程式執行環境的限制。「列印」可以有各種理解,比如向螢幕輸出,向磁碟輸出,向印表機輸出,向記憶體輸出,等等,但是本質上都是一樣的,因此不做限制。

「自身」的一種理解是源程式,另一理解是機器碼。提到「機器碼」這個名詞,你可能立刻就想到了「病毒」了吧,對,病毒就是最典型的自我列印程式,病毒的起源「磁心大戰」就是各個程式在儲存器中複製自己搶地盤,發展到現在更是豐富多彩,從傳統的檔案到時髦的email,無所不用其極。

但是各種病毒型的自列印程式都有乙個限制,即需要宿主系統提供的服務才能完成關鍵的「得到自己」的動作。如果自身**在記憶體中,如何才能知道其起始位址呢?問作業系統吧。或者讀取指令指標的值再做調整,這其實也是利用了執行環境之一的「指令指標」。又如郵件病毒,甚至不需要知道自己在**,只要呼叫軟體的「**」介面就行了。作為病毒當然是不錯的構思,但是要作為「列印自身」的題解,顯然就是耍賴皮了。推向極端,作業系統提供乙個「把我自己打出來」的api,程式呼叫一下不就完了?

因此,限定程式的執行環境是討論問題的必要前提。現代計算機和作業系統的「外部環境」實在太複雜了,搬出圖靈機理論在這裡也不太適合,乾脆就這麼定義命題:使用某種高階語言(比如說c語言),除了螢幕輸出(如printf函式)以外不得使用其他系統相關函式和io函式,列印出程式自身的源**。雖說這種定義細究下去也有許多含糊的地方(比如,c語言規定全域性變數的初值會自動賦為零值,這個特性就不應該被允許),我們還是把它拋到一邊,趕緊進入正題吧。

為了簡化討論,我們假設有一種智慧型的print語句,所謂智慧型就是說,看到語句print x時,程式會自動根據x的型別按照常規把x的內容打出來,還有就是可以隨著我們討論的進行它可以自然獲得我們希望它有的特性,這樣我就不用一上來就很突兀地給它定義很多特性。

好,不管怎樣,程式裡一定是會有一句列印語句的:

print x [1]

執行後螢幕上就出現了x的內容。

為了滿足「列印自身」的要求,必定還有一句把上面那句中的「print」給打出來:

print y [2]

最簡單的當然是寫成這樣:

print print x

這裡,第乙個print自動認出了後面的是乙個字串常量,將之列印出來。

我想,你一定意識到了,這樣做是行不通的,因為每次都會多出乙個print,這樣下去就陷入了無限。

[花絮],如果允許無限長的程式話,倒真是可以這麼寫程式:

print

print print

print print print

......

第乙個print因為沒有引數,所以什麼也不打。還有一種特殊的程式是「空」,就是一句話也沒有的程式。呵呵,這篇文章以趣味性為主,千萬不要找我抬槓喲。

[/花絮]

因此,[2]中的y必然不能是常量,而是間接引用的變數。或者說就是一段儲存,裡邊放著乙個串「print」,後面跟著x的內容。給這段儲存起個名字叫做s,型別是字串,這樣,[2]就成為:

print s [3]

現在[1]已經由[3]列印出來了,[3]又由誰來列印呢?只有[1]了。於是,我們的程式變成:

print print s [4]

print s [5]

雖然看上去有迴圈依賴的嫌疑,但是關鍵點在於,[5]是個間接列印,只是把某個固定起始位址的串傳送到螢幕上,是個完全固定的操作,並不依賴於其他語句!更進一步,[5]也不一定非要是print語句不可,可以換成任何固定的對s的操作序列,而[4]只需要把[5]的語句原汁原味列印出來就可以了:

print a(s) b(s) c(s) [6]

a(s) b(s) c(s) [7]

[6][7]就是解決「自我列印」的基本方案。也許並不是非常明顯,讓我對它做乙個特殊的解釋就清楚了。

假定s不是普通記憶體,而是字元模式下的「視訊記憶體」,也就是裡邊有什麼字元螢幕上就會出現什麼字元;而a(s) b(s) c(s)是作用在字串s上的一連串操作**,對s操作完成後s的新值成為 print s [換行] s。在這種解釋下,[6][7]就是乙個「自我列印」程式。

緊接著我們用乙個簡單的技巧把「視訊記憶體」給乾掉:

s=a(s) b(s) c(s) [8]

a(s) b(s) c(s) [9]

[8]不直接列印到螢幕,而是列印到字串s。[9]中,a(s) b(s) c(s)把s變換為 's'= [換行] (其中's'指單個字元s,指s的內容),然後在螢幕上列印出s。

這就是「列印自身」的乙個解的模式,可以適用於任何高階語言。如果把a(s) b(s) c(s)具體寫出來的話,就是這樣的偽碼:

s= s="s="++"/n"+ print s [10]

s="s="++"/n"+ print s [11]

當然這個寫法中的語法是需要「靈活理解」的,如果真的要用現實的程式語言來寫的話,還有許多細節要處理。文章最後附有乙個我用c語言寫的程式,有興趣的話可以自行分析。核心思想就是[10][11],只是用了很多技巧來處理字元。

主題內容講完了,接下來就是雜談了。

其實,如果用自然語言來描述程式的思想,其實可以寫成這樣:

把下面這個字串抄兩遍,並且在第二句上加上引號。

「把下面這個字串抄兩遍,並且在第二句上加上引號。」

之所以要用兩句話,是因為程式語言中沒有「把我自己抄一遍」的對應物。抽象的「自我指稱」可是很危險的東西喲,「羅素悖論」就是這麼來的。

乙個很迷惑人的觀點是,工具機比螺絲複雜,汽車廠比汽車複雜,創造者要比被創造者複雜。而生命是可以自我複製的(繁殖),因此生命無法用機械原理來理解。假如是在2023年(dna是2023年發現的),你該如何反駁呢?我們的「自我列印」程式證明了,「創造者要比被創造者複雜」是錯誤的。我們的程式加以擴充套件,可以攜帶任意的資訊,執行任意額外的動作,而仍可以完全複製自身,這就是計算理論中的「遞迴定理」。20世紀三四十年代,在現代電子計算機還沒有誕生的時候,計算理論的先行者圖靈、丘奇、哥德爾等就已經解決了「什麼是計算」、「什麼是可計算的」等深刻的問題。我們這些現代的程式設計師,在解決了「程式設計技巧」、「專案管理」等吃飯相關的問題之餘,如果不花點時間去領略一下計算理論的美麗與神奇,不是太可惜了嗎?

#include

char buf[100][1000]; int cur=-1;

void p1(char *p) //record p to buf

void p2(char *p) //print p and change line

void p3() for(i=0;ivoid main() ");

p1("void p2(char *p) ");

p1("void p3() for(i=0;ip2("#include ");

p2("char buf[100][1000]; int cur=-1;");

p2("void p1(char *p) ");

p2("void p2(char *p) ");

p2("void p3() for(i=0;ip3();

}#if 0 //read p3() convinently

void p3()

for(i=0;iputchar(125); //'}'

putchar(10);

}#endif

null

列印自身的程式

閒來沒事,想起還有這樣乙個玩意,所以就做了,不知道符合要求不 記得以前有個很短的,不過無法通過編譯 file name print self.cpp author keakon create date 2006 5 27 last edited date 2006 5 28 description ...

列印自身的程式雜談

這篇文章發表於2004年第 csdn開發高手 第5期。本來是投稿給 程式設計師 雜誌的,但是給 調劑 到 csdn開發高手 上去了,是一大遺憾。csdn開發高手 目前已經停刊。include int main printf s,10,34,s,34 return 0 列印自身的程式 雜談 寫乙個程式...

有趣的發現 列印自身源程式

最近看 找規律的列印表 問題,就無意間看到了這個 列印自身源程式 問題,不禁感嘆有時候 真的神奇呀,學的越多就覺得自己知道的越少 作為真正的 quine 有一些約定 程式不能接受輸入或者是開啟檔案,因為那樣就可以直接輸入源 或者是把源 檔案直接開啟再重新列印出來,就沒有什麼意思了 同時,乙個完全空白...