2024-09-10|閱讀時間 ‧ 約 18 分鐘

編譯四階段| 直譯語言 | 陣列概念 | 除錯三妙招

程式並不是寫完就可以自動運行,就跟不是買了水龍頭回家就有水的意思一樣。部分的程式語言還需要經過「編譯」的階段,轉成 0 和 1,本篇會講解與示範編譯的四個階段。另外帶入「陣列」的觀念,讓你了解資料儲存結構的形式,再分享三個除錯的方法。

真正的 C 語言運行需要四個編譯階段,第一篇提到的 make 檔名 是在 CS50 的平台中只是其中方便初學者理解有編譯這個過程存在,真正的編譯需要有四個階段。下面是這四個階段的簡單說明:

編譯四階段總覽

  1. Preprocessing

這個階段會處理程式中的指令,比如 #include,用來引入外部函式庫。編譯器會找到這些函式庫並將內容加入程式,確保函式庫功能可以被正確編譯。

  1. Compling

編譯成 Assembly language ,更難直觀看懂的語言。不同的編譯器適用不同的指令,Linux C 語言常用 gcc 當作編譯器。在 CS50 的編譯器中使用 Clang 編譯

  1. Assembling

轉成電腦能看懂的 0 和 1,每個標頭檔函示庫的功能會個別編譯。

  1. Linking

將個別已轉成 0 和 1 的標頭檔內容連結,讓程式可運行。

Preprocessing

這次我們改用 clang 指令進行編譯:

使用 clang 編譯

左邊出現 ./a.out 的預設檔名編譯檔案

此時檔名尚未更新,需要運行./a.out的預設檔名才有結果

如何自訂檔名?

clang -o 自定義檔名 檔案名稱.c

顯示錯誤,原因是沒有編譯到 <cs50.h> 這個函式庫的功能,電腦就不知道 get_string 是什麼意思。

如果程式有使用其他的 library ,就要另外編譯。

clang -o 自定義檔名 檔案名稱.c -l函式庫

補充說明:gcc 也是一種編譯模式,你也可以試試。

gcc 是 C 語言常用的編譯方式


Compling 編譯

編譯器將 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

Assembling

轉成 0 和 1 的二進位語言,即機器語言(machine code)。

10111000 00000001 00000000 00000000 00000000  //節錄 hello,world 部分machine code

Linking

將多個機器語言輸入:

  • main.o(主程式的機器語言)。
  • 標頭檔中 printf 函式所屬的函式庫機器語言。
  • 如果有 <cs50.h>get_string 功能也會在這個階段連結成完整的程式。

每個程式語言都要編譯嗎?

不一定。世界上也有「直譯語言」,也有介於兩者中間的型態。

Compiled Language 編譯語言:

知名的 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 位學生轉學,你還必須手動去修改變數名稱,甚至重寫部分程式碼。

此外,若還需要儲存學生的姓名、學號等資訊,這樣的寫法會讓程式變得更複雜且難以維護。因此,我們需要使用「陣列」來簡化這些操作。

陣列(Array)是什麼?

每個數值都需要占用一些儲存空間,而這個空間的大小取決於數值的型態(例如,整數、浮點數等)。當我們使用陣列時,電腦可以根據每個數值的位置快速找到它,而不是從頭一個一個地搜尋。

可以把陣列想像成一列火車,每個「車廂」就是一個存放數值的位置。我們提前規劃好車廂的位置,這樣當我們需要找到某個特定車廂(數值)時,不需要從火車頭開始搜尋,而是可以直接跳到那個車廂去取東西,而這展現陣列的高效率。

記憶體在硬體上呈現的樣子 ( 本圖截圖自 CS50 Lecture 2)



#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 開始的,所以即使陣列可以存 3 個值,它們的索引分別是 012

優化程式,使用迴圈:

如果我們有 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; // 變浮點數,平均才會有小數
}



鏈結串列(Linked List):

另外一種資料結構是鏈結串列,儲存的資料不必是連續的,電腦要找資料必須從第一個開始查找。鏈結串列會談到指標(Pointer)的概念,之後的章節會再深入探討。



除錯 (debug)三妙招

  1. 邊打邊念,或是先寫下思考邏輯。

範例:計算 5 位學生的成績總和。

  • 寫下一般數學運算總和如何表示,再一邊寫程式一邊模擬教學講出此程式碼代表的意義。
  1. 善用 printf() 打印出每一行程式,觀察變化。

範例:寫 1 加到 50 ,發現輸出錯誤,可以在中間使用printf() 檢查每一次的輸出。

  1. 外掛程式偵錯。在 cs50 中可以使用 debug50 ./檔名 來偵錯,而其他的介面平台也會有適合的外掛程式。





分享至
成為作者繼續創作的動力吧!
© 2024 vocus All rights reserved.