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

C/C++ 字符串字面值與字符數組的內存區別:為什麼 char* 不可修改而 char[] 可以

     編輯:關於C語言

前言

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)可修改可修改

三、編譯器與鏈接器實現細節

3.1 編譯階段

  • 遇到 "resource" 字符串字面值,編譯器在 .rodata 段 分配空間,存儲字符序列 'r','e','s','o','u','r','c','e','\0'
  • char* str = "resource"; 將該字面值的首地址賦給指針變量 str (存儲在棧上)
  • char str[] = "resource"; 編譯器在棧上分配 9 字節空間,生成代碼在運行時將字面值內容逐字節拷貝到這塊棧內存

3.2 鏈接與加載階段

  • .rodata 段標記為 READONLY,映射到虛擬內存時設置頁面屬性 PAGE_READONLY
  • 任何寫入 .rodata 頁面的嘗試觸發 頁面錯誤,OS 發送 SIGSEGV (Linux) / EXCEPTION_ACCESS_VIOLATION (Windows)
  • 棧/堆頁面屬性為 PAGE_READWRITE,允許寫入

3.3 標準規範 (C99/C11/C++98/11/17/20)

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;
}

五、常見誤區與最佳實踐

5.1 錯誤模式識別


// ❌ 危險:返回局部數組指針
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; }

5.2 const 正確性


// ✅ 推薦:明確 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);               // ✅

5.3 現代 C++ 最佳實踐

  • 優先使用 std::string / std::string_view (C++17) 管理字符串
  • 需要可修改緩衝區時用 std::vector<char> / std::string / new char[] (配合智能指針)
  • API 設計:輸入參數用 const char* / string_view,輸出緩衝區用 char* + 長度 / string&
  • 啟用編譯器警告:-Wwrite-strings (GCC/Clang) / /Zc:strictStrings (MSVC) 強制檢測

六、總結

本質區別:

  1. char* p = "abc":p 指向 只讀數據段 的字面值,修改觸發硬件頁面保護異常
  2. char a[] = "abc":a 是 棧上可讀寫數組,內容由啟動代碼從字面值拷貝而來
  3. char* p = new char[N]:p 指向 堆上可讀寫內存,需手動拷貝內容

核心原則:字符串字面值不可修改。始終使用 const char* 指向字面值,需要修改時創建可寫副本 (數組/堆/string)。這不僅是避免崩潰,更是編寫正確、可移植、標準合規 C/C++ 代碼的基礎要求。

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