July 20, 2017

[CS50] Week 1 C - 課程筆記

*此課程筆記是依據 CS50x 2017 版本(也就是哈佛大學 2016 秋季班的 CS50)所寫。

CS50 原本就提供很詳細的課程筆記了(第一週筆記),所以這個筆記偏向記錄我比較不熟悉的部分而已(事實上這個筆記也是很長,廢話不小心寫太多)。

雖然我學程式的資歷還很淺,但已經不算是完全新手,看完第一週的課程影片後,覺得如果當初 CS50 是我的第一門程式課,我應該是痛苦不堪然後快速投降 XD

剛開始學程式的時候,我花了不少時間才完全理解第一週裡面教到的東西,像是函式、迴圈、條件判斷、資料型態等等,有些甚至到現在都還不算完全理解,像是浮點數。而 CS50 用一堂課就全部都講完了。雖然看完課程之後,我自認有 80% 以上的內容對我而言都是以前學過的東西,或容易理解的概念,但是在寫作業的時候仍然卡關嚴重(因為之前都沒有認真練寫程式 XD)。

即使第一週課程大部分的內容對我來說都不陌生,不過上完課還是收穫很多,Malan 教授上課的脈絡非常清晰,課程的步調也抓得很好,我甚至懷疑他在課堂上連出錯都是原本就安排好的,因為想要讓學生了解寫程式會發生的錯誤和一些邏輯思考,不過到底是不是真的出錯這就不得而知了。

課程影片如果是點進 Lecture 可以選不同觀看模式:

進入之後左下角有 4 種觀看模式,1) Main Cameras 就是一般的觀看模式,2) Multi Screen 是主畫面加上老師的投影片畫面,是蠻好用的觀看模式:

3) 360 Video 觀看模式在一般解析度下(720 或 1080)畫質都比較差,需要把解析度設定成 4k 才會有清楚的畫質。4) Explained 的觀看模式蠻有趣的,在時間軸有灰點的地方,會跑出 Malan 教授跟一個助教以聊天的方式,解釋那一段落為什麼要這樣講解之類的,有興趣的人可以看看:

除了主要的課程影片之外,還有助教對當週課程的補充影片 Shorts,如果單看課程影片還是有不懂的地方,非常推薦把助教的補充影片都看過:


--
不知不覺又打了很多廢話,總算要進入筆記的部分了:

1. 從原始碼 (Source code) 到機器碼 (Machine code)。
原始碼就是人類可讀的電腦語言指令,像是這個:

但是電腦只懂得由 0 和 1 組成的機器碼,像是這個:

要把原始碼「翻譯」成機器碼,這樣電腦才能理解,就要利用編譯器 (compiler)。這三者的關係就是:
原始碼 (Source code) -> 編譯器 (compiler) 翻譯 -> 機器碼 (machine code)

2. CS50 IDE。
CS50 IDE 是 CS50 課程統一使用的雲端整合開發環境 (Integrated Development Environment),可以從 cs50.io 登入。進入 cs50.io 之後在選單下選 edX:

然後輸入 edX 的帳號跟密碼就可以進入 CS50 IDE 裡面。

3. 執行 C 語言所寫的檔案。
建立新的檔案時要在檔名後面加上 .c 的副檔名,這樣電腦才知道那個檔案是 C 的檔案。

hello.c 這個檔案為例,要執行就要輸入下面的兩個指令:

其中的 clang 是 C language 的意思,它是編譯器,所以輸入 clang hello.c 就是叫它編譯 hello.c 這個檔案。
編譯後的程式的預設名字是 a.out,所以輸入 ./a.out 就是執行檔案的意思。


a.out 不是直觀易記的指令,所以我們可以藉由輸入參數,叫 clang 把編譯後的程式改成容易記的名字,於是就可以輸入下面兩個指令:

其中-ooutput,並指定 hello.c 編譯後輸出到 hello
所以現在我們只要輸入 ./hello 就可以執行檔案了。


但看起來似乎沒有比一開始的指令還少,所以最後用 make 這個程式來幫我們簡化:

make 不是編譯器,它是一個程式,用來觸發編譯器 clang 執行。所以 make hello 就會自動從 hello.c 這個檔案建立執行檔 hello

所以以後我們只要利用 make 就可以編譯檔案了,然後輸入 ./hello 就可以執行檔案。

4. command line 指令。
由於在 CS50 IDE 裡面需要使用 terminal 來執行許多指令,所以需要熟悉一些常用的指令。在 Linux 作業系統(因為 CS50 IDE 雲端電腦是用 Linux 作業系統)的 command line 指令,常見的有:

-cd:(change directory) 用來在不同資料夾之間移動。. 表示目前所在的資料夾,.. 則表示上一層的資料夾。
-pwd:(present working directory) 用來印出目前所在資料夾的名稱。
-ls:(list) 用來列出所在資料夾內的所有檔案。
-mkdir:(make directory) 用來建立一個資料夾。
-cp:(copy) 用來複製一個檔案,在指令後面需要打兩個檔案名稱及副檔名,第一個是被複製的檔案名稱,第二個是要複製到的目的檔案名稱。如果要複製資料夾,要加上參數 --r (recursive),才能夠一起複製資料夾內的所有檔案。
-rm:(remove) 用來刪除一個檔案。刪除檔案前 terminal 會再次確認是否要刪除檔案,如果想要略過確認問題,可以加上參數 -f (force),如果要刪除資料夾可以加上參數 -r,如果想要刪除資料夾而且略過 terminal 的確認問題,可以加上參數 -rf,使用 -rf 一定要小心,刪除之後就無法回復了。
-rmdir:(remove directory) 用來刪除一個資料夾。
-mv:(move) 用來修改檔案的名稱,在指令後面需要打兩個檔案名稱及副檔名,第一個是原始檔案名稱,第二個是新的檔案名稱。

5. CS50 Library。
CS50 的團隊有寫一個函式庫 (Library),用來幫助初學者快速學習 C 語言。只要在檔案的開始引入 CS50 Library 即可:
#include <cs50.h>

函式庫顧名思義就是有很多函式 (function) 的地方,在檔案一開始引入的 stdio.h (Standard Input and Output 的意思)也是函式庫:
#include <stdio.h>

stdio.h 內含 C 語言的基本函式,像是 printf 這個函式。要把字串印出來都會用到 printf (print formatted 的意思)。

C 語言在取得使用者的輸入值 (input)  這方面比較弱,所以 CS50 Library 裡包含一些可以取得使用者輸入值的函式,像是:

CS50 Library 還有包含 C 語言裡沒有定義的一些資料型態 (Data type),像是 boolstring

C 語言在創建變數的時候,第一次使用那個變數時要指定資料型態,這在課程之後會學到的 Python 跟 JavaScript 這兩個語言則不需要指定變數的資料型態。

-bool:布林值(只有 True 跟 False 兩種),在 C 語言是 1 byte (8 bits) 的大小。
-char:用來儲存一個字母的資料型態,在 C 語言是 1 byte (8 bits) 的大小,儲存的值範圍是 -2^7 到 2^7 - 1。
-double:用來儲存浮點數的資料型態,但大小是 float 的雙倍,所以在 C 語言裡是 8 bytes (64 bits) 的大小,由於大小比 float 大,所以有比較高的精準度。
-float:浮點數,也稱為實數 (real numbers),在 C 語言裡是 4 bytes (32 bits) 的大小,浮點數較難定義它的值的範圍。浮點數要注意精準度的問題。
-int:(正/負)整數,在 C 語言裡是 4 bytes (32 bits) 的大小,所以整數範圍是 -2^31 到 2^31 -1。
-unsigned intunsigned 並不是一種資料型態,而是 qualifier,可以放在某些資料型態前面(例如 int)。當你確定所需要的整數只有正整數時,unsigned int 可以拿掉負整數佔用的空間並將正整數加倍,所以數字範圍變成 0 到 2^32 - 1(這個是在助教的 Data Types 補充影片裡)。
-long long:用來儲存整數的資料型態,大小是 int 的雙倍,所以在 C 語言是 8 byte (64 bits) 的大小。
-string:字串,用來儲存一串字母(單字、句子或一個段落都可以),在 C 語言是 8 byte (64 bits) 的大小(之後會教到如何增加字串大小)。

6. 佔位字元 (placeholder)。
因為變數有不同的資料型態,所以它們的 placeholder 也要區分不同的資料型態。

%c => char
%f => float
%d => double
%i => int
%lld => long long
%s => string

其中浮點數的 placeholder 可以指定要顯示小數點以下幾位的數字,例如要小數點以下二位數字,就是 %.2f,小數點以下十位數字,就是 %.10f

7. 數學符號。
+:加號。
-:減號。
*:乘號,是用星號而不是用 x,因為 x 是變數。
/:除號。
%:(modulus) 計算餘數的符號。

=:一個等號是「指定」的意思,int i = 0 表示把變數 i 設成 0。一個等號的運作方式是:把等號右邊的值「指定」給等號左邊的值。
==:兩個等號才是「等於」的意思。

x = x + 1; 等於 x += 1; 等於 x++;
x = x - 1; 等於 x -= 1; 等於 x--;
x = x * 5; 等於 x *= 5;
x = x / 2; 等於 x /= 2;

8. 布林表式 (Boolean expressions)。
1) Logical operators:
-AND(用 && 表示)

-OR(用 || 表示)

-NOT(用 ! 表示)

(圖片是斜的,因為是從助教的 Operators 補充影片裡截圖下來的,請見諒)

2) Relational operators:
小於:x < y
小於等於:x <= y
大於:x > y
大於等於:x >= y

等於:x == y
不等於:x != y

9. 單引號與雙引號。
C 語言裡面有分單引號 (' ') 是用在一個字母的 char,而雙引號 (" ") 是用在字串 string

10. 條件判斷 (conditional statements)。
1) if else:ifelse 組合後變成 else if,可用在判斷多個條件上,這裡我只有截圖最簡單的一個條件判斷。

2) switch:有加 break; 跟沒有加 break; 都可以,端看需求而決定,這裡我只有截圖有加 break;


3) ternary operator (? :):比較特殊的用法,用在快速判斷上。

(圖片是從助教的 Conditional statements 補充影片裡截圖下來的)

11. 迴圈 (loops)。
1) while loop:通常在不知道需要重複幾次的情況下使用,也有可能一次都沒執行。
無限迴圈:
while (true)
{

}
// this is a infinite loop, ctrl + c to kill infinite loop

有限迴圈:
while (boolean-expr)
{

}
// this is a finite loop, it continues until it's false

2) do-while loop:通常在不知道需要重複幾次的情況下使用,但希望它至少執行一次。
do
{

}
while(boolean-expr);
// it will run at least one time

3) for loop:通常在知道要重複幾次的情況下使用。
for (int i = 0; i < 10; i++)
{

}

12. 原型 (prototype)。
這個部分是關於如何設計程式碼,除了用函式庫裡面的函式之外,自己也能定義函式,讓程式碼的可讀性跟可利用性提高。

在定義函式的時候,要另外指定函式是否會回傳值,如果不會的話用 void,會回傳整數用 int,並且在 int main(void) 函式前面要先宣告自己定義的函式。
用課堂上的例子解說不會回傳值的函式:
#include <cs50.h>
#include <stdio.h>

void print_name(string name);

int main(void)
{
    string s = get_string();
    print_name(s);
}

void print_name(string name)
{
    printf("hello, %s\n", name);
}

int main(void) 函式裡面的第 9 行 是自己定義的函式,自己命名 print_name,並拿使用者的輸入值 s 當作參數,所以在下面第 12 行寫我們想要 print_name(s); 做什麼事情,因為要教 C 語言我們要 print_name(s); 做什麼。

一開始要先指定 print_name() 會不會回傳值,我們只要它做 printf,所以不會回傳值,於是在函式前面加上 void。小括號內的 (string name) 代表使用的參數是字串 string,並且我們把它命名為 name

void print_name(string name) 這個函式要做什麼事情?就是 printf("hello, %s\n", name);,把 hello 這個字串跟使用者輸入的字串結合起來。

最後在 int main(void) 函式前面先宣告 void print_name(string name);,告訴 C 之後有我們自己定義的函式。因為程式碼是由上往下讀取,如果沒有先宣告函式,C 會出現錯誤。但如果把 print_name(string name) 函式整個搬到 int main(void) 之前,則會喧賓奪主跟影響程式的可讀性,畢竟主要程式是在 int main(void) 裡面,所以傳統上會讓他在前面。

另一個例子是有回傳整數,所以用 int 指定函式:
#include 
#include 

int square(int n);

int main(void)
{
    printf("x is ");
    int x = get_int();
    printf("x^2 is %i\n", square(x));
}

int square(int n)
{
    return n * n;
}

這次我們自己定義的函式叫做 square,並拿使用者的輸入值 x 當作參數。

要先指定 square() 會不會回傳值,我們要它做整數運算,所以會回傳整數,於是在函式前面加上 int。小括號內的 (int n) 代表使用的參數是整數 int,並且我們把它命名為 n

int square(int n) 這個函式要做什麼事情?就是把使用者輸入的整數平方,並且回傳 (return)。回傳的概念有點像是,把拿到的值先儲存起來,而不是像 printf 直接印出來,儲存起來之後回傳到我們使用它的地方。拿以前數學學過的 y = f(x) 舉例,f 是方程式,x 是參數,y 就是 return 的值(感謝胡立老師補充)。

最後在 int main(void) 函式前面一樣要先宣告 int square(int n);

如果有人跟我一樣剛開始分不清「變數」 (variable) 跟「參數」 (parameter) 的話,它們的差別在於,參數是在函式裡面的變數,所以在函式小括號內的會稱為「參數」,在函式外的會稱為「變數」。

還有一個是「引數」 (argument),引數跟參數類似,詳細的區分是「引數是用於呼叫函式,參數是方法宣告」(參考:引數 (Argument) vs. 參數 (Parameter))。剛開始只要能分別變數與參數就可以,熟悉之後再細分引數與參數的差別。

把自訂的函式抽離出主要函式,讓每個函式負責小部分,都是為了讓程式碼的重複利用性以及可讀性提高,這些步驟可以稱為抽象化 (abstraction),在多人一起工作寫程式時非常重要。


第一週的課程筆記就到這裡,其實還有幾個重要的部分沒有寫出來,像是浮點數的精準度、數字 overflow 跟編譯的詳細情況,但這篇的篇幅已經蠻長了,所以寫在胡立老師的中文導讀筆記裡 [CS50] Week 1 C - 中文導讀筆記,如果有需要也可以參考那篇筆記。

寫那麼多字可能多少會出錯,如果有錯還請指正。


No comments:

Post a Comment