1. 程式人生 > >腳踏實地寫程式碼(1):說在前面

腳踏實地寫程式碼(1):說在前面

寫這個系列的博文,不在計劃之內。最近大部分時間都花在機器學習方面,因此數學部落格寫的比較多。初步的打算是循著數學分析,概率統計,最優化,隨機過程這條路徑展開。如果有時間的話,會再去學習泛函,拓撲學,測度論,代數,傅立葉。如果這些知識都有個全面的瞭解,我想機器學習沒有什麼方向是我們不能做的。

既然要講寫程式碼的事情,那不如從頭學起。畢竟,寫程式碼本身沒有什麼難度,難的是背後的數學知識。要寫好程式碼,不止需要分析學的基礎,還需要離散數學的知識。數學這門學科,如果要提綱挈領分一下,無非是連續與離散兩類。連續的基礎是分析學。“分析”這個詞,是從英語直譯過來,本身沒什麼可說的。可是我聽一個老師告訴我,“分”是微分,“析”是解析。這裡說的解析,是指解析式。我們常用的多項式,指數,對數,三角函式都是解析式。這樣的解釋是否全面,我不敢妄下結論,但確實對我助益很大。離散數學,包含的東西也十分豐富。圖論,數論,組合數學,代數,邏輯學都屬於離散數學的範疇。我個人認為,離散數學比分析學更難,分析學有章法可循,離散數學完全是天馬行空,智慧的迸發。記得我師兄常和我開玩笑說,編碼理論裡大部分證明都很短,不到一夜紙,但夠你看上好幾天。所以到現在,我對編碼理論,都是敬而遠之。

說了這麼多,只想說明一件事。想寫好程式碼嗎?學數學吧。

在講數學之前,還是先說說程式碼的事情。畢竟,作為一個碼農,寫程式碼才是天職。

所謂程式碼,就是我們想對計算機說的話。計算機被製造出來,是為了給人類幹活的。可是我們要它幹活,就得告訴它該幹什麼。也就是說,我們需要和計算機“通話”。這裡的“通話”加個引號是因為我們與計算機的通話是單向的。我可以向計算機發出命令,但是計算機不會與我們互動。計算機能做的,就是嚴格執行我們的命令。它永遠不會說:“我累了,不幹了”,也不會說:“你的命令不合理,我可以做得更好”。計算機是一個忠實的服務生,但是要它為我們服務,首先計算機要聽懂我們的命令,讀懂我們的程式碼。為了達到這個目的,就需要建立我們與計算機溝通的語言,我們稱為程式語言。

計算機畢竟是沒有思想的。因此,它能理解的語言,都是通過電路來實現。這就好比是一個電燈泡,我們控制燈泡的亮與滅,可以通過開關的開與合來實現。這樣,開關的開與合就是我們與燈泡之間通話的語言。當我們說“開”(接通開關),燈泡就會服從我們的指令點亮。當我們說“關”(斷開開關),燈泡就會熄滅。我們將開與關稱作是燈泡能識別的機器指令,而集合{開,關}稱作是一個指令集

一個開關和一個燈泡,可以設計出一個包含兩個指令的指令集,也即{開,關}。為了簡化起見,我們用1表示開,0表示關,這樣的指令集就變成了{1, 0}。如果我們設計一個電路,包含兩個開關和兩個燈泡,那麼我們的指令集中的指令就變成4個了,也即{00, 01, 10, 11}。其中00表示兩個開關都斷開,01表示第一個開關斷開,第二個開關接通。以此類推。顯然,不同的電路對相同的指令的響應結果是不一樣的。例如,如果我們將開關和燈泡都串聯在一起,那麼只有在我們傳送11指令(即兩個開關都接通)時,燈泡才亮。但是,如果兩個燈泡是並聯的,同時將兩個開關分別放在兩個燈泡的支路上,那麼我們傳送指令10時只有第一個燈泡亮,01時只有第二個燈泡亮,11時燈泡全亮。這就說明機器指令是與機器密切相關的

不同型號的機器有不同的指令集。不同的機器對不同的機器指令,響應可能不同。我們把指令中0和1的個數之和,稱作是指令的位數或者長度。例如,指令集{0, 1}中,所有指令的位數都為1;而指令集{00,01,10,11}中,所有指令的位數都為2。一般來說,一個指令集中指令的位數是固定,不會出現有的指令位數長,有的指令位數短的情況。因為使用變長指令,除了給我們的電路設計增加麻煩以外,毫無用處。

上面的例子中,只介紹了帶有四個電學原件構成的電路。而一臺計算機,其電學原件的個數以億記。因此,可以設計出非常複雜的電路,實現非常複雜的功能。但是,我們計算機的指令長度卻不是上億位的。畢竟,一條上億位的指令,儲存在哪裡,都是個大問題,更別說執行了。目前,我們計算機上使用的指令集都是64位的。也就是說,一條指令有64個0和1構成。一個64位的指令集,已經完全可以表示一個非常複雜的系統了。一個64位的01串,有超過10^{17}個不同的值,可以表示10^{17}個不同的指令。顯然,目前還沒有誰能夠設計出那麼龐大的指令系統。一個大型的指令集,也不過包含數百條指令而已。我們之所以把指令設計得那麼長,是為了節約執行的時間。這個問題有些複雜,後面再說。

一個指令集再龐大,它也只能執行有限多個任務。因此,在實際應用中,我們不可能只執行一條指令就完事兒了。這就需要考慮時序的問題。例如,我們可以讓燈泡先開後關,或者先關後開,或者開兩秒關一秒。所謂時序,就是指令執行的先後次序。而一段程式碼,就是一個指令的序列,它告訴計算機先執行哪條指令,再執行哪條指令。而語言是什麼呢?語言是所有合法的程式碼構成的集合。如果非要做類比的話,我們可以把指令當做是一個個的漢字,而一段程式碼是用漢字書寫的文章或者語句。我們的漢語,就是所有合法的文章和語句構成的集合。漢字再多,也不過數萬個,而用它書寫的錦繡文章,卻不計其數。

如果碼農能把程式碼寫得驚世駭俗,那麼他就不是碼農,他是計算機藝術家。是的,程式設計是一種藝術。世界上有許多偉大的計算機藝術家,其中之一是高納德。它編寫了一個tex排版軟體。我現在能夠在csdn上很方便地寫出各種公式,全仰賴這款軟體。他在tex軟體完成的那一年,發起了一個懸賞遊戲,誰能找到他的bug,獎勵2.56美金。(256,碼農需要銘記的數字之一)。此後每年,獎金翻倍。只要是對等比數列有所瞭解的人都知道,這個遊戲可以讓高納德變成世上最大的負翁。可是,結果並非如此。只有兩個人,最後認領了這份獎勵。要寫出完美無缺的程式碼,需要極大的耐心和細心,以及驚人的記憶力。因為程式碼一旦上千行,過上一兩個月,你自己寫了什麼,早就忘得一乾二淨。所以在公司幹活的人,必須要兩臺顯示器,一臺看以前寫的程式碼,一臺寫新的程式碼。高納德有一套書,叫《計算機程式設計藝術》。比爾蓋茨說如果誰能做完書上所有的題,就直接來微軟上班吧。比爾大叔說這話有些言過其實。微軟並沒有那麼大的面子。一個人演算法功底達到了一定的高度,想要去哪裡做什麼工作,那完全看他的心情,又豈會拘泥於區區一家公司。當然,我離計算機藝術家還差得很遠。但是,這應該是每個碼農最終的目標。《計算機程式設計藝術》這本書,總共計劃出七卷,目前第四卷的上半部分已經出版。同學們慢慢看吧。

當然,我們說的程式設計,很少需要去寫機器程式碼。機器程式碼有兩個問題,導致它很難廣泛使用。1. 機器程式碼都是由0和1構成的,可讀性極差。2. 機器程式碼與機器的內部電路密切相關,相同的程式碼在不同機器之間不通用,可移植性差。為了解決這兩個問題,便有了組合語言和高階語言。

組合語言,是指利用助記符代替機器指令和運算元的一種語言。這樣,我們在編寫程式碼時,就不需要直接面對01串,增加了程式碼的可讀性,也減少了編寫出錯的概率。例如,下面的程式碼執行了兩個整數的相加運算。

MOV %eax,[%esp+8]
MOV %ebx,[%esp+12]
ADD %eax, %ebx

上面這段程式碼看不懂的並不打緊,因為我也不是特別懂。大體來說,程式碼共有3行,每行對應一條機器指令。第一行,是將第一個整數從記憶體拷貝到暫存器eax。第二行是拷貝第二個整數到暫存器ebx。第三行,將暫存器eax和ebx中的兩個數相加。

組合語言對於軟體開發,還是存在很大的難度。因為我們需要了解機器的所有細節。它的暫存器有哪些,都是做什麼用的,記憶體有多大,資料放在記憶體的哪些位置。考慮這些問題當然是很有價值的,因為它可以讓我們最充分地利用我們的機器。可是,當我們開發一個大型的軟體時,我們希望把所有的精力都放在業務邏輯上面,而不想被硬體的細節分散精力。另外,我們也不希望為不同的機器開發不同的軟體,我們希望我們的軟體具有通用性。於是高階語言便應運而生。

高階語言克服了機器語言和組合語言可讀性和移植性差的兩個缺點。

第一點,高階語言,都是由一些簡單的單詞和有意義的功能名稱組成,因此具有極好的可讀性。例如,在C語言中,要實現兩個數的加,可以通過下面的程式碼實現。

a+b;

上面程式碼中,a和b是兩個變數。所謂變數,與數學函式的自變數類似。它可以取定義域內的任意值。至於這兩個變數具體取什麼值,是由實際的需求而定。

第二點,高階語言幾乎是與硬體無關的。我們在用高階語言程式設計時,只需要實現核心的業務邏輯,而不需要關心資料儲存在記憶體的哪塊地址空間,什麼資料要放在哪個暫存器。當然,這樣必然會引出一個問題。我們的計算機是讀不懂高階語言的,它不知道什麼是變數,什麼是功能模組。它能夠識別的,是一個個具體的指令。因此,必須建立一個高階語言到組合語言和機器語言之間的橋樑。這個橋樑叫編譯器。所謂編譯,是將高階語言,翻譯為機器可以識別的機器語言。因為機器語言是與機器有關的,因此編譯器也與機器有關。當然,一個成熟的編譯器,它可以相容多種硬體平臺。這樣,我們就完全不必考慮硬體的細節了。

高階語言解決了組合語言可讀性差,機器相關的問題。同時,它還有一個好處,就是它封裝了許多成熟的庫函式,給了我們極大的方便。以C語言為例,當我們需要向終端輸出一個字串時,我們只需要編寫如下語句。

printf("My statement.");

這裡的printf是一個庫函式。圓括號中由雙引號包含的內容My statement.叫做字串。這個字串是我們傳遞給printf函式的一個引數。一個函式的引數,相當於數學函式裡面的自變數。當然,我們在研究數學時,自變數可以是實數。而在C語言中,自變數不僅可以是實數,還可以是一個字串。因為C語言中的函式,不僅僅是計算一個數值,它還可能是執行某個操作。我們這裡的printf函式,就是向終端列印字串"My statement."。

這裡的終端,是一個黑乎乎的視窗。當我們在windows的開始選單輸入“cmd”,搜尋到一個叫做“命令列提示符”的應用程式,並開啟它時,我們就看到一個黑乎乎的視窗,這就是一個終端。有了printf函式,我們可以很方便地向終端列印各種訊息。我們只需要關心訊息的內容是什麼。至於終端的大小是多少,已經顯示了什麼訊息,我們的訊息應該顯示在終端視窗的哪個位置。這些都有printf函式處理。

我們已經瞭解了高階語言的優點。可是世上的高階語言種類有幾千種之多。我們應該選擇哪種語言入門呢?這裡,我建議必須從強型別語言開始。強型別語言是相對於弱型別語言而言的。出名的強型別語言有C/C++, C#,Java,Go等,其特點是特點是運算速度快,但是靈活性差。出名的弱型別語言有Javascript,Python等,其特點是靈活性高,但運算速度慢。弱型別語言大多是指令碼語言,用弱型別語言編寫的程式碼往往不需要編譯,它有一個解析器,一邊解析程式碼,一邊執行程式碼。而弱型別語言的解析器,都是用強型別語言開發的。例如Python的解析器是用C語言開發的。這就是為什麼弱型別語言執行效率相對較低。當然,從強型別語言著手,其根本原因不是因為效率問題,而是因為如果沒有強型別語言的基礎,弱型別語言是不可能寫好的。弱型別語言入門簡單,但是精通很難。

C語言是所有語言的鼻祖,這是毫無懸念的。我們常用的作業系統,如windows, Linux, iOS, Android,無一例外,都是用C編寫的。這就奠定了C語言無可撼動的地位。因為任何機器都不能脫離作業系統存在。否則,我們不得不回到茹毛飲血的機器語言時代。同時,由於作業系統是用C語言編寫的,因此C語言包含了很多的機器硬體特性。如果我們想徹底瞭解計算機,網路,資料庫如何運作,學習C語言是不二法門。最後一點,C語言作為第一個被廣泛使用的語言,其它的語言或多或少,都有C語言的影子。因此,掌握了C語言,學習其它語言都是信手拈來的事。