在 C 語言中,函數返回指標 (T*) 是常見模式:字符串字面量、單例對象、靜態緩衝區、查找表等。但一個關鍵問題常被忽視:調用方是否應被允許修改指向的內容?
若函數實現方返回內部靜態變量或只讀數據區地址,而調用方意外寫入,將導致:
.rdata/.text)編譯器無法自動阻止這類錯誤 —— 除非我們在類型系統中引入 const 約束。
/* 正確模式:返回類型為 const int* */
const int* GetConfigValue(int key)
{
static const int configTable[] = { 100, 200, 300 }; // 僅讀數據
if (key >= 0 && key < 3)
return &configTable[key];
return NULL;
}
/* 調用方 */
void UseConfig()
{
const int* pVal = GetConfigValue(1); // 編譯器強制 pVal 為 const int*
// *pVal = 999; // ❌ 編譯錯誤: assignment of read-only location
printf("Value: %d\n", *pVal); // ✅ 僅讀取合法
}
/* 錯誤代碼:試圖通過非 const 指標修改只讀數據 */
int* GetConfigValue_Bad(int key) // 返回非 const 指標
{
static const int configTable[] = { 100, 200, 300 };
return (int*)&configTable[key]; // 強制轉型掩蓋 const (極度危險)
}
void CrashDemo()
{
int* p = GetConfigValue_Bad(0);
*p = 999; // 編譯器不報錯,但運行時觸發 Access Violation (Windows) / SIGSEGV (Linux)
}
MSVC 錯誤示例 (正確模式下):
error C3892: 'TestPointer' : you cannot assign to a variable that is const
const char* GetErrorMsg(int code)
{
static const char* msgs[] = {
"Success",
"Invalid Parameter",
"Out of Memory"
};
return (code >= 0 && code < 3) ? msgs[code] : "Unknown Error";
}
// 調用方: const char* msg = GetErrorMsg(err); // 安全
typedef struct { int id; const char* name; double score; } StudentRecord;
const StudentRecord* FindStudent(int id)
{
static const StudentRecord db[] = {
{1, "Alice", 92.5},
{2, "Bob", 87.0}
};
for (size_t i=0; i
const,調用方的接收變量必須也聲明為 const T*,否則編譯警告/錯誤。這迫使「只讀語義」沿調用鏈傳遞。static const,雙重保險 (編譯期 + 運行期頁面保護)。const_cast/C式強轉:除非極少數與無 const 設計的舊代碼交互,且確信數據確實可寫。@return 指向只讀數據的指標,調用方不得修改。| 誤區 | 正確理解 |
|---|---|
const int* 表示指標本身不可變 | 錯。它表示 指向的 int 不可變;指標本身可重新賦值指向別處。指標不可變是 int* const。 |
返回 const int* 就能防止所有修改 | 只能防止 直接通過該指標修改。若調用方強制轉型 (int*)ptr 或數據本身在可寫段,仍可繞過。 |
| 所有返回指標的函數都應加 const | 僅當 語義上數據屬於只讀/共享不可變狀態 時。若函數分配新內存交由調用方擁有 (如 malloc/strdup),則不應加 const。 |
const T* 作為返回類型,是 C 語言類型系統提供的零成本、編譯期強制的只讀保護機制。它將「不可修改」的語義從實現細節提升為接口契約,讓編譯器成為第一道防線。掌握這一技巧,能顯著降低指針相關的運行時崩潰與邏輯錯誤,是編寫健壯 C 庫的必備素養。