/* 錯誤示範 */
char* GetStaffNameById(int id)
{
StaffInfo db[] = { {"Socrates",1}, {"dyx1024",2}, {"Kevin",3}, {"Jim",4} };
for (int i=0; i<4; i++)
if (db[i].id == id) return db[i].name; // db 在棧上,函數返回即銷毀
return "No match";
}
int main()
{
char* name = GetStaffNameById(2);
printf("%s\n", name); // 運行時未定義行為:可能輸出正確、亂碼或崩潰
}
根因:db 是自動存儲期局部變量,函數返回後棧幀釋放,內存被後續調用覆蓋。返回指標變成懸空指標 (Dangling Pointer)。
char* GetStaffNameById_v1(int id)
{
static StaffInfo db[] = { {"Socrates",1}, {"dyx1024",2}, {"Kevin",3}, {"Jim",4} };
// static 使 db 存儲於 .data/.bss 段,生命週期 = 程序全周期
for (int i=0; i<4; i++)
if (db[i].id == id) return db[i].name;
return "No match";
}
| 優點 | 缺點 |
|---|---|
| 零分配開銷、調用簡單 | 非線程安全 (共享緩衝區) |
| 無需調用方管理內存 | 不可重入 (遞歸/信號處理中調用會覆蓋) |
| 適用於單例、查找表、錯誤字符串 | 數據不可修改 (應返回 const char*) |
static StaffInfo g_Db[] = { ... }; // 文件內可見,鏈接時不導出符號
char* GetStaffNameById_v2(int id) { /* 同 v1 */ }
本質同方案一,但作用域可控。適用於模塊內部共享狀態。
/* 接口契約:調用方分配緩衝區,傳入指針與容量 */
void GetStaffNameById_v3(int id, char* outBuf, size_t bufSize)
{
StaffInfo db[] = { {"Socrates",1}, {"dyx1024",2}, {"Kevin",3}, {"Jim",4} };
const char* src = "No match";
for (int i=0; i<4; i++)
if (db[i].id == id) { src = db[i].name; break; }
if (outBuf && bufSize > 0) {
strncpy(outBuf, src, bufSize - 1);
outBuf[bufSize - 1] = '\0'; // 保證以 null 結尾
}
}
int main()
{
char name[32];
GetStaffNameById_v3(2, name, sizeof(name));
printf("%s\n", name); // 完全安全
}
| 優點 | 缺點 |
|---|---|
| 完全線程安全、可重入 | 調用方需預分配緩衝區 |
| 內存所有權清晰 (調用方擁有) | 接口參數較多 |
| 無動態分配失風險 | 需處理截斷 (bufSize 不足) |
| 適用於嵌入式、實時系統、安全關鍵代碼 |
/* 返回堆內存,調用方負責 free */
char* GetStaffNameById_v4(int id)
{
StaffInfo db[] = { {"Socrates",1}, {"dyx1024",2}, {"Kevin",3}, {"Jim",4} };
const char* src = "No match";
for (int i=0; i<4; i++)
if (db[i].id == id) { src = db[i].name; break; }
char* ret = malloc(strlen(src) + 1);
if (!ret) return NULL; // 或 abort()
strcpy(ret, src);
return ret;
}
int main()
{
char* name = GetStaffNameById_v4(2);
if (name) {
printf("%s\n", name);
free(name); // 必須釋放!
name = NULL;
}
}
| 優點 | 缺點 |
|---|---|
| 返回緩衝區大小動態適配 | 極易內存洩漏 (調用方忘記 free) |
| 接口簡潔 (無輸出參數) | 分配失敗處理麻煩 |
| 適用於可變長數據 (字符串、序列化) | 碎片化、實時性差,嵌入式慎用 |
| 場景 | 推薦方案 | 關鍵理由 |
|---|---|---|
| 只讀查找表/錯誤信息/單例 | 方案一 | 零開銷、語義清晰,配合 const char* 返回 |
| 高併發/可重入/嵌入式/安全認證 | 方案三 | 調用方控制存儲、無鎖、無堆、確定性 |
| 可變長結果/字符串拼接/序列化 | 方案四 | 動態大小,但必須文檔化「調用方擁有所有權」 |
| C++ 代碼庫 | std::string/std::optional | RAII 自動管理,拒絕裸指標 |
const char* GetErrorMsg(int code); // 明確告知:不可修改
/* SAL 註釋:調用方必須提供有效緩衝區 */
void GetName(_In_ int id, _Out_writes_(bufSize) char* buf, _In_ size_t bufSize);
/* Clang 線程安全註釋 */
const char* GetConfig() __attribute__((const)); // 純函數
size_t GetStaffNameMaxLen(void); // 返回所需最小緩衝區 (含\0)
void GetStaffNameById(int id, char* buf, size_t bufSize);
返回指標函數的核心矛盾在於內存生命週期與所有權歸屬。牢記:
_malloc/_free 契約,配合靜態分析工具檢查泄漏。掌握這四種模式及其權衡,是編寫健壯 C 接口的必修課。