0%

PHP7中对象的引用传递

  1. 实现链表过程中的疑惑
  2. PHP对象数据类型的引用传递

本文代码较多,如果不对/疑惑之处请提出


链表

PHP实现

这几天在写链表相关的代码,以前都是用C++打的,没有考虑过PHP实现方式。可能是我深受C++的影响吧,一时竟然不知道PHP中如何表示指针。大家可以试着先实现一下链表,再来看下面这段PHP实现的链表代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class ListNode
{
public $val = 0;
public $next = null;
function __construct($val) { $this->val = $val; }
}

$linkList = new ListNode(null);
$p = $linkList; //传递链表类,原地址计数+1
for ($i=1; $i < 5; $i++) {
$t = new ListNode($i);
$p->next = $t;
$p = $p->next; //究竟是怎么把p的值传递给$linkList呢?
//对象不是引用传递吗?此时改变p变量不也就改变了$linkList变量吗?
}
var_dump($linkList);

可以看到其实就是一个单链表的实现过程。先创建了一个头节点,$p是指向 $linkList 的指针,接着使用尾插法插入数据。

当我一步步阅读代码的时候,疑惑产生了。就在这一行 $p = $p->next;

不知道你有没有理解我在说什么,首先一个前提是:PHP的对象是引用传递的。所以我们才能在下面这行代码中改变$linkList的值

1
$p->next = $t;

接下来是修改变量p的值

1
$p = $p->next;

很幸运,linkList变量没有被修改,我们实现了链表的功能

why???

在我的理解中引用传递就是使用不同变量名访问同一个zval容器。并且,使用引用时,是关闭了变量的写时复制,正如下面这段经典的代码:

1
2
3
4
5
6
7
8
9
10
$a = 1;
$b = $a;
$c = &$a;

$a = 2;
/*结果
$a = 2
$b = 1
$c = 2
*/

C++的实现

接着我开始思考指针和引用的区别,再来看看C++中利用指针的实现吧!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};

//头插法
ListNode* head1 = new ListNode;
for(int i = 0; i < 5; i++)
{
ListNode* temp = new ListNode(i);
temp->next = head1->next;
head1->next = temp;
}

//尾插法
ListNode* head2 = new ListNode;
ListNode* p = head2;
for(int i = 0; i < 5; i++)
{
ListNode* temp = new ListNode(i);
p->next = temp;
p = p->next;
}

指针,就是指向数据内存地址,指针变量就是用来保存这些地址的变量。利用指针,可以轻松的访问修改被指向变量的数据。也就是浅拷贝,修改了指针就等于修改了该内存地址上的数据。然而深拷贝,就像PHP中的写时复制,修改数据互不影响。

搞清了指针的概念,我们好像看出点什么来了,尽管在PHP中,某些时候引用可以实现指针的功能,但指针引用并不是一个东西。

接着这里提一句,如果对PHP7对象引用传递有怀疑的,大家可以自行实现一下。这里仅提供一份代码,不再赘述。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php

//数组
$arrA = ['k1' => 'v1','k2'=>'v2','k3'=>'v3'];

$arrB = $arrA;

$arrB['k1'] = 'v100';

var_dump($arrA);

/**
* 证明对象是引用传递
*/
class easyObj
{
public $val;
function __construct($val)
{
$this->val = $val;
}
}

$objA = new easyObj(1);

$objB = $objA;

$objB->val = 2;

var_dump($objA);

调试一探究竟

我们用gdb来调试下

1
2
3
4
5
6
7
8
9
10
11
<?php
class ListNode
{
public $val = 0;
public $next = null;
function __construct($val) { $this->val = $val; }
}

$linkList = new ListNode(0);
$p = $linkList;
$p->val = 1;

此时我们来查看变量$linkList(zval) 中的 zend_object的内存地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 以下是运行到此处的gdb调试结果
# $linkList
(gdb) p args.value.obj
$7 = (zend_object *) 0x101a65150
# $p
(gdb) p args.value.obj
$7 = (zend_object *) 0x101a65150
# 相同的zend_object
(gdb) p *args.value.obj
$4 = {gc = {refcount = 3, u = {v = {type = 8 '\b', flags = 0 '\000', gc_info = 49154},
type_info = 3221356552}}, handle = 1, ce = 0x101a05018, handlers = 0x100b1dd68, properties = 0x0,
properties_table = {{value = {lval = 1, dval = 4.9406564584124654e-324, counted = 0x1, str = 0x1, arr = 0x1,
obj = 0x1, res = 0x1, ref = 0x1, ast = 0x1, zv = 0x1, ptr = 0x1, ce = 0x1, func = 0x1, ww = {w1 = 1,
w2 = 0}}, u1 = {v = {type = 4 '\004', type_flags = 0 '\000', const_flags = 0 '\000',
reserved = 0 '\000'}, type_info = 4}, u2 = {next = 0, cache_slot = 0, lineno = 0, num_args = 0,
fe_pos = 0, fe_iter_idx = 0, access_flags = 0, property_guard = 0}}}}

此时的$p$linkList其中的zend_object地址是相同的,修改了$p->val 就相当于修改了$linkList中的zend_object,并且zend_object的 引用计数为3。这就是对象的引用传递。在zval中的obj指针指向对象的实际地址。

代码继续执行:

1
$p = $p->next; //next 为 null

此时变量$p变为null,那么$linkList会不会也变成null呢?我们来看看$p$linkList的zval结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# $p的zval
(gdb) p *args
$8 = {value = {lval = 4322644304, dval = 4.9406564584124654e-324, counted = 0x1, str = 0x1, arr = 0x1, obj = 0x1,
res = 0x1, ref = 0x1, ast = 0x1, zv = 0x1, ptr = 0x1, ce = 0x1, func = 0x1, ww = {w1 = 1, w2 = 0}}, u1 = {
v = {type = 0 '\004', type_flags = 0 '\000', const_flags = 0 '\000', reserved = 0 '\000'}, type_info = 4},
u2 = {next = 0, cache_slot = 0, lineno = 0, num_args = 0, fe_pos = 0, fe_iter_idx = 0, access_flags = 0,
property_guard = 0}}

# $linkList
(gdb) p *args
$9 = {value = {lval = 4322644304, dval = 2.1356700497977457e-314, counted = 0x101a65150, str = 0x101a65150,
arr = 0x101a65150, obj = 0x101a65150, res = 0x101a65150, ref = 0x101a65150, ast = 0x101a65150,
zv = 0x101a65150, ptr = 0x101a65150, ce = 0x101a65150, func = 0x101a65150, ww = {w1 = 27677008, w2 = 1}},
u1 = {v = {type = 8 '\b', type_flags = 12 '\f', const_flags = 0 '\000', reserved = 0 '\000'},
type_info = 3080}, u2 = {next = 0, cache_slot = 0, lineno = 0, num_args = 0, fe_pos = 0, fe_iter_idx = 0,
access_flags = 0, property_guard = 0}}

可以看到,$p已经变成 type=0NULL类型,而$linkList还是原先的结构,保留着之前对象。这时发生了分离。

至此,我们可以想到,对象的引用传递是不发生在zval上的,当你修改zval的值,那么zval结构当然会产生变化。当你修改zval中的zend_object时,保存该对象的所有zval也发生改变。(把它说成是指针传递会不会更好理解?)

自问自答

  • Q:既然zval和对象是分开的,是否意味着对象一旦创建就无法真正的销毁?

    A:不会的,PHP7中对象本身自带引用计数,不再是PHP5中的双重引用了。当zend_object的引用计数为0时,也就进入了垃圾回收管理。

总结

  • PHP对象是引用传递
  • 引用不等于指针,引用可以看作是变量的别名
  • 这些内容平时开发相对少用到,但使用时需要注意数据的严谨性

参考文章