在 C/C++ 核心編程中,內存管理是繞不開的話題,也是讓無數開發者「欲仙欲死」的痛點。若想真正駕馭 C/C++,必須沉下心來,徹底搞懂動態內存在函數間傳遞的機制。令人驚訝的是,許多經典教材對此輕描淡寫,甚至眾多資深工程師也難寫出無誤的代碼。本文將深入剖析問題根源,並提供三種經典解決方案。
假設我們想編寫一個庫函數,在函數內部分配動態內存,並通過參數將指針「返回」給調用者:
void MyFunc(char *pReturn, size_t size) {
// ...
pReturn = (char *)malloc(sizeof(char) * num);
// ...
}
調用者期望如下使用:
char *pMyReturn = NULL;
MyFunc(pMyReturn, 10);
代碼編譯通過,但運行時崩潰。為什麼?
malloc/new 申請,由程序員手動 free/delete 釋放,靈活但易出錯。NULL:int *p = NULL;p = new int(3); 或 p = &i;if (p != NULL) { ... }free(p);p = NULL;編譯器會為每個參數創建臨時副本。若參數是指針 p,副本 _p 複製的是地址值。函數內修改 _p 指向的內容會影響原對象,但修改 _p 本身(如重新賦值地址)不會影響外部的 p。這正是指針能作「輸出參數」修改數據內容,卻不能修改指針本身指向的根本原因。
在 MyFunc(char *pReturn, size_t size) 中,參數 pReturn 的副本 _pReturn 獲得了 malloc 返回的新地址,但外部的 pMyReturn 仍指向 NULL。更嚴重的是,每次調用都會洩露一塊堆內存,因為新分配的地址未被保存,無法釋放。
利用引用別名特性,直接操作原指針變量,代碼直觀且不易出錯。
void MyFunc(char* &pReturn, size_t size) {
pReturn = (char*)malloc(size);
memset(pReturn, 0x00, size);
if (size >= 13) strcpy(pReturn, "Hello World!");
}
int main() {
char *pMyReturn = NULL;
MyFunc(pMyReturn, 15);
if (pMyReturn != NULL) {
cout << pMyReturn;
free(pMyReturn);
pMyReturn = NULL;
}
return 0;
}
傳遞指針的地址(即 char **),函數內通過解引用修改一級指針的值。
void MyFunc(char **pReturn, size_t size) {
*pReturn = (char *)malloc(size);
}
int main() {
char *pMyReturn = NULL;
MyFunc(&pMyReturn, 100); // 傳遞地址
if (pMyReturn != NULL) {
strcpy(pMyReturn, "hello");
cout << pMyReturn << endl;
free(pMyReturn);
pMyReturn = NULL;
}
return 0;
}
原理:&pMyReturn 是一級指針的地址。副本 _p 指向這個地址,*_p = malloc(...) 實際修改了 pMyReturn 存儲的地址值。
最直觀的方式:函數直接返回分配好的指針。
char *MyFunc(void) {
char *p = new char[20];
memset(p, 0x00, 20);
return p;
}
int main() {
char *str = MyFunc();
if (str != NULL) {
strcpy(str, "Hello, baby");
cout << str << endl;
delete[] str; // 配套 new[] 使用 delete[]
str = NULL;
}
return 0;
}
避坑指南:切勿返回字符串常量或局部數組地址(如 return "Hello"; 或 char buf[10]; return buf;),前者位於只讀區不可修改,後者棧內存函數返回即失效。
指針與內存操作是 C/C++ 的靈魂,亦是分水嶺。作為專業開發者,必須建立清晰的內存模型心智:誰分配、誰釋放、生命周期邊界在哪。掌握上述三種模式,並根據項目規範(C 或 C++、是否允許異常)選擇最適方案,方能寫出健壯、高效的系統級代碼。