程式並不是寫完就可以自動運行,就跟不是買了水龍頭回家就有水的意思一樣。部分的程式語言還需要經過「編譯」的階段,轉成 0 和 1,本篇會講解與示範編譯的四個階段。另外帶入「陣列」的觀念,讓你了解資料儲存結構的形式,再分享三個除錯的方法。
真正的 C 語言運行需要四個編譯階段,第一篇提到的 make 檔名
是在 CS50 的平台中只是其中方便初學者理解有編譯這個過程存在,真正的編譯需要有四個階段。下面是這四個階段的簡單說明:
這個階段會處理程式中的指令,比如 #include
,用來引入外部函式庫。編譯器會找到這些函式庫並將內容加入程式,確保函式庫功能可以被正確編譯。
編譯成 Assembly language ,更難直觀看懂的語言。不同的編譯器適用不同的指令,Linux C 語言常用 gcc
當作編譯器。在 CS50 的編譯器中使用 Clang
編譯
轉成電腦能看懂的 0 和 1,每個標頭檔函示庫的功能會個別編譯。
將個別已轉成 0 和 1 的標頭檔內容連結,讓程式可運行。
這次我們改用 clang
指令進行編譯:
clang -o 自定義檔名 檔案名稱.c
如果程式有使用其他的 library ,就要另外編譯。
clang -o 自定義檔名 檔案名稱.c -l函式庫
補充說明:gcc
也是一種編譯模式,你也可以試試。
編譯器將 C 語言的原始碼轉換為組合語言(assembly language),這是比 C 語言更接近電腦用語的語言。不同的編譯器,組合語言的指令可能有所不同。以下的組合語言由 ChatGPT 提供,一般人類很難直接寫出組合語言。
section .data
hello db 'Hello, World!', 0xA ; The string to print (0xA is a newline character)
section .text
global _start ; Entry point for the program
_start:
; System call to write (sys_write)
mov rax, 1 ; System call number for sys_write (1)
mov rdi, 1 ; File descriptor (1 = stdout)
mov rsi, hello ; Pointer to the string
mov rdx, 13 ; Length of the string
syscall ; Call kernel
; System call to exit (sys_exit)
mov rax, 60 ; System call number for sys_exit (60)
xor rdi, rdi ; Exit code 0
syscall ; Call kernel
轉成 0 和 1 的二進位語言,即機器語言(machine code)。
10111000 00000001 00000000 00000000 00000000 //節錄 hello,world 部分machine code
將多個機器語言輸入:
main.o
(主程式的機器語言)。printf
函式所屬的函式庫機器語言。<cs50.h>
的 get_string
功能也會在這個階段連結成完整的程式。不一定。世界上也有「直譯語言」,也有介於兩者中間的型態。
知名的 C 語言 C++ 都屬於編譯語言,編譯語言的概念如下圖:
左邊是你的電腦,右邊是我的電腦,而編譯語言會先將程式碼編譯成0和1並儲存成「新的檔案」,再把這個檔案給我的電腦執行。此圖片截圖自,影片版請點以下連結:
Compiler and Interpreter: Compiled Language vs Interpreted Programming Languages
Interpreted Language 直譯語言:
網頁常用的 JavaScript 就屬於直譯語言。
直譯語言直接把原始的程式碼,傳送到我的電腦,直接在我的電腦逐條直譯。相較於編譯語言,直譯語言需要給對方「原始程式碼檔案」。
稍微了解編譯的過程後,我們來看另一個常用的功能:陣列。
老師希望計算每個學生的平均成績,如果學生人數不多,可能會這樣寫:
#include <stdio.h>
int main()
{
int score1 = 72; // 定義每個變數表示一個成績
int score2 = 73;
int score3 = 33;
printf("Average: %f\n", (score1 + score2 + score3) / 3.0); // 計算平均分數
}
問題: 這個方法可以處理少數學生的成績,但如果有 100 位學生,這樣寫程式就非常不方便,因為你需要定義 100 個變數,並在計算平均時把它們一個個相加。萬一第 50 位學生轉學,你還必須手動去修改變數名稱,甚至重寫部分程式碼。
此外,若還需要儲存學生的姓名、學號等資訊,這樣的寫法會讓程式變得更複雜且難以維護。因此,我們需要使用「陣列」來簡化這些操作。
每個數值都需要占用一些儲存空間,而這個空間的大小取決於數值的型態(例如,整數、浮點數等)。當我們使用陣列時,電腦可以根據每個數值的位置快速找到它,而不是從頭一個一個地搜尋。
可以把陣列想像成一列火車,每個「車廂」就是一個存放數值的位置。我們提前規劃好車廂的位置,這樣當我們需要找到某個特定車廂(數值)時,不需要從火車頭開始搜尋,而是可以直接跳到那個車廂去取東西,而這展現陣列的高效率。
#include <stdio.h>
int main()
{
int score[3]; // 我需要有存放3個數值的空間
int score[0] = 72; // 注意第一個位置是0
int score[1] = 73;
int score[2] = 33;
printf("Average:%f\n", (score[0] + score[1] + score[2]) / 3.0);
}
score[3]
表示我們需要 3 個整數的空間,並且它們的索引(位置)會是 0 到 2。0
、1
和 2
。如果我們有 100 位學生,老師不可能手動為每個學生輸入成績 100 次。所以,當我們發現有重複行為時,應該想到使用「迴圈」來自動化這些操作。這樣可以減少複製貼上,讓程式更有效率。
#include <stdio.h>
#include <cs50.h>
int main()
{
int score[3]; // 我需要有存放3個數值的空間
int score[0] = get_int("Score: ");
for(int i = 0; i < 3; i++)
{
score[i] = get_int("Score: ");//請使用者輸入分數,分數會存入記憶體
}
printf("Average:%f\n", (score[0] + score[1] + score[2]) / 3.0);
}
當你發現有「固定數字」重複出現在程式裡(例如 int score[3];
跟 (score[0] + score[1] + score[2]) / 3.0);
),代表可以使用變數來替代。這樣當學生人數改變時,只需要改變一個地方,程式就能處理不同的人數。
#include <stdio.h>
#include <cs50.h>
int main()
{
const int N = 3; // 指定 N 為常數,表示這裡 N 只能等於 3,無法更改
int score[N]; // 使用 N 來宣告一個大小為 N 的陣列,儲存成績
// 使用迴圈讓使用者輸入 N 個學生的成績
for(int i = 0; i < N; i++)
{
score[i] = get_int("Score: "); // 使用者輸入的成績依次存入陣列中
}
// 計算並輸出成績的平均值
printf("Average: %f\n", (score[0] + score[1] + score[2]) / (float) N);
// 用 (float) 將 N 轉成浮點數,確保結果是小數
}
const
宣告常數,使學生人數固定在 C 語言中,我們可以使用 const
關鍵字來宣告一個常數,這樣變數的值在程式運行期間不能被修改。適合用在不能被改變的數值,像是固定的學生人數、圓周率等等。在專案協作中,使用 const
有助於工程師溝通時了解這是不能被改變的數值,增加程式易讀性和安全性。
不使用固定常數,讓老師自行輸入
以下提供,不固定學生人數的寫法 (節錄版):
#include <stdio.h>
#include <cs50.h>
int main()
{
int n = get_int("Number of students: "); // 讓使用者輸入學生人數
int score[n]; // 根據使用者輸入的學生人數來設定陣列大小
最後還是有 score[0] + score[1] + score[2]
更聰明的寫法如下:
average
函數中。#include <cs50.h>
#include <stdio.h>
const int N = 3; // 適用於全域,其他程式編輯者可在一開始改變陣列長度
float average(int length, int array[]);//prototype宣告等一下使用此函數
int main()
{
int scores[N]; // 我需要有存放N個數值的空間
for (int i = 0; i < N; i++)
{
scores[i] = get_int("Score: "); // 請使用者輸入分數,分數會存入記憶體
}
printf("Average:%f\n", average(N, scores)); // float使 N 轉浮點數
}
float average(int length, int array[])// 定義新函數,平均的算法。
{
int sum = 0;
for (int i = 0; i < length; i++)
{
sum = array[i] + sum;
}
return sum / (float) length; // 變浮點數,平均才會有小數
}
另外一種資料結構是鏈結串列,儲存的資料不必是連續的,電腦要找資料必須從第一個開始查找。鏈結串列會談到指標(Pointer)的概念,之後的章節會再深入探討。
範例:計算 5 位學生的成績總和。
printf()
打印出每一行程式,觀察變化。範例:寫 1 加到 50 ,發現輸出錯誤,可以在中間使用printf()
檢查每一次的輸出。
debug50 ./檔名
來偵錯,而其他的介面平台也會有適合的外掛程式。