我們都在學習

C&C++ Part 5:記憶體模型

第五部份:記憶體模型(Stack、Heap 與 sizeof)

本文介紹C/C++的記憶體模型。

Stack vs Heap

在C/C++中,記憶體主要分為兩個區域:Stack(堆疊)和 Heap(堆積)。

  • Stack:用於存儲局部變數和函數呼叫的相關資訊。Stack的大小通常較小,且由系統自動管理,當函數結束時,Stack上的資料會自動釋放。
  • Heap:用於動態分配記憶體,大小通常較大,需要由程式員手動管理,使用newdelete(C++)或mallocfree(C)來分配和釋放記憶體。

生命週期(Lifecycle)

  • Stack上的變數在宣告的作用域內存在,當作用域結束時自動銷毀。
  • Heap上的變數在被分配後存在,直到被手動釋放,否則會導致記憶體洩漏(Memory Leak)。

生命週期指的是變數從創建到銷毀的過程。Stack上的變數在函數呼叫期間存在,而Heap上的變數則需要手動管理其生命週期。

#include <iostream>
void stackExample() {
    int x = 10; // x存在於Stack上
    std::cout << "Stack variable: " << x << std::endl;
} // x在此處被銷毀

void heapExample() {
    int* y = new int(20); // y存在於Heap上
    std::cout << "Heap variable: " << *y << std::endl;
    delete y; // 手動釋放y
} // y在此處被銷毀

int main() {
    stackExample();
    heapExample();
    return 0;
}

Memory Leak

如果在Heap上分配的記憶體沒有被釋放,就會導致記憶體洩漏,這會消耗系統資源,最終可能導致程式崩潰。

void leak_example() {
    int* y = new int(20);
    // 忘記 delete y;
    // 函數結束,但 Heap 上的記憶體沒有被釋放
    // 如果這個函數被呼叫一百萬次,記憶體就會一直被佔用
}

這在嵌入式系統或長時間運行的服務中尤其嚴重,因為它們可能會持續運行很長時間,記憶體洩漏會逐漸累積,最終導致系統資源耗盡。

[閱讀全文]

C&C++ Part 4:位元運算

第四部份:位元運算(Bitwise Operations)

本文介紹C/C++的位元運算。
包含:

  • 位元運算子:AND(&)、OR(|)、XOR(^)、NOT(~)
  • 位元移位運算子:左移(«)、右移(»)

位元運算子

  • &&:邏輯運算子,對整個表達式進行評估,結果為true或false。
  • &:位元運算子,對每個位元進行AND運算,結果為一個新的數值。
  • ||:邏輯運算子,對整個表達式進行評估是否有任意一個為true,結果為true或false。
  • |:位元運算子,對每個位元進行OR運算,結果為一個新的數值。
  • ^:位元運算子,對每個位元進行XOR運算,結果為一個新的數值。
  • ~:位元運算子,對每個位元進行NOT運算,結果為一個新的數值。

邏輯運算可以參考真值表:

ABA && BA & BA || BA ^ B~A~B
00000011
01001110
10001101
11111000

等等,是一個非常複雜但完整的表格。

位元移位運算子

  • <<:左移運算子,將位元向左移動指定的位數,右側補0。
  • >>:右移運算子,將位元向右移動指定的位數,左側補0(對於無符號整數)或補符號位(對於有符號整數)。

所謂左移與右移的意思是:

0x00000001 << 1  // 結果為 0x00000010 轉成10進位為 2
0x00000001 >> 1  // 結果為 0x00000000 轉成10進位為 0

直接將所有位元向左或向右移動,並在空出的位置補0或補符號位。

[閱讀全文]

C&C++ Part 6:Endianess與記憶體對齊

第六部份:Endian 與 記憶體對齊

本文介紹Endian與記憶體對齊。

Endian

Endian是指多位元組資料在記憶體中的儲存順序。
其中:

  • Big Endian:高位元組在前,低位元組在後
  • Little Endian:低位元組在前,高位元組在後

例如:

uint32_t value = 0x12345678;

在Big Endian系統中,這個值會以以下順序儲存在記憶體中:

0x12 0x34 0x56 0x78

而在Little Endian系統中,則會以以下順序儲存在記憶體中:

0x78 0x56 0x34 0x12

具體影響在於,當不同的系統之間進行交換資料、讀取binary檔案、網路傳輸與跨平台開發時,會因為Endian的不同而導致資料解讀錯誤。
例如,當在Big Endian(TCP/IP protocol)上傳輸的資料被Little Endian系統(Windows/Linux/macOS/ARM Android/iOS)讀取時,數值可能會被錯誤解讀。
因此,在跨平台開發中,通常會使用標準函式庫提供的函數(如htonlhtonsntohlntohs)來確保資料在不同Endian系統之間正確轉換。

函數用途原文
htonl將32位元整數從主機字節序轉換為網路字節序Host to Network Long
htons將16位元整數從主機字節序轉換為網路字節序Host to Network Short
ntohl將32位元整數從網路字節序轉換為主機字節序Network to Host Long
ntohs將16位元整數從網路字節序轉換為主機字節序Network to Host Short

Pros

  1. 自動化平台判別:這些函數在 Big Endian 系統(如早期 PowerPC)上通常被定義為空操作(No-op)的巨集,而在 Little Endian 系統上則會自動執行位元交換(Byte Swap)。開發者無需撰寫 #ifdef 來判斷環境。
  2. 程式碼意圖明確:htonl (Host to Network Long) 的名稱直接告訴閱讀者這是在做「格式轉換」而非一般的數學運算,增加了代碼的可讀性與維護性。
  3. 效能優化:現代編譯器會將這些函數優化為專用的 CPU 指令(如 x86 的 BSWAP),執行效率極高。

Cons

  1. 命名與長度誤導:名稱中的 Long 在當前多數系統代表 32-bit,但在 64-bit 時代,這容易產生誤解。
    • 標準的 POSIX 庫中並沒有定義 htonll(64-bit),導致處理 64 位元整數時需要自行實作或引用非標準庫。
  2. 類型限制:這些函數僅針對unsigned integers。對於float/double或struct,無法直接作用,仍需手動處理每個成員。
  3. 隱藏的重複轉換風險:如果開發流程中對資料層級定義不明,可能會發生"轉兩次"的情況(Host -> Network -> 又被當作 Host 轉一次),導致數值徹底錯誤。

替代方案

  • C++23 std::byteswap: 這是 C++ 標準庫中更現代的寫法,意圖更直觀且支持模板。
  • Rust 的位元方法: Rust 的 .to_be_bytes()/.from_be_bytes() 和 .to_le_bytes()/.from_le_bytes()以及 to_ne_bytes()/.from_ne_bytes() 方法。這種顯式的轉換方式比 C 語言的巨集更不容易出錯。
  • 手動位移運算 (Bit-shifting): 在處理二進位檔案解析時,手動使用 (data « 8) | (data » 8) 雖然繁瑣,但它是與平台無關(Platform-independent)的,無論在什麼端序的系統上執行,結果都一致,能從根本上避免 Endian 問題。

Rust:

[閱讀全文]

C&C++ Part 3:型別與資料結構

第三部份:型別與資料結構

本文"簡略"說明C/C++的型別與資料結構,包含基本型別、指標、陣列、結構體和類別。

前言

Q:為何簡略說明?
A:C/C++的型別系統非常龐大且複雜,涵蓋了:

  • 基本型別
  • 指標
  • 陣列
  • 結構體
  • 類別
  • 聯合體
  • 枚舉

基本型別

Integer

整數,包含:

  • int:一般整數,通常為32位元(可能因作業系統而異)
  • short:短整數,通常為16位元(可能因作業系統而異)
  • long:長整數,通常為64位元(可能因作業系統而異)
  • long long:更長的整數,通常為64位元(可能因作業系統而異)
  • unsigned int:無符號整數,僅表示非負數(可能因作業系統而異)
  • uint8_tuint16_tuint32_tuint64_t:固定寬度的無符號整數,分別是8、16、32、64位元

Floating-point

浮點數,包含:

  • float:單精度浮點數,通常為32位元(可能因作業系統而異)
  • double:雙精度浮點數,通常為64位元(可能因作業系統而異)
  • long double:更長的浮點數,通常為80位元(可能因作業系統而異)

文字型別

字元:

  • char:字元,通常為8位元(可能因作業系統而異),用於表示單一字元

字串:

  • C風格字串:以char陣列表示,結尾以空字元(\0)結束
  • C++風格字串:使用std::string類別定義。

指標

指標是C/C++中非常重要的概念,用於存儲變數的地址。

指標的宣告方式:

int* ptr; // ptr一個int指標,為一個int變數的地址

指標的使用:

int value = 42;
int* ptr = &value; // ptr指向value的地址
printf("%d\n", *ptr); // 解引用ptr,輸出value的值42

其中,在變數前面加上&符號表示取地址,使用*符號表示取值。
所謂取值就是從指標中獲取實際的值,可能是任何型別,例如整數,字串,浮點數甚至struct甚至另一個地址。
例如:

int x = 10;
int* ptr = &x; // ptr指向x的地址
printf("%d\n", *ptr); // 解引用ptr,輸出x的值10

int** ptr2 = &ptr; // ptr2指向ptr的地址
printf("%d\n", *ptr2); // 解引用ptr2一次,輸出ptr的值(即x的地址)
printf("%d\n", **ptr2); // 解引用ptr2兩次,輸出x的值10

指標也可以用來動態分配記憶體,例如:

/* C使用 malloc/free(需include <stdlib.h> 與 <string.h>) */
const char *src = "Hello, World!"; // const 是一種防呆,以防src被修改。
char *str = malloc(strlen(src) + 1); // 分配 strlen(src)+1,包含終止字元 '\0'
if (str == NULL) {
	/* 處理配置失敗 */
}
strcpy(str, src); // strcpy 會複製終止字元,不需手動加 '\0'
/* 使用 str */
free(str); // 釋放記憶體
// C++ 風格(示範 new/delete,不建議在modern C++ 中直接管理原始指標)
const char *src = "Hello, World!";
char *buf = new char[strlen(src) + 1];
strcpy(buf, src); // strcpy 會複製終止字元
/* 使用 buf */
delete[] buf;
// modern C++:推薦使用 std::string,自動管理記憶體並避免buffer overflow
#include <string>
std::string s = "Hello, World!";

至於modern C++的std::unique_ptr和std::shared_ptr:

[閱讀全文]

C&C++ Part 2:基本語法

第二部份:基本語法

本文介紹C/C++的基本語法,包括變數、迴圈、Statement和函數

範例

#include <cstdio>

void for_counter();
void while_counter();


int main() {
    if(true) {
        printf("Statement sample in main\n");
    } else {
        printf("This will never be printed\n");
    }
    for_counter();
    while_counter();
    return 0;
}

void for_counter() {
    printf("For loop counter:\n");
    for (int i = 0; i < 10; i++) {
        printf("%d", i);
    }
    printf("\n");
}

void while_counter() {
    int i = 0;
    printf("While loop counter:\n");
    while (i < 10) {
        printf("%d", i);
        i++;
    }
    printf("\n");
}

Statement

Statement是程式中的一行指令,告訴電腦要做什麼。

[閱讀全文]

C/C++ -1:編譯與編譯器

第一部份:編譯

本文介紹C/C++編譯過程與編譯器。 簡單解釋GCC/G++和Clang的差異。

準備

安裝:

sudo apt update
sudo apt install build-essential clang -y

範例

// hello_world.cpp
#include <cstdio>

int main(int) {
    printf("Hello, World!\n");
    return 0;
}

編譯與執行

# compile with clang
clang hello_world.cpp -o hello_world # -o指定輸出檔案名稱為hello_world
# run binary with clang output file named hello_world
./hello_world

結果:

Hello, World!

編譯器

C++編譯器有分很多種

  • Clang (LLVM的一部分)
  • GCC (GNU Compiler Collection)
  • G++ (GNU C++ Compiler)
  • MSVC (Microsoft Visual C++)

GCC和G++是傳統的編譯器,穩定且廣泛應用,可以在很多系統中看到。
Clang則是較新的編譯器,提供更好的錯誤訊息和更快的編譯速度,特別是在大型專案中常見。

特性GCC/G++Clang
錯誤訊息較簡單詳細且易懂
編譯速度較慢較快
平台支援廣泛新、但增長中
toolchain傳統現代,與LLVM生態系統整合
標準支援速度較快較慢

簡單來說,如果你需要跑單一檔案測試GCC/G++和Clang都可以,但如果你需要更好的錯誤訊息和更快的編譯速度,Clang會比較好。
但是同時你會遇到一些兼容性問題,特別是在使用較新的C++標準或特定平台特性時。