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

https://www.geeksforgeeks.org/memory-layout-of-c-program/

### 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 確保變數的最新值被讀取

## 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;
}
}
}
```