PHP之对象传递问题
本来以为对象作为参数传递或者赋值是引用传递的,但是不是这样的,其实是对象标识符(即对象句柄)传递,这个句柄允许访问实际的对象。但是有引用传递的效果。
示例:
class Test {
public $value = 1;
}
function modify(Test $obj) {
$obj->value = 99;
}
$a = new Test();
modify($a);
echo $a->value; // 输出 99,说明 modify 中的修改影响了原始对象
class Person {
public string $name = 'Alice';
}
$a = new Person();
$b = $a;
$b->name = 'Bob';
echo $a->name; // 输出 Bob ✅
如果想创建一个新的独立对象,必须使用 clone:
$c = clone $a;
$c->name = 'Charlie';
echo $a->name; // 仍然是 Bob ✅
echo $c->name; // 是 Charlie ✅
官方文档说明: https://www.php.net/manual/en/language.oop5.references.php
对象句柄和&引用传递的区别:
对于 $b = $a 来说, $b 和 $a 是两个变量,分别有自己的内存地址,但是指向的内容是一样的, $b 如果再次被赋值为$c,那就和$a 没关系了
对于 $b = &$a 来说, $b就是$a的alias(别名), 它们是同一个变量的两个名字, b 如果再次被赋值为$c, 那么$a 也要变
通过zval的角度分析:
我们现在来深入底层探讨:
当执行
$a = new StdClass(); $b = $a;时,$a 的 zval 到底是什么结构,$b 的 zval 和 $a 的有什么不同?
我们将分两步:
- 先理解 zval 是什么(PHP 内部结构)
- 具体讲
$a = new StdClass(); $b = $a;时发生了什么
🧠 第一步:什么是 zval?
zval 是 PHP 用来存储变量的内部结构。
一个 zval 包括这些核心部分:
| 字段 | 含义 |
|---|---|
value | 存储实际值(int、string、object 句柄等) |
refcount | 引用计数(这个值被几个变量引用) |
is_ref | 是否是显式引用(通过 & 赋值) |
type | 类型信息,如 IS_STRING、IS_OBJECT 等 |
🧪 第二步:执行 $a = new StdClass(); 后发生了什么?
$a = new StdClass();
这一步会创建:
🔹 一个对象结构(heap 上):
对象实例(如地址 0xABC123):
class_name => StdClass
properties => []
🔹 一个 zval 给 $a:
zval_a:
type => IS_OBJECT
value => handle 指向 0xABC123
refcount => 1
is_ref => 0
变量 $a 指向这个 zval_a。
然后 $b = $a; 又做了什么?
这一步不是复制对象,也不是复制 zval,而是:
- 复制 zval 的指针(zval_b 指向和 zval_a 同一块内存)
- zval 的引用计数
refcount++
所以现在:
$a 和 $b 都指向同一个 zval:
zval_shared:
type => IS_OBJECT
value => handle → 0xABC123
refcount => 2
is_ref => 0
注意:$a 和 $b 是两个变量名,但它们指向同一个 zval(zval 本身在内存中只有一份)
✅ 修改对象属性时的行为
$b->x = 123;
你是在操作对象句柄(0xABC123)指向的内存,所以 $a->x 也变了。
❗但再举一个反例,看看不一样的地方:
$a = new StdClass();
$b = $a;
$b = new StdClass(); // 此时 b 不再指向原对象
现在 $b 会指向新的 zval:
$b → zval_b_new
value => handle → 0xDEF456
refcount => 1
$a → 原 zval_a
value => handle → 0xABC123
refcount => 1(原来是 2,现在 -1)
🔁 如果你用了引用 &:
$a = new StdClass();
$b = &$a;
此时会合并 $a 和 $b 的符号表变量名,让它们指向同一个 zval,并把 is_ref 设置为 1:
zval_ref:
type => IS_OBJECT
value => handle → 0xABC123
refcount => 2
is_ref => 1
现在,无论你对 $a 还是 $b 赋新值、改指向,都完全等价了(引用语义)。
🧪 如何验证这些行为?
可以用 PHP 的 debug_zval_dump():
$a = new StdClass();
$b = $a;
debug_zval_dump($a);
输出类似:
object(stdClass)#1 (0) refcount(2)
说明 $a 的 zval 被引用了 2 次($a 和 $b)。
✅ 总结
| 操作 | 行为 |
|---|---|
$a = new StdClass(); | 创建 zval,refcount = 1,is_ref = 0 |
$b = $a; | 指向同一 zval,refcount++ |
$b = &$a; | 真正引用,两个变量名绑定同一个 zval,is_ref = 1 |
| 修改对象属性 | 改的是对象实例本身,两个变量看到的结果都变 |
重新赋新对象给 $b | $b 指向新 zval,$a 不受影响 |
可以发现,refcount 都为2,但是 is_ref 一个是0 一个是1
官方文档说明: https://www.php.net/manual/zh/function.debug-zval-dump.php
关于写时复制:
我们现在来深入讲解:
🧠 PHP 的 Copy-On-Write(COW,写时复制)机制 + zval 管理图解
一、背景:PHP 的变量实现方式
PHP 每个变量都不是直接保存值,而是通过一个叫 zval 的结构来管理值和引用关系。
一个 zval 结构包含:
value: 变量的值(string/int/object/array...)refcount: 有多少个变量共享这个 zval(引用计数)is_ref: 是否为“引用模式”(即通过&建立 alias)
二、赋值行为图解
情况 1:普通赋值(非引用)
$a = 'hello';
$b = $a;
🔍 发生了什么:
+------+ +-----------------------+
| $a | -----> | zval: |
| | | value: "hello" |
| $b | -----> | refcount: 2 |
| | | is_ref: 0 |
+------+ +-----------------------+
$a和$b指向同一个zvalrefcount = 2- 但
is_ref = 0,表示它们不是绑定 alias - 改
$a = 'world'会发生 写时复制(Copy-On-Write)
写时复制触发(COW)
$a = 'hello';
$b = $a;
$a = 'world';
🔍 发生了什么:
Step 1:
$a, $b --> "hello" (refcount = 2, is_ref = 0)
Step 2:
$a = 'world'; // 修改触发 Copy-On-Write:
-> PHP 创建一个新的 zval 给 $a
-> 原 zval 的 refcount--(变 1)
-> 新 zval 的 refcount = 1
结果:
$a ---> "world" (refcount = 1)
$b ---> "hello" (refcount = 1)
👉 这样就节省了内存,直到你真的需要修改才复制。
情况 2:引用赋值(使用 &)
$a = 'hello';
$b = &$a;
🔍 发生了什么:
+------+ +------------------------+
| $a | -----> | zval: |
| $b | -----> | value: "hello" |
| | | refcount: 2 |
| | | is_ref: 1 <--------- 绑定 alias
+------+ +------------------------+
$a和$b完全绑定,是 同一个变量的两个名字- 改任意一方,另一方立即变
- 赋值也影响彼此(不是写时复制)
$b = 'world'; // $a 也变成 'world'
三、对象赋值与 COW 的不同
$obj1 = new stdClass();
$obj2 = $obj1;
在对象赋值中:
- 变量指向的是一个对象句柄(handle)
- PHP 不会复制对象,默认行为就是“引用共享对象”
$obj2->x = 123; // $obj1->x 也变
但重要的是:
$obj2 = new stdClass(); // ❌ $obj1 保持原对象
对象赋值 不会触发 COW,因为对象内部不是简单值,而是指向堆上对象内存。
四、变量表、zval、内存图
PHP 的变量机制可视化如下:
Symbol Table (符号表)
+------+ +-----------+
| $a | --> | zval A |
| $b | --> | zval A |
+------+ +-----------+
当赋值或引用时:
- 变量表指向同一个 zval
- 修改时,根据
refcount和is_ref判断是否需要 copy refcount > 1 && is_ref == 0→ 会触发 COWis_ref == 1→ 强制引用,绝不复制
五、最佳口诀记忆:
| 类型 | refcount | is_ref | 是否复制 |
|---|---|---|---|
| 普通赋值 | ✅ 增加 | ❌ 否 | ✅ 修改时复制 |
引用赋值(&) | ✅ 增加 | ✅ 是 | ❌ 不复制 |
| 对象赋值 | ✅ 共享 | ❌ 否 | ❌ 不复制,操作引用 |
🧪 推荐调试方法
使用 debug_zval_dump($var) 查看:
$a = 'abc';
$b = $a;
debug_zval_dump($a);
/*
string(3) "abc"
refcount(2)
is_ref=0
*/
再用 $b = &$a; 试试看区别。
