# C 語言筆記 > 好讀版:https://54ming.notion.site/C-15437766f44f8055b3b3e293484367bb ## Outline - [Memory Layout](https://www.notion.so/C-15437766f44f8055b3b3e293484367bb?pvs=21) - Keywords - [static](https://www.notion.so/C-15437766f44f8055b3b3e293484367bb?pvs=21) - [Static Variable](https://www.notion.so/C-15437766f44f8055b3b3e293484367bb?pvs=21) - [Static Function](https://www.notion.so/C-15437766f44f8055b3b3e293484367bb?pvs=21) - [extern](https://www.notion.so/C-15437766f44f8055b3b3e293484367bb?pvs=21) - [volatile](https://www.notion.so/C-15437766f44f8055b3b3e293484367bb?pvs=21) - [define](https://www.notion.so/C-15437766f44f8055b3b3e293484367bb?pvs=21) - [const](https://www.notion.so/C-15437766f44f8055b3b3e293484367bb?pvs=21) - [inline (function)](https://www.notion.so/C-15437766f44f8055b3b3e293484367bb?pvs=21) - Types - [Type Size](https://www.notion.so/C-15437766f44f8055b3b3e293484367bb?pvs=21) - [Struct Size and Memory Alignment](https://www.notion.so/C-15437766f44f8055b3b3e293484367bb?pvs=21) - [union](https://www.notion.so/C-15437766f44f8055b3b3e293484367bb?pvs=21) - [enum](https://www.notion.so/C-15437766f44f8055b3b3e293484367bb?pvs=21) - [Pointer](https://www.notion.so/C-15437766f44f8055b3b3e293484367bb?pvs=21) - [Operator Precedence](https://www.notion.so/C-15437766f44f8055b3b3e293484367bb?pvs=21) - [__attribute__](https://www.notion.so/C-15437766f44f8055b3b3e293484367bb?pvs=21) - [Interrupt Service Routine (ISR)](https://www.notion.so/C-15437766f44f8055b3b3e293484367bb?pvs=21) ## Memory Layout https://blog.gtwang.org/programming/memory-layout-of-c-program/ ![截圖 2024-12-13 凌晨1.47.58.png](https://prod-files-secure.s3.us-west-2.amazonaws.com/badf38d2-440d-4d71-b29f-2de6b2a37749/ab7870f4-a92f-4324-a96c-8778bd01038d/%E6%88%AA%E5%9C%96_2024-12-13_%E5%87%8C%E6%99%A81.47.58.png) https://www.geeksforgeeks.org/memory-layout-of-c-program/ ![image.png](attachment:9baa1660-2564-4c1e-8954-a41dd2042547:image.png) ### Text Segment - 儲存已編譯的程式碼(機器碼),包含函數與指令 - 唯讀(Read-Only),防止程式碼被意外修改,確保執行安全性 - 通常位於記憶體的低位址區域 ### Data Segment - 存放全域變數與靜態變數 - 資料區段可進一步劃分為: - **Initialized Data Segment:** - 儲存已初始化的全域變數與靜態變數 - **Uninitialized Data Segment (BSS):** - **BSS(Block Started by Symbol)**:源自早期組合語言的指令 - 儲存未初始化的全域變數與靜態變數 - 變數在程式啟動時自動初始化為 0 ### Heap Segment - 動態記憶體配置區域 - 動態記憶體分配(malloc()、calloc()、realloc())發生在此區段 - 從 BSS 區段的結尾開始,向高位址方向增長 - 與 Stack 方向相反增長,若與 Stack 指標相遇,將導致記憶體資源耗盡(Stack Overflow) - 例如:遞迴深度過大或過多區域變數 - 堆積區段是共享的,供程序中的共享函式庫及動態載入模組使用 ### Stack Segment - 儲存區域變數與函數呼叫資訊 - 函數呼叫時,會建立 Stack Frame,用來存放: - 函數的區域變數 - 函數參數 - 返回地址 - 位於記憶體的高位址,向低位址方向增長 ## Static ### Static Variable - **Static Sariable** 在編譯時必須初始化,因此只能使用常量值進行初始化 - 如果未明確初始化,默認值為該型別的「零值」,例如: - pointer: `NULL` - int: `0` - double: `0.0` - char: `'\0'` - bool: `false` - **Static Variable** 僅初始化一次,並且存活至程式結束 - 這是因為它們被分配在 C 語言記憶體空間中的 BSS 或 Data 區域,而非函式的 Stack 區域 - BSS 區域:用於未初始化的靜態變數 - Data 區域:用於已初始化的靜態變數 - **Static Variable** 不能在 **Struct** 中宣告 - 這是因為 Struct 要求所有成員變數必須分配在連續的記憶體空間(因為結構體的成員通過偏移量計算來訪問)中,但被 static 修飾的變數會被單獨分配到 BSS 區域 或 Data 區域,因此無法滿足結構體記憶體連續性要求 - 適用場景: - **函式內持久化變數值** - 適用場景 - 在函式中需要變數的值在多次調用之間保持不變,但又不希望將其暴露為全域變數 - 用途 - 計數、累積結果或追蹤狀態 - **Singleton Pattern** - 適用場景 - 確保某個物件或資源在整個程式中只被初始化一次,例如 Buffer (限制記憶體分配在有限的大小內)或 DB Connect Pool 等 - 用途 - 節省有限的記憶體空間,避免多次初始化 - **限制全域變數的作用域** - 適用場景 - 當需要變數具有全域變數的生命週期,但希望其作用域僅限於當前檔案,避免其他檔案干擾 - 用途 - 提高程式的模組化和可維護性 - **快取變數** - 適用場景 - 需要重複計算的值,可以使用靜態變數進行快取,以提高效率 - 用途 - 減少不必要的重複計算,尤其是在遞迴運算中 ```c int fibonacci(int n) { static int cache[100] = {0}; // 快取結果 if (n <= 1) return n; if (cache[n] != 0) return cache[n]; // 使用快取 cache[n] = fibonacci(n - 1) + fibonacci(n - 2); // 計算並存入快取 return cache[n]; } ``` ### Static Function - 使用 Static 將函數的作用域限制為所在檔案,避免其他檔案的命名衝突 - 可允許其他檔案中定義相同名稱的函數,而不會互相干擾 ## extern - extern 用於聲明變數或函式,告訴編譯器該變數或函式的定義在其他地方(可能是其他檔案中) - 它是跨檔案共享變數或函式的核心工具 - 雖然全域變數默認具有外部鏈接,但明確使用 extern 可以提高程式的可讀性和維護性 - 注意: - extern 無法用於初始化變數,只能用於聲明 - 可有效避免命名衝突,特別是在多檔案專案中 ## volatile - 告訴編譯器該變數的值可能會在程式控制範圍外被修改,要求編譯器不要優化,每次都從記憶體中讀取該變數的值,而不是使用暫存的值 - 適用場景: - 多執行緒環境:變數可能由其他執行緒修改,使用 volatile 確保其他執行緒讀取最新值 - 硬體操作:當變數的值由外部事件(例如硬體中斷或 I/O 暫存器)改變時,需使用 volatile 修飾 - 例子: 早期硬體沒有 Interrupt 機制的情況下,CPU 通常使用 busy waiting 的方式檢查某 buffer 是否可以使用(作業系統筆記 - Process Concept - Shared Memory 的例子),這時需要 volatile 確保變數的最新值被讀取 ![code.png](https://prod-files-secure.s3.us-west-2.amazonaws.com/badf38d2-440d-4d71-b29f-2de6b2a37749/5beb857b-7ccb-45af-ac4f-f11085df7dab/code.png) ## define - `#define` 是 C 語言中的預處理指令,用於定義巨集(macro),在編譯前由預處理器進行文本替換,不會涉及型別檢查或編譯器優化,也因此較難偵錯 - `#define` 的內容在編譯前就會被展開,直接替換至程式碼中,不會佔用執行時的記憶體 - 巨集中的運算應使用括號包住參數與整個運算式,確保運算優先順序正確 - 適合簡單的常數定義與條件編譯,但不適合複雜運算或函數替換,應考慮使用 const 或 inline - 條件編譯:配合 `#ifdef` 或 `#ifndef`,控制某些程式碼是否需被編譯 ```c #define SQUARE(x) ((x) * (x)) // 正確 #define SQUARE(x) x * x // 錯誤,可能導致優先順序問題 int result = SQUARE(1 + 2); // 替換為 ((1 + 2) * (1 + 2)) ``` - 適用場景: - 條件編譯 ```c #ifdef _WIN32 #define PLATFORM "Windows" #else #define PLATFORM "Unix-like" #endif ``` - 不需要型別安全的數值或常量 ```c #define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0])) ``` - 編譯器、平台、硬體相關的指令 ```c #define INLINE __attribute__((always_inline)) #define NOP __asm__("nop") ``` ## const - 不可修改(Read-Only)的屬性,當某個變數或資料被定義為 const 時,這表示其值在初始化後不應該再被改變 - 與 `#define` 相比: - `const` 是創建一個只讀的變數,具有型別檢查,且遵循作用域規則 - `#define` 是文本替換,沒有型別檢查 - 範例: - 指向的資料是只讀的: ```c const int *p = &x; // 或 int const *p = &x; *p = 20; // 錯誤:不能修改指向的資料 p = &y; // 正確:可以更改指標指向的位置 ``` - 指標本身是只讀的: ```c int *const p = &x; *p = 20; // 正確:可以修改指向的資料 p = &y; // 錯誤:不能更改指標指向的位置 ``` - 指標本身和指向的資料都是只讀的: ```c const int *const p = &x; *p = 20; // 錯誤:不能修改指向的資料 p = &y; // 錯誤:不能更改指標指向的位置 ``` - 字串陣列: ```c // strings1 是一個陣列,陣列的每個元素是指向不可改變內容的字串指標 const char *strings1[] = { "Hello, world!", "C programming is fun.", "Generic strings are cool!" }; strings1[0] = "New String"; // 合法:指標本身可以改變 // strings1[0][0] = 'h'; // 錯誤:指向的字串內容不可修改 // strings2 是一個陣列,陣列的每個元素是不可改變指向的字串指標 char *const strings2[] = { "Hello, world!", "C programming is fun.", "Generic strings are cool!" } // strings2[0] = "Hi"; // 錯誤:指標本身不可改變 strings2[0][0] = 'h'; // 合法:指向的內容可以修改 ``` ## inline (function) - inline 關鍵字提示編譯器將函式展開至呼叫處,避免傳統函式呼叫的開銷,提高執行效率 - 傳統函式呼叫需要: - 堆疊管理(保存與恢復呼叫環境) - 參數傳遞(推入堆疊或暫存器) - 內聯函式透過編譯器展開,可省略這些開銷 - 不適用於大型函式,因為展開後會造成程式碼膨脹(Code Bloat),影響效能 ## Type Size - 型別決定了資料的儲存方式與運算行為,不同型別的變數會佔據不同大小的記憶體空間 - char 是唯一被標準明確定義大小的型別,固定為 1 byte,其他型別的大小則是 implementation-defined(依據編譯器與平台決定) - 一般情況下,常見型別的大小如下(以 64-bit 系統為例): | **型別** | **大小(bytes)** | | --- | --- | | char | 1 | | short int | 2 | | int | 4 | | long int | 4 或 8 | | long long int | 8 | | float | 4 | | double | 8 | | pointer | 8 | ## Struct Size and Memory Alignment - Memory Alignment 是將資料存放在符合其對齊需求的記憶體地址上,以最佳化記憶體存取並提升效能 - 不同的編譯器或平台可能會有不同的對齊規則,通常根據成員型別的大小來決定 - 可以透過 `#pragma pack` 或 `__attribute__((packed))` 減少 Padding,但需權衡效能與兼容性 - 範例: ```c typedef struct example_t { char a; // 1 byte // padding: 3 bytes (對齊到 4 bytes 邊界) int b; // 4 bytes short c; // 2 bytes // padding: 6 bytes (對齊到 8 bytes 邊界) double d; // 8 bytes char e; // 1 byte // padding: 7 bytes (滿足結構體總大小為其成員中最大對齊需求的倍數) } example_t; // a: 0x16fbf30c8 // b: 0x16fbf30cc // d: 0x16fbf30d0 // d: 0x16fbf30d8 // e: 0x16fbf30e0 // sizeof example_t is 32 bytes ``` ## Union - Union 是一種特殊的資料結構,允許不同型態的資料共用同一個記憶體區域 - Union 的大小等於其最大成員的大小,因為所有成員共用相同的記憶體位址 - 適用場景: - 節省記憶體(只需要存放其中一個值) - 硬體相關開發(如解析通訊協議) - 資料型態轉換(如位元操作、型別別名) ### **Union vs Struct** | | **Union** | **Struct** | | --- | --- | --- | | **記憶體分配** | 所有成員共用相同記憶體空間,大小取決於最大成員 | 每個成員擁有獨立記憶體空間,大小需經過適配計算 | | **應用場景** | 需節省記憶體,或做型別轉換 | 存放多個相關變數 | | **存取限制** | 同一時間只能存取一個成員,否則行為未定義(UB) | 可同時存取所有成員 | - 範例: ```c union Data { int intVal; float floatVal; }; int main() { union Data data; data.intVal = 0x40490FDB; // IEEE 754 表示的 3.14 printf("Interpreted as float: %f\n", data.floatVal); return 0; } ``` ```c #include <stdio.h> union IP { unsigned int address; unsigned char bytes[4]; // 4 個 byte 組成的 IP }; int main() { union IP ip; ip.address = 0xC0A80101; // 192.168.1.1 printf("IP Address: %d.%d.%d.%d\n", ip.bytes[0], ip.bytes[1], ip.bytes[2], ip.bytes[3]); return 0; } ``` - UB 範例: ```c #include <stdio.h> union Data { int intVal; float floatVal; }; int main() { union Data data; data.intVal = 42; // 設定 intVal printf("intVal: %d\n", data.intVal); data.floatVal = 3.14; // 設定 floatVal,覆蓋 intVal printf("floatVal: %f\n", data.floatVal); // 🚨 **未定義行為!因為 intVal 已經被 floatVal 覆蓋** printf("intVal (after modifying floatVal): %d\n", data.intVal); return 0; } ``` ## enum - enum(enumeration)是一種用於定義具名整數常量的資料型態,避免使用 Magic Numbers,使程式更具可讀性 - 特性: - 定義一組具名的整數常量 - 每個 enum 成員預設為 **0** 開始,自動遞增 - 可指定成員初始值,後續成員的值會在此基礎上遞增 - 主要用途 - 取代 Magic Numbers,提升可讀性與維護性 - 適用於定義 狀態、標誌、選項,例如錯誤代碼、顏色、模式等 - 底層實現 - enum 成員的值實際上是整數(int),可以與整數進行比較或運算 - 範例: ```c #include <stdio.h> enum Color { RED, // 預設值 0 GREEN = 100, // 指定 100,後續成員會遞增 BLUE // 遞增為 101 }; int main() { enum Color myColor = GREEN; printf("My color is: %d\n", myColor); // 輸出:100 return 0; } ``` - 注意事項: - 在 **Bit Flags** 場景,`#define`(或 `typedef` 搭配 `const`)比 enum 更適合,因為它更靈活,且更容易進行位元運算 - 簡單來說: - 如果成員之間需要進行運算組合,`#define` 更適合,enum 需要手動指定每個值,相較之下不夠直觀與靈活 ```c #define FLAG_READ (1 << 0) // 0001 #define FLAG_WRITE (1 << 1) // 0010 #define FLAG_EXEC (1 << 2) // 0100 #define FLAG_DELETE (1 << 3) // 1000 // 使用 #define 靈活擴展 #define FLAG_ALL (FLAG_READ | FLAG_WRITE | FLAG_EXEC | FLAG_DELETE) #define FLAG_RW (FLAG_READ | FLAG_WRITE) // 相較之下,enum 需要手動設置數值,且容易出錯 enum Permission { READ = 1, // 0001 WRITE = 2, // 0010 EXEC = 4, // 0100 DELETE = 8, // 1000 RW = READ | WRITE, // 0011 ALL = READ | WRITE | EXEC | DELETE // 1111 }; ``` ## Pointer ### Pointer - 指標是地址的型別,且指標的型別決定了如何解讀該地址的內容 - 指標本質上是儲存地址的變數,它的值就是一個記憶體地址 - 指標的型別決定了解讀該地址的方式,也就是該地址指向的資料應該被視為哪種類型 ### Function Pointer - 函式指標用於儲存函式的記憶體地址,使程式能夠在執行時動態調用函式,提供更高的靈活性與可擴展性 - 工作原理 1. 函式名稱本質上是一個函式地址 - 在 C 語言中,函式名稱(如 `print_func`)本質上是一個函式的地址,可以視為函式指標的簡寫形式 - 函式名稱與函式地址的等價性: ```c void my_function() { printf("Hello, Function Pointer!\n"); } int main() { printf("%p\n", my_function); // ✅ 輸出函式地址 printf("%p\n", &my_function); // ✅ 等價,顯式取得函式地址 } ``` - `my_function == &my_function`,因此可以直接將函式名稱賦值給函式指標 2. 語法的簡化 - C 語言允許函式指標直接調用函式,不需顯式解引用 `*` ```c func_ptr(); // 直接調用函式 (*func_ptr)(); // 顯式解引用,與上方等價 ``` - 函式名稱可隱式轉換為函式指標,因此兩種寫法完全等價 - 編譯器會自動處理解引用,允許更直觀的語法 3. 函式指標的宣告 - 為了讓編譯器識別變數為函式指標(儲存函式地址),需顯式使用 `*` 進行宣告 ```c void (*func_ptr)(int) = some_function; ``` ### Dangling Pointers and Wild Pointers - **Dangling Pointers** - 指標指向的記憶體已被釋放或重新分配,但指標仍保留舊地址,繼續使用會導致 UB - 如何避免 - 釋放記憶體後,將指標設置為 NULL ```c free(ptr); ptr = NULL; // ✅ 設為 NULL,避免懸空指標 ``` - 避免返回局部變數的地址 ```c void safe_function(int *out) { *out = 10; // ✅ 使用傳遞指標的方式避免 UB } ``` - **Wild Pointers** - 未初始化的指標,可能指向隨機記憶體地址,存取時導致 UB ### Pointers and Arrays - **Pointers** - `char *ptr` 表示一個指標,可以指向不同的記憶體位置 - 指標可以動態變更所指向的地址 ```c char c = 'A'; char *ptr = &c; // ptr 指向變數 c 的地址 ``` - **Arrays** - `char name[]` 表示一個固定大小的陣列,名稱 `name` 本質上是一個常量指標,指向陣列的第一個元素 - 陣列名稱不可修改,即 `name = some_other_array;` 是非法的 ```c char name[] = "Hello"; // name 是常量指標,指向 'H' name = "World"; // ❌ 錯誤,不能改變 name 的地址 ``` - 指標與陣列的關聯 - 當陣列名稱作為函式參數傳遞時,會隱式轉換為指向陣列第一個元素的指標 - 這使得函式無法直接得知陣列的大小 ### Pointer Operation - 確定 `++` 作用的對象,再按照運算順序執行 - `*p++` = `*p; p = p + 1;` - `++` 作用於 `p`,且為後置處理,所以先進行 `*p` 取值,再執行 `p = p + 1` - `*++p` = `p = p + 1; *p;` - `++` 作用於 `p`,且為前置處理,所以先進行 `p = p + 1`,再執行 `*p` - `++*p` = `*p = *p + 1; *p;` - `++` 作用於 `*p`,且為前置處理,所以先進行 `*p = *p + 1`,再執行 `*p` - `(*p)++` = `*p; *p = *p + 1;` - `++` 作用於 `*p`,且為後置處理,所以先進行 `*p`,再執行 `*p = *p + 1` ## Operator Precedence - C 語言運算子的優先級(由高到低) | 優先級 | 運算子 | 描述 | 結合性 | | --- | --- | --- | --- | | 1 | `()` `[]` `->` `.` | 函數呼叫、陣列下標、結構成員訪問 | 左至右 | | 2 | `!` `~` `++` `--` `+` `-` `*` `&` | 一元運算符(邏輯非、位元非、遞增、遞減等) | 右至左 | | 3 | `*` `/` `%` | 乘法、除法、取餘 | 左至右 | | 4 | `+` `-` | 加法、減法 | 左至右 | | 5 | `<<` `>>` | 位元左移、右移 | 左至右 | | 6 | `<` `<=` `>` `>=` | 比較運算 | 左至右 | | 7 | `==` `!=` | 等於、不等於 | 左至右 | | 8 | `&` | 位元 AND | 左至右 | | 9 | `^` | 位元 XOR | 左至右 | | 10 | `|` | 位元 OR | 左至右 | | 11 | `&&` | 邏輯 AND | 左至右 | | 12 | `||` | 邏輯 OR | 左至右 | | 13 | `? :` | 條件運算符 | 右至左 | | 14 | `=` `+=` `-=` `*=` `/=` 等 | 賦值運算符 | 右至左 | | 15 | `,` | 逗號運算符(順序求值) | 左至右 | - **位元運算的優先順序 (位元運算優先度都小於算術運算)** 1. 位元左移 (`<<`)、右移 (`>>`) 2. 位元 AND (`&`) 3. 位元 XOR (`^`) 4. 位元 OR (`|`) - 範例: ```c int a = 12; // 0b1100 int b = 5; // 0b0101 int c = 2; // 0b0010 int result = ~a + (b & c) << 1 ^ (b | c) >> (a > b) * c % 3; printf("Result: %d\n", result); // -25 ``` - **負數的二進位表示方式** - 在 C 語言中,負數的二進位通常使用二補數(Two’s Complement) 表示: - 取正數的一補數(One’s Complement)(即所有位元取反) - 加 1 得到最終的二補數 - 若要將負數的二進位轉換為十進位,可以先取反(一補數),再加 1,轉換為十進位後再加上負號 ## __attribute__ - `__attribute__` 是 GCC 和 Clang 提供的編譯器擴展,非 C 語言標準的一部分,主要用於優化程式、增強可讀性、避免潛在錯誤 - 常見用途 - **優化提示** - **noreturn**:告訴編譯器該函式不會返回(如 `exit()`) - **優點**:避免不必要的警告,幫助編譯器進行優化 ```c void terminate_program() __attribute__((noreturn)); void terminate_program() { exit(1); } ``` - **指定屬性** - **aligned(N)**:指定變數的對齊要求,可用於 SIMD 優化或特定硬體需求 - **優點**:確保變數對齊到指定的記憶體邊界,提高存取效能 ```c int arr[4] __attribute__((aligned(16))); ``` - **避免錯誤** - **format(printf, 1, 2)**:告訴編譯器,該函式的第一個參數是格式字串,第二個參數開始是格式對應的變數 - **優點**:讓編譯器檢查格式字串是否正確,避免類似 printf("%d", "string") 的錯誤 ```c void my_printf(const char *fmt, ...) __attribute__((format(printf, 1, 2))); ``` ## Interrupt Service Routine (ISR) - ISR(Interrupt Service Routine,中斷服務程序) 是專門用來處理硬體中斷的函式,當中斷發生時,處理器會暫停當前執行的程式,跳轉至 ISR 進行處理,然後再返回原程式 - 特點: 1. **自動執行**:當中斷發生時,ISR 會被處理器自動調用,而不是由主程式直接呼叫 2. **執行速度快**:ISR 必須儘可能快地執行完畢,避免影響系統效能 3. **避免阻塞**:ISR 不能進行長時間運行的操作(例如 printf() 或動態記憶體分配) 4. **ISR 與主程式的通訊**: - **共享變數**:ISR 可更新全域變數,讓主程式檢查變數狀態 - **旗標(Flags)**:ISR 可設置一個旗標(flag),讓主程式知道某個事件已發生 - 用途: - ISR 在嵌入式系統、即時控制、作業系統核心(Kernel)中廣泛應用,負責處理: - **外部設備中斷**(例如鍵盤、滑鼠、定時器) - **I/O 處理**(例如資料傳輸完成) - **計時器中斷**(用於精確時間控制) - **硬體錯誤**(例如記憶體錯誤或溢出) - 範例: ```c #include <avr/interrupt.h> #include <avr/io.h> volatile int timer_count = 0; // 使用 volatile 避免編譯器優化影響 ISR ISR(TIMER1_COMPA_vect) { // 定時器中斷函式 timer_count++; // 更新計數變數 } int main() { cli(); // 關閉中斷 TCCR1B |= (1 << WGM12) | (1 << CS12); // 設定計時器模式 TIMSK1 |= (1 << OCIE1A); // 啟用計時器中斷 sei(); // 啟用中斷 while (1) { if (timer_count >= 10) { // 每 10 次計時器中斷執行某些操作 timer_count = 0; } } } ```