[初章] 05 無盡迴廊的門扉

前面都在單挑,假如今天要做的是道具欄,怎麼處理?
先分析一下要求:
做出一個簡易道具欄,具備以下功能。
  一、顯示所持道具及數量,依取得順序排列。相同道具會重疊。
  二、獲得指定道具。

為了先不讓事情變得太複雜,功能先別太多太雜。反正之後可以再慢慢加!
與其把目標訂得太遠大,先從能確實完成小目標開始做起!


要做出這種道具欄,要能做到以下兩件事:
  一、能儲存一個道具
  二、能儲存道具順序
WTF?
回想一下我們學過什麼。用輸出表現服從、用輸入下達命令、記憶的操作以及改變上述的流程。有哪些能用?輸出和輸入顯然不像,改變流程也要有指令可以改變,那麼難道是用記憶的操作?
可是我們只會存整數,連實數都存不了,還想存一個道具這麼抽象的概念?喂喂這篇其實是二還三章吧是不是標錯了,壓根不連續啊!剛打完史萊姆馬上要打龍了!?
更別提道具順序了,能做到第一點之前怎麼想都不可能啊!

如果我跟你說,電腦其實只能儲存數字,你信不信?
當然不信啊,鬼才信啊!那我看到這些文字都是見鬼?聽音樂看圖片甚至看電影難道都是見鬼了?別跟我說文字音樂圖片甚至電影都是數字!

哈哈你突破盲點啦!沒錯這些通通都是數字!
電影都能用數字存,區區道具卻不能只用數字存?沒道理、沒可能、這不科學!
其它東西如何儲存留待番外篇討論,先讓我們想想道具怎麼用整數來表達。

我們怎麼分辨一個人?看名字,名字會重覆;那什麼東西不會重覆?想想你的身份證怎麼識別你的身份,沒錯!身份證字號!
你說身份證字號本身有意義?不,其實沒有,看起來只是一段亂碼。
但是當你出生,國家發給你一個身份證字號,假設H011235813,從此之後這段亂碼就有了一個新的意義: 當它作為身份證字號去解讀時,它就代表著「你」這個人。

也就是說,這個國家擅自、強迫、任性地賦予這段亂碼一個意義,並且讓整個國家擁有相同的共識,用來識別一個人的身份。只要沒有多個人被賦予相同的身份證字號,就能確實地辨別不同的人。
只要強迫所有人認同,且所有人認知相同時,它就是對的!

對程式而言,我們這些創造者,就是老大,擁有無上的權力!
所以只要對每個道具擅自、強迫、任性地賦予一個獨一無二、不與其它道具重複的東西,強迫程式和我們持有共同認知,我們同樣能識別每個道具!
現在我們所擁有的只有整數,那就用整數代表每個道具吧!

比如說,我們這樣定義:
1: 美味昆蟲汁
2: 屠龍報紙劍
3: 抗打空氣甲

這數字我們通常叫它ID,Identity(身份)的縮寫。
以後看到道具ID為1,就是美味昆蟲汁;ID為2就是屠龍報紙劍,依此類推!

好,我們解決了第一點,可以著手第二點了: 如何儲存道具順序?

如果我們有一個序列,第一個數字代表第一個道具ID,第二個數字代表第二個道具ID,…,豈不完美?但我們能儲存整數的,只有單一的獨立變數可用。假如今天有5個道具的話…

int item1, item2, item3, item4, item5;

如果我們要取第N個道具是什麼…名字是寫死的,所以好像只能這樣搞:

if(n == 1)
{
    id = item1;
}
if(n == 2)
{
    id = item2;
}
…WTF不寫了啦!道具隨便都百個起跳,豈不寫到崩潰!?

沒關係,今天就要教大家怎麼宣告一個陣列,可以看成是變數大家族!一次就能誕生成千上萬的變數,而且能夠隨意指定今晚要誰!很棒吧?
它長得像這樣:

int item_bag[5];

看起來和一般宣告單一變數頗相似,只差在[5]而已;讓我們拆解看看是什麼意思:

int    <== 變數的屬性。int屬性代表整數!
item_bag    <== 變數大家族的家名!一樣要慎重地取唷!
[    <== 中括號。帶中括號代表是大家族,不是獨自一人!中括號裡放家族成員數!
5    <== 成員數,整串必須代表一個「常數」。
];

這指令做了一件事: 宣告了一個名為item_bag的變數大家族,共有5個成員!相當於一口氣宣告了5個變數!

什麼叫常數?常數就是我們可以直接確定數值的固定數。比如說8是常數,8+7也是常數,8+7-2*4*5也是常數。只要一串運算只包含常數,不管算幾次、何時算,結果都是固定的,所以會是常數。
什麼時候不固定?包含變數時。比如說變數a,不同時期、甚至不是同一次執行都可能不一樣!最簡單的例子是變數a用來接使用者輸入時,誰知道使用者會輸入什麼鬼東西?

家族的成員數必須是固定的、一開始就知道的才行!所以只能使用常數喔!
家族的所有成員,都有一個唯一的整數作為ID。當我們指定要誰出來時,只要指定他的ID,就會叫到那人。比如說,

item_bag[1] = 2;

這就相當於指名item_bag家族的二姊出來,把他掌管的記憶改成2。
只要家族名加上[]指定ID後,就跟一般的變數沒兩樣了!很方便吧?

什麼?你說為什麼是二姊?很簡單,因為大姊是0啊!所以2就是三妹囉~
宣告時雖是[5],但是item_bag[5]其實不存在喔,是空號。
因為ID是從0開始的,又說家族只有5個成員
0 <== 大姊
1 <== 二姊
2 <== 三妹
3 <== 四妹
4 <== 五妹
5 <== 空號,已經五個人了!

看今晚想找誰就敲誰的門,敲空房可沒人理你喔!

學會了陣列,也就是變數大家族,道具順序還不簡單嗎!
道具欄第一項放第一間房,第二項放第二間房,順序不就有了!再也不用寫上千個if啦哈哈哈超愉悅的啦~

那麼,我們就把它寫出來吧!為了方便測試,我們讓使用者能夠操作這個道具欄!
輸入顯示指令display就印出道具欄目前內容,輸入獲得道具指令add xx就把它加進道具欄。

WTF!? 我們不會讀文字啊,怎辦!?嘿嘿,可別跟我說你就不會寫了!咱們剛不才提過身份證字號,舉一反三一下啊!沒人說一定要用文字啊?
比如說現在劇情來到分歧點,選項1是display,選項2是add,那輸入1表示display,輸入2表示add不就成了!有身份證概念這還不簡單嗎?
所以要輸入一個數字,代表選的選項!之後再依選項不同,做不一樣的事!

int cmd, id;    <== 指令command的縮寫
scanf("%d", &cmd);    <== 先看看使用者想做什麼
if(cmd == 1)    <== 顯示道具欄
{
    display_item();    <== 顯示道具欄的指令!先留著之後煩惱,解決指令判斷再說!
}
if(cmd == 2)    <== 增加道具
{
    scanf("%d", &id);    <== 需要額外讀入一個整數,表示獲得的道具的ID
    add_item(id);    <== 獲得道具!一樣留著之後煩惱
}

總覺得好像缺少了什麼…執行完就結束了,要顯示道具欄就不能增加道具,可一開始是空的;要增加道具就不能顯示道具欄,也不知道結果對不對…
所以,我們任性地認為程式身為我們的女僕就應該要服侍到我們滿意才可以結束!
直到我們滿意以前,服侍!有感覺了吧?while對不對?

int cmd, id, emo;  <== 多加一個emo代表我們滿足了沒
emo = 0;  <== 一開始當然不滿意,根本還沒開始服侍嘛!
while(emo == 0)  <== 當心情為不滿意時,持續執行!
{
    scanf("%d", &cmd);    <== 先看看使用者想做什麼
    if(cmd == 0)    <== 追加一個,告訴女僕我們滿意了!用的
    {
        emo = 1;    <== 什麼數字代表滿意還不滿意,由我們決定,女僕服從!
    }
    if(cmd == 1)    <== 顯示道具欄
    {
        display_item();    <== 顯示道具欄
    }
    if(cmd == 2)    <== 增加道具
    {
        scanf("%d", &id);    <== 需要額外讀入一個整數,表示獲得的道具的ID
        add_item(id);    <== 獲得道具
    }
}

完美!只差把兩隻小妖精補上就行了,結構上清楚易懂!接著把剩下的部份完成就行了!

int item_bag[5], item_cnt;    <== 5格大的道具欄,以及記錄目前道具個數!
int item_num[5];    <== 記錄道具欄每一格的數量

接下來是顯示道具,文字太麻煩,我們顯示ID就好。
顯示所有道具這件事,看起來好像沒有對應的語法?沒關係,讓我們換句話說!
將所有道具依序顯示一次
將道具欄一格一格顯示出來
將道具欄從第一格開始顯示
將道具欄從第一格顯示到最後一格
從第一格開始,直到最後一格為止,顯示當前格道具ID

好,經過很錯蹤複雜根本沒道理可循的變換之後,我們找到了初始值、while流程控制以及輸出一個整數。
一開始學程式,大半瓶頸都是卡在如何將想法轉換成程式執行步驟!我們可以看到上面做了什麼:

顯示所有道具?太模糊了吧,先定義什麼叫作顯示、什麼叫所有道具行嗎?
行,顯示就是輸出讓我們看,顯示道具就是輸出道具ID,所有道具就是從道具欄所有道具
=> 將道具欄一格一格顯示出來

什麼叫一格一格顯示?太模糊了,沒有順序的話亂序輸出也不算錯;沒有順序也沒有起終點之類的,對電腦而言就是模糊!
=> 將道具欄從第一格開始顯示

意圖越來越明顯,敘述也越來越精楚了!但是,有了起點,沒有終點,一樣模糊!
=> 將道具欄從第一格顯示到最後一格

喔喔,起終點都有了!同時也有了順序,最後再將從起點到終點換句話說:
=> 從第一格開始,直到最後一格為止,顯示當前格道具ID

一般中文敘述其實是很含糊的,我們會用所謂的「常識」去解讀並補完不足的部份;但是電腦不行,所以要試著讓自己思維變笨,學著把步驟拆解得更詳細、把曖昧不明的部份通通弄清楚!否則曖昧會讓人受盡委屈的喔!古文喜歡矇矓美,但寫程式可不能如此!清楚、精確、詳細、沒有曖昧!
一開始會不習慣,且思考會轉很慢,因為我們非常不適應這種思考邏輯;所以這需要慢慢練習去習慣它,第一次總是比較痛,習慣後就慢慢懂得享受它了!

void display_item()    <== void屬性的小妖精不需要也不能回報任何東西
{
    int now;    <== 目前在第幾格
    now = 0;    <== 一開始是第一格,大姊在0
    while(now < item_cnt)    <== 直到最後一格為止
    {
        printf("slot %d: id %d", now, item_bag[now]);    <== 顯示當前格
        printf(", num: %d\n", item_num[now]);    <== 太長拆兩行寫,合併也無所謂
        now = now + 1;    <== 前進下一格
    }
}

哈哈,結果比想像中容易嘛!倒是細化步驟的過程比較容易錯亂打結,拆完後轉成程式碼根本秒殺!

接下來是獲得道具的部份,要考慮重疊,所以要做的事如下:
增加道具,重疊時數量加1,不重疊時放在最後一格

重疊太模糊,我們練習描述一下什麼叫作重疊:
當道具欄裡有相同ID的道具時叫作重疊

道具欄裡有相同ID的道具,我們還是不知道要怎麼做,才能知道是否有相同ID的道具存在。所以,再敘述得更詳細、更笨一點!
檢查道具欄每一格,看是否有相同ID的道具存在

同理檢查每一格太模糊,再把它寫詳細點:
從道具欄第一格開始,直到最後一格為止,檢查是否有相同的ID存在

好,到這邊已經能看到結局了!讓我們試試看:

void add_item(int id)
{
    int now, is_exist, loc;    <== 多一個變數記錄是否存在,以及位置
    now = 0;
    is_exist = 0;    <== 還沒開始找時,先當作不存在
    while(now < item_cnt && is_exist == 0)    <== 找到了或確定找不到了為止
    {
        if(item_bag[now] == id)    <== 如果出現了相同ID
        {
            is_exist = 1;    <== 標記為存在
            loc = now;    <== 筆記一下出現在哪邊
        }
    }
    if(is_exist == 0)    <== 如果不存在
    {
        item_bag[item_cnt] = id;    <== 在新的一格放入新的ID
        item_num[item_cnt] = 1;    <== 且新的一格數量為1
        item_cnt = item_cnt + 1;    <== 道具種類多了一種
    }
    else    <== 如果已存在
    {
        item_num[loc] = item_num[loc] + 1;    <== 道具數量增加1
    }
}

感覺好像複雜了不少,可能需要思考一下!如果能自己正確地寫出來那就偉大啦~
試著不要管上面的範例程式碼,自己動手慢慢試試看!然後執行,試想幾種情形看看結果正不正確;比如說先放道具A再放道具B,看順序對不對;再放道具A看重疊處理正不正確,再放道具C看看會不會正確放到道具欄最後面,等等!
你就想,你是老師,拿著學生寫的程式,想辦法挑他毛病,用最刁難的方式測試它!相信我,這樣一來你就會想到各種刁難的數據!

如果出錯,那麼就拿一張紙一張筆,從main開始用人腦執行這個程式的每一個步驟,把所有變數的改變都用紙筆寫下來!
這樣你就能把握你寫的程式的每一步都做了些什麼、有什麼意義,以及缺了什麼或哪邊怪怪的!這會是相當有意義的練習喔~

如果你夠細心,是不是有注意到,我好像沒有指定一開始的道具種類數量應該是0呢?
這裡告訴大家一個秘密: 全域變數的初始值是0,但是區域變數初始值就不一定了喔!
詳情請見番外篇~

最後,我們換個想法處理這道具欄!我們原本的定義是:
item_bag  <== 道具欄每一格的道具ID
item_num  <== 道具欄每一格的道具數量

先前,身為世界統治者的我們,賦予了它們這樣的意義!沒錯,這些都是我們擅自賦予的意義。所以可以因我們的任性而更改,只要全世界一起改!沒錯,改變世界的時刻到啦!
每次去找道具欄看是不是重疊太麻煩了,如果我們可以知道某道具的數量,不就知道它存不存在了?
就像我想找大姊就會給ID為0,想找三妹就會給ID為2,那如果我命令大姊一定拿昆蟲汁數量,二姊一定拿報紙,那我想問昆蟲汁不會直接找大姊嗎?
所以我們發動任性,顛覆世界認知!

item_bag  <== 道具欄每一格的道具ID
item_num  <== 特定道具ID的數量

然後全世界的規則都要跟著改:


void display_item()
{
    int now;
    now = 0;
    while(now < item_cnt)
    {
        printf("slot %d: id %d", now, item_bag[now]);
        printf(", num: %d\n", item_num[item_bag[now]]);    <== ******
        now = now + 1;
    }
}

搞什麼嘛,鬧得那麼大,宣告也沒變,顯示只改一行!可是這是什麼鬼?太複雜了吧誰看得懂啊!
item_num[item_bag[now]]

沒關係,我們來慢慢拆。我們要拿的是道具數量,而我們知道道具數量放在item_num。
item_num[??????]

在新世界的定義,??????是道具ID,而整個item_num[??????]會代表道具ID??????的數量。
比如說昆蟲汁找大姊,而大姊ID是0,所以item_num[0]代表美味昆蟲汁有幾杯!
但我們定義中昆蟲汁ID是1(詳見前面表格),我們希望讓ID是1的家族成員拿它,所以交給二姊,這樣丟進昆蟲汁的ID就可以馬上從二姊手上拿到昆蟲汁數量!

而當前格的道具ID是什麼?從item_bag[]可以拿到!
item_bag[xxxxxx]

在新世界定義,同舊世界,代表第xxxxxx格的道具ID。目前在第幾格是由now拿著,所以找now就可以得到當前格ID!所以當前格ID就是:
item_bag[now]
而把當前格ID丟進去item_num就可以拿到該ID的道具數量!所以得出:
item_num[  <== 丟道具ID
item_bag[now]  <== 道具ID
]
合起來就是item_num[item_bag[now]]了!雖然看似很複雜,但是但是!第一眼看似複雜,實際上我們不要急著想要解讀整件事的意義,一層一層解讀就會變得很簡單了!
這也是寫程式的技巧之一: 不要想一次解決一件很麻煩的大問題,要將其化繁為簡!化得每一步越笨越好!

接下來我們來看剛剛還很複雜的add_item:

void add_item(int id)
{
    if(item_num[id] == 0)    <== 如果不重覆,WTF!? 一行直接解決?前面還要while..
    {
        item_bag[item_cnt] = id;
        item_cnt = item_cnt + 1;
        item_num[id] = 1;
    }
    else    <== 如果不重覆
    {
        item_num[id] = item_num[id] + 1;    <== 數量+1
    }
}

WTF!? 怎麼這麼乾淨!原來前面只複雜在判斷重覆以及位置,現在這些都超簡化啦!
甚至還可以再超簡化一下!


void add_item(int id)
{
    if(item_num[id] == 0)    <== 如果不重覆,WTF!? 一行直接解決?前面還要while..
    {
        item_bag[item_cnt] = id;
        item_cnt = item_cnt + 1;
    }
    item_num[id] = item_num[id] + 1;
}

WTFFFF!! 竟然比顯示還乾淨俐落!見鬼了!
不過這邊在邏輯上,因為按照「事實」做過「沒有道理」的整理,相對需要點時間理解它,所以這是有好有壞的!也是「取捨」之一,拿所謂「可讀性」換取縮短長度和減少執行的指令數!
有時換個角度想,問題的難易度會差上數倍!這也是它博大精深的地方,每個人擁有的都是相同的語法,寫起來卻能有如此多的變化!

在此我們看到兩種基於任性而賦予的意義,寫法差很多,但是功能完全相同!
所以寫程式是一種沒有唯一解的藝術創作,只有不斷的練習精進並且不斷思考檢討!所以巫術之道是永無止境的,就像寫詩一樣!
如果只是練習而不多思考並檢討,可能會只滿足於一開始想到的第一種寫法,就找不到第二種了喔!任想像奔馳吧!

最後,給大家一道練習題,記得從頭自己一筆一劃把它刻出來喔:
同上述的道具欄,但加入「減少某樣道具數量」的功能!
同時,道具ID最大只能到10,最小到1,不在範圍內要提示使用者輸入有誤,輸入無法辨識的指令也要提示!
道具欄只能5格,獲得道具後會超過也要提示並取消獲得!
道具數量只能到9,超過時也要提示並且該次獲得道具作罷!

提示:
  小心處理減少某樣道具後,數量歸0的情形,這時應該消失並且後面的道具要補位!

記得要多多練習喔!有空的話也可以自己出題目給自己練習的!
記住,當你還在等人餵食的時候,別人已經開始學習狩獵了;儘管一開始成效不彰,哪天你發現不能只坐等餵食時,別人早已是狩獵高手了!
如果你想走在人前,就不能只等走在前面的人餵食。因為你只能分到吃剩的,甚至是別人老早嚼過的食物,永遠也趕不到別人前面。
即使你有幸被餵到走在人前,抱歉,前面沒有人了。因為換你走在人前!

如果你有野心,就學習自行狩獵吧!這裡只是想傳承過來人的經驗,讓後人少走些冤枉路,並且希望有一天你們也能回頭傳承自己的經驗與知識,如同我們現在正在做的事一樣!
人類壽命有限,而知識無窮;若每人都要從頭累積,肯定現在還住在山洞!
希望在你們受惠於傳承之時,也能理解傳承的重要性,並且在有所成之後,傳承下去!

沒有留言:

張貼留言