2011年12月20日 星期二

C 語言入門 - 在線上批改系統練功

如何練習使用基本語法

  自己出個練習題試著寫寫看?寫完隨手測幾個數字發現正確無誤就好了嗎?這樣其實比較辛苦,不如我們找找網路上既有的練習題試試,說不定別人想到我們沒想到的巧妙題目,寫完了只要上傳程式碼還能自動判斷執行效率以及正確與否,多麼方便啊!這種網站去哪找?網路上現在不少,這裡介紹個我比較熟悉,也相當常見的網站。


UVa Online Judge

  台灣俗稱 ACM 的網站。由來是它收錄了不少 ACM ICPC 競賽的題目。詳細介紹留待之後的文章。網址如下:

http://uva.onlinejudge.org

  雖然是英文的,不過也有些好心人翻譯了不少題目出來。網址如下:

http://163.32.78.26/

  網站本身不太穩定,如果死掉了可以先用別人架的鏡像站。但是鏡像站的資料較古舊,有些比較新的翻譯可能沒更新到。但也已有相當數量的題目翻譯了。如果原網站沒問題,建議還是不要用鏡像站。以下是其中一個鏡像站:

http://w.csie.org/~b97115/luckycat/

  但是並非所有題目均有翻譯。翻譯的速度通常是跟不上寫的速度,也與各譯者擅長與不擅長有關,日後仍有必要直接閱讀英文題目。現階段主要是在這裡可以方便找到許多難度僅有一顆星的題目。未翻譯的題目中也有許多難度僅有一顆星的程度,但是並不好找,對初學者而言更無從著手。就以目前到這篇為止,也不足以解開所有一顆星程度的題目。文末會列出此時較適合寫的題目。

  首先我們連上 UVa 的網站,左上角登入欄的底下有個 Registre 的連結,點它。Username 是你的帳號,而 Name 是你的名字。如果你沒有使用過它的舊網站,也沒印象持有舊網站 ID 的話,在 Online Judge ID 填上 00000,否則填上舊網站 ID 則可以把舊帳號整合過來。Results Email 則可以自行選擇勾或不勾。勾的話你之後有上傳題目,它會把結果寄到你的信箱。沒有勾的話也能在網站上看到結果,所以不影響。如果不嫌它很煩的話建議是勾一下,好處是它若發現某道題目需要重新判定大家上傳的程式碼正確與否 (通常是測試不夠嚴謹以致於錯誤的程式碼被判成正確) 的話,在收信時會發現。

  之後請到信箱收認證信,收到後就完成註冊了。接下來在 UVa 登入後,就可以上傳你的程式碼了。請注意如果勾選 Remember me 再登入的話,以後有可能導致沒辦法連上。如果遇到這種情形,請開啟 Chrome 的無痕瀏覽,或 Firefox 的私密瀏覽試試,或是把瀏覽器的 cookie 清掉。

  目前能解的題目,有以下幾題:

10071
10300
11172
100
488

  未列出不代表不能解,這裡只是列出可解的幾道題。但是在開始寫之前,有幾個地方需要特別注意。

  這篇文章放在這個順位,理應存在相當多目前難以說明的東西,硬要作說明則會出現不少新東西要記,也有許多很難講明白的地方。這裡已經盡量講得淺白易懂,若一時記不完可以先大致看過,等到寫一寫出問題再回來查。


征服 UVa 的行前準備:了解線上批改系統

  首先我們必須了解它的基本規則。這類網站人稱 Online Judge,多譯作「線上批改系統」或類似的詞。通常會置有許多題目供人練習,題目會給詳盡的敘述,讓你知道你必須寫出具備什麼功能的程式。你的程式必須能夠接受題目所規定的指定格式的輸入,依題目指定的格式輸出結果。通常會有幾組作為範例的輸出輸入供參考,也可用作測試程式是否正常運行,且給出預期答案。

  線上批改系統之所以有「批改」二字,是因為你可以將你的程式碼上傳到該網站,它會為你測試你的程式。但是程式是無法判斷程式對錯的,又不可能進行人工檢測。因此,最常見的方法是,準備一定數量的測試用數據,稱為「測試數據」,又稱「測試資料」或簡稱「測資」,將你的程式編譯並執行後,餵入測試數據,看你的程式能否在可允許的時間內,產生正確的結果。

  線上批改系統會測試你的程式是否能正常編譯,運行過程有無不正常結束 (也就是當掉) 以及能否在時間內,對於給定的測試數據,將結果計算完畢並輸出。最後,如果前面都通過了,就會檢查你的結果是否正確。結果如下表:

Accepted (AC) - 測試結果正確。
Presentation Error (PE) - 測試結果正確,但格式上稍有不符。
Wrong Answer (WA) - 測試結果不正確,缺漏或夾雜不相干的輸出,或格式嚴重不符。
Time Limit Exceeded (TLE) - 程式無法在允許的時間內完成計算。通常是沒寫法,也可能是方法不夠有效率。
Run-Time Error (RE) - 程式不正常結束。通常是當掉,像除以 0 或是拜訪不該拜訪的記憶體位址。
Compilation Error (CE) - 編譯錯誤。可能選錯語言,傳錯檔案,複製不完全,或你的程式碼根本有問題。

  它餵入測試數據的方法是,執行你的程式,將存放測試數據的檔案導入至輸入中,並將你所有的輸出導出至檔案,再對該檔案進行核對。

  大部份題目會有「多重輸入」。大致有三種形式,題目都會詳述。一種是會在一開始輸入一個數字,代表接下來你必須處理多少組輸入,這種相當容易,先讀完第一個數字就知道有幾組了。第二種是當特定的輸入出現,比如說輸入物品數是 0 或是 -1 的時候,代表的是輸入已結束。第三種較特別,就是它不會告訴你。你必須自行判斷輸入是否已結束。由於檔案不論再大,都一定有固定的大小。在讀到檔案尾的時候,scanf() 或其它讀入用的函數,會告訴你已經到檔案尾了。scanf() 是透過回傳 EOF (即 End-of-File) 來告訴你。所以我們可以寫成這樣:

while(scanf("%d", &n) != EOF)
{
}

  來做反覆的輸入。while 你可以將它看作是沒有 A 和 C 區段的 for。這樣一來,如果輸入還不是 EOF 就會繼續執行程式。若要在程式執行中手動模擬輸入結束,可以輸入 CTRL+Z。按了之後會顯示 ^Z,按下 ENTER 就相當於遇到 EOF 了。

  輸出是否正確,是在程式結束後,才進行判斷,因此留待最後一次輸出,和每讀入一組,便處理後輸出一組,最後得出的結果會完全相同。由於只看輸出,所以你在手動輸入時打入的東西,會確實出現在畫面上,實際上不存在最後的輸出結果中,這點也要注意。

  千萬不要輸出任何關於提示輸入的文字,或其它訊息。例如「please enter a number」之類的絕對不要。這夾雜在輸出中只會被認為你的輸出結果是錯誤的。

  請務必留意換行問題。每一行的「結束」之時,請一律輸出「\n」。這符號雖被稱為「換行符號」,實質的意義卻不是告知「我們需要新的一行」,而是宣告「目前這一行結束了」。請在每一行的結束之時確實輸出「\n」而不要只在需要新的一行時才輸出。

  請務必詳讀題目在輸出格式中,關於「空白行」的敘述。空白行即是什麼都沒有的一行。若是一行之中完全沒有任何東西,直接遇上「\n」的話,就代表它是個空白行。

  通常有以下幾種情形,一種是沒特別要求,這種別輸出多餘空白行即可。要是好心怕批改時搞混而每組之間多輸出一行空白行,也會被認定是錯誤。一種是每組數據「之後」空一個空白行。這意味著最後一組數據「之後」也要空一個空白行,所以一律多空一行即可。一種是每組數據「之間」空一個空白行,這比較麻煩。像是以 EOF 結束輸入的情形,我們不會知道有沒有下一組,所以不能隨意多輸出空白行。幸運的是,我們可以判斷這是不是第一組數據,如果不是的話,在輸出這組結果之前先輸出一個空白行即可。

  判斷方法相當容易。我們用個變數記錄現在是不是第一組數據。一開始還沒輸入時預先設定為「是」,在第一組處理完之後,將這個變數改為「不是」。至於如何使用變數記錄「是」與「不是」,我們可以用 0 或 1 代表。C 語言中 0 即代表「不是」,1 代表「是」。這樣我們就能夠判斷目前是不是第一組。

int first;
first = 1;
while(scanf("%d", &n) != EOF)
{
    ....
    if(first != 1)
    {
        ....
    }
    first = 0;
}

  如此一來,在第一組時 first 的值會是 1。第二組開始就會是 0。

  如果空白行多了或少了,幾乎都會被判成 Wrong Answer。在得到 WA 的結果時,不妨先檢查看看換行是否正確。

  當輸出要求在一行中輸出多個數字並以空白隔開時,請詳細按照敘述去做,別空太多格或是完全不空格,也別在一開始或最後一個數字之後,輸出空格。雖然最後一個空白是看不到的,但它實際存在,批改系統是能夠判斷出來的。

  請不要在程式中保留 while(1); 之類的程式碼。前面說過這方便我們觀察程式輸出結果,因為它「會讓程式無法自行結束」。前面提到有一種錯誤是「在允許的時間內無法處理完所有數據,輸出結果並結束程式」,也就是 TLE,如果放了 while(1); 且執行到了的話肯定是「無法在允許時間內結束程式」,因為它根本不可能「自行結束」。我們知道處理完了,所以看完結果後會手動結束它,但是批改系統可不知道。它可是一板一眼不知變通的。

  儘管我們學習的是 C 語言,但目前上傳時,最好在語言部份選擇 C++。UVa 上面使用的是 ANSI C,這版本的編譯器相當嚴格,容易出現編譯錯誤。

  這樣一來,我們對批改系統有了初步的認識,也擁有了解決批改系統上的基本題的能力。雖然我們連程式的基本語法都還沒有學全,但從此時開始練習,有助於增進對這些基本語法中的基本更加了解與熟練,之後學習其它語法會更加輕鬆,也能更快上手。語法是累積的,而不是分開單獨存在的,從根本開始練習有助於學習更進一步的東西。

  請記得保留你每一題的程式碼。若是使用公用電腦,可以使用 E-mail 夾帶程式碼的檔案,寄給自己。未來會有諸多好處。程式碼通常也不大,就算完成了數千道題,每題都寫到檔案大小 10K (這需要 10240 個字) 也不到 100MB。一部小說通常也不到 10 MB以上的,不用擔心硬碟沒地方放遊戲或其它東西。定期把執行檔清理掉即可,留著程式碼的話執行檔要生幾份有幾份。

  保留程式碼的用意並非要你用在相似題上。請紮紮實實地從頭完成你的每一道題目,而不要剪剪貼貼的,自己思考整個程式的架構與方法,並自己親手打上每一個字。這對於寫程式的經驗絕對是有益無害的。你會對語法更加了解,對程式的運作以及之後學到的每個方法更加熟悉,儘管這比較花時間。

  即使請教他人也不要流於抄襲,在聽完高人指點與精闢分析之後,試著自己從頭思考一遍,親手實作一次,盡量不要參閱程式碼。程式碼建議在解決之後再作參考,看看是否有更好的寫法。在自己親手實作前便參考的話,容易受制於殘存的印象,而只是試著「重現」,並非自行思考每一步的意義並寫出來。這樣會比較辛苦,但是完成後會直接刻在內心,日後更能運用自如。

  題目也不要寫過就算了,之後想到或知道更好的做法,或是有別的想法,都可以多寫幾次試試看。題數也並非絕對重要,要在寫的過程慢慢累積經驗,多嘗試多學習。能從寫題目中獲得多少才是最重要的。

2011年12月16日 星期五

C 語言入門 - 運算式及其真意

進一步了解運算式

  到現在為止,我們還未對運算式有更進一步的了解,只在介紹 if 以及變數時稍有提及。運算式 (Expression) 包含了零或多個運算子 (Operator),零或多對小括號,以及至少一個運算元 (Operand) 所組成。事實上它是個相當重要的概念,在 if 篇說到它是敘述的一種,而 C 語言主要由敘述所組成,由此可見一個程式碼的主體有絕大部份都是運算式。到底什麼是運算式?讓我們一起來了解它吧。這會對你理解 C 語言帶來相當大的幫助。


何謂運算子與運算元

  運算子,簡單地說就是運算方式,例如「+」、「-」等等,而運算元則是被運算的對象,通常是數字或變數,例如「3」、「-5」等等。其中由我們在程式碼中寫出固定數值的,例如「3」或「-5」,這些是寫死的,並不像變數般可以改變,這些我們稱為「常數」(constant)。

  舉個例子,3 + 5 * 7 這個運算式,包含了兩個運算子和三個運算元。運算子分別是「+」和「*」,而運算元則是「3」、「5」以及「7」。在 C 語言中,是以「*」代表乘法,「/」代表除法。其中斜線有兩種,一種是斜線「/」,一種是反斜線「\」,小心別搞混了。記法可以用分數去記。分數記作 1/2 而不是 1\2,這相當容易記。因為分母在後面,所以用斜線在直立起來後,分母才會在底下。用反斜線就會反過來前面的數字在底下,所以不對。記住反斜線不是運算子。

  在 C 語言,運算子又分成三種,一元運算子、二元運算子、以及三元運算子。它們差在哪兒?答案是,它們能,或者說,它們需要幾個運算元來做運算。例如加法會是二元運算子,因為它需要兩個運算元來計算,只有一個運算元是無法做加法運算的。負號則是一元運算子,它將一個運算元變號,從負變正,或者從正變負。三元運算子比較特別,而且只有一個,這個留待以後補充。每個運算子在工作時,會吃掉被用來作運算的運算元,消化後轉化成一個新的運算元,消化方式與結果則視運算子的運算方法而定。

  在運算子的世界中,也有一定的規矩存在。C 語言是循序的語言,無法同時進行多件事情,包括運算。即使在同一個運算式中,運算子的運算也是有順序的。吃掉一些運算元並轉化成其它運算元,是它們的工作,所以必須按照一定的規矩。而工作先後順序,會按照它們的工作以及位置,決定應該先做還是後做。

  我們可以將一個運算式,看成是一座工廠,運算子是工作人員,而運算元則是原料。我們最後的目的是把原料按一定程序,製作成最後的成品,也就是該運算式的運算結果。優先度高的負責把小原料吃掉,轉化成小組件。優先度低的則是負責最後的大組件合成,所以必須等小組件都完成了才能開始動工。我們舉個小例子來看看。

3 + 5 * 4

  「*」先進行運算,它將原料吃掉轉成小組件,再交由「+」把整個都吃掉組合起來。整體的工作流程雖然是「*」先動作,但是實際上主宰整個工作流程的是「+」,而不是「*」。優先度低的「+」就相當於是負責全盤計劃的主管,流程上要先問過「+」,它說需要「*」來組合小組件後,才去命令「*」先進行工作。這個先後順序相當重要,也相當容易被忽略或誤會。現階段看起來一樣,因為結論總是「*」先工作。文末會提到相關例子。就因為怕造成誤會,這裡才以工廠工作先後為例,而不是進食先後。

  如果運算優先度一樣的時候怎麼辦?答案是視運算子而定。一般以數學而言,左邊優先運算,所以對於所有的「算數運算子」都是左邊優先。不過在其它類型的運算子也有右邊優先的例子,這個先放一邊不管。最常見的還是算數運算子。我們舉個例子。

3 * 4 / 2 / 3

  乘除的運算優先度相同。所以會先計算左邊的,我們加上括號會比較清楚。

(((3 * 4) / 2) / 3)

  我想應該沒有人會先將 2 除以 3,再將 4 除以 (2/3),再乘以 3 的。這個優先順序的由來是數學,所以這裡不多作說明。



括號視為絕對優先事項

  括號也是很重要的一環。它讓你可以無視運算子優先順序,確保按照你希望的順序進行。請習慣數學的人注意了,在 C 語言中只有小括號是決定運算優先順序用的,中括號與大括號各有其用途,並非是用在決定運算優先順序,這點要切記。這是程式語言,不是數學。雖然多半相同,卻是兩個不一樣的世界。即使需要多層的括號,全部都用小括號就對了。不確定哪個先運算時也可以加上小括號,確保它們的順序。


  剛剛提到了算數運算子,那麼是不是還有其它類型的運算子?沒錯。除了一元、二元等等分法之外,也可以依照其種類來分類。


算數運算子

  基本上就是算數會用到的運算子,除了基本的「+」「-」「*」「/」外,還有「%」這個特別的運算子。它也是做除法,但它的運算結果不是商,而是餘數。其它還有負號「-」也算,會吃掉一個運算元後,將其正負反轉。值得一提的只有「/」和「%」如果除數為 0 的話,會造成程式當掉。儘管它們非常想為你完成任務,但除以 0 它們不知道該怎麼辦,只好以死謝罪了。


比較運算子

  多半用於條件式中,作為比較之用。有大於「>」、小於「<」、等於「==」、大於等於「>=」、小於等於「<=」、不等於「!=」六種。比較需要注意的是,「大於等於」以及「小於等於」是不可以寫反的,例如「=<」是不行的。寫成「≦」也是不行的。被承認的只有「<=」和「>=」兩種寫法。「等於」必須是兩個等號,若只有一個的話是代表賦值的意思。它們長得很像,但是卻天差地遠。它們各有代表的符號而且不會變動,請特別注意。

  它們都是二元運算子,會吃兩個運算元來做運算。運算的結果只有「零」和「非零」兩種。C 語言中「零」代表「false」的意思,也就是錯誤。「非零」則代表「true」的意思,也就是正確。雖然 true 和 false 是以「零」和「非零」來認定,但運算子算出來的「true」與「false」的結果則多以 1 和 0 來表示,通常不會以其它非零數字表示 true。

  之前提到的 if 也是這麼認為的。嚴格來說 if 吃的並不是條件式,這只是比較好理解的說法。if 吃的是運算式,如果結果是「零」,也就是錯誤,就做 else 抓住的敘述,而保護自己抓著的敘述不被執行;如果沒有 else 就不做事。如果結果是「非零」,也就是正確,就執行自己抓著的敘述,如果有 else 就保護 else 抓住的敘述不被執行。所以以下的寫法都是正確的:

if (a)

if (3)

if (a+b*c)

  條件式其實只是運算式的一小部份,因為比較運算子也是運算子,所以也會按照上述的運算式求值的過程來計算結果。廣義的來看 if 並沒有侷限在條件式。比較運算子的運算優先度比算數運算子低,因為通常會先運算完才會進行比較,例如:

if (a+3 <= b*2)

  有些人可能還沒將思考轉換到程式上,還停留在數學上,可能就不把比較運算子當運算子,而寫出以下的條件式:

if (10 <= a < 20)

  這個我們不能說它是錯的,因為它在語法上沒有錯誤,而且可以正常執行,只是結果可能和預期不符。為什麼呢?通常這樣寫是希望 a 在 10 到 19 之間時可以執行某些敘述,但是這是數學的看法。在 C 語言中它會被作為運算式來解讀,結果會變成:

if ((10 <= a) < 20)

  如果 10 <= a 則運算結果會是「非零」,通常是 1。如果不成立則結果為「零」。也就是說不論 a 的值為何,幾乎算出來都是 0 和 1。不論怎樣都會比 20 來得小,所以可以說永遠是成立的,也就是結果永遠是 true。這顯然不太符合我們預期中的,a 要在 10 到 19 之間。不過多數人都是從小接觸數學,而幾乎沒接觸過程式,產生誤會也是相當正常的事。所以才會在此特別提及。

  你可能會想,如果我們希望 10 <= a < 20 卻又不能直接這樣寫,該怎麼辦比較好呢?沒關係,除了巢狀的 if 之外還有其它方法。


邏輯運算子

  邏輯運算子有三種,代表「否定」的「!」,代表「或者」的「||」,代表「而且」的「&&」,也就是 Not、And、Or 三種。運算結果通常也都是 0 或 1 代表 false 與 true。注意如果少寫一個符號,也就是寫成「&」或是「|」的話,編譯並不會錯,在許多情形下都會正確,但是那些是不一樣的運算子,請注意這一點。這三個運算子都是視對像的 true 與 false 來做運算。

  「!」是把原先 true 的轉變為 false,也就是非零會轉成零。原先是 false 會被轉成 true,也就是零會轉成非零,通常是轉成 1。

  「&&」和「||」比較特別。「&&」是「而且」的意思,所以必須是前者為 true「而且」後者也為 true,結果才會是 true,否則會是 false。「||」則是「或者」的意思,所以只要前者為 true「或者」後者為 true,結果就會是 true,否則會是 false。結果通常也是以 1 代表 true。這個可以用簡單的表格來表示。

And:

 | T F
-+----
T| T F
F| F F


Or:

 | T F
-+----
T| T T
F| T F

  為什麼說它們特別呢?因為它們有時可以只看一個運算元就知道結果。And 只要知道左邊運算元是 false,不論右邊是什麼,結果必然是 false。同樣的 Or 只要知道左邊是 true 則不論右邊是什麼都會是 true。這種情形下它們不會去看右邊的運算子,也就是屬於它們右邊的運算式是不會被計算的,因為沒有這個必要,所以它們會偷懶。例如:

3 || b*6-6

  這個運算式中,b*6-6 並不會真的進行運算,因為光看左邊是 3 就確定這個「||」會算出 true 的結果,它們就決定偷懶,放右邊的運算子一天假。也許你覺得是否進行運算並不那麼重要,這是錯誤的。有些運算式的進行與否是會影響變數的值的,也有些具有更大的影響,比如說除以 0。如果你總認為電腦很勤勞一定會進行運算,那麼可能會出現許多預料之外的錯誤,而且不知道這點便不知道問題會出在哪兒,抓不出哪裡有錯。以下就是個例子:

a && b/a == 7

  在這個例子,這性質就顯得非常重要。我們用 And 來防止 a 為 0 的時候,仍然進行 b/a 的運算。因為 a 如果是 0,&& 的運算結果就一定是 false,所以不會對右邊 b/a==7 做運算,也就不會發生除以 0 的慘劇。有時我們也會利用它會偷懶的特性,做一些有效利用。

  就算沒有危險性,運算與否不會影響結果,也會影響程式執行效能。不論電腦運算再快,總是需要時間運算。即使一次運算只花千分之一秒,將來執行數萬數億次時,仍會有相當大的影響。善用這點有時也能提高程式的執行效率,不過那是比較進階的課題了。現階段先知道有這樣的特性就可以了。


其它運算子

  其它還有位元運算子,或一些難以分類的運算子。前面提到的 ++ 和 -- 也是運算子的一種,這算是比較特別的,因為它們的運算元僅限於單一的變數。它們會改變對象的值,所以和賦值的左邊一樣,對象必須是變數才行。

  之前也說過它們之間有微妙的不同,便是在運算的結果上有些不一樣。a-- 的運算結果,是本來的 a,而 --a 的運算結果則是之後的 a。什麼意思呢?這麼說吧。a-- 是先照張相,然後去剪頭髮。而 --a 是先去剪頭髮,再照張相。兩種情形最後頭髮都剪了,所以在運算完之後 a 的值會相同,都是減少了 1。但是交給你的運算結果,也就是相片,卻是不一樣的。

  運算的結果,指的是這個運算子把運算元吃掉後,會吐出什麼樣的結果。與運算子對運算元本身造成的影響不一定有關係。不管把 -- 寫在前面還是後面,影響都是相同的,而運算結果卻不一樣。如同上面說的,請它剪頭髮並交一張照片給我們。運算結果是照片,它可能先照完交給你,再去剪頭髮。也可能先剪完頭髮再照給你,不管怎樣對它造成的影響就是頭髮剪掉了,這點不變。不同的是傳回來的照片,也就是我們拿到的「運算結果」的「值」,是剪完前的,還是剪完後的。

  我們可以寫個程式實際執行看看:

int a;
a = 1;
if(a-- == 1)
{
    printf("11111111\n");
}
printf("%d\n", a);

  執行下去應該會輸出一堆 1,最後輸出的 a 的值會是 0。這代表照片是還沒剪頭髮的,但最後還是剪了。如果是 --a 呢?

int a;
a = 1;
if(--a == 1)
{
    printf("11111111\n");
}
printf("%d\n", a);

  執行下去不會輸出一堆 1,因為 --a 的運算結果會是 0。謹慎起見我們多測試一下。

int a;
a = 1;
if(--a == 0)
{
    printf("00000000\n");
}
printf("%d\n", a);

  這樣就會輸出一堆 0 了,因為 --a 運算結果確實是 0。最後 a 的值也是 0。這代表照片上已經剪完頭髮了,最後也確實是剪了。

  ++ 的部份,和 -- 是完全一樣的模式,只是從減 1 變成加 1 的差別。但是請注意,同一個運算式可以有很多變數做 ++ 和 --,可是在同一個運算式對同一個變數做多次 ++ 和 -- 的動作,屬於 C 語言中未定義的行為,也就是說 C 語言的規格上並沒有對此行為的結果做出規範,編譯器不論怎麼處理都不算違反規格,所以難以預測結果,不同編譯器也可能編出不同的結果。我們應該避免讓它們做出未定義的行為。

  另一個常見卻還沒提到的運算子,就是賦值運算子「=」了。它能將右邊的運算式的結果,存放到左邊的變數之中。這是它的影響,那麼它的運算結果呢?答案是,被改變值之後的左邊變數。我們舉個例子:

int a;
a = 0;
if((a = 6) == 6)
{
    printf("666666666\n");
}
printf("%d\n", a);

  執行結果會是輸出一堆 6,第二行輸出 a 的值也會是 6。賦值會將 a 的值變為 6,而且將這個值作為運算結果。

  同時這個運算子,也是少數右邊優先運算的例子,與算數運算子的順序相反。例如以下例子:

int a, b, c;
b = 5;
c = 6;
a = b = c = 7;
printf("%d %d %d\n", a, b, c);

  最後會輸出 7 7 7。如果它是左邊優先,那麼一開始 a 的值就會變成 5,之後會變成將 c 的值賦予 a,再將 7 賦予 a,最後 a 會變成 7,但其它不變。如果是右邊優先,那麼 c 會先變成 7,然後 b 也變成 7,最後 a 變成 7,符合我們得到的結果。我們試試如果讓它左邊優先會怎麼樣。

int a, b, c;
b = 5;
c = 6;
((a = b) = c) = 7;
printf("%d %d %d\n", a, b, c);

  最後輸出 7 5 6,與我們前面推測的也完全符合。

  最後介紹一些賦值的夥伴,其它運算子暫時不會用上,之後再介紹。這些運算子分別是「+=」、「-=」、「*=」、「/=」、「%=」。它們是類似捷徑的運算子。還記得如何將 a 的值減 1 嗎?

a = a-1;

  改變變數自身的數值其實相當常用,所以誕生了這些運算子來幫助我們。要將 a 的值減 1 也就可以寫成這樣:

a -= 1;

  其它可以依此類推,比如說要將 b 的值除以 8,可以寫作:

b /= 8;

  這會相當於

b = b/8;

  不過在運算上,「/=」會稍快一些。這些運算子相當的方便,寫起來也比較直觀好懂,所以好好和它們相處吧。

  至於前面為什麼說到運算順序重要,主要就是和它們偷懶有關。「&&」的運算優先度高於「||」,不過以下的例子卻會說明一切。我們利用 ++ 或 -- 來看看變數有沒有去剪頭髮,就知道實際上有沒有偷懶了。

int a, b, c;
a = 0;
b = 0;
c = 0;
if (++a || ++b && ++c)
{
    printf("YEAH I'm working!\n");
}
printf("%d %d %d\n", a, b, c);

  輸出的結果會是 1 0 0。如果我們這樣解釋:運算優先度高的先算,那麼「&&」會先計算,這時 b 會是 1,而且 && 會需要計算右邊來判斷結果,所以 b 也會是 1。之後在 || 的時候 a 也會變成 1。這樣一來應該要輸出三個 1。

  實際上卻不是這樣,而是 || 看到 ++a 的值是 1,所以告訴 && 說你們可以偷懶不用算了,所以 ++b 和 ++c 都沒執行,b 和 c 也就自然是 0 了。事實告訴我們優先度低的會先被考慮,在需要時才會讓底下優先度較高的運算式進行計算。也就是說,階層較高的 || 先被考慮了,然後它判斷右邊不用計算,所以沒有讓 && 工作。所以並不是優先度高的 && 先進行工作,再輪到 || 進行工作,而是由於 || 需要,才讓 && 進行工作的。如果 || 不需要的話,那麼 && 根本不會也不能進行工作。

  總結來說,當不是很確定運算優先順序時,除了查詢網路資料、自行設計各種嚴謹實驗來測試以外,一律加上括號也是一個好方法。關於 && 和 || 的偷懶則必須非常小心運用,如果同一個運算式有不只一個 && 和 || 出現的話,最好避免掉偷懶與否會導致結果不同的寫法比較好。

  學到這裡應該能夠寫一些較複雜的運算,if 也能夠使用更複雜的條件了。比如說輸入一個女僕的評分,將其依分數範圍給個評價,簡單點如 S、A、B、C、D 等等,複雜點像是「辛苦了」「有你在真好」「最喜歡你了」之類的也都可以。當然還是避免中文為妙。甚至弄個海龍公式的計算或是寫數學作業啦化學啦物理作業時,覺得這計算煩死人了,數字又醜得要命,也可以寫個程式計算。

2011年12月14日 星期三

C 語言入門 - 條件式路線分歧

控制程式的行進流程與路線

  到目前為止,我們已能夠讀取使用者的輸入,做出簡單的處理並輸出最後結果了。然而仍有許多問題沒有解決。我們不會希望我們的程式毫無判斷能力,好比你的女僕不會分辨訪客和不速之客,所以會用同一套方式對待他們。所以你會對於是要一律友善對待,還是一律踢飛感到相當糾結。這是相當嚴重的問題,所以我們必須更進一步了解,如何控制程式的運作流程。

  現在我們要介紹 if 這個流程控制用的語法。它能夠讓你有條件地執行某一段程式碼。也就是說,在滿足某些條件的情形下,才執行某一段程式碼。這讓你的程式具有基本的判斷能力。比如說你可以寫個借錢程式,看使用者能借你多少錢。如果他輸入負數就取個絕對值混過去,如果他輸入太小的數字,比如說不到一百,就可以罵他小氣。然後依借錢多寡決定要罵他還是感謝他,以及程度。這時就會需要依賴 if 按照使用者的輸入做判斷,決定要執行哪些程式碼。if 的語法大致如下:

if (a < 20)
    printf("WTF!?\n");

if (a > 2000)
    printf("Oh, you are a GOOD man!\n");

  這是 if 最基本的樣子。要讓 if 為我們條件性地執行某些程式碼,理所當然地要告訴他條件,因此後面必須有小括號。當 if 後面的小括號內的條件成立之時,就會執行後面的程式碼。你可能會想問,所謂的「後面的程式碼」它的範圍是?定義上是 if 的下一個敘述 (statement) 而已。敘述可以是一個流程控制 (例如 if),一段以分號為結尾的運算式,或是一個以大括號圍起來的區塊 (block)。

  如果沒有條件成立時要執行的程式碼會很奇怪,這會讓 if 懷疑它的存在意義,所以即使你不說,他也會自己伸手去抓住下一個敘述。他認為這是他的工作,也認為下一個敘述會是你希望他做的事。所以條件不成立時,if 將為你確保它抓住的這個敘述不會被執行。換句話說,它會為你嚴格把關一個敘述是否會被執行。只有在得到你允許的情形下,才讓由它把關的程式碼被執行。

  如果你覺得很難理解何謂敘述,沒關係。我們一律使用大括號將我們希望 if 在條件成立時執行的程式碼包起來,這樣就萬無一失了。即使只有一行也使用大括號,會導致程式碼稍微冗長一點,但比較不易發生錯誤,日後要追加程式碼也方便。因此建議寫成這樣:

if (a < 20)
{
    printf("WTF!?\n");
}

if (a > 2000)
{
    printf("Oh, you are a GOOD man!\n");
}

  這樣做的話不只自己看的時候比較清楚明白,就連 if 也會感謝你的用心,因為它也樂得免去許多誤會的產生,也較不會被怪罪。這種寫法最不容易發生因誤解 if 魔爪的影響範圍,而造成程式執行結果不如預期。這相當的重要,待會會提個有名的例子。

  平常可能只是像咖啡要不要加糖,這種做與不做的事。然而有時,我們希望在條件成立時做一件事,不成立時做另一件事。像是你家女僕問你要喝可樂還是白開水時,會按照你的選擇做不一樣的事,而且不論你怎麼選都有事要做。這時可以請 if 伸出另一隻手,也就是 else 來幫忙。所以會變成這樣:

if (a < 1)
{
    printf("ok. Here is the cola.\n");
}
else
{
    printf("ok. Here is the juice.\n");
}

  由於 else 本身算是 if 延伸出去的一隻手,所以不能夠脫離 if 單獨存在。但是你可以自由決定一個 if 要不要伸出 else 這隻手。也就是說它並非必要的存在。這時在 if 的條件式成立時,會執行 if 的下一個敘述;若不成立,則執行 else 的下一個敘述。else 已經註定是條件式不成立時執行,沒有別的可能,所以不需要小括號。同樣地,出於想要忠實地為你完成任務,else 這隻手也必定會去抓住它的下一個敘述。


數量即力量、同心協力的 if 如何合作

  儘管你可以用相反的條件,使用另一個 if 達成 else 的效果,但這在條件複雜時相當方便。但是若是你的女僕今天心情特別好,準備了比平常更多種飲料的話,怎麼辦?一個比較直覺的方法是這樣:

if (a < 5)
{
    printf("COLA!!\n");
}
else
{
    if (a < 10)
    {
        printf("JUICE!!\n");
    }
    else
    {
        printf("BLOOD!!\n");
    }
}

  雖然飲料是平等的,不過仍然可以先二分成「要可樂」與「不要可樂」,然後把其它選項放在「不要可樂」的前提底下,再去用一樣的手段做二分法,如此就能把這些分歧並列起來。如果硬要將所有選擇平等擺在一起,反而使問題變得過於複雜而難以處理。我們先處理其中一個選項,也就是說,先考慮你要的是不是可樂,不是時再去考慮別的飲料。不過這裡會告訴你更好的解決方案。

if (a < 5)
{
    printf("COLA!!\n");
}
else if (a < 10)
{
    printf("JUICE!!\n");
}
else
{
    printf("BLOOD!!\n");
}

  你可能會想抱怨說,什麼嘛,我們親愛的 if 不是能伸很多隻手的嗎,怎麼不早說就好了還用到兩隻 if 才解決問題?好吧大概又有人要哭了。且讓我們換個方式,讓那位哭泣中的 if 顯眼一點。


if (a < 5)
{
    printf("COLA!!\n");
}
else
    if (a < 10)
    {
        printf("JUICE!!\n");
    }
    else
    {
        printf("BLOOD!!\n");
    }

  不知你是否還記得,流程控制語法也是敘述的一種。和上面使用兩隻 if 的解決方法很像吧?實際上也是有兩隻 if,只是第二隻被第一隻用 else 那手抓得緊緊的貼在一起,所以你沒注意到它的存在,以為其實有很多隻手。這是個天大的誤會。一隻 if 只有一隻本來的手和一隻 else 的手而已,想要並列更多選擇還是得靠很多隻 if 同心協力才行。如果你認為只有一隻那其它的可是會傷心的。

  如果需要多重條件時,比如說既要 a < 3 又要 b < 3 的話,也可以先把它簡化後拆成許多小步驟,再使用多重的 if 來處理。我們可以這樣子寫:

if (a < 3)
{
    if (b < 3)
    {
        printf("wafuuuuuuu\n");
    }
}

  我們先不要考慮什麼 a < 3 與 b < 3 要「同時成立」這種想法。換個方式想,可以看成是「在 a < 3 成立時」,也就是進入 if (a < 3) 條件成立的情形時,「如果 b < 3 也成立的話」那不就是 a < 3 且 b < 3 的情形了嗎?也就是說,a < 3 且 b < 3。

  總結一下,在出現多重選擇時,像是將家裡女僕依照表現評價分成 S、A、B、C、D 五種等級時,只會有其中一種成立,所以這時的 if 會是透過 else 來並列的形式。else 本來就與原本 if 的條件是互斥的,並不可能同時成立,所以透過 else 並列時,會在出現第一個條件成立的 if 並執行完屬於它的程式碼後,結束這一整串並列的 if。後面即使有滿足條件的,也會因為是掛在前面 if 的 else 底下,而無法成立。比如說,

if (a < 10)
{
    printf("D\n");
}
else if (a < 20)
{
    printf("C\n");
}

  這時如果 a 是 5 的話,只會輸出 D,雖然也滿足後面的 a < 20,但是因為滿足了 a < 10 所以 else 根本不成立,掛在 else 底下的 a < 20 就會被前面的 if 伸出來的 else 抓住並確保不會執行到。也就是說第二個 if 其實因為 else 的關係,帶有隱藏的條件 a >= 10。

  在出現多重條件時,也就是多個條件要同時滿足,所以必須多個 if 層層把關才行。這稱為巢狀 if (nested if) 也就是 if 條件成立後又有其它 if 來確保每個條件都符合,這樣層層包起來。像是一個好的女僕必須同時具備高忠誠度、高智力、高反應和高體力,這種就是多重條件。比如說,

if (a > 100)
{
    if (b > 70)
    {
        if (c > 90)
        {
            if (d > 130)
            {
                printf("This is a perfect maid!\n");
            }
        }
    }
}

  當忠誠確認 > 100 之後,還必須經過確認智力 > 70,之後還得要反應 > 90,最後體力 > 130 才能被認定是好的女僕。雖然多重條件每個都一樣重要,但因為都很重要所以都必須成立,先後順序就比較沒關係,確保每個都成立即可。


if 之間的爭吵與互不相讓

  當然 if 也不總是同心協力的,多少也會因為爭寵之類的原因吵架,比如說下面的例子。

if (a < 5)
    if (a < 3)
        printf("so small..\n");
else
    printf("so large!!\n");

  實際上那隻 else 應該是要誰伸出去好呢?這時就會吵架了。第一隻 if 會說光看主人的排版,就知道應該要它伸出 else 這隻手。但是第二隻 if 會說明明是離它比較近,應該是要它伸出去才對,於是爭吵不休。最後大法官編譯器判決,應該給第二隻 if 來伸出 else 才對。這讓第一隻 if 傷心欲絕,同時它也擔心要是主人其實是希望他伸出 else 的話該怎麼辦。實際上這種可能性相當大,第二隻可能也因為最後結果不符主人預期而感到失落。

  這種情形稱為 dangling else,編譯器會在這種難以判定的例子,把 else 交給最接近的 if。這也是為什麼我會推薦不管怎樣一律加大括號。如果出現這種情形,沒有經驗的話是很難看出錯誤的,也就是說這個錯誤非常地難找。如果寫成以下的形式就不會引起爭吵,也不會產生非預期中的結果了。

if (a < 5)
{
    if (a < 3)
    {
        printf("so small..\n");
    }
}
else
{
    printf("so large!!\n");
}

  現在我們可以試著寫寫看更富變化的程式。比如說輸入兩個整數,輸出它們的差。或是輸入兩個整數,比較看看誰比較大。甚至,可以透過讓使用者輸入數字,代表選擇的選項,來寫個簡單的文字冒險遊戲了呢。依使用者選擇的路線決定故事的發展,最後邁向感人的或悲劇的或慘敗的結局。有耐心的話已經足以完成了喔。

C 語言入門 - 變數的運算與賦值

進一步了解變數

  之前說過變數是能為我們儲存資料的容器。既然能為我們儲存資料,自然不只用在存放使用者的輸入,我們也能請它們存放特定的資料,這個動作我們稱之為「賦值」 (assign) 並且使用了一個相當容易導致誤會的符號,也就是等號「=」。

int a, b;
a = 3;
b = 5;

  好了,請不要說出什麼 a 等於 3 或 b 等於 5 之類的話,就像把女大學生叫成阿姨一樣,是相當傷人的。請入境隨俗,在 C 語言的世界裡面,「=」和「等於」是完全無關的兩件事情。「=」就是「賦值」的符號,所以請說「對於變數 a,我們賦予 3 這個值」或是「我們使變數 b 的值變成 5」或是「變數 a,能請你為我儲存 3 這個值嗎?」甚至是「變數 b,聽好了,從現在起你的值就是 5 了」。絕對絕對和「等於」沒有任何的關係,絕對。它們不會希望被誤解的。你也不會希望身為一個程式高手,而你迅速地敲著鍵盤寫出一份非常藝術堪稱完美的程式碼時,卻被當作在做化學實驗。

  現在我們可以任意地賦予一個變數特定的數值了。人總是喜歡得寸進尺,做到了這一步就想更進一步。我們希望不只是一個特定的值,怎麼辦?比如說,使用者輸入了一個數,我就是喜歡改成他輸入的數減一。這不難,因為我們懂得賦值。假設使用者輸入了 a,那麼它的減一是多少?a-1 嘛,這猴子都知道。所以事情圓滿解決了。

int a, b;
scanf("%d", &a);
b = a-1;

  相當完美。首先我們宣告變數,用作儲存使用者的輸入,然後在有了適當容器後著手讀取。之後將它減一的值賦予變數 b,一切看起來相當完美。簡直就是最佳結局了。

  賦值的規則是,它的左邊必須要是被賦值的變數,而不可以是別的東西。它的右邊必須是一個運算式 (Expression, 或稱表達式) ,在程式執行時會先將右邊的結果計算出來,然後將這個值賦予左邊的變數。以下是一些錯誤的示範。

3 = a;

  賦值符號的左右兩邊在意義上是不相等的。左邊是目標容器,右邊是要儲存的資料。這樣寫的話是將 a 的值賦予給 3,但是 3 是個整數而不是容器。你可以想像你是把水壺往水裡扔,而不是把水往水壺裡倒。也就是說我們搞錯目標物和目的地了。這樣是不行的。

a,b = 3;

  在規定上目標只能有一個。同時有多個變數想賦予同一個值的時候,請別想著偷懶,老實地一件件事分開做。在寫程式時需要的是將問題簡化後,拆成許多小步驟,而不是反過來將許多小步驟湊在一起處理。

a+b = 5;

  這已經不知所謂了。在做加法運算時會將 a 和 b 所儲存的資料拿出來,然後相加。相加之後只會是一個值,而不是一個變數,所以也是錯誤的。


讓變數的值增加或是減少

  你可能會想,如果我要增減一個變數現存的值,怎麼辦?照剛剛的邏輯,若要讓變數 a 的值多一,新的值顯然是 a+1,要賦予新值的目標是 a,於是我們得到的結論是:

a = a+1;

  如果你覺得看到這行都要崩潰了,天啊!a 怎麼可能等於 a+1,這程式跑下去一定會爆炸!那麼我們可愛的等號可能會哭給你看,或者已經在哭了。都說了多少次這不是等於,這是賦值。沒錯,這是正確的寫法。你可以不必寫成這樣

b = a;
a = b+1;

  即使照數學來講這也是必錯的,但是以賦值的角度來看這是正確的。如果你還沒習慣,沒關係慢慢來,賦值還是會長伴你左右不會離你而去,只是抱持著這樣的誤會,將會相當程度影響你的思考。最好試著早些調整思考方式,以程式語言的角度去思考、去看這個程式的世界。

  除此之外,由於讓變數加減一,實在太常用了。因此 C 語言特地準備了專為加減一量身打造的符號,比起一般的先加減再賦值,運算速度會快上許多。以下示範兩種讓 a 的值加一的方法。

a++;
++a;

  理所當然地如果是要減一,會寫成

a--;
--a;

  單純用來對變數做增減的話,兩種寫法的結果是相同的。++a 和 --a 會比 a++ 及 a-- 來得快速,不過只用在增減時,優秀的編譯器通常會將它們優化至差不多的效能,所以挑順手的方式寫就可以了。它們在並非單純用於增減時會有微妙的差異,這在以後關於運算子與運算元的文章中會提及。

  現在你可以試著寫個程式,輸入一個整數,然後輸出該整數的平方。或是輸入兩個整數,試著輸出這兩個整數的和。

C 語言入門 - 使用者輸入與變數宣告

如何接收使用者的輸入?

  在談這個話題之前,我們必須先想想,使用者輸入的資料要儲存在哪?如果不先找個地方放著,我們如何使用?所以我們先介紹如何儲存資料。要儲存東西必須要有容器,現在我們來看看如何變出一些容器,來儲存我們想存的東西。


作為容器的變數及其宣告方式

  程式在執行的時候,肯定會有各種各樣的資料需要暫時存放著。程式結束後可能就沒用了,但是在執行的過程卻是必須的。例如我們必須把使用者輸入的資料存起來,否則讀取完輸入後,也不知道到哪兒去了,如何去使用它?就像人腦,只是路上看過一塊招牌而沒多留意,一般而言是不會記下來的,之後要回想也想不起來。如果不先把運算結果存起來,像是在腦中算完就馬上忘了,輸出時又豈知該輸出的結果是什麼?

  舉個例,像是向對方要電話號碼時,如果沒有紙筆可以記錄,至少也會默唸個數十次來記住它。相同的,程式在讀取使用者輸入時,也必須將它記憶起來,才能使用。而儲存這樣子的記憶,就是作為容器的「變數」的工作。我們可以用下列的方式宣告變數,變數在經過宣告後就會成為實際可用的容器。日後會提及更多關於變數的事情,現在先來談談如何宣告。

int a, b;

  其中 int 是表示變數的類型為整數 (integer),若想宣告其它類型的變數,則將 int 置換成其它類型即可。在此先不提其它還有哪些類型。變數的類型稱為「型態」。之後是宣告 int 型態的變數 a 和 b,中間以逗號隔開,最後以分號結束這個部份,表示宣告已經告一段落。注意 C 語言是不在意換行的,所以必須要有分號,表示這個部份告一段落了。否則如果寫成這樣:

int a, b
printf("kinniku YEAH YEAH\n")

  編譯器會認為你在宣告完 a 和 b 之後因為沒有分號,表示宣告還沒結束,但是 b 和後面的 printf 之間沒有逗號,它會以為你的 printf() 是要對變數 b 的宣告做出修飾,卻又不是語法規定的修飾用字,格式也不符,但是又不能擅自猜測你的意思,所以會產生編譯錯誤,要求你按照語法寫得清楚明白,否則它無法解讀。

  現在我們知道如何宣告 int 型態的變數了,所以我們有能力讀取使用者所輸入的整數了。那麼,立刻來試試看吧。


讀取使用者的輸入並輸出

  這裡我們試著讀取使用者輸入的整數,然後把它輸出在畫面上。你可能會想這怎麼辦得到,我們又不知道使用者會輸入什麼,怎麼知道如何輸出?沒關係,如果我們成功讀取並儲存起來的話,就能夠直接拿來使用,也就知道該輸出什麼了。

int a, b;
scanf("%d%d", &a, &b);

  新函式出現了。我們可以先和他握個手,交個朋友。有了上次 printf() 的經驗,應該能夠從外觀了解到,它的名字是 scanf 而且是個函式,而我們傳遞了一些參數給它,讓它知道我們希望它幫我們什麼忙。我想這不難猜測到,我們希望它做的事情,是讀取使用者輸入的兩個整數,然後將讀取到的整數分別儲存至 a 和 b 這兩個變數中。可是我們怎麼知道到底有沒有正常運作?沒關係,先讓我們輸出看看。

#include <stdio.h>

int main()
{
    int a, b;
    scanf("%d%d", &a, &b);
    printf("%d %d\n", a, b);
    while(1);
    return 0;
}

  試著執行看看,然後輸入兩個整數,中間以 ENTER 或是空白鍵隔開。輸入完按下 ENTER 看看結果如何。請最多輸入 9 位數就好。沒有意外的話,應該會正確地顯示出你剛剛輸入的兩個數字,並且以空白隔開才對。是不是很神奇啊?你的程式真的知道你輸入了什麼。你可能還不了解這些程式碼的意思,不過至少它會動了。接下來只要慢慢地來了解它們就可以了。

  通常我們會先有個目的,再去想辦法把程式寫出來,達成我們的目的。我們希望可以讓使用者輸入兩個整數,然後為了確認結果,輸出讀取到的兩個整數。這是我們所期待的功能。所以我們思考,要怎麼樣利用我們所學會的程式碼,達成這樣的目的。首先必須思考解決這個問題的流程,再來思考各項細節。記住程式碼是依序執行的,所以它的運作流程,也就是先後順序相當重要。

  我們希望讀入兩個整數,所以必須有 scanf()。讀入的數必須有地方存起來,所以必須先宣告變數。我們希望知道是否有正確運作,也就是將讀到的整數輸出,讓我們可以確認,所以需要 printf() 輸出。同時我們希望看到結果而不要過早關閉程式,所以要用 while(1) 讓它卡住。那麼我們怎麼安排它們的順序?

  scanf() 在使用時需要先有變數儲存讀取到的結果,所以在 scanf() 之前必須宣告變數。printf() 要輸出的是我們讀取到的整數,因為它的目的,只是確認我們是否正確讀到。因此它一定是放在讀取完輸入之後,將讀取的整數輸出。如果放在讀取輸入之前,就失去它的存在意義了。而 while(1) 則必須在其它步驟全部執行完之後才能執行,因為這些都是必要的動作,但 while(1) 會卡死整個程式的執行。它只是用來讓程式停下來,以免在做完動作後自動關掉,沒時間讓我們看輸出的結果。為了避免在執行完所有動作之前,程式執行就被卡死,我們得把它擺最後面。

  因此得到的唯一合理順序,就如上面所述那樣了。我們可以試著交換任意兩行的先後次序試試,會發現光是流程上就相當不合邏輯,雖然這可能讓解讀錯誤的程式碼意外地有趣,也就是變成了不錯的笑話。這同時也告訴我們,除了正確地分析目的並找到合適的程式碼以外,正確的執行次序也是相當重要的。

  接下來則是來看看我們的宣告和 scanf()、printf() 要怎麼用。首先我們目的是兩個整數,那麼最少要宣告兩個 int 型態的變數,先暫時命名為 a 和 b,到這裡沒什麼問題。

  scanf() 則要先傳遞第一個參數,告訴它我們要讀取兩個整數。我們必須告訴它一個字串,讓它知道我們預期讀取到什麼樣的輸入。因為是字串所以用雙引號 "" 括起來。我們希望兩個整數,而且並非預期特定的兩個整數,所以使用 %d 來代表整數,這是約定。因為我們必須告訴它,預期讀取的是非特定的兩個整數,這不好表達,所以必須約定一個雙方都看得懂的暗語。因為要兩個所以寫兩個 %d 上去。

  現在我們傳遞了第一個參數,scanf() 已經知道我們想要讀取什麼了。但是它還不知道我們希望讀取到的整數被存放在哪兒,所以會希望我們附上。否則它到時會感到不知所措,既希望完成任務,又不敢隨便亂動。程式當掉而自動關閉時,通常是有非做不可的事,又不知道該怎麼做,只好以死明志。無法完成任務就自殺謝罪。為了避免這樣的慘劇,我們把 &a 和 &b 作為錦囊傳進去,並且以逗號「,」隔開,以免混淆。等到它遇到第一個疑問時就會打開第一個錦囊,看到 &a,然後把第一個整數放進去。遇到第二個時會打開第二個,所以先輸入的會放在 a,後輸入的放在 b。

  接下來的 printf() 也和 scanf() 非常相似。我們也不希望輸出特定的字串,而是會因應情況變動的整數。幸運的地 printf() 和 scanf() 非常要好,他們使用同樣的約定與暗語,所以我們也用 "%d %d\n" 來輸出,中間的空白是怕兩個整數黏在一起輸出,就變成一個整數了。理所當然地 printf() 不是我們,所以不會知道我們心中想著誰,自然不會知道我們想輸出什麼樣的整數,只知道是整數而已。為了避免它因為無法完成任務而尋死,我們必須告訴它是哪兩個。於是我們把 a 和 b 也作為錦囊傳進去,等到 printf() 輸出到第一個 %d 而開始不知所措時,打開第一個救命錦囊就會知道該怎麼做了。同樣地按照順序會先輸出 a 然後輸出空白,再來輸出 b 然後按照你的要求幫你換行。

  你可能會覺得很奇怪,為什麼 scanf() 要用 &a 和 &b 而 printf() 卻是用 a 和 b 呢?因為它們預期的錦囊內容不一樣,解讀方式也不一樣。在變數前面加上 & 表示我們要的不是變數自身的值,而是變數的家。變數是儲存用的容器,但容器也會有放置的地點。讀入時並不會去關心變數裡有著什麼,而是關心這個變數它到底在哪裡,好找到這個容器並置換存放的東西。所以我們要用 &a 將 a 的位置傳遞過去,而不是用 a 將 a 的值給傳遞過去。

  光是知道值並不會知道確切的容器位置,因為可能有其它容器存放著相同的東西。何況它預期你告訴它的是「位置」而不是「值」,所以就算告訴它「值」它也會將其作為「位置」解讀,然後產生誤會。如果解讀後是不存在的地址,或是該地址是去放東西會被守衛砍死的地方,程式就只好死給你看了。

  相對的 printf() 的 %d 預期是被告知一個整數,所以告知它位置是不對的。它也會將「位置」作為「值」來解讀,雖然仍然能解讀出來,但是很難是我們所預期的結果。對於 C 語言來說「位址」和「值」是沒有差別的,所以也無法做出它是「位址」或是「值」的判斷。這點在日後關於記憶體或指標的文章中會再次提及。

  現在你應該對 scanf() 和 printf() 有更多的了解了,想必也比較了解如何與它們溝通,以及溝通時的暗語了。你可以多試著和它們玩玩,指派任務給它們,然後看看是否有正確傳達並執行。例如輸入兩個整數,然後按照輸入先後順序,反過來輸出,或是試著讀入三個整數等等。多嘗試一些突發奇想,有時可以幫助你更了解它們,也能累積更多經驗。

2011年12月12日 星期一

C 語言入門 - 如何輸出文字

如何讓程式輸出我們要的文字

  要讓程式在畫面上輸出我們所希望的文字,並不是那麼容易的事。不過現在你只需要知道它很難就夠了。C 語言會提供你一個函式,供你呼叫。函式就像是數學的函數一般,你將一個或多個數字丟進去,它就會依照固定的式子告訴你結果。這裡也是一樣,函式是一個由程式碼所寫成的小程式,你如果要使用它,就呼叫它,它就會執行它內部的程式碼為你做事,你可以不知道它怎麼做到的,只要知道它能做什麼,就可以使用它了。你可以看成是一把槍,你可以不懂它怎麼發射子彈,但是你知道它的功能是射出子彈,知道使用時要扣動扳機,就可以使用它了。

  要在畫面上輸出文字,使用的是 printf() 這個函式,如同前面的例子:

printf("I'm fucking pro\n");

  函式大多需要搭配特定的「參數」,你呼叫它意味著你想做些什麼,但它必須知道你想怎麼做。比如說,我們希望在畫面上輸出文字,所以我們呼叫它,也就是叫它的名字,然後在後面加上一對小括號,表示我們叫的是,名為 printf 的函式,最後以分號結束這個動作。

printf();

  但是它必定覺得很困惑。你想在畫面上輸出文字,但是它不會讀心術,不會知道你想輸出什麼。你並沒有告訴它這件事,所以它會產生錯誤。就算一樣是人也不會知道你心裡想著什麼,所以請坦率地告訴它,你想輸出的文字。後面那一對小括號,便是為此而生。你可以將你希望的文字,作為「參數」寫在小括號中,就能夠傳遞給它。以「Happy Birthday」為例,

printf("Happy Birthday\n");

  相當好,你明確地告訴電腦你想輸出的是「Happy Birthday」。同時,顯然你已經找到另一種贈送生日卡或甚至生日禮物的方式了,你的程式會為對方獻上滿滿的祝福。但是你必須確定對方不會寫程式,然後送他執行檔。如果他懂得如何編譯,他應該只會訝異怎麼如此簡陋無趣。

  你可能會想問,我寫成這樣為什麼不可以?

printf(Happy Birthday\n);

  為什麼要用雙引號括起來?這是個好問題。這是為了讓電腦能夠準確辨識你想輸出的東西是什麼。我舉個最簡單的例子吧。

printf(printf(abc)\n);

  好的,請問咱們親愛的編譯器該如何解讀它?我們有幾個選項。

  一、輸出「printf(abc)\n」
  二、輸出 printf(abc) 的運行結果,加上 \n。當然 abc 也會在運行時輸出。

  假設它選擇方法一,那麼如果哪天,我們希望是方法二怎麼辦?它如何判斷?反之亦然。正因為這種寫法曖昧不清,以致有多種解讀方式,而且沒有可以正確判斷的方法。即使今天其中一種解讀方法會導致當機,你也不能假設另一種才是對的。因為我們可能就是惡意想讓它當機,或是示範給他人看,這樣會當機。所以,我們用各種符號與規定,防止這種「同樣的程式碼有多種解讀方式」的情形。也就是用雙引號 "" 來表示這些是文字,也就是所謂的「字串」(string),從而避免將這些解讀成其它程式碼的情形。而只要不加 "" 就能夠避免將程式碼當作文字直接輸出的情形。

  由於我們要輸出的確實是文字,因此要加上雙引號,讓它變成字串,作為字串解讀。字串就是一個由零或多個文字組成的有序的排列。而文字在 C 語言中稱為「字元」,也就是一個字。所以字串就是一個字元序列,也就是零或多個字元的有序排列。

  那麼現在你應能掌握如何輸出一段文字了。但仍要避免幾乎任何在英數模式下打不出來的文字。這點在字元相關的文章中會解釋為什麼。

2011年12月11日 星期日

C 語言入門 - 人生中第一個程式

你的第一個程式

  在環境設定好之後,便可以開始動手寫人生中第一個程式了。在左上角找到「檔案」 -> 「開新檔案」 -> 「原始碼」,或是用熱鍵 CTRL+N,即可開啟人生新的一頁。沒錯,馬上你就會寫程式了,而你人生中的第一個作品即將誕生。請輸入下列咒文:

#include <stdio.h>

int main()
{
    printf("I'm fucking pro\n");
    while(1);
    return 0;
}

  好吧,也許在你為自己人生中第一個程式,嶄新的一頁竟然長這個樣子而後悔前,把「I'm fucking pro」改成其它你覺得不會後悔的句子。請避免中文、注音或全形標點符號,甚至日文韓文一類的東西,否則日後你對人生中第一個程式的印象,很可能就是沉睡在一堆不明所以的錯誤之中不讓你執行。拜託也請不要開始想一篇座右銘或自傳或得意的文章之類的東西,不如繼續學習,讓寫程式變成你自傳中的得意技,或者早點放棄它然後繼續尋找你的興趣。

  在輸入完之後,按下上方的「執行」 -> 「編譯並執行」,或是熱鍵 F9,或是上面小圖示第二列找到「編譯並執行」然後按下去,稍等一下下,萬一很幸運地每個符號都打對了,那麼應該會跳出一個小黑窗,顯示著「I'm fucking pro」。萬一很不幸地某一行變紅了,卻沒跳出小黑窗,那麼請回頭檢查看看有沒有哪兒打錯了吧。你可以為你多了一項技能而歡呼,甚至開個慶功宴,但別忘了繼續強化它。請記住你還沒去弄懂這段程式碼的任何一行。

  之後寫了任何新的程式碼,請記得在編譯之前是無法執行的。若是進行任何修改,然後按了「執行」而非「編譯並執行」的話,因為並沒有在修改之後進行編譯,所以可執行檔還是之前的編譯結果,這時執行的將會是舊的編譯結果,也就是最後一次進行編譯時的程式碼。所以你的任何改動都不會產生任何影響。因此比較好的習慣是,每次都重新編譯並執行,能避免掉許多可能的問題。


何謂編譯?

  電腦只看得懂 0 和 1 的指令而已。換句話說,它看不懂你的程式碼。事實上它甚至只把你費盡千辛萬苦寫出來的寶貝程式碼,當作一個純文字檔案來看待。因為我們沒有告訴它任何事情,在它看來你只不過建立了一個純文字檔案。那麼,怎麼辦呢?首先我們必須翻譯成電腦看得懂的指令,這就是編譯 (Compile)。

  透過 C 語言的編譯器,它會將你的程式碼試著翻譯成電腦看得懂的形式,這個過程就是編譯。如果在編譯過程,發現你寫的程式碼不符合語法,就會產生編譯錯誤 (Compile Error)。好心的編譯器會提示你發生了哪些錯誤,以便改正。若是成功,則編譯器會產生出一個可執行檔,透過執行它,就可以讓你的程式開始運作。

  IDE 將產生程式碼所需的文字編輯器 (Editor) 以及編譯程式碼用的編譯器 (Compiler) 整合在一個軟體中。當然可能還包含除錯用的 Debugger 或是其它功能。由於並非是把所有東西重新寫成一個程式,而是透過一個程式將其包裝起來,所以只是「整合」。就像是把快過期的牛奶和剛研發的新口味果汁綁在一起賣一樣,只是拿快過期的牛奶,以及新口味果汁,綁在一起販售,而不是整個倒在一起加料後換個新瓶子裝起來。原本的東西還是原本的樣子,只是被整理起來了。

  如果沒有使用 IDE,也可以只用文字編輯器來編輯程式碼,再自行使用編譯器將程式碼編譯成可執行檔,再手動執行。雖然 IDE 提供你編輯環境,還讓你一個按鍵就能直接編譯並且執行,相當方便,卻也不是那麼不可或缺。即使沒有 IDE 的輔助,仍然可以寫程式。甚至可能會像以前舉的女僕的例子,IDE 為你做了太多考量,導致效能低下,時常為你補上它猜測你接下來要打的字,卻常常猜錯,反而會造成種種不便。沒有絕對的好壞,選擇自己喜歡的方式即可。


回頭看你的第一個程式

  C 語言的程式碼最常見的起手,就是這個樣子。讓我們慢慢研究它。當然我不會講得太過深入,現階段努力記住它的長相比較重要,之後會分別在屬於它們的章節,一一介紹關於它們的一切。

#include <stdio.h>

  以 # 開頭的一行,表示屬於前置處理 (preprocessor, 中文翻譯不明) 的意思。這在編譯期間會先被處理掉,然後才會對其它部份進行編譯。

  #include 用來載入「標頭檔」 (header file)。在 C 語言中,已經為我們內建了不少方便的函式 (function) 供我們使用,例如將文字輸出到畫面上,讀取使用者的輸入,甚至計算三角函數等等。這些並不需要我們自行動手完成,只要載入我們想使用的函式所在的標頭檔,並且呼叫它們,就可以做到這些相當複雜的事,而不用自己設計程式碼來處理。

  在 #include 之後,會用大於和小於符號,將目標標頭檔的檔名給括起來。因此這行是載入名為 stdio.h 的標頭檔。這類檔案一般副檔名為 .h,而 stdio 則是「標準輸入輸出」 (standard input and output) 的縮寫,裡面都是一些與輸出和輸入相關的函式。

  輸出是將資訊用各種方式呈現出來,如螢幕、喇叭等等。輸入則是接受來自使用者的資訊,例如鍵盤滑鼠等等。假使電腦做了相當複雜的演算,卻沒有把結果顯示出來就結束了;或是無法針對使用者的操作而改變運算目標,像是執行時永遠只能算 1 + 1 而不能讓使用者輸入想計算總和的兩個數字,都是相當大的問題。因此 stdio.h 是非常非常重要的,幾乎每個程式都會用上它裡面的函式。

int main()
{
    return 0;
}

  這裡則是函式宣告,詳細可以參考之後與函式相關的文章,現階段可以先記住長相就好。在 C 語言中,名為 main 的函式是最重要且不可或缺的。因為程式執行時的進入點就是在 main 這個函式。也就是說,任何 C 語言的程式,都是從 main 函式開始執行的,並且在執行完 main 函式裡的程式碼後,程式便結束了。

printf("I'm fucking pro\n");

  這一行是在螢幕中輸出「I'm fucking pro」的意思。「\n」代表換行,如果沒有輸出換行的話程式是不會自動幫你換行的。即使在顯示過長時被自動換行顯示,本質上還是沒有換行的。關於 printf() 的事情很快就會提及。

while(1);

  這一行是讓程式卡住,讓它無法結束。為什麼要這麼做?你可以試著把它拿掉,編譯並執行看看。編譯能夠正確地進行,程式也能夠正確的執行,但會在執行的一瞬間自動關閉。那是因為整個 main 函式,只有一件事要做,就是在螢幕上顯示一行字。因此顯示完的瞬間,main 函式裡的程式碼也就執行完了,程式就會直接關閉,這樣會很難看清楚結果。所以加上 這行,讓程式執行到這邊時卡住,我們也就有足夠的時間,從容地檢視結果。

  到這邊大致有個了解就行。詳細部份在介紹到它們時會講得更清楚,在那之前先試著把長相記住。

C 語言入門 - 前置準備

如何在電腦上寫程式?


  對於初學者而言,最好準備一套用於寫程式的軟體,這類軟體稱為整合開發環境 (Integrated Development Environment, IDE),它們將寫程式外的所有麻煩事都為我們處理好了。因此,我們可以專注於學習如何寫程式,而不必理會其它事情。一個好的 IDE 能讓寫程式變得相當省力。以下會介紹最常見的 IDE。若非已對其相當熟悉,請務必遵從底下提及的設定,會有相當大的幫助。


Dev-CPP

  C 語言比較常見的 IDE 是 Dev-CPP。在常見的 4.9.9.2 之後曾停止更新一陣子,現推出最新版本 5.0.0.4,但最廣泛常見的、公用電腦安裝的,多半仍是 4.9.9.2。可以在 http://orwellengine.blogspot.com/2011/09/dev-c-5004-released.html 免費下載新版使用。安裝暫且略過不提。安裝好之後,建議先進行設定的修改。若還在使用或已有安裝 4.9.9.2 版本,請更新至最新版本。若使用公共電腦沒有權限更新,可以下載免安裝版本使用。


Dev-CPP 建議設定

  新版本的 Dev-CPP 預設設定較貼近本文建議設定,但目前流通版本仍多為 4.9.9.2 故仍放上舊版的建議設定。以下大部份選項,新版本已變更為預設選項。只需參考介面語言與風格相關設定。

  在執行之後,先進行初次使用的設定。Dev-CPP 為多國語言版本,可自行選擇操作介面的語言。繁體中文為 Chinese (TW),沒有標 (TW) 的是簡體中文,而且可能因為 Windows 是繁體語系而出現亂碼。之後皆使用繁體中文介面的用詞。之後的詢問選擇 No 即可。如果不小心選錯語言了,可以在主畫面上方 Tools -> Environment Options 中,上方分頁找到 Interface 再在 Language 的地方選擇 Chinese (TW) 即可。

  在結束設定後會進到 Dev-CPP 的主要畫面。左上角會有檔案、編輯等等按鈕,稱為功能表列。找到「工具」這一項,點選第三項「編輯器選項」,注意它和第一項「編譯器選項」長得很像,但是並不一樣。之後請取消勾選「智慧型 Tab」以及「Highlight Current Line」,再勾選「使用 Tab 字元」以及「將相對應括號反白」。之後會一一解釋為何如此設定。

  新版本亦追加了新選項「Append closing braces」且預設開啟,在你打「上括號」時,自動幫你補上對應的「下括號」。這對於許多用慣舊版本或一般文字編輯器的人,會較難適應。可視情形關閉。我相當討厭編輯器預設有這功能,都會設法關閉。

  同樣是在編輯器選項,在上方分頁找到「顯示」,可以視需求調整字型大小。這點視個人習慣而定,之後覺得有需要再回來調整也可以。重要的是請勾選「顯示行號」。

  再來請到分頁「語法」,這裡可以調整程式碼的配色。可以在預覽中點選想調整的部份進行調整。建議先在「語法風格快速設定」調整成「Twilight」。當然這部份多半為個人喜好,但是寫程式時常需要長期盯著程式碼看,所以將背景調成黑色會對眼睛比較好。若不喜歡的話可以維持原樣或者試用其它風格。取消勾選「Highlight Current Line」主要是在這配色中會有相當影響。

  若要自行調整配色,建議等學習一段時間後,對 C 語言有一定程度的了解,再回來依喜好調整較佳。仍然建議使用快速設定,如此能在更換寫程式的環境時,有較快速的適應能力。

  設定大約到此告一段落。請務必遵從設定,否則程式碼會顯得相當凌亂。這點在稍後關於 程式碼風格的文章中會提及,也會解釋其中的差異。

C 語言入門 - 前言

何謂程式語言?它能做什麼?為什麼要學習它?

  在現今資訊發達的時代,猴子都知道電腦的運算能力之強大。但是電腦不如人腦擁有意志,能夠溝通,所以不會主動為人類做任何事。幸運的是,我們可以命令它們做任何事,而不會受到拒絕或是反抗。它們不會變通,不會做你沒下令的事情,所以能夠信賴。因此將那些單調枯燥繁瑣的工作交給電腦來做,將困難複雜需要思考的工作交給人來做,會是較有效率的分工方式。

  而程式語言,就是對電腦下命令用的語言。只要將希望電腦做的事情,使用程式語言,寫成程式並命令電腦執行,即可讓電腦為我們做事。所以若要讓電腦按我們希望的方式,為我們處理掉許多麻煩事,就必須學會程式語言。一旦學會了,就能夠靈活運用電腦的計算能力,輕易完成許多人力難以辦到的事情。這也是它的魅力所在。


學習程式語言需要會些什麼?

  最基本的能力是 26 個字母大小寫全部認得,找得到所有鍵盤上存在的符號,擁有一台電腦並且知道基本的使用方式。最好具備一些數學底子,至少要到國中數學,但不算必備;最好具備英文能力,但若能像筆者初學時,語法全部一個個字母硬記,也不算必備。學習並不需要太多基礎,但要學到精通卻極為困難,不論是否具備上述基礎。


何謂程式?

  程式就是以程式語言所撰寫的一套明確的指令流程。執行此程式時電腦將依上面的流程,依序來執行指令,完成一條後前進到下一條。所有我們使用過的軟體都算是一個程式,雖然它們通常複雜得多,但仍沒有脫離這個範圍。如此一來,我想應該可以了解程式能做的事情有多麼廣。


為何選擇 C 語言?

  其它還有好多程式語言,為何我們選擇學習 C 語言?程式語言大致分成兩種,低階語言與高階語言。低階語言非常貼近實際電腦所執行的動作,但是對於人類而言相對困難。高階語言則相當貼近人類的語言,寫起來如同一篇文章一樣。

  舉例來說,你今天聘請了四位女僕來為你工作,協助你生活。你想試試他們的打掃能力來決定讓誰打掃。所以你佈置了四個一模一樣的房間讓他們公平競爭。

  你對第一位女僕說,請打掃這個房間。接著他花了十個小時將這房間打掃得無可挑剔,一塵不染。

  你對第二位女僕說,請打掃這個房間。接著他花了兩個小時將這房間大致打掃過,看起來一塵不染,仔細檢查仍有些地方沒打掃乾淨,但是對於生活而言已是相當乾淨。

  你對第三位女僕說,請打掃這個房間。他露出一副困惑的眼神,恭敬地向你詢問各種打掃細節,例如是否要拖地,窗戶要擦哪幾個,等等。並按照你的要求,在半小時內將其全部完美地完成了。但是你發現他並沒有做任何你沒要求的事,比方說你忘了要求的擦桌子。

  你對第四位女僕說,請打掃這個房間。他開始顯得手足無措,你努力了半天後決定開始一步步教他如何打掃,並將如何做各種打掃講得非常詳盡,包括拿掃把的方式、使力的方法以及打掃順序等等。接著你驚人地發現這位女僕用著完美而毫無冗餘的動作,只花了五分鐘便完成了你所教導並下令的所有掃除工作。

  在這個例子中,前兩位女僕便是不同的高階語言。只接受了「打掃」這個曖昧不清的命令,以及「房間」這個不夠詳細的目標對象,便用他們各自的想法猜測你想要的是什麼樣的結果,然後為你工作。他們可能不會想知道你的詳細要求,也不必知道。

  你也可以選擇要求,或是下達比「打掃」更精密但更繁瑣的指令。但他們各種周詳的考慮,可能會使他們做些你預期外的事,也會影響他們工作的速度。你難以預估他們做了什麼樣的考慮。他們可能怕你下的命令會打破花瓶而拒絕或改變做法,但你若對他們了解不夠深入,你會難以預期結果。

  第三位女僕則像是 C 語言,這個比較接近低階語言的高階語言。他無法接受打掃這樣過於籠統的命令,你必須詳細指定各種細節。但是這讓你能夠更深入地控制他們的打掃方式,讓他們做的事情更貼近你詳細的需求。代價就是命令會比較繁瑣、麻煩。

  同時他不會為你考慮太多事情。例如你要求的打掃方式可能會打破花瓶,他只會在時候到時毫無顧慮地打破它。所以他的打掃會更快更簡潔,但你必須更加細心,更加留意小細節。他只會在你的命令使他的生命受到威脅時對你發出警告,若你並不在乎或你認為沒那麼嚴重,他也會老實接受並確實執行,即使會死也做到至死方休。他只會在你提出超出規格的要求時拒絕你,例如要他從原地將硬幣扔到美國之類的。

  第四位女僕就是低階語言了。他們幾乎只學會了最最最最低限度的動作,但是舉手投足之間所有細節,幾乎完全在你的掌控之下。若你對掃除有獨到的心得,可以讓它用最適合你房間的打掃順序與方式,在完全無任何多餘的動作與考量下,可以針對性地獲得最佳的結果。但是若你不那麼熟悉時,你可能必須花相當多時間下指示,而他會用比他人更笨拙的動作,花上更多的時間獲得更差的結果。

  總結來說,這些語言至今仍能並存,主要是因為在各種方面均各有優劣,無法完全取代彼此。優點同時也是兩面刃,必須因應需求做出取捨。

  C 語言雖然入門的門檻較高,但若有心學習,會是相當合適的語言。它在語法上相當嚴謹,寫法上卻又相當寬鬆。正因為寬鬆,你必須考慮更多其它語言不用考慮的事。這些細節你應該知道,然而學習其它高階語言將無法學習到這些事,因為它們幫你處理掉了。因為比較接近低階語言,會更貼近電腦實際的運作,同時不會提供你太多偷懶的指令,你會被要求親自處理,而這能讓你學習更多。

所有一切的開端

一切皆已解明。並非出自任何目的,是非善惡優劣已無關緊要,文章任憑喜好決定。


單純只是,想寫這些東西,並分享給同樣喜歡它們、需要它們的人。


能夠幫上誰、想要幫上誰、又會認識到誰,全部隨緣。


就讓所有的一切,在這新生之地歸零,然後重新來過。


若有興趣便自行跟著吧。