Skip to content

[L1] PHP 中引用传递与值传递的区别

一句话结论

值传递复制值、各自独立;引用传递共享同一变量,修改互相可见。

体系讲解

原理:两种传递方式的语义

  • 值传递(pass by value):函数接收的是实参的副本,函数内修改形参不影响外部变量。PHP 中标量(int / float / string / bool)和数组默认都是值传递。
  • 引用传递(pass by reference):函数接收的是实参的别名(用 & 声明),形参和实参指向同一个变量,函数内修改直接反映到外部。

机制:PHP 的 Copy-On-Write(COW)优化

PHP 并不会在值传递时立即复制数据。赋值或传参时,新旧变量先共享同一块内存,仅当其中一方发生写操作时才真正复制——这就是写时复制(COW)。因此值传递在大多数场景下并不比引用传递慢。

引用传递(&)则会破坏 COW:一旦变量被标记为引用,任何赋值都不再触发复制,而是直接修改共享数据。

对象的特殊情况

PHP 5+ 中对象变量存储的是一个对象标识符(object identifier),传参时复制的是这个标识符,而非对象本身。因此函数内修改对象属性,外部可见——但这不是引用传递,而是标识符的值传递。区别在于:函数内对变量重新赋值($obj = new Other())不会影响外部。

结论:对开发的直接影响

  • 绝大多数场景使用默认的值传递即可,COW 保证了性能。
  • 引用传递适用于需要函数"就地修改"调用方变量的场景(如 preg_match()$matches 参数)。
  • 滥用引用传递会让数据流难以追踪,增加调试难度。

考察意图

  • 验证候选人是否理解 PHP 参数传递的默认行为
  • 考察对"对象是引用传递"这一常见误区的认知
  • 判断候选人是否了解 COW 机制,能解释"值传递不一定慢"

追问链

  1. PHP 中对象是引用传递吗?

    简答:不是。对象变量传递的是对象标识符的副本(值传递)。函数内修改对象属性会影响外部(因为指向同一个对象),但对变量本身重新赋值不会影响外部。只有显式加 & 才是真正的引用传递。

  2. 什么是 Copy-On-Write?它对值传递的性能有什么影响?

    简答:COW 是 PHP 的内存优化策略。赋值或传参时不立即复制,而是共享内存,直到某一方写操作时才真正复制。因此值传递一个大数组但不修改它,内存开销几乎为零。

  3. foreach 中使用引用 &$value 后不 unset 会产生什么问题?

    简答:循环结束后 $value 仍然是数组最后一个元素的引用。如果后续代码再次对 $value 赋值,会意外修改数组的最后一个元素。正确做法是循环结束后立即 unset($value) 断开引用。

  4. 函数签名中 function foo(array &$arr)function foo(array $arr) 在实际使用中应如何选择?

    简答:需要就地修改调用方数组时用 &(如往数组中追加元素)。否则用值传递——COW 保证性能,且不会产生副作用。现代 PHP 更推荐用返回值传递结果,而非引用参数。

易错点

  1. 以为"PHP 对象是引用传递":这是最广泛的误区。对象变量存的是标识符,传参复制的是标识符。在函数内 $obj = null 不影响外部,但 $obj->name = 'x' 影响外部。能否区分这两点是关键。

  2. foreach 引用变量未 unset 导致数据污染:这是真实项目中极高频的 bug。循环结束后引用变量仍绑定数组最后一个元素,后续任何对该变量的赋值都会"莫名其妙"改掉数组末尾。

  3. 以为值传递大数组性能差:由于 COW,值传递一个百万元素的数组但不修改它,内存和时间开销极小。只有在函数内写操作时才触发复制。盲目用引用传递反而可能破坏 COW 导致不必要的复制。

代码示例

php
<?php

// 1. 值传递 vs 引用传递
function addOne(int $n): void {
    $n += 1;
}

function addOneRef(int &$n): void {
    $n += 1;
}

$x = 10;
addOne($x);
echo $x; // 10 — 值传递,外部不变

addOneRef($x);
echo $x; // 11 — 引用传递,外部被修改

// 2. 对象传递的真相
class User {
    public function __construct(public string $name) {}
}

function rename(User $user): void {
    $user->name = '李四';   // 修改属性 → 外部可见
}

function replace(User $user): void {
    $user = new User('王五'); // 重新赋值 → 外部不可见
}

$u = new User('张三');
rename($u);
echo $u->name; // 李四

replace($u);
echo $u->name; // 仍然是李四,不是王五

// 3. foreach 引用陷阱
$arr = [1, 2, 3];
foreach ($arr as &$value) {
    $value *= 2;
}
// 此时 $value 仍是 $arr[2] 的引用

$value = 99; // 意外修改了 $arr[2]!
print_r($arr); // [2, 4, 99] — 期望是 [2, 4, 6]

// 正确做法:循环后断开引用
$arr2 = [1, 2, 3];
foreach ($arr2 as &$v) {
    $v *= 2;
}
unset($v); // ← 关键:断开引用

$v = 99;
print_r($arr2); // [2, 4, 6] — 正确

基于 Apache License 2.0 开源