一、基本語法和概念: 1. 解釋C語言的變數和資料型別 整數型別: * int: 用於表示整數值。通常佔用4個位元組,可以保存範圍內的整數值,例如-2147483648 到 2147483647。 * short: 用於表示較小範圍的整數值。通常佔用2個位元組,範圍為-32768 到 32767。 * long: 用於表示較大範圍的整數值。通常佔用4個或8個位元組,具體大小取決於系統架構,例如-2147483648 到 2147483647 或更大。 浮點數型別: * float: 用於表示單精度浮點數,通常佔用4個位元組。可以表示約7位有效數字,具有小數點。 * double: 用於表示雙精度浮點數,通常佔用8個位元組。可以表示約15位有效數字,具有小數點。 * long double: 用於表示更高精度的浮點數,通常佔用10個位元組或更多。 字元型別: * char: 用於表示單個字元。通常佔用1個位元組,可以保存ASCII字符或其他字符編碼的值。 * 布林型別: * _Bool: 用於表示布林值(true或false)。通常佔用1個位元組,值可以是0(false)或1(true)。 額外修飾符: * signed:修飾符signed用於聲明有符號整數變數。有符號整數可以表示正數、負數和0。默認情況下,如果您只使用int而沒有使用signed或unsigned,則int被視為有符號整數。 * unsigned:修飾符unsigned用於聲明無符號整數變數。無符號整數只能表示非負數(包括0),這樣可以擴大表示的正數範圍。例如,無符號整數可以用來表示位元運算或計數。 * long:修飾符long用於聲明長整數變數。長整數可以存儲更大的整數值,因為它佔用更多的記憶體位元組。這在需要較大範圍的整數時很有用。 * short:修飾符short用於聲明短整數變數。短整數佔用較少的記憶體位元組,因此可以節省記憶體,但其範圍較小。 二、說明指標及其用途,以及如何使用指標進行記憶體操作: 指標的定義和用途: * 指標是一個變數,它存儲了記憶體位置的位址。該記憶體位置可以是其他變數、數組、函式或資料結構的位址。 * 指標的用途之一是允許您在程式中直接存取和修改特定記憶體位址上的資料,而不需要複製整個資料。這在處理大量資料時可以節省記憶體和提高效能。 如何聲明指標: * 指標變數的聲明使用星號(*)作為前綴,例如 int *ptr; 表示聲明一個指向整數的指標變數 ptr。 * 指標的型別必須與它指向的資料型別相符。例如,指向整數的指標的型別是 int *,指向字符的指標的型別是 char *。 如何使用指標進行記憶體操作: * 取址運算符(&)用於取得變數的位址,例如 &variable。 * 解參考運算符(*)用於獲取指標所指向位址的數值,例如 *ptr。 * 可以使用指標來訪問、讀取和修改變數的值。例如:*ptr = 10; 將變數賦值為 10。 指標的運算和算術: * 指標可以進行算術運算,如加法、減法等。這對於遍歷陣列和資料結構非常有用。 * 指標算術運算是基於資料型別的大小。例如,增加一個 int * 指標會使其指向下一個整數的位址。 三、說明函式的定義和呼叫,以及參數傳遞的方式: 函式的定義: ```c= 返回型別 函式名稱(參數列表) { // 函式體(執行特定的操作) // 函式內部的程式碼 return 返回值; // 可選的返回值,視情況而定 } ``` 參數傳遞的方式: * 傳值(Pass by Value):在這種方式下,函式接收的是實際參數的一個拷貝,函式對拷貝的修改不會影響原始參數的值。 * 傳址(Pass by Reference):在這種方式下,函式接收的是參數的地址(指標),函式對該地址處的數據的修改將影響原始參數的值。 ```c= #include <stdio.h> // 函式,使用指標修改呼叫處的變數 void modifyValue(int *num) { // 通過指標修改變數的值 (*num) += 10; } int main() { int x = 5; printf("原始值 x = %d\n", x); // 傳遞變數 x 的地址給函式 modifyValue(&x); printf("修改後的值 x = %d\n", x); return 0; } ``` 四、記憶體管理和指標: 解釋記憶體的低階結構,例如堆疊(stack)和堆(heap): * 堆疊(Stack): 堆疊是一種記憶體區域,用於存儲函式呼叫的相關資訊,例如函式的局部變數、函式的參數、函式呼叫的返回地址等。 堆疊是一種後進先出(Last-In-First-Out,LIFO)的資料結構,這意味著最後壓入堆疊的資料將首先被彈出。 堆疊的大小通常是固定的,並由操作系統或編譯器在程式開始執行時分配。因此,堆疊上的記憶體分配和釋放是自動的,您不需要手動管理。 * 堆(Heap)(動態記憶體): 堆是一種動態分配的記憶體區域,用於存儲在程式運行時分配的資料。這些資料的生存期不受函式呼叫的限制,因此可以在需要時進行動態分配和釋放。 堆的大小通常比堆疊大得多,並且可以根據需要進行動態擴展。 堆上的記憶體分配和釋放需要手動管理。在C語言中,您可以使用 malloc、free、calloc 和 realloc 等函式來分配和釋放堆上的記憶體。 ![](https://hackmd.io/_uploads/SJ8LpMXR3.png) * text:程式碼 文字區段(text segment)也稱為程式碼區段(code segment),這裡存放的是可執行的 CPU 指令(instructions)。 這個區段通常位於 heap 或 stack 之後,避免因 heap 或 stack 溢位而覆寫 CPU 指令。 通常文字區段的資料是可以共用的,當多個同樣的程式在執行時,在記憶體中只需要存有一份就夠了,而這個文字區段通常都是唯讀的,避免程式本身誤改了自己的 CPU 指令。 * data:初始化靜態變數 初始化資料區段(initialized data segment)儲存的是一些已經初始化的靜態變數,例如有經過初始化的 C 語言的全域變數(global variables)以及靜態變數(static variables)都是儲存於此處。 這個區段的變數又可分為唯讀區域(read-only area)以及可讀寫區域(read-write area),可讀寫區域用於存放一般變數,其資料會隨著程式的執行而改變,而唯讀區域則是存放固定的常數。 * bss:未初始化靜態變數 未初始化資料區段(uninitialized data segment)又稱為 bss 區段(這個名稱的起源來自於古老的組譯器,代表 block started by symbol)是儲存尚未被初始化的靜態變數,而這些變數在程式執行之前會被系統初始化為 0 或是 null。 * stack:區域變數 堆疊區段(stack segment)用於儲存函數的區域變數,以及各種函數呼叫時需要儲存的資訊(例如函數返回的記憶體位址還有呼叫者函數的狀態等),每一次的函數呼叫就會在堆疊區段建立一個 stack frame,儲存該次呼叫的所有變數與狀態,這樣一來同一個函數重複被呼叫時就會有不同的 stack frame,不會互相干擾,遞迴函數就是透過這樣的機制來執行的。 * heap:動態配置變數 heap 區段的記憶體空間用於儲存動態配置的變數,例如 C 語言的 malloc 以及 C++ 的 new 所建立的變數都是儲存於此。 堆疊區段一般的狀況會從高記憶體位址往低記憶體位址成長,而 heap 剛好從對面以相反的方向成長。 ![](https://hackmd.io/_uploads/rJ_ipGQ0h.png) 五、動態記憶體配置: 在C語言中,您可以使用函式 malloc、calloc 和 realloc 來動態分配記憶體。 * malloc(memory allocation)函式用於分配指定大小的連續記憶體塊。它接受一個參數,即要分配的字節數,並返回一個指向新分配記憶體的指標。例如: ![](https://hackmd.io/_uploads/SkSe74X03.png) * calloc(contiguous allocation)函式也用於分配連續記憶體塊,但它會初始化所有位元組為零。它接受兩個參數,分別是元素的個數和每個元素的大小。例如:![](https://hackmd.io/_uploads/SkLWQNm0n.png) 使用動態分配函式分配的記憶體需要在使用完畢後釋放,以防止記憶體洩漏(memory leaks)。 * 使用 free 函式來釋放動態分配的記憶體。它接受一個指向動態分配記憶體的指標作為參數。例如:![](https://hackmd.io/_uploads/BJFVXVQ03.png) 如果需要更改已分配記憶體的大小,可以使用 realloc 函式。它接受兩個參數,一個是現有記憶體的指標,另一個是新的字節數。例如:![](https://hackmd.io/_uploads/BkbvQ4XAn.png) 分配記憶體注意事項: * 動態記憶體分配和釋放是您的責任。如果您成功分配了記憶體,則應該在不再需要它時使用 free 釋放它,以避免記憶體洩漏。 * 當動態分配記憶體時,請確保檢查分配是否成功。malloc 和 calloc 可能無法分配所需的記憶體,它們可能會返回 NULL,因此應該檢查返回值。 * 當使用 realloc 時,新的記憶體大小不一定需要與舊的一樣。您可以增加或減少記憶體大小,但要小心處理舊數據的遷移和複製。 談談記憶體洩漏(memory leaks)和釋放後的指標(dangling pointers)問題,以及如何避免這些問題: * 記憶體洩漏(Memory Leaks): 記憶體洩漏是指在程式運行時,已分配的記憶體未被釋放或回收,導致系統中的可用記憶體不斷減少。 常見的記憶體洩漏情況包括: 1. 忘記使用 free 釋放動態分配的記憶體。 2. 在迴圈或函式內重複動態分配記憶體而未釋放。 3. 保存指向已經釋放的記憶體的指標。 * 如何避免記憶體洩漏: 1. 始終在使用 malloc、calloc 或 new 等動態分配函式後,使用 free 或 delete 釋放記憶體。 2. 注意動態分配記憶體的生存週期,確保每次分配都有對應的釋放。 3. 使用工具,如內存分析器(memory profiler)來檢測和解決記憶體洩漏問題。 釋放後的指標(Dangling Pointers): * 釋放後的指標是指當您釋放了一塊記憶體後,但仍然保留指向該記憶體的指標。當您試圖訪問或修改這個指標時,它將指向無效的記憶體,可能導致未定義的行為或程式崩潰。 * 釋放後的指標通常發生在以下情況下: 1. 忘記將指標設置為 NULL 或其他無效值,以防止意外訪問。 2. 使用指向局部變數的指標,當局部變數超出其作用範圍時,指標將變為釋放後的指標。 3. 重複釋放同一記憶體。 如何避免釋放後的指標: 1. 當您釋放記憶體後,立即將指標設置為 NULL 或將其指向有效的記憶體位置。 2. 避免使用指向局部變數的指標,或者確保局部變數的生存週期不超出指標的使用範圍。 3. 始終檢查指標的有效性,確保它指向有效的記憶體位置,再進行訪問。 六、位元運算和位址操作: * 按位AND(&):將兩個位元做AND操作,產生新的位元。 * 按位OR(|):將兩個位元做OR操作,產生新的位元。 * 按位XOR(^):將兩個位元做XOR操作,產生新的位元。 這個運算將兩個位元進行XOR(異或)操作,只有當兩個位元不相同時,結果位元才為1。![](https://hackmd.io/_uploads/HJXDENQA3.png) * 按位NOT(~):反轉位元的值,1變為0,0變為1。 * 位元左移(<<)和右移(>>):將位元向左或向右移動指定的位數。 說明如何使用位址運算來進行位元級操作: 1. 位元運算: 假設您有一個整數,並且想要對其二進位表示進行位元運算。您可以使用位址運算和位元遮罩來實現這一目標。例如,要將整數中的第3位(從右邊數)設置為1,可以使用以下方式: ![](https://hackmd.io/_uploads/SJMnSEX03.png) 2. 位元複製: 要複製一個整數的某些位元到另一個整數,您可以使用位元遮罩和位址運算。例如,要複製第2至5位(含)的位元到另一個整數,可以使用以下方式: ![](https://hackmd.io/_uploads/Skp2S4m0h.png) 3. 位元儲存: 要將一個整數的某些位元儲存到另一個整數,可以使用位元遮罩和位址運算。例如,要將4個位元的值儲存到一個整數中,可以使用以下方式: 利用& 1111 … 1111 七、多執行緒和同步: • 說明多執行緒程式設計的基本概念,以及如何使用C語言的線程庫。 1. 執行緒: 執行緒是操作系統中執行程序的基本單位。一個進程可以包含多個執行緒,這些執行緒共享進程的資源,如記憶體空間,但每個執行緒都有自己的執行緒上下文和程式計數器。執行緒可以同時運行,但必須受到操作系統的管理和調度。 • 談談避免競爭條件和使用互斥鎖的方法。 1. 競爭條件(Race Conditions): 當多個執行緒同時訪問和修改共享資源時,可能會發生競爭條件。這種情況下,執行順序不確定,可能導致意外的結果。例如,如果兩個執行緒同時嘗試增加一個計數器的值,可能會導致計數器值不正確。 2. 解決競爭條件的一種方法是使用鎖(Locks),如互斥鎖(Mutex)。互斥鎖可以確保在任何時候只有一個執行緒可以訪問共享資源,從而防止競爭條件的發生。 八、記憶體映射和物理位址: 說明記憶體映射和物理位址的概念。 1. 記憶體映射(Memory Mapping): 記憶體映射是一種將虛擬地址空間映射到物理記憶體或其他地址空間的過程。在現代計算機系統中,操作系統通常負責記憶體映射的管理。當應用程序訪問虛擬地址時,操作系統將虛擬地址轉換為相應的物理地址或其他位置。 2. 記憶體映射的一個重要應用是將硬體設備映射到虛擬地址空間,以便應用程序可以通過讀取和寫入虛擬地址來與硬體設備通信。 3. 物理位址(Physical Address): 物理位址是計算機系統中實際的硬體記憶體位置。每個物理記憶體單元都有一個唯一的物理位址。物理位址是硬體層級的地址,通常由硬體控制器(例如記憶體控制器)使用。 九、BIOS相關問題: 說明BIOS的基本功能,例如開機自檢(POST)、硬體初始化、設備驅動程式載入等。 1. BIOS(Basic Input/Output System)是一種固件,位於計算機硬體和操作系統之間,具有重要的基本功能,這些功能對於計算機的正確運行至關重要。以下是BIOS的一些基本功能: 2. 開機自檢(POST,Power-On Self-Test): 當您啟動計算機時,BIOS首先執行開機自檢。這是一個自動化的檢測程序,目的是確保硬體組件正常運作。POST檢查硬體,包括CPU、記憶體、硬碟、顯示卡等,以確保它們無故障。 3. 硬體初始化: BIOS負責初始化和配置計算機的硬體組件。這包括設置系統時鐘、內存控制器、各種硬體控制器等。初始化過程確保硬體處於正確的初始狀態,以便操作系統能夠運行。 4. 引導程序(Bootstrapping): BIOS負責啟動計算機的操作系統。當計算機啟動時,BIOS會搜索引導設備(通常是硬碟或固態驅動器),並將控制權轉交給引導記錄(Boot Loader)或操作系統的起始代碼。這樣,操作系統可以開始加載並運行。 5. 設備驅動程式載入: BIOS還負責加載和初始化一些基本的設備驅動程式,這些驅動程式通常被稱為「硬體抽象層(Hardware Abstraction Layer,HAL)」。這些驅動程式有助於操作系統識別和與硬體設備通信,以便操作系統和軟體能夠有效地運行。 6. 設置BIOS設置: BIOS提供了一個介面,通常在計算機啟動時按下特定的按鍵(例如Del、F2等)來訪問。通過此介面,用戶可以設置 • 解釋UEFI和傳統BIOS的區別,以及其在系統開機過程中的角色。 設計和架構: 1. 傳統BIOS: 傳統BIOS是一種較老的技術,通常採用16位元實模式運行,限制了其功能和效能。它通常存儲在計算機的ROM或Flash記憶體中。 2. UEFI: UEFI是一種更現代、模塊化和可擴展的固件技術。它支援32位元和64位元的CPU,使用C語言編寫,並具有更強大的功能。UEFI存儲在固件可擴展性固件(EFI)分區中,通常使用GPT(GUID Partition Table)磁區配置表。 3. 啟動方式: * 傳統BIOS: 傳統BIOS使用MBR(Master Boot Record)方式來引導操作系統。MBR是一個小型的啟動代碼,通常存儲在硬碟的開頭。它限制了分割區和啟動設備的數量。 * UEFI: UEFI使用GPT方式來引導操作系統。GPT允許更多分割區,支援大容量硬碟並提供更多啟動選項。UEFI還支援UEFI Shell,允許用戶執行腳本和命令。 10. 驅動程式和擴展性: * 傳統BIOS: 傳統BIOS的驅動程式通常存儲在固件中,並且固定不變。要升級傳統BIOS,需要更新整個固件。 * UEFI: UEFI支援可插拔驅動程式,這意味著硬體廠商和開發人員可以添加新的驅動程式,而無需更改固件。這增加了系統的擴展性和可升級性。 5. 圖形界面: * 傳統BIOS: 傳統BIOS通常使用文本界面,不支援圖形界面。這使得設置和配置較為簡單,但界面較為有限。 * UEFI: UEFI支援圖形界面,使得用戶可以使用滑鼠和觸摸螢幕進行交互。這提供了更直觀的用戶體驗。 十、除錯和測試: 描述您在C程式設計中使用的除錯工具和技術,以及如何解決常見的程式錯誤。 解決常見的程式錯誤: 1. 空指標引用: 當使用未初始化的指標或已被釋放的記憶體時,會導致空指標引用錯誤。解決方法包括確保指標初始化為NULL,並在使用前檢查指標是否為NULL。 2. 陣列越界: 當訪問陣列元素超出其範圍時,會發生陣列越界錯誤。解決方法包括確保索引在合法範圍內,使用動態記憶體分配,或使用安全的陣列函式(如memcpy)。 3. 記憶體泄漏: 當分配的記憶體未正確釋放時,會導致記憶體泄漏。使用動態記憶體分配的情況下,確保在不再需要時釋放記憶體。 4. 無限循環: 當程式陷入無限迴圈時,通常是由於循環條件未正確設置所致。檢查循環條件,確保它能夠退出循環。 5. 線程競爭(Thread Race Condition): 在多執行緒程式中,線程之間的競爭條件可能導致未定義的行為。使用同步機制(如互斥鎖)來保護共享資源,以避免競爭條件。 6. 錯誤的資料類型轉換: 當進行不合適的資料類型轉換時,可能會導致數據損壞或錯誤。謹慎處理資料類型轉換,使用適當的轉換函式(如atoi或sprintf)。 -------------------------------------------------------- 一、 結構定義和初始化:結構的定義和初始化的理解。 Q: 請定義一個名為Student的結構,該結構包含學生的姓名(name)、年齡(age)和學號(studentID)這三個成員變數。然後初始化一個Student結構變數,姓名為"John",年齡為20,學號為12345。 ![](https://hackmd.io/_uploads/BJ94MHQC3.png) 二、 訪問結構成員: 從結構變數中讀取特定成員的值。 ![](https://hackmd.io/_uploads/H1WYjw7Ch.png) 三、 指向結構的指標: 指向結構的指標的概念,並能夠使用指標來訪問結構的成員。 Q: 請定義一個名為Student的結構,該結構包含學生的姓名(name)和年齡(age)這兩個成員變數。然後 創建一個Student結構變數並初始化,姓名為"Bob",年齡為20。接著,請使用指向結構的指標來訪問結構變數的姓名成員並輸出。 ![](https://hackmd.io/_uploads/HyP5oDQAh.png) 四、 結構內含結構: 結構的能力。問題可能要求在結構中包含另一個結構,並對其進行初始化和訪問。 結構嵌套是一種將一個結構作為另一個結構的成員的概念。這樣可以創建更複雜的數據結構,將相關的數據分組存儲。 ![](https://hackmd.io/_uploads/B1X_nwXR2.png) 五、 結構與指標運算符: 考察候選人使用指標運算符(->)訪問結構指標的成員。候選人可能會被要求使用結構指標來訪問成員。 結構指標與指標運算符(->)是在C語言中用來訪問結構指標的成員的重要概念。通過結構指標,我們可以動態地訪問結構的成員,而不必通過結構變數名稱進行訪問。 ![](https://hackmd.io/_uploads/SJFF2vQC3.png) 六、 動態分配結構: 考察候選人如何在堆上動態分配結構,以及如何使用該結構。這可能涉及到malloc和free函數的使用。 ![](https://hackmd.io/_uploads/H1Ej2Dm0n.png) 七、 結構和記憶體管理: 考察候選人對結構和記憶體管理的理解。問題可能要求候選人避免內存洩漏,適時釋放動態分配的結構記憶體。 1. 動態分配結構:動態分配一個結構 Student 的記憶體。結構 Student 包含 name(字符串)和 age(整數)成員。確保在完成使用後釋放記憶體,以避免內存洩漏。![](https://hackmd.io/_uploads/BJOh3vXAh.png) 2. 結構陣列的內存管理:如果您創建一個結構的陣列,例如 Student students[10],該如何確保釋放這個陣列的記憶體?請編寫代碼來動態分配和釋放這個結構陣列的記憶體。  動態分配的結構陣列 ![](https://hackmd.io/_uploads/Hy2-6wmC2.png) snprintf(students[i].name, sizeof(students[i].name), "學生 %d", i + 1); 這行程式碼將學生結構中的 name 成員初始化為一個字串。snprintf 函數是一個安全的字串格式化函數,它將格式化的字串 "學生 %d" 替換為 "學生 1"、"學生 2" 和 "學生 3",並將結果存儲在對應學生的 name 成員中。sizeof(students[i].name) 用於確保我們不會溢出 name 陣列的空間。 3. 結構成員的動態記憶體:如果結構中的某個成員是指向動態分配記憶體的指標,您如何確保在結構釋放時也釋放該指標所引用的內存? ![](https://hackmd.io/_uploads/H1qQpvX0n.png) 八、 結構陣列: 如何定義和使用結構陣列。問題可能要求將多個結構存儲在陣列中並進行操作。 ![](https://hackmd.io/_uploads/Hko4TDX0n.png)