PHP 變數不只是「名字綁定值」,其核心機制是 **Zval 容器**、**引用計數**、**Copy-on-Write (寫時複製)**。理解 & 引用賦值與 unset() 銷毀的真實行為,是寫出高效能、無記憶體洩漏 PHP 程式的關鍵。
$b = $a (值傳遞 / 寫時複製)<?php
$a = "hello world";
$b = $a; // 符號表新增 $b → 同一個 Zval,refcount=2
// 此時 $a 和 $b 指向同一記憶體,但 is_ref=0
// 修改 $b 會觸發 Copy-on-Write:複製字串,refcount 分離
$b = "test new value";
// 現在:$a → "hello world" (refcount=1), $b → "test new value" (refcount=1)
echo $a; // hello world (未受影響)
?>
$b = &$a (真正共享同一 Zval)<?php
$a = "hello world";
$b = &$a; // 符號表新增 $b → 同一個 Zval,refcount=2, **is_ref=1**
// 修改 $b 直接改變共享 Zval,不複製
$b = "test new value";
echo $a; // test new value (同步改變!)
?>
<?php
function modify($param) {
$param++; // 觸發複製,原變數不變
}
$k = 8;
modify($k);
echo $k; // 8
?>
function test(&$param)<?php
function test(&$param) {
$param++; // 直接修改原 Zval
}
$k = 8;
test($k);
echo $k; // 9 (原變數被修改!)
?>
function &getRef()<?php
$globalVar = 100;
function &getRef() {
global $globalVar;
return $globalVar; // 回傳引用而非複製
}
$ref = &getRef(); // $ref 成為 $globalVar 的引用
$ref = 200;
echo $globalVar; // 200
?>
<?php
$a = "hello";
$b = $a; // refcount=2
unset($b); // 符號表移除 $b,Zval refcount=1
// $a 仍存在,資料未釋放
?>
<?php
$a = 'd';
$b = &$a; // 共享 Zval,refcount=2, is_ref=1
$b = 8; // 修改共享值
var_dump($a, $b); // int(8), int(8)
unset($b); // 移除 $b 符號表項
var_dump($a, $b); // int(8), NULL
// $a 仍保有值 8!資料未被釋放,因 refcount 從 2 降為 1
?>
當 unset($b) 發現 $b 屬於引用集合 (is_ref=1) 且 refcount > 1 時,PHP **不會銷毀 Zval**,而是:
$b。refcount-- (2→1)。is_ref 保持 1 (仍屬於引用集合,只是剩 1 個變數指向)。$a 繼續擁有完整資料存取權。**設計初衷**:防止「刪除引用變數導致其他同引用變數資料消失」的驚人副作用。
<?php
$long = "big_long_variable_name";
$$long = "PHP"; // $big_long_variable_name = "PHP"
$short = &$big_long_variable_name; // $short 引用 $big_long_variable_name
echo "01: \$short = $short\n"; // PHP
echo "02: \$big_long_variable_name = $big_long_variable_name\n"; // PHP
$big_long_variable_name .= " rocks!";
echo "03: \$short = $short\n"; // PHP rocks! (同步)
echo "04: \$big_long_variable_name = $big_long_variable_name\n"; // PHP rocks!
$short .= "Programming \$short";
echo "07: \$short = $short\n"; // PHP rocks!Programming PHP rocks!
echo "08: \$big_long_variable_name = $big_long_variable_name\n"; // 同步
unset($big_long_variable_name); // 銷毀其中一個引用
echo "13: \$short = $short\n"; // 仍保有完整字串!
echo "14: \$big_long_variable_name = $big_long_variable_name\n"; // NULL (未定義)
?>
refcount == 0 時立即釋放記憶體。gc_enable(), gc_collect_cycles(), gc_disable() 可控制 GC 行為。| 場景 | 建議 | 理由 |
|---|---|---|
| 大型陣列/字串傳遞給函數 | **一般不需要**引用 | Copy-on-Write 已自動優化,唯讀時零複製 |
| 函數需修改外部變數 | 使用引用參數 &$var | 明確語意,避免全域變數 |
| foreach 修改陣列元素 | foreach ($arr as &$v) | 直接修改原陣列,迴圈後 unset($v) 斷開引用 |
| 物件傳遞 | **永遠不需要**引用 | PHP 5+ 物件預設為「物件識別符」傳遞,行為類似引用 |
| 效能極致優化 | 基準測試後決定 | 過早優化是萬惡之源 |
<?php
$arr = [1, 2, 3];
foreach ($arr as &$v) { $v *= 2; }
unset($v); // **必須 unset**,否則 $v 仍引用 $arr[2]!
// 後續使用 $v 會意外修改 $arr[2]
$v = 999;
echo $arr[2]; // 999 (災難!)
?>
深入理解這套機制,你就能自信地回答:「為什麼不用引用也很快?」、「unset 後記憶體什麼時候真正釋放?」、「foreach 後為什麼要 unset 引用?」——這就是 PHP 進階工程師的核心內功。