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

C++ 深度解析:char* 與 char[] 的記憶體模型差異與常見陷阱

     編輯:關於C++

前言:一個經典的崩潰案例

char* c1 = "abc";
c1[0] = 'x';  // 執行期崩潰:Access Violation

char c2[] = "abc";
c2[0] = 'x';  // 正常執行,c2 變為 "xbc"

為什麼長得差不多,行為卻天差地別?關鍵在於 **字串字面值存放區** 與 **棧上陣列** 的記憶體屬性差異。本文從編譯器記憶體佈局、執行期行為、組合語言層面深度剖析。

程式記憶體分區速覽

區域別名存放內容讀寫屬性生命週期管理方式
程式碼區.text機器指令、常數字面值唯讀進程存續OS 載入器
資料區.data / .bss全域/靜態變數讀寫進程存續編譯器配置
堆疊Stack區域變數、函式參數、返回位址讀寫函式呼叫期間編譯器自動 (LIFO)
堆積Heap動態配置記憶體讀寫程式設計師控制malloc/free, new/delete
常數區.rdata / .rodata字串字面值、const 全域變數**唯讀**進程存續編譯器配置,OS 標記頁面保護

兩種宣告的記憶體佈局對比

char* c1 = "abc";

記憶體圖示:

常數區 (.rdata)           棧區 (Stack)
+------------------+      +------------------+
| 'a' 'b' 'c' '\0' |←─────│ c1 (指標變數)    │  地址值:0x0040xxxx (常數區地址)
+------------------+      +------------------+
  地址:0x00403000           地址:0x0012FF7C
  • "abc" 是 **字串字面值**,編譯期存入唯讀區 .rdata。
  • c1 是棧上指標變數,儲存字面值地址。
  • **修改 c1[0] = 寫入唯讀頁面 → CPU 觸發頁面保護錯誤 → OS 送 SIGSEGV/Access Violation。**
  • 編譯器最佳化:相同字面值可能合併 (String Pooling),多個指標指向同一地址。

char c2[] = "abc";

記憶體圖示:

棧區 (Stack)
+------------------+------------------+
| c2[0]='a'        │ c2[1]='b'        │ c2[2]='c' │ c2[3]='\0' │
+------------------+------------------+
  地址:0x0012FF70
  • c2 是 **棧上陣列**,大小固定 4 bytes (含結束符)。
  • **執行期初始化**:函式序曲中將字面值 "abc" 從唯讀區 **複製** 到棧上陣列。
  • 陣列內容可讀可寫,屬於當前堆疊幀,函式返回自動釋放。
  • sizeof(c2) == 4,&c2 == &c2[0] (陣列名即首元素地址)。

進階變體:malloc + 複製

char* c3 = (char*)malloc(4);
strcpy(c3, "abc");  // 正確:複製到可寫堆積記憶體
c3[0] = 'x';        // 正常

// 錯誤寫法:
char* c3 = (char*)malloc(4);
c3 = "abc";         // 1. 記憶體洩漏 (原 malloc 區塊丟失)
                    // 2. c3 指向唯讀區,後續修改崩潰

組合語言層面驗證 (x86 MSVC)

; char c[] = "abc";
; a = c[1];
mov  cl, byte ptr [ebp-0Fh]   ; 直接從堆疊相對位址讀取
mov  byte ptr [ebp-4], cl

; char *p = "abc";
; a = p[1];
mov  edx, dword ptr [ebp-14h] ; 先載入指標值 (常數區地址)
mov  al, byte ptr [edx+1]     ; 再間接尋址讀取
mov  byte ptr [ebp-4], al

結論:陣列直接索引 少一條指令,無指標間接尋址,存取速度略快。

常見誤區與最佳實務

誤區 1:以為 char* 指向可寫記憶體

// 錯誤
void UpperCase(char* s) { while(*s) *s++ = toupper(*s); }
UpperCase("hello");  // 崩潰!

// 正確:呼叫端確保傳入可寫緩衝區
char buf[] = "hello";
UpperCase(buf);

誤區 2:函式回傳區域陣列指標

char* GetString() {
    char local[] = "abc";
    return local;  // 錯誤:回傳棧上地址,函式結束後失效
}

誤區 3:sizeof 混淆

char* p = "abc";
char a[] = "abc";
sizeof(p);  // 4/8 (指標大小)
sizeof(a);  // 4 (陣列大小,含 \0)
strlen(p);  // 3
strlen(a);  // 3

最佳實務建議

  1. 只讀字串:使用 const char* 或 const char[],編譯器會攔截修改嘗試。
    const char* msg = "Read-only";  // 好習慣
    // msg[0] = 'x';  // 編譯錯誤:cannot modify const object
    
  2. 需修改的字串緩衝區:使用 char[]、std::vector<char>、std::string、std::unique_ptr<char[]>。
  3. API 介面設計:
    • 輸入字串:const char* 或 std::string_view (C++17)
    • 輸出緩衝區:char* buffer, size_t size 或 std::string&
  4. 現代 C++ 首選 std::string/std::string_view:自動管理記憶體、支援 SSO (Small String Optimization)、避免手動記憶體錯誤。

總結對照表

特性char* = "abc"char[] = "abc"malloc + strcpy
字元存放區唯讀區 (.rdata)棧區 (Stack)堆積區 (Heap)
可修改性**不可修改** (崩潰)可修改可修改
變數大小指標大小 (4/8 bytes)陣列長度 (含 \0)指標大小
sizeof 結果4/8N (含 \0)4/8
生命週期程式全期所在作用域手動管理
初始化開銷無 (僅指標賦值)執行期複製malloc + 複製
適用場景只讀字串、API 傳入本地可修改緩衝區動態大小、跨函式生命週期

結語

char* 指向字串字面值是 **唯讀指標**,char[] 是 **可修改的棧上陣列**。理解記憶體分區與生命週期,是避免 C/C++ 低階記憶體錯誤的關鍵。現代開發請善用 std::string/string_view,將裸指標限制在介面邊界與效能熱點。

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