C 語言只有值傳遞。理解「形參是實參的副本」、「指針值傳遞仍是值傳遞」、「傳遞地址才能修改原變量」三層真相,是寫出正確函數接口的前提。本文以二叉樹創建為例,徹底講透。
C 函數參數傳遞 = 將實參的值拷貝給形參。
形參與實參:同值、不同址、不同生命周期。
| 傳遞內容 | 形參類型 | 能否修改實參變量本身 | 能否修改實參指向的數據 | 典型場景 |
|---|---|---|---|---|
| 普通變量值 | int x | ❌ | ❌ | 只讀輸入 |
| 指針值 (地址) | int *p | ❌ (修改 p 不影響實參) | ✅ (通過 *p 修改 *實參) | 輸出參數、大結構體避免拷貝 |
| 指針的地址 (二級指針) | int **pp | ✅ (通過 *pp 修改實參指針) | ✅ | 修改指針本身 (如分配內存、鏈表頭插) |
// ❌ 錯誤:形參 Tree *T 是副本,malloc 給副本,實參不變
void initTree_wrong(Tree *T) {
char ch = getchar();
if (ch == '#') { T = NULL; return; }
T = malloc(sizeof(Tree));
T->t = ch;
initTree_wrong(T->lchild); // 遞歸傳遞的是副本的成員,依然無效
initTree_wrong(T->rchild);
}
// ✅ 正確:形參 Tree **T 接收實參地址,*T 即實參本身
void initTree(Tree **T) {
char ch = getchar();
if (ch == '#') { *T = NULL; return; }
*T = malloc(sizeof(Tree));
(*T)->t = ch;
initTree(&(*T)->lchild); // 遞歸傳遞成員指針的地址
initTree(&(*T)->rchild);
}
// 調用
Tree *root;
initTree(&root); // 傳遞 root 的地址
調用 initTree(&root)
┌──────────────┐
main: │ root 0x7fff… │ ← 實參 (一級指針變量)
└──────┬───────┘
│ &root (取地址)
▼
initTree: ┌──────────────┐
│ T 0x7fff… │ ← 形參 (二級指針,存放 &root)
└──────┬───────┘
│ *T 解引用
▼
┌──────────────┐
│ root 0x7fff… │ ← 同一塊內存!修改 *T 即修改 root
└──────────────┘
// 分配內存並返回指針
int create_buffer(void **out_ptr, size_t size) {
*out_ptr = malloc(size);
return *out_ptr ? 0 : -1;
}
// 修改調用者的指針 (如鏈表頭插)
void list_push(Node **head, int val) {
Node *n = malloc(sizeof(Node));
n->val = val; n->next = *head;
*head = n; // 修改實參 head
}
| 誤區 | 正確理解 |
|---|---|
| 「C 有引用傳遞」 | 無。C++ 才有 int&。C 必須手動傳地址模擬。 |
| 「傳指針就能修改指針」 | 只能修改指針指向的數據;修改指針本身需傳指針的地址。 |
| 「數組傳遞是引用」 | 數組退化為指針,本質仍是值傳遞指針值。 |
| 「結構體大就傳指針」 | 正確,但若需在函數內重新分配/指向新內存,必須傳二級指針。 |
C 語言參數傳遞鐵律:**只有值傳遞**。
要修改實參變量本身 → 傳遞該變量的地址 (指針)。
要修改實參指針指向 → 傳遞指針的地址 (二級指針)。
二叉樹創建、鏈表頭插、內存分配函數、所有「輸出參數」場景,均遵循此規律。刻入肌肉記憶,告別指針困擾。