首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

C語言入門教程(五):字符串字面量不可修改——常量區與棧區的內存分佈真相

     編輯:關於C語言

C語言入門教程(五):字符串字面量不可修改——常量區與棧區的內存分佈真相

字符串字面量存儲在只讀常量區,嘗試修改導致崩潰。本節通過崩潰案例、內存佈局圖解、正確修改姿勢、const 防護,建立字符串內存安全意識。

一、崩潰案例還原

錯誤代碼:crash.c

int main() {
    char *msg = "ABC";   // msg 指向常量區
    msg[0] = msg[1];     // 嘗試修改只讀內存 → 崩潰!
    puts(msg);
    return 0;
}

編譯與運行

$ gcc crash.c -o crash
$ ./crash
Segmentation fault (core dumped)  // Linux
// 或 Access violation (Windows)

二、內存佈局真相

進程虛擬地址空間(簡化):
┌─────────────────────────────────────┐
│ 內核空間 (3GB~4GB)                  │
├─────────────────────────────────────┤
│ 用戶空間                            │
│  ┌───────────────────────────────┐  │
│  │ 棧區 (向下增長)                │  │
│  │   局部變量、函數幀              │  │
│  └───────────────────────────────┘  │
│  ┌───────────────────────────────┐  │
│  │ 堆區 (向上增長)                │  │
│  │   malloc/free                  │  │
│  └───────────────────────────────┘  │
│  ┌───────────────────────────────┐  │
│  │ 數據區 (.data/.bss)            │  │
│  │   全局/靜態變量                │  │
│  └───────────────────────────────┘  │
│  ┌───────────────────────────────┐  │
│  │ 代碼區 (.text)                 │  │
│  │   機器指令                     │  │
│  └───────────────────────────────┘  │
│  ┌───────────────────────────────┐  │
│  │ 常量區 (.rodata)  ← "ABC" 在此 │  │
│  │   字符串字面量、只讀數據        │  │
│  │   **頁屬性:只讀 (READ ONLY)**  │  │
│  └───────────────────────────────┘  │
└─────────────────────────────────────┘

關鍵點

  • 字符串字面量 "ABC" 編譯時存入 .rodata 段
  • 加載時映射為**只讀內存頁**(MMU 屬性)
  • char *msg = "ABC" 使 msg 指向只讀區
  • msg[0] = ... 觸發寫保護頁錯誤 → SIGSEGV

三、正確修改姿勢:棧上副本

方案 1:數組初始化(推薦)

int main() {
    char msg[] = "ABC";  // 在棧上分配 4 字節,複製 "ABC\0"
    msg[0] = msg[1];     // 修改棧上副本 → 安全!
    puts(msg);           // 輸出 "BBC"
    return 0;
}

機制:char msg[] = "ABC" 等價於 char msg[4] = {'A','B','C','\0'},編譯器生成代碼在棧幀分配空間並複製內容。

方案 2:動態分配

int main() {
    char *msg = strdup("ABC");  // 堆上分配副本
    if (!msg) return 1;
    msg[0] = msg[1];
    puts(msg);
    free(msg);  // 釋放
    return 0;
}

四、字符串字面量 vs 數組初始化:本質對比

寫法類型存儲位置可修改生命週期sizeof
char *p = "ABC"char*.rodata (只讀)否程序全周期指針大小
char a[] = "ABC"char[4]棧/數據區 (可寫)是作用域/程序周期4
const char *p = "ABC"const char*.rodata (只讀)編譯器阻止程序全周期指針大小

五、const 防護:編譯期攔截錯誤

最佳實踐

// 僅讀取:用 const 承諾不修改
const char *msg = "ABC";
puts(msg);
// msg[0] = 'X';  // 編譯錯誤:assignment of read-only location

// 需修改:用數組
char msg[] = "ABC";
msg[0] = 'X';  // 合法

規範:所有不修改字符串的指針參數、返回值、變量,**一律加 const**。

六、函數參數傳遞:const 正確性

// 錯誤:承諾不改卻能改
void bad_print(char *str) { puts(str); }

// 正確:const 保護調用者傳入的字面量
void good_print(const char *str) { puts(str); }

// 調用
good_print("Hello");      // 合法:字面量傳給 const 指針
good_print(msg);          // 合法:數組退化為 const 指針
bad_print("Hello");       // 危險:字面量傳給非 const,內部若修改會崩潰

七、進階:字符串字面量合併優化

const char *s1 = "Hello";
const char *s2 = "Hello";
printf("%p %p\n", s1, s2);  // 可能相同!編譯器合併相同字面量

char a[] = "Hello";
char b[] = "Hello";
printf("%p %p\n", a, b);    // 必須不同!各自在棧上獨立分配

八、調試技巧:檢測只讀內存訪問

  1. GDB:watch *(char*)0xaddress 監視內存寫入
  2. Valgrind:valgrind ./program 檢測非法內存訪問
  3. 編譯器警告:-Wwrite-strings 將字面量類型視為 const char*,捕獲隱式去 const

九、本節作業

  1. 實驗:char *p = "ABC"; p[0] = 'X'; 在 GDB 中觀察崩潰堆棧
  2. 編寫安全字符串複製函數 char* safe_strdup(const char* src)
  3. 閱讀 man 7 signal 理解 SIGSEGV、SIGBUS
  4. 思考:為什麼 C++ 標準庫 std::string 構造函數接受 const char*?

總結

「字面量在只讀區,副本在可寫區」。char *p = "..." 指向只讀區,**絕不修改**;char a[] = "..." 在棧上創建副本,**可隨意修改**。const 是編譯器契約,**所有不修改的字符串指針必須加 const**。這不僅是語法規範,更是內存安全的第一道防線。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved