Skip to content

引用是什么

概述

引用(Reference)是 PHP 中一种特殊的机制,允许不同的变量名访问同一个变量内容。引用不是指针,而是一种"别名"机制——两个或多个变量名指向同一份内存数据。理解引用的工作原理对于编写高效、正确的 PHP 代码至关重要,尤其在处理大型数据结构、构建函数 API 以及优化内存使用时。

PHP 版本要求

本文基于 PHP 8.1+ 编写。引用机制自 PHP 4 引入,其核心行为在 PHP 7/8 中保持一致,但内部实现(zval 结构)有重大变化。

基础概念

什么是指针

在 C/C++ 等语言中,指针(Pointer)是一个变量,其值为另一个变量的内存地址。通过指针可以直接操作内存地址上的数据。指针支持指针运算(如 ptr + 1),可以指向数组的不同位置。

php
<?php
declare(strict_types=1);

// C 语言中的指针概念(伪代码示意)
// int a = 10;
// int *ptr = &a;   // ptr 存储的是 a 的内存地址
// *ptr = 20;       // 通过指针修改 a 的值

PHP 引用与 C 指针的区别

PHP 的引用不同于 C 语言的指针。PHP 引用是一个符号别名(Symbol Alias),它让两个变量名共享同一份数据。引用不支持指针运算,也不能像 C 指针那样直接操作内存地址。

php
<?php
declare(strict_types=1);

// PHP 引用——两个变量名指向同一份数据
$a = 10;
$b = &$a; // $b 成为 $a 的引用(别名)

$b = 20;
echo $a;  // 输出: 20 — 因为 $a 和 $b 是同一个变量

echo $a === $b ? 'true' : 'false'; // 输出: true

注意

PHP 引用不是 C 风格的指针。你不能对引用进行算术运算,也不能获取变量的实际内存地址。PHP 引用本质上是在符号表(symbol table)中让多个变量名关联到同一个 zval。

& 符号的作用

& 是 PHP 中的引用操作符,用于创建引用。它可以用在变量赋值、函数参数传递和函数返回值三种场景中。

php
<?php
declare(strict_types=1);

// 1. 变量赋值时创建引用
$original = 'hello';
$alias = &$original; // $alias 是 $original 的引用

// 2. 函数参数声明引用传递
function increment(int &$value): void
{
    $value++;
}

// 3. 函数返回引用
$counter = 0;
function &getCounter(): int
{
    global $counter;
    return $counter;
}

语法与代码

引用 vs 值赋值

理解引用赋值与普通值赋值的区别是掌握引用的第一步。

php
<?php
declare(strict_types=1);

// --- 值赋值(Copy-on-Write) ---
$x = 'apple';
$y = $x;   // 值赋值:$y 获得 $x 的副本
$y = 'banana';

echo $x;   // 输出: apple(不受 $y 修改影响)
echo $y;   // 输出: banana

// --- 引用赋值 ---
$m = 'apple';
$n = &$m;  // 引用赋值:$n 和 $m 指向同一份数据
$n = 'banana';

echo $m;   // 输出: banana($m 随 $n 的修改而改变)
echo $n;   // 输出: banana

引用的行为验证

通过 debug_zval_dump(注意:此函数在 PHP 8.2+ 已弃用)或简单的赋值测试来验证引用行为。

php
<?php
declare(strict_types=1);

// 验证引用关系
$first = 42;
$second = &$first;

// 修改其中一个,另一个同步变化
$second = 100;
echo "first: {$first}, second: {$second}\n";
// 输出: first: 100, second: 100

// 修改另一个,同样同步变化
$first = 200;
echo "first: {$first}, second: {$second}\n";
// 输出: first: 200, second: 200

对数组元素的引用

可以对数组的单个元素创建引用,这在遍历并修改数组时非常有用。

php
<?php
declare(strict_types=1);

$fruits = ['apple', 'banana', 'cherry'];

// 对数组元素创建引用
$ref = &$fruits[1];
$ref = 'blueberry';

print_r($fruits);
// Array ( [0] => apple [1] => blueberry [2] => cherry )

详细说明

zval 与引用计数

在 PHP 7+ 的内部实现中,每个变量都由一个 zval 结构体表示。zval 包含值本身和一个引用计数器(refcount)。当引用计数降为 0 时,内存被释放。

操作refcount 变化说明
$a = 1;$a 的 refcount = 1新建变量
$b = $a;(值赋值)$a$b 的 refcount 均为 2(共享同一个 zval)Copy-on-Write
$b = &$a;(引用赋值)通过 is_ref 标记区分引用关系引用绑定

Copy-on-Write 机制

PHP 的值赋值采用 Copy-on-Write(写时复制)策略:赋值时并不立即复制数据,而是共享同一份内存。只有当其中一个变量被修改时,才会真正复制数据。这使得值赋值在大多数情况下非常高效。

php
<?php
declare(strict_types=1);

// Copy-on-Write 示例
$data = str_repeat('x', 1000000); // 大字符串
$copy = $data; // 此时并未真正复制,$data 和 $copy 共享内存

// 此时 $data 和 $copy 引用计数均为 2,共享同一块内存
// 当 $copy 被修改时才触发真正的复制
$copy .= ' modified';

// 现在内存中有两份独立的数据
echo strlen($data); // 1000000
echo strlen($copy); // 1000010

性能提示

在 PHP 7+ 中,由于 Copy-on-Write 机制的存在,普通值赋值的性能通常足够好。不需要为了"性能"而滥用引用传递。事实上,不必要的引用反而会降低性能,因为引用会禁用 Copy-on-Write。

引用的内部表示

当一个变量是引用时,其 zval 的 is_ref 标志被设置为 true,并且引用计数器记录的是有多少个变量名指向这个引用。引用本身也有自己的引用计数。

实战示例

引用链

多个变量可以形成一条引用链,它们全部指向同一份数据。

php
<?php
declare(strict_types=1);

// 引用链:三个变量指向同一份数据
$alpha = 'initial';
$beta = &$alpha;
$gamma = &$beta;

// 修改任意一个,所有变量同步变化
$gamma = 'changed';

echo "alpha: {$alpha}\n"; // alpha: changed
echo "beta: {$beta}\n";   // beta: changed
echo "gamma: {$gamma}\n"; // gamma: changed

引用与 foreach 循环

foreach 循环中引用的常见陷阱:循环结束后引用仍然有效。

php
<?php
declare(strict_types=1);

$items = ['a', 'b', 'c'];

// 错误示例:foreach 中的引用泄漏
foreach ($items as &$item) {
    $item = strtoupper($item);
}
// 此时 $item 仍然是对 $items[2] 的引用!

// 下一次使用 $item 可能意外修改 $items[2]
$item = 'oops';
print_r($items);
// Array ( [0] => A [1] => B [2] => oops )

// 正确做法:循环结束后立即 unset 引用
$items = ['a', 'b', 'c'];
foreach ($items as &$item) {
    $item = strtoupper($item);
}
unset($item); // 断开引用

常见陷阱

foreach 循环中使用引用时,循环结束后 $item 仍然持有对数组最后一个元素的引用。这是一个非常常见的 Bug 来源。务必在循环结束后调用 unset($item) 来断开引用。

引用与对象

PHP 中的对象变量本身就持有对象的"标识符"(ID),而非对象的副本。因此,对象变量的赋值天然就是"共享"语义的,不需要使用 &

php
<?php
declare(strict_types=1);

class User
{
    public function __construct(
        public string $name = ''
    ) {}
}

$userA = new User('Alice');
$userB = $userA; // 无需 & — $userB 和 $userA 指向同一个对象

$userB->name = 'Bob';

echo $userA->name; // 输出: Bob — 两个变量指向同一个对象

// 对象的引用赋值(&)是多余的,但不影响结果
$userC = &$userA;
$userC->name = 'Charlie';

echo $userA->name; // 输出: Charlie

注意事项

避免不必要的引用

不推荐

不要为了"优化性能"而到处使用引用。在现代 PHP(7+/8+)中,Copy-on-Write 机制已经让值传递非常高效。滥用引用会导致代码难以理解和维护,甚至可能降低性能。

php
<?php
declare(strict_types=1);

// 不推荐:不必要的引用
function processData(array &$data): void
{
    $data[] = 'processed';
}

// 推荐:值传递,更清晰
function processDataBetter(array $data): array
{
    $data[] = 'processed';
    return $data;
}

引用与 unset

对引用变量调用 unset() 只会断开该变量名与数据的关联,不会删除数据本身,也不会影响其他引用同一数据的变量。

php
<?php
declare(strict_types=1);

$a = 100;
$b = &$a;
unset($a); // 只断开 $a 的引用,$b 不受影响

echo isset($a) ? 'exists' : 'not exists'; // 输出: not exists
echo $b; // 输出: 100 — 数据仍然存在

最佳实践

  1. 默认使用值传递:PHP 的 Copy-on-Write 机制使得值传递在大多数场景下性能优良。
  2. 仅在必要时使用引用:当需要函数修改调用者的变量时,才使用引用传递。
  3. 对象无需引用:对象变量默认就是共享语义的,不需要 &
  4. foreach 中使用引用后 unset:在 foreach 中使用引用遍历后,务必 unset 循环变量。
  5. 明确标记引用参数:在函数签名中用 & 明确标记引用参数,让调用者知道该参数可能被修改。
  6. 优先返回新值:比起引用返回,优先考虑返回新创建的值,语义更清晰。

深入理解 zval 与引用机制

PHP 内部变量表示:zval

PHP 内部使用 zval 结构来存储变量。每个 zval 包含以下关键信息:

字段说明
value实际存储的值(联合体,可以是 long、double、string 等)
type数据类型标识(IS_NULL、IS_LONG、IS_STRING 等)
refcount引用计数(有多少变量指向这个 zval)
is_ref是否为引用类型(1 表示是引用,0 表示普通值)
php
<?php
declare(strict_types=1);

// 分析引用计数的变化
$a = 'hello';    // zval: refcount=1, is_ref=0
$b = $a;         // zval: refcount=2, is_ref=0 (COW)
$b = 'world';    // $b 分离: 两个独立的 zval

Copy-on-Write(写时复制)

PHP 的值赋值默认采用写时复制策略。当多个变量指向同一个 zval 时,只有在其中一个变量被修改时才会进行真正的数据复制。这使得值赋值非常高效——只有修改操作才触发复制。

php
<?php
declare(strict_types=1);

$original = str_repeat('x', 10000);
$copy = $original;
// 此时 $copy 和 $original 共享同一个 zval,refcount=2
// 内存使用量没有翻倍

$copy .= 'y';
// 触发 COW,$copy 获得独立的 zval
// 此时内存使用量翻倍

COW 的陷阱

在循环中使用大数组时,意外的修改会触发 COW,导致内存翻倍。使用引用可以避免这个问题,但也增加了代码复杂度。

引用计数 vs 引用变量

需要区分两个概念:

  • 引用计数(refcount):追踪有多少个变量共享同一个 zval。这是 PHP 内存管理的核心机制。
  • 引用变量(& 操作符创建的):将 is_ref 标记设为 1,表示这个 zval 是一个引用组。

is_ref=1 时,对该 zval 的任何修改都会影响所有指向它的变量。

php
<?php
declare(strict_types=1);

// 引用计数(COW 模式)
$a = [1, 2, 3];
$b = $a;
// $a 和 $b 共享同一个 zval,refcount=2, is_ref=0
// 修改任一变量会触发分离

// 引用变量(引用模式)
$c = [1, 2, 3];
$d = &$c;
// $c 和 $d 共享同一个 zval,refcount=2, is_ref=1
// 修改任一变量,两个变量同时变化

常见误区与 FAQ

引用就是指针吗?

不是。PHP 的引用是符号表别名,类似于 Unix 文件系统的硬链接。C 语言中的指针存储的是内存地址,可以执行指针运算。而 PHP 引用只是让两个变量名指向同一份数据,不能进行地址运算。

为什么不建议滥用引用?

  1. 代码可读性降低:引用使数据流向不明确,难以追踪哪个函数修改了数据。
  2. 调试困难:引用变量的值可能在任何地方被修改,排查 bug 时需要追踪所有引用关系。
  3. 性能不一定更好:PHP 的写时复制(COW)已经很高效,小数据使用引用反而有额外开销。
  4. 副作用:引用可能导致难以预料的副作用,尤其在循环中。

什么时候应该使用引用?

场景是否推荐引用原因
函数需要修改参数明确的输出参数语义
大数组遍历避免复制视情况而定COW 在只读时已经无复制
返回引用链式调用Fluent Interface 模式
foreach 遍历修改元素PHP 官方推荐方式
全局变量应使用依赖注入

参考链接