C 語言中宣告 const int a = 10;,直接賦值 a = 20 編譯報錯,但透過指針 *(int*)&a = 20 卻能「修改成功」並印出新值?本文揭示背後的編譯器最佳化、記憶體模型與未定義行為 (UB) 真相。
const int a = 10;
printf("a = %d\n", a); // 輸出 10
int* p = (int*)&a;
*p = 20;
printf("a = %d\n", a); // 輸出 10(或 20,視最佳化)
printf("*p = %d\n", *p); // 輸出 20
printf("&a = %p, p = %p\n", &a, p); // 位址相同
const int a 告訴編譯器:程式碼不應修改 a,編譯器可據此最佳化(如常數摺疊、置於 .rodata 段)。const 物件置於硬體唯讀記憶體(嵌入式系統除外)。const int a = 10;
// 編譯器常數摺疊:所有讀取 a 直接替換為 10
printf("%d", a); // 實際機器碼:mov eax, 10 / call printf
// 即使記憶體位址 &a 內容已被指針改為 20,程式碼仍印出 10
這就是為何 a 仍印 10,而 *p 印 20 的原因:編譯器信任 const 承諾,直接內嵌常數值。
C11 標準 6.7.3/6:「若嘗試修改以 const 限定的物件,行為未定義。」
.text (程式碼) .rodata (唯讀資料) .data/.bss (讀寫資料) Stack/Heap
───────────────── ────────────────── ───────────────────── ──────────
main: a = 10 (可能在這) int b = 20; ...
mov eax, 10 ◄──┐ ▲
call printf │ │
│ *(int*)&a = 20 ──────────────┘ (UB:寫入 .rodata 可能觸發保護錯誤)
│
└── 常數摺疊:直接用 10,不讀取記憶體
// 1. 真正需要修改
int a = 10;
a = 20; // 合法
// 2. 函式參數唯讀保護(正確用法)
void print(const int* p) {
// *p = 20; // 編譯錯誤:保護呼叫端資料不被修改
printf("%d", *p);
}
// 3. 編譯期常數(C99 static const / C++ constexpr)
static const int MAX_SIZE = 100; // 有位址,但語意唯讀
#define MAX_SIZE 100 // 預處理替換,無位址
enum { MAX_SIZE = 100 }; // 編譯期常數,無位址
若 const 變數配置在 Flash/ROM(透過 linker script 或 __attribute__((section(".rodata")))),硬體會阻擋寫入,指針修改直接觸發 HardFault/段錯誤。這是「const 進入唯讀記憶體」的少數場景。
| 操作 | 合法性 | 結果 |
|---|---|---|
const int a = 10; a = 20; | ❌ 編譯錯誤 | 語法層面阻擋 |
int* p = (int*)&a; *p = 20; | ⚠️ 編譯通過,但 UB | 行為不可預測,嚴禁使用 |
int a = 10; a = 20; | ✅ 完全合法 | 正常可修改變數 |
const 是給編譯器與維護者的「契約」,而非硬體鎖。違反契約即進入 UB 風險區,現代編譯器會無情最佳化導致難以除錯的離譜行為。敬畏 const,善用它表達設計意圖,而非試圖繞過它。