Yakim shu Hi, 這是我擴充腦內海馬體的地方。

【筆記】CS50 - week 1 ( 2019年更新 )

Week 1 主要課程素材 :

助教課程:


註:我在 2019 年又重看了一次 2018 年新出的影片,所以內容有些不一樣。

Week 0 的 pseudocode (虛擬碼)如圖,對於一個懂英文的成年人應該可以了解其中的內容。 查找電話簿的虛擬碼

但要是對一個小朋友說:「請先翻開書的一半」。

他可能連「書」跟「一半」的概念是什麼都還不懂,那要怎麼期待會回應正確的動作呢?語言大部分都很抽象,更何況電腦語言。所以我們要盡可能的把指令描述明確且具體,讓電腦接收到我們的指令時不產生誤解,正確的執行我們下的命令。

Week 1 以一個好玩的實驗開頭,來證明 指令精準的重要性。

三明治鬧劇

實驗是這樣的,David 請兩位同學上台,桌上擺了三組「製作三明治的食材與工具」,讓台下同學發號施命,一個口令一個動作,讓台上的三人執行一個「製作三明治」的動作。

  1. 打開麵包袋。(指令不夠精準)
    • 從中間把袋子撕開
    • 乖乖不破壞包裝地打開
  2. 把麵包從袋子拿出。(指令不夠精準)
    • 拿兩片吐司出來
    • 拿整包吐司出來
  3. 擰開草莓果醬的蓋子。(指令適當、大家都有同樣的解讀)
    • 大家都一樣
  4. 拿起奶油刀。(指令不夠精準)
    • 握著刀子底部
    • 握著刀口(嚇!!)
  5. …..最後還是做出一個三明治了,但完成品之嚇人,完全可以投稿臉書社團 婦仇者廚房(非常舒壓、建議加入)

目的是讓大家很清楚的意識到,台上的都是讀到哈佛的高材生,對英文的理解自然沒有問題,但是每個人對指令的解讀還是天差地遠。(當然很大程度是故意搞笑,但不影響理解這個概念)


Scratch vs. C

這節開始比較 Week 0 所使用的圖像化 Scratch 轉換成傳統的 程式語言 C

什麼?你說上節分明就沒有介紹到 Scratch ,對我懶但你不能跟著懶,請自己點進去 開始一個專案 玩玩~

之所以使用 Scratch 當做入門,而不是 C 語言,是因為讓初學者可以專心在邏輯設計上,而不會又出現哪裡有報錯而感到很無力。(像我一開始學程式的時候,就很不解為什麼結束非得要加 ;

話說最近在練習 Canvas,引入 CreateJs 實作 電流急急棒 ,看似很簡單,但還是碰了不少壁,但如果只是想練習寫個小程式,真得有必要熟讀操作文件去實現那些簡單的邏輯嗎?

但再看看網路上找到的 學校老師做的 scratch 實作電流急急棒 ,用圖像化把語言模塊相接在一起,比我那一大篇 js 還要親民多了吧,而且最終成果也比我屌很多哈哈哈哈(我太弱還做不出障礙物)


Hello.c

所有程式教學第一步都要對世界打個招呼!

這是一個最簡單的程式,功能只有:印出一行 “ hello, world “

在 scratch 裡可以這麼做,很清楚一看就懂

scratch - hello

那用C語言寫的話,會是以下這段

#include <stdio.h>
int main(void) // → (黃色塊)小綠旗
{
    printf("hello, world\n"); // → (紫色塊)say
}

\n 代表的是「這行文字已經結束了」的表示符號。


if else

if else 也是程式語言中共通的判斷式,以下圖為例:

  1. if 括號內的內容如果正確,就執行對應 { } 裡的內容,
  2. 反之,如果錯誤了,會尋找 else if 括號內容,正確就執行對應 { } 內容,
  3. 如果都沒找到,執行 else 對應的 { } 內容。

ifelse

  1. x 為 1, y 為 2
    • 會輸出 “x is less than y”
  2. x 為 2, y 為 1
    • 會輸出 “x is greater than y”
  3. x 為 1, y 為 1
    • 會輸出 “x is equal to y”

這邊 David 提出一點思考,在第三個判斷式中,我們需不需要寫成 else if ( x == y )?

從比較兩個數字大小的動作中,我們知道只會得到三種結果,大於、小於跟等於。 也就是說,前面已經比較完大於跟小於,最後剩下的只有 等於 這個結果,那到底需不需要寫呢?

寫下的優點

程式非常的明確好懂。

因為這是個很簡單、大家都可以輕易理解,所以並沒有太大問題。 但要是今天換成一個複雜的判斷,或者是只有你懂的情況,接手同事可能會感到很困擾。

寫下的缺點

電腦多了一層判斷。

就算現在電腦的速度跑非常快,就算多了這一行判斷的時間,也快到你察覺不了。 但如果平常沒有維持好習慣,努力縮小這些不必要的判斷時間,在你寫出成千甚至上萬行的程序後,會漸漸體會到這些差異。


loops

前面是 說一次 “hello, world”,現在來試試 不斷地說 “hello, world”

實現循環在程式語言中叫做 loops

while

上圖右邊的程式碼就是說,當 while 括號內容 為真 (true),就會不斷執行大括號 { } 裡的內容

while (true) // → 括號內容為 true
{
    // 不斷地運作這裡的程式碼
}

懂得 不斷地hello, world,那要是想要做 有限次數 的循環呢?

說 100 次、 50 次、 2 次 hello, world,這時候就可以用 for loops

for 後括號內容為:(起始值; 條件式; 更新值)

for loop

迴圈順序如下:

  1. 設定變數 i 為 0 ( 起始值 )
  2. 檢查 i 是否在 50 以內 ( 條件式 ),要是不成立就跳出迴圈( 步驟6. )
  3. 說一次 hello, world ( 執行 { } 內容 )
  4. i + 1 ( 更新值 )
  5. 回到 ( 步驟 2. )
  6. 結束迴圈
for (var i = 0; i < 50; i++) // 當i小於50,就繼續執行
{
    // 執行50次這裡的程式碼
    printf("hello, world\n");
}

以下是讓使用者輸入名字,讓電腦在螢幕上輸出的小程式。 檔名為 hello.c

#include <cs50.h>
#include <stdio.h>

int main(void)
{
    string name = get_string("what's your name?\n");
    printf("hello, %s\n", name);    
}

C 語言中沒有字串這種資料型態,所以透過引入課程中已經寫好的 cs50.h 的函式庫 (或稱 library),我們就可以使用 string 當作變數,也可以使用 get_string 來得到使用者輸入的變數。

同樣的,printf( ) 也是透過引入 stdio.h 後才能夠使用,將電腦接收到的文字輸出在螢幕上。


原始碼、編譯器、機械碼

程式在電腦執行的過程應該是這樣的。

編譯原始碼

所以當我們寫好上一個例子 hello.c,要怎麼讓他執行呢?這裡並沒有像 Scratch 裡有個神奇的綠色旗子可以按,所以我們要輸入一些指令,將 hello.c編譯執行編譯後的機械碼

編譯成機械碼

在這裡課程也包裝了一個指令 make,可以將編譯的指令寫得比較精簡,如下:

make hello,這行代表著「 請將 hello.c 的檔案編譯成 hello 」

如果沒有 make 可以用,就不能編譯了嗎?當然不是,只是要打的字多一點而已,如下:

clang -o hello hello.c ,跟上述是一樣意思,但使用 make 是不是簡單多了呢?

執行編譯後的機械碼

執行很簡單,只要在剛剛編譯好的機械碼前加上 ./ 就好了,所以當我們要執行剛剛的程式為 ./hello


注意資料型態

2 除以 10 的答案是什麼?

「 0.2 阿!還用說嗎?」 「這要看變數存什麼樣的資料型態而定,還有看指定回傳的數字有多精確。」

出錯的 float

例如上圖,使用者輸入的 x y 雖然是整數,但以 float 的資料型態儲存 (截的這張圖看不出來),在 printf( ) 裡指定要存到小數點後 50 位數 ( %.50f ),結果就是小數點後第 17 位數開始,登愣!壞掉了。

為什麼會造成這樣的結果呢?

電腦的記憶體是有限的,當我們想運算無限位數的數字該怎麼辦呢?結果就是出現 bug 了,電腦能做的只有非常接近,但無法達到100%的準確,所以應該要盡量避免去比較太多位數的數字。

台下同學發問,那是不是永遠都使用 double 以保持更精確?

當然可以這麼想,但是要付出代價。使用 double 變數佔用的記憶體空間是 float 的兩倍,記憶體空間是有限的,代表這也佔去了其他重要的位置,更何況如果只是儲存更多的 0 呢?所以是端看當時的情況來做出取捨。


Overflow 溢位

剛剛的例子說明了記憶體空間的有限性,那當然 int 整數型也是有上限的,一般來說是 32 bit (4 byte),即為 -2147483648 ~ 2147483647。一但數字超過這範圍,系統就會出錯。

例如著名的波音 787 overflow 事件,請參考 Jserv與他愉快的小夥伴 - 臉書貼文

想深入了解搞錯資料型態的可以參考: