C/C++ 開發者常遇到的經典問題:char* str = "hello"; str[0] = 'H'; 導致運行時崩潰,而 char str[] = "hello"; str[0] = 'H'; 卻完美運行。本文從編譯器實現、內存佈局、C/C++ 標準規範三個維度,徹底解析這一核心差異的本質。
// 情況 1:字符指針指向字符串字面值 - 崩潰
char* str1 = "resource";
str1[6] = 'k'; // 段錯誤 / 訪問違例
*(str1 + 6) = 'k'; // 同樣崩潰
// 情況 2:動態分配內存 - 正常
char* str2 = new char[12];
strcpy(str2, "resource");
str2[6] = 'k'; // 正常
// 情況 3:字符數組初始化 - 正常
char str3[] = "resource";
str3[6] = 'k'; // 正常
*(str3 + 6) = 'k'; // 正常
| 特性 | 字符串字面值 | 字符數組 | 動態分配 |
|---|---|---|---|
| 代碼形式 | char* p = "abc"; | char a[] = "abc"; | char* p = new char[4]; |
| 存儲區域 | 只讀數據段 (.rodata/.rdata) | 棧 (或數據段/全局) | 堆 |
| 內存屬性 | 只讀 (PAGE_READONLY) | 讀寫 (PAGE_READWRITE) | 讀寫 (PAGE_READWRITE) |
| 生命週期 | 程序全周期 (靜態存儲期) | 作用域結束 (自動存儲期) | 手動釋放 (動態存儲期) |
| 內容初始化 | 編譯期固定 | 運行期拷貝自字面值 | 運行期手動拷貝 |
| 可修改性 | 不可修改 (UB) | 可修改 | 可修改 |
"resource" 字符串字面值,編譯器在 .rodata 段 分配空間,存儲字符序列 'r','e','s','o','u','r','c','e','\0'char* str = "resource"; 將該字面值的首地址賦給指針變量 str (存儲在棧上)char str[] = "resource"; 編譯器在棧上分配 9 字節空間,生成代碼在運行時將字面值內容逐字節拷貝到這塊棧內存PAGE_READONLYSIGSEGV (Linux) / EXCEPTION_ACCESS_VIOLATION (Windows)PAGE_READWRITE,允許寫入C 標準 (ISO/IEC 9899):字符串字面值具有靜態存儲期,類型為「字符數組」,但嘗試修改字符串字面值的行為未定義 (UB)。編譯器可選擇將其放入只讀存儲區。
C++ 標準 (ISO/IEC 14882):字符串字面值類型為
const char[N],隱式轉換為char*僅為兼容 C (C++11 起棄用),修改導致未定義行為。正確類型應為const char*。
#include
int main() {
char* literal = "resource";
char array[] = "resource";
char* heap = (char*)malloc(9);
strcpy(heap, "resource");
printf("Literal: %p (%.2x...)\n", literal, (unsigned char)*literal);
printf("Array: %p (%.2x...)\n", array, (unsigned char)*array);
printf("Heap: %p (%.2x...)\n", heap, (unsigned char)*heap);
// 典型 64位 Linux 輸出:
// Literal: 0x55a3b2c4d004 (在 .rodata, 低地址 ~0x400000)
// Array: 0x7ffc12345670 (在棧, 高地址 ~0x7fff...)
// Heap: 0x55a3b2c8e260 (在堆, 中間地址)
free(heap);
return 0;
}
// ❌ 危險:返回局部數組指針
char* GetString() {
char str[] = "hello";
return str; // UB: 棧內存函數返回後失效
}
// ✅ 正確:返回字符串字面值 (只讀)
const char* GetString() {
return "hello"; // 安全: 靜態存儲期
}
// ✅ 正確:動態分配 (調用者負責 free)
char* GetString() {
char* p = malloc(6);
strcpy(p, "hello");
return p;
}
// ✅ 正確:C++ string / 靜態局部數組
std::string GetString() { return "hello"; }
const char* GetString() { static char buf[] = "hello"; return buf; }
// ✅ 推薦:明確 const,編譯器阻止修改
const char* str = "resource";
// str[0] = 'H'; // 編譯錯誤:cannot convert 'const char*' to 'char*'
// ❌ 歷史兼容:非 const 指向字面值 (C++11 棄用,C++20 可能移除)
char* str = "resource"; // 警告: deprecated conversion
// 函數參數傳遞
void Print(const char* s); // ✅ 接受字面值、數組、堆內存
void Modify(char* s); // ✅ 僅接受可修改緩衝區 (數組、堆)
Print("hello"); // ✅
char buf[] = "hello"; Print(buf); // ✅
Modify("hello"); // ❌ UB (即使編譯器允許)
Modify(buf); // ✅
std::string / std::string_view (C++17) 管理字符串std::vector<char> / std::string / new char[] (配合智能指針)const char* / string_view,輸出緩衝區用 char* + 長度 / string&-Wwrite-strings (GCC/Clang) / /Zc:strictStrings (MSVC) 強制檢測本質區別:
char* p = "abc":p 指向 只讀數據段 的字面值,修改觸發硬件頁面保護異常char a[] = "abc":a 是 棧上可讀寫數組,內容由啟動代碼從字面值拷貝而來char* p = new char[N]:p 指向 堆上可讀寫內存,需手動拷貝內容核心原則:字符串字面值不可修改。始終使用 const char* 指向字面值,需要修改時創建可寫副本 (數組/堆/string)。這不僅是避免崩潰,更是編寫正確、可移植、標準合規 C/C++ 代碼的基礎要求。