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

閱讀時間約 2 分鐘
程式並不是寫完就可以自動運行,就跟不是買了水龍頭回家就有水的意思一樣。部分的程式語言還需要經過「編譯」的階段,轉成 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 編譯

使用 clang 編譯

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

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

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

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

如何自訂檔名?

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

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

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

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

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

raw-image

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

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

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++ 都屬於編譯語言,編譯語言的概念如下圖:

raw-image

左邊是你的電腦,右邊是我的電腦,而編譯語言會先將程式碼編譯成0和1並儲存成「新的檔案」,再把這個檔案給我的電腦執行。此圖片截圖自,影片版請點以下連結:

raw-image

Compiler and Interpreter: Compiled Language vs Interpreted Programming Languages



Interpreted Language 直譯語言:

網頁常用的 JavaScript 就屬於直譯語言。

raw-image

直譯語言直接把原始的程式碼,傳送到我的電腦,直接在我的電腦逐條直譯。相較於編譯語言,直譯語言需要給對方「原始程式碼檔案」。


raw-image






兩者比較:

raw-image

稍微了解編譯的過程後,我們來看另一個常用的功能:陣列

沒有陣列的程式

老師希望計算每個學生的平均成績,如果學生人數不多,可能會這樣寫:

#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)

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


raw-image


raw-image
#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)的概念,之後的章節會再深入探討。

raw-image



除錯 (debug)三妙招

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

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

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

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

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





留言0
查看全部
avatar-img
發表第一個留言支持創作者!
本文深入探討程式設計中的迴圈概念,介紹三種常見的迴圈使用方式:for、while 和 do...while。透過實例如計算 1 到 100 的總和以及九九乘法表,讓讀者能夠理解這些迴圈的應用場景及其運作邏輯。文章強調掌握迴圈及函數的重要性,並提供了避免常見錯誤的提示,非常適合初學者。
為什麼從C開始? 為什麼不是python? 因為 C 是個「麻煩」的程式語言,而python 其實已經簡化很多步驟。先學C語言了解程式語言的架構,再學其他語言就會覺得更簡單! 我是搭配 CS50 的免費課程,看完影片還是不太懂,可以再搭配我的筆記複習。釐清基礎架構對電腦科學更有概念,可以更精準
在我們深入探討程式設計之前,讓我們先掌握 Linux 作業系統的基礎,學習如何在命令提示字元(CMD)中靈活運用指令,並了解位元與位元組之間的差異。這樣的學習路徑雖然乏味但有助於打下穩固的基礎,一起在電腦新手村獲得經驗值吧!
本文深入探討程式設計中的迴圈概念,介紹三種常見的迴圈使用方式:for、while 和 do...while。透過實例如計算 1 到 100 的總和以及九九乘法表,讓讀者能夠理解這些迴圈的應用場景及其運作邏輯。文章強調掌握迴圈及函數的重要性,並提供了避免常見錯誤的提示,非常適合初學者。
為什麼從C開始? 為什麼不是python? 因為 C 是個「麻煩」的程式語言,而python 其實已經簡化很多步驟。先學C語言了解程式語言的架構,再學其他語言就會覺得更簡單! 我是搭配 CS50 的免費課程,看完影片還是不太懂,可以再搭配我的筆記複習。釐清基礎架構對電腦科學更有概念,可以更精準
在我們深入探討程式設計之前,讓我們先掌握 Linux 作業系統的基礎,學習如何在命令提示字元(CMD)中靈活運用指令,並了解位元與位元組之間的差異。這樣的學習路徑雖然乏味但有助於打下穩固的基礎,一起在電腦新手村獲得經驗值吧!
你可能也想看
Google News 追蹤
Thumbnail
Hi 我是 VK~ 在 8 月底寫完〈探索 AI 時代的知識革命:NotebookLM 如何顛覆學習和創作流程?〉後,有機會在 INSIDE POSSIBE 分享兩次「和 NotebookLM 協作如何改變我學習和創作」的主題,剛好最近也有在許多地方聊到關於 NotebookLM 等 AI 工具
Thumbnail
這是張老師的第三本書,我想前二本應該也有很多朋友們都有讀過,我想絕對是受益良多,而這次在書名上就直接點出,著重在從投資的角度來切入
Thumbnail
這篇內容,將會講解什麼是陣列,以及與陣列相關的知識。包括陣列的簡介、陣列的資料限制、陣列的維度、一維陣列、二維陣列。
“所有人寫的程式會變成指令 每一道指令是由CPU執行 而CPU所能理解的指令類型有限”
Thumbnail
如果你也是從事軟體相關工作的人,一定會遭遇突然需要你去學習一套你不熟悉的程式語言狀況吧,此時你會怎麼做呢? 是趕快去買書來看嗎? 還是趕快找一門程式課來上? 又或者乾脆去找會的同事來教學?
Thumbnail
此章節旨在介紹Java的基本語法、註解和變數的使用。透過學習,讀者將了解Java程式的基本結構、程式進入點的定義、如何撰寫單行和多行註解,以及如何宣告和初始化變數。
Thumbnail
成功加入Anytype之後就可以開始探索這一個開源的筆記軟體了^_^ 開始Anytype之前..... 1.刪除所有初始物件 2.思考自己的使用需求 3.不著急學會所有功能
Thumbnail
因為最近想嘗試編碼風格,於是就選了一套比較"不嚴格"的輔助工具來摸索。 編輯器 VS CODE 框架 VUE3 打包工具 VITE 編碼風格 Standard 環境 version { "nodejs":"v18.18.0", "npm":"9.8.1" }
Thumbnail
Hi 我是 VK~ 在 8 月底寫完〈探索 AI 時代的知識革命:NotebookLM 如何顛覆學習和創作流程?〉後,有機會在 INSIDE POSSIBE 分享兩次「和 NotebookLM 協作如何改變我學習和創作」的主題,剛好最近也有在許多地方聊到關於 NotebookLM 等 AI 工具
Thumbnail
這是張老師的第三本書,我想前二本應該也有很多朋友們都有讀過,我想絕對是受益良多,而這次在書名上就直接點出,著重在從投資的角度來切入
Thumbnail
這篇內容,將會講解什麼是陣列,以及與陣列相關的知識。包括陣列的簡介、陣列的資料限制、陣列的維度、一維陣列、二維陣列。
“所有人寫的程式會變成指令 每一道指令是由CPU執行 而CPU所能理解的指令類型有限”
Thumbnail
如果你也是從事軟體相關工作的人,一定會遭遇突然需要你去學習一套你不熟悉的程式語言狀況吧,此時你會怎麼做呢? 是趕快去買書來看嗎? 還是趕快找一門程式課來上? 又或者乾脆去找會的同事來教學?
Thumbnail
此章節旨在介紹Java的基本語法、註解和變數的使用。透過學習,讀者將了解Java程式的基本結構、程式進入點的定義、如何撰寫單行和多行註解,以及如何宣告和初始化變數。
Thumbnail
成功加入Anytype之後就可以開始探索這一個開源的筆記軟體了^_^ 開始Anytype之前..... 1.刪除所有初始物件 2.思考自己的使用需求 3.不著急學會所有功能
Thumbnail
因為最近想嘗試編碼風格,於是就選了一套比較"不嚴格"的輔助工具來摸索。 編輯器 VS CODE 框架 VUE3 打包工具 VITE 編碼風格 Standard 環境 version { "nodejs":"v18.18.0", "npm":"9.8.1" }