基本概念

程序结构

PHP 程序 由一个或多个源文件组成,正式称为 脚本

script:
   script-section
   script   script-section

script-section:
   textopt   start-tag   statement-listopt   end-tagopt   textopt

start-tag:
   <?php
   <?=

end-tag:
   ?>

text:
   arbitrary text not containing any of   start-tag   sequences

脚本中的所有部分都视为属于一个连续的部分,但任何插入的文本都视为传递给 echo 语句 的字符串字面量。

脚本可以通过 脚本包含操作符 导入另一个脚本。

语句列表语句部分 中定义。

脚本的顶层简称为 顶层

如果使用 <?= 作为 起始标签,引擎将按如下方式进行处理,就好像 语句列表echo 语句开始一样。

程序启动

程序从以某种未指定方式指定的 脚本 的开头开始执行。此脚本称为 启动脚本

程序执行后,它可以访问某些 环境信息,其中可能包括

  • 通过预定义变量 $argc 获取 命令行参数 的数量。
  • 通过预定义变量 $argv 获取一系列一个或多个命令行参数作为字符串。
  • 一系列 环境变量 名称及其定义。

可用的环境变量的精确集是实现定义的,并且可能会因引擎的类型和构建以及它执行的环境而异。

顶层 是脚本的主要入口点时,它将获得全局变量 范围。当顶层通过 include/require 调用时,它将继承其调用者的变量范围。因此,当孤立地查看一个脚本的顶层时,无法静态地确定它将具有全局变量范围还是某个局部变量范围。它取决于脚本的调用方式,以及它被调用时程序的运行时状态。

实现可以接受多个启动脚本,在这种情况下,它们以实现定义的顺序执行并共享全局环境。

程序终止

程序可以通过以下方式正常终止

  • 执行到达 启动脚本 的末尾。对于多个启动脚本,执行到达最后一个脚本的末尾。
  • 在最后一个启动脚本的顶层执行 return 语句
  • 显式调用内在的 exit

前两种情况的行为等效于对 exit 的相应调用。

程序在各种情况下可能会异常终止,例如检测到未捕获的异常或缺少内存或其他关键资源。如果执行通过致命错误到达启动脚本的末尾,或通过未捕获的异常到达启动脚本的末尾,并且没有由 set_exception_handler 注册的未捕获的异常处理程序,则等效于 exit(255)。如果执行通过未捕获的异常到达启动脚本的末尾,并且由 set_exception_handler 注册了未捕获的异常处理程序,则等效于 exit(0)。是否运行 对象析构函数 是未指定的。在所有其他情况下,行为是未指定的。

__halt_compiler

PHP 脚本文件可以包含在编译脚本时应被引擎忽略的数据。此类文件的示例包括 PHAR 文件。

为了使引擎忽略从特定点开始脚本文件中所有数据,使用 __halt_compiler(); 结构。此结构不区分大小写。

__halt_compiler(); 结构只能出现在脚本的 顶层。引擎将忽略此结构之后的所有文本。

__COMPILER_HALT_OFFSET__ 常量 的值设置为紧随结构中 ; 字符之后的字节偏移量。

示例

// open this file
$fp = fopen(__FILE__, 'r');

// seek file pointer to data
fseek($fp, __COMPILER_HALT_OFFSET__);

// and output it
var_dump(stream_get_contents($fp));

// the end of the script execution
__halt_compiler(); the file data which will be ignored by the Engine

内存模型

一般

本节及其紧随其后的部分描述了 PHP 用于存储变量的抽象内存模型。只要从任何可测试的角度看,它都表现得好像遵循了这个抽象模型,符合的实现可以使用任何想要的方法。抽象模型对性能、内存消耗和机器资源使用没有明确或隐含的限制或声明。

这里介绍的抽象模型定义了三种抽象内存位置

  • 变量槽 (VSlot) 用于表示程序员在源代码中命名的变量,例如局部变量、数组元素、对象的实例属性或类的静态属性。VSlot 基于在源代码中显式使用变量而存在。VSlot 包含指向 VStore 的指针。
  • 值存储位置 (VStore) 用于表示程序值,并由引擎根据需要创建。VStore 可以包含标量值(例如整数或布尔值),或者它可以包含指向 HStore 的句柄。
  • 堆存储位置 (HStore) 用于表示 复合值 的内容,并由引擎根据需要创建。HStore 是一个包含 VSlot 的容器。

每个现有变量都有自己的 VSlot,它在任何时候都指向 VStore。VSlot 可以随着时间的推移而更改为指向不同的 VStore。多个 VSlot 可以同时指向同一个 VStore。当创建一个新的 VSlot 时,也会创建一个新的 VStore,并且 VSlot 最初被设置为指向新的 VStore。

VStore 可以随着时间的推移而更改为包含不同的值。多个 VStore 可以同时包含指向同一个 HStore 的句柄。当创建 VStore 时,它最初包含 NULL 值,除非另有指定。除了包含值之外,VStore 还带有 类型标签,它指示 VStore 值的 类型。VStore 的类型标签可以随着时间的推移而更改。值的标签包括与引擎类型匹配的类型,并且可能包括由实现定义的其他标签,前提是这些标签不向用户公开。

HStore 表示复合值的内容,它可以包含零个或多个 VSlot。在运行时,引擎可能会根据需要添加新的 VSlot,并且可能会删除和销毁现有的 VSlot,以支持添加/删除数组元素(对于数组)以及支持添加/删除实例属性(对于对象)。HStore 支持通过整数或区分大小写的字符串键访问其中包含的 VSlot。VSlot 在 HStore 中的存储和管理的具体方式是未指定的。

HStore 除了 VSlot 之外,还可能包含其他信息。例如,对象的 HStore 还包含有关对象类的信息。实现也可以根据需要向 HStore 添加其他信息。

HStore 的 VSlot(即包含在 HStore 中的 VSlot)指向 VStore,每个 VStore 包含一个标量值或一个指向 HStore 的句柄,依此类推,通过任意级别,允许表示任意复杂的数据结构。例如,单向链表可能由一个名为 $root 的变量组成,它由一个 VSlot 表示,该 VSlot 指向一个包含指向第一个节点的句柄的 VStore。每个节点由一个 HStore 表示,该 HStore 包含一个或多个 VSlot 中该节点的数据,以及一个指向包含指向下一个节点的句柄的 VStore 的 VSlot。同样,二叉树可能由一个名为 $root 的变量组成,它由一个 VSlot 表示,该 VSlot 指向一个包含指向根节点的句柄的 VStore。每个节点由一个 HStore 表示,该 HStore 包含一个或多个 VSlot 中该节点的数据,以及一对指向包含指向左分支节点和右分支节点的句柄的 VStore 的 VSlot。树的叶子将是 VStore 或 HStore,根据需要。

VSlot 不能包含指向 VSlot 的指针或指向 HStore 的句柄。VStore 不能包含指向 VSlot 的指针或指向 VStore 的指针。HStore 不能直接包含指向任何抽象内存位置的指针或句柄;HStore 只能直接包含 VSlot。

以下是一个展示 VSlot、VStore 和 HStore 之间一种可能排列的示例

[VSlot $a *]-->[VStore object *]-->[HStore Point [VSlot $x *] [VSlot $y *]]
                                                           |            |
                                                           V            V
                                                      [VStore int 1]  [VStore int 3]

在这幅图中,左上角的 VSlot 表示变量 $a,它指向一个表示 $a 的当前值的 VStore,它是一个对象。此 VStore 包含一个指向 HStore 的句柄,该 HStore 表示类型为 Point 的对象的內容,具有两个实例属性 $x$y。HStore 包含两个表示实例属性 $x$y 的 VSlot,并且每个 VSlot 都指向一个包含整数值的不同的 VStore。

尽管 资源 不被归类为标量值,但出于内存模型的目的,假定它们的行为类似于标量值,而标量值假定为资源描述符。

实现说明: php.net 的实现可以粗略地映射到抽象内存模型,如下所示:zval 指针 => VSlot,zval => VStore,HashTable => HStore,以及 zend_object/zend_object_handlers => HStore。但是请注意,抽象内存模型并非旨在完全匹配 php.net 实现的模型,出于通用性和简洁性,两种模型之间存在一些表面差异。

对于大多数操作,VSlot 和 VStore 之间的映射保持不变。只有以下程序结构可以更改 VSlot 以指向不同的 VStore,所有这些都是 byRef 敏感 操作,并且所有这些(除了 unset)都使用 & 标点符号

回收和自动内存管理

引擎需要使用某种形式的自动内存管理来管理 VStore 和 HStore 的生命周期。特别是,当创建 VStore 或 HStore 时,将为其分配内存。

稍后,如果 VStore 或 HStore 无法通过任何现有的 VSlot 访问,它们将有资格进行回收以释放它们占用的内存。引擎可以在 VStore 或 HStore 有资格进行回收以及脚本执行结束之间的任何时间进行回收。

在回收表示 对象 的 HStore 之前,引擎应调用对象的 析构函数(如果已定义)。

当对应变量的存储时长结束时,当程序员显式地unset变量时,或者当脚本退出时,引擎必须回收每个VSlot,以先发生者为准。在VSlot包含在HStore中的情况下,当程序员显式地unset变量时,当包含的HStore被回收时,或者当脚本退出时,引擎必须立即回收VSlot,以先发生者为准。

引擎使用的自动内存管理的精确形式未指定,这意味着VStore和HStore回收的时间和顺序未指定。

VStore的refcount定义为指向该VStore的未回收VSlot的数量。由于未指定自动内存管理的精确形式,因此由于VSlot、VStore和HStore在不同时间被回收,因此在给定时间,VStore的refcount在符合规范的实现之间可能会有所不同。尽管使用了refcount一词,但符合规范的实现不需要使用基于引用计数的实现来进行自动内存管理。

在下面的一些图中,存储位置框显示为(dead)。对于VStore或HStore,这表示VStore或HStore不再可以通过任何变量访问,并且可以被回收。对于VSlot,这表示VSlot已被回收,或者在VSlot包含在HStore中的情况下,表示包含的HStore已被回收或可以被回收。

赋值

一般

本节以及紧随其后的章节描述了抽象模型中值赋值按引用赋值的实现。首先描述将非数组类型的值赋值给局部变量,然后是使用局部变量进行按引用赋值,然后是将数组类型的值赋值给局部变量,最后是使用复杂左侧表达式进行值赋值,以及使用左侧或右侧的复杂表达式进行按引用赋值。

值赋值和按引用赋值是PHP语言的核心,本规范中的许多其他操作都是用值赋值和按引用赋值来描述的。

将标量类型的值赋值给局部变量

值赋值是程序员创建局部变量的主要方法。如果出现在值赋值左侧的局部变量不存在,引擎将创建一个新的局部变量,并为存储局部变量的值创建一个VSlot和初始VStore。

考虑以下将标量值的值赋值给局部变量的示例

$a = 123;

$b = false;
[VSlot $a *]-->[VStore int 123]

[VSlot $b *]-->[VStore bool false]

变量$a被创建,并由一个新创建的VSlot表示,该VSlot指向一个新创建的VStore。然后将整数123写入VStore。接下来,$b被创建,由一个VSlot和对应的VStore表示,并将布尔值false写入VStore。

接下来,考虑值赋值$b = $a

[VSlot $a *]-->[VStore int 123]

[VSlot $b *]-->[VStore int 123]

$a的VStore中读取整数123,并写入$b的VStore,覆盖其先前的内容。正如我们所看到的,这两个变量是完全独立的,每个变量都有自己的VStore,其中包含整数123。值赋值会读取一个VStore的内容并覆盖另一个VStore的内容,但VSlot与VStore之间的关系保持不变。改变$b的值不会影响$a,反之亦然。

在值赋值的右侧使用字面量或任意复杂的表达式,其工作方式与变量相同,只是字面量或表达式没有自己的VSlot或VStore。字面量或表达式产生的标量值或句柄被写入左侧的VStore,覆盖其先前的内容。

实现说明: 为简单起见,抽象模型中对值赋值的定义永远不会改变VSlot与VStore之间的映射。但是,符合规范的实现不需要实际为两个变量保留单独的内存分配,只需要表现得好像它们是独立的一样,例如,写入一个VStore不应该改变另一个VStore的内容。

例如,php.net实现的模型,在某些情况下会将两个变量槽设置为指向同一个zval,当执行值赋值时,会产生与这里呈现的抽象模型相同的可观察行为。

为了进一步说明值赋值的语义,请考虑++$b

[VSlot $a *]-->[VStore int 123]

[VSlot $b *]-->[VStore int 124 (123 was overwritten)]

现在考虑$a = 99

[VSlot $a *]-->[VStore int 99 (123 was overwritten)]

[VSlot $b *]-->[VStore int 124]

在这两个示例中,一个变量的值发生变化,而不影响另一个变量的值。虽然上面的示例只演示了对整数和布尔值的值赋值,但相同的机制适用于所有标量类型。

注意,由于字符串值是标量值,因此模型假设整个字符串表示(包括字符串字符及其长度)都包含在VStore中。这意味着模型假设在赋值时会复制整个字符串数据。

$a = 'gg';

$b = $a;
[VSlot $a *]-->[VStore string 'gg']

[VSlot $b *]-->[VStore string 'gg']

$a的字符串值和$b的字符串值彼此不同,并且修改$a的字符串不会影响$b。例如,考虑++$b

[VSlot $a *]-->[VStore string 'gg']

[VSlot $b *]-->[VStore string 'gh']

实现说明: 为了性能原因,符合规范的实现可以使用实际表示,其中字符串字符存储在表示VStore的结构之外,并且不会在赋值时立即被复制。PHP中的应用程序通常被编写为假设对字符串的值赋值是一个相当廉价的操作。因此,实现通常使用延迟复制机制来降低对字符串的值赋值的成本。延迟复制机制通过在值赋值期间不复制字符串来工作,而是允许多个变量无限期地共享字符串的内容,直到要对字符串执行一个修改操作(例如,增量运算符),这时会复制部分或全部字符串的内容。符合规范的实现可以选择延迟复制字符串的值赋值,只要它对任何可测试的观点(排除性能和资源消耗)都没有可观察到的行为影响。

将对象的值赋值给局部变量

为了演示将对象的值赋值给局部变量,请考虑我们有一个Point类,该类支持二维笛卡尔坐标系的情况。Point的实例包含两个实例属性,$x$y,它们分别存储x坐标和y坐标。Point(x, y)形式的构造函数调用与运算符new一起使用,会创建一个位于给定位置的新点,而move(newX, newY)形式的方法调用会将Point移动到新位置。

使用Point类,让我们考虑值赋值$a = new Point(1, 3)

[VSlot $a *]-->[VStore object *]-->[HStore Point [VSlot $x *] [VSlot $y *]]
                                                           |            |
                                                           V            V
                                                      [VStore int 1]  [VStore int 3]

变量$a被赋予自己的VSlot,该VSlot指向一个VStore,该VStore包含一个句柄,该句柄指向由new分配的HStore,并且由Point的构造函数初始化。

现在考虑值赋值$b = $a

[VSlot $a *]-->[VStore object *]-->[HStore Point [VSlot $x *] [VSlot $y *]]
                                     ^                     |            |
                                     |                     V            V
[VSlot $b *]-->[VStore object *]-----+             [VStore int 1] [VStore int 3]

$b的VStore包含一个句柄,该句柄指向与$a的VStore的句柄指向同一个对象。注意,Point对象本身没有被复制,并且注意$a$b的VSlot指向不同的VStore。

让我们使用$b->move(4, 6)修改存储在$b中的Point的值

[VSlot $a *]-->[VStore object *]-->[HStore Point [VSlot $x *] [VSlot $y *]]
                                     ^                     |            |
                                     |                     V            V
[VSlot $b *]-->[VStore object *]-----+            [VStore int 4] [VStore int 6]
                                       (1 was overwritten) (3 was overwritten)

正如我们所看到的,改变$b的Point也会改变$a的Point。

现在,让我们让$a指向一个不同的对象,使用$a = new Point(2, 1)

[VSlot $a *]-->[VStore object *]-->[HStore Point [VSlot $x *] [VSlot $y *]]
                                                           |            |
[VSlot $b *]-->[VStore object *]-----+                     V            V
                                     |             [VStore int 2] [VStore int 1]
                                     V
                                   [HStore Point [VSlot $x *] [VSlot $y *]]
                                                           |            |
                                                           V            V
                                                   [VStore int 4] [VStore int 6]

$a可以采用新Point的句柄之前,它对旧Point的句柄必须被移除,这使得$a$b的句柄指向不同的Point。

我们可以使用$a = NULL$b = NULL移除所有这些句柄

[VSlot $a *]-->[VStore null]    [HStore Point [VSlot $x *] [VSlot $y *] (dead)]
                                                        |            |
[VSlot $b *]-->[VStore null]    [VStore int 2 (dead)]<--+            V
                                                          [VStore int 1 (dead)]

                                [HStore Point [VSlot $x *] [VSlot $y *] (dead)]
                                                        |            |
                                [VStore int 4 (dead)]<--+            V
                                                        [VStore int 6 (dead)]

通过将null赋值给$a,我们移除了对Point(2,1)的唯一句柄,这使得该对象可以被销毁。$b也发生了类似的事情,因为它也是对它自己的Point的唯一句柄。

虽然上面的示例只显示了两个实例属性,但相同的机制适用于所有对象类型的值赋值,即使它们可以具有任意数量的任意类型的实例属性。同样,相同的机制也适用于所有资源类型的值赋值。

使用局部变量对标量类型进行按引用赋值

让我们从上一节中的相同值赋值开始,$a = 123$b = false

[VSlot $a *]-->[VStore int 123]

[VSlot $b *]-->[VStore bool false]

现在考虑按引用赋值$b =& $a,它具有按引用语义

[VSlot $a *]-->[VStore int 123]
                 ^
                 |
[VSlot $b *]-----+     [VStore bool false (dead)]

在这个例子中,按引用赋值会改变$b的VSlot指向与$a的VSlot指向同一个VStore。$b的VSlot以前指向的旧VStore现在不可访问了。

当多个变量的VSlot指向同一个VStore时,这些变量被称为彼此的别名,或者被称为具有别名关系。在上面的示例中,在执行按引用赋值后,变量$a$b将成为彼此的别名。

注意,即使在赋值$b =& $a中,变量$b在左侧,$a在右侧,但在成为别名后,它们在与VStore的关系中是绝对对称和相等的。

当我们使用++$b改变$b的值时,结果是

[VSlot $a *]-->[VStore int 124 (123 was overwritten)]
                 ^
                 |
[VSlot $b *]-----+

$b的值存储在$b的VSlot指向的VStore中,被改变为124。由于该VStore也被$a的VSlot别名化,因此$a的值也是124。实际上,任何VSlot与该VStore别名化的变量的值都将是124。

现在考虑值赋值$a = 99

[VSlot $a *]-->[VStore int 99 (124 was overwritten)]
                 ^
                 |
[VSlot $b *]-----+

可以使用unset对变量$a或变量$b进行显式操作来破坏$a$b之间的别名关系。例如,考虑unset($a)

[VSlot $a (dead)]      [VStore int 99]
                         ^
                         |
[VSlot $b *]-------------+

取消设置$a会导致变量$a被销毁,并将其与VStore的链接移除,留下$b的VSlot作为指向VStore的唯一剩余指针。

其他操作也可以破坏两个或多个变量之间的别名关系。例如,$a = 123$b =& $a,以及$c = 'hi'

[VSlot $a *]-->[VStore int 123]
                 ^
                 |
[VSlot $b *]-----+

[VSlot $c *]-->[VStore string 'hi']

在按引用赋值之后,$a$b现在具有别名关系。接下来,让我们观察对$b =& $c会发生什么

[VSlot $a *]-->[VStore int 123]

[VSlot $b *]-----+
                 |
                 V
[VSlot $c *]-->[VStore string 'hi']

正如我们所看到的,上面的按引用赋值会破坏$a$b之间的别名关系,现在$b$c成为彼此的别名。当按引用赋值将VSlot更改为指向不同的VStore时,它会破坏赋值操作之前左侧变量存在的任何现有别名关系。

也可以使用按引用赋值使三个或更多个VSlot指向同一个VStore。请考虑以下示例

$b =& $a;
$c =& $b;
$a = 123;
[VSlot $a *]-->[VStore int 123]
                 ^   ^
                 |   |
[VSlot $b *]-----+   |
                     |
[VSlot $c *]---------+

与值赋值一样,按引用赋值提供了一种方法,让程序员创建变量。如果出现在按引用赋值左侧或右侧的局部变量不存在,引擎将创建一个新的局部变量,并为存储局部变量的值创建一个VSlot和初始VStore。

注意,字面量、常量和其他不指定可修改左值的表达式不能用在按引用赋值的左侧或右侧。

使用局部变量对非标量类型进行按引用赋值

通过引用赋值非标量类型的工作机制与通过引用赋值标量类型相同。然而,描述一些示例以阐明通过引用赋值的语义是值得的。回想一下 使用 Point 类的示例

$a = new Point(1, 3);

[VSlot $a *]-->[VStore object *]-->[HStore Point [VSlot $x *] [VSlot $y *]]
                                                           |            |
                                                           V            V
                                                  [VStore int 1]  [VStore int 3]

现在考虑按引用赋值$b =& $a,它具有按引用语义

[VSlot $a *]-->[VStore object *]-->[HStore Point [VSlot $x *][VSlot $y *]]
                 ^                                         |           |
                 |                                         V           V
[VSlot $b *]-----+                                  [VStore int 1] [VStore int 3]

$a$b 现在是彼此的别名。请注意,通过引用赋值产生的结果与 $b = $a 不同,在 $b = $a 中,$a$b 将指向指向相同 HStore 的不同 VStore。

让我们使用 $a->move(4, 6) 修改 $a 别名的 Point 的值

[VSlot $a *]-->[VStore object *]-->[HStore Point [VSlot $x *] VSlot $y *]]
                 ^                                         |           |
                 |                                         V           V
[VSlot $b *]-----+                              [VStore int 4] [VStore int 6]
                                        (1 was overwritten) (3 was overwritten)

现在,让我们使用值赋值 $a = new Point(2, 1) 更改 $a 本身

[VSlot $a *]-->[VStore object *]-->[HStore Point [VSlot $x *][VSlot $y *]]
                 ^                                         |           |
                 |                                         V           V
[VSlot $b *]-----+                                [VStore int 2] [VStore int 1]

                               [HStore Point [VSlot $x *]   [VSlot $y *] (dead)]
                                                       |              |
                                                       V              V
                                     [VStore int 4 (dead)] [VStore int 6 (dead)]

正如我们所看到的,$b 继续与 $a 存在别名关系。以下是该赋值涉及的内容:$a$b 的 VStore 指向 Point(4,6) 的句柄被移除,Point(2,1) 被创建,$a$b 的 VStore 被覆盖,包含指向该新 Point 的句柄。由于现在没有 VStore 指向 Point(4,6),因此可以销毁它。

我们可以使用 unset($a, $b) 删除这些别名

[VSlot $a (dead)]       [HStore Point [VSlot $x *] [VSlot $y *] (dead)]
                                                |            |
                                                V            V
[VSlot $b (dead)]             [VStore int 2 (dead)]  [VStore int 1 (dead)]

一旦所有指向 VStore 的别名消失,VStore 就可以被销毁,在这种情况下,不再有指向 HStore 的指针,它也可以被销毁。

数组类型到局部变量的值赋值

数组类型的值赋值的语义不同于其他类型的值赋值。回想一下 示例 中的 Point 类,并考虑以下 值赋值 及其抽象实现

$a = array(10, 'B' => new Point(1, 3));

[VSlot $a *]-->[VStore array *]-->[HStore Array [VSlot 0 *] [VSlot 'B' *]]
                                                         |             |
                                                         V             V
                                               [VStore int 10]   [VStore Obj *]
                                                                             |
                                [HStore Point [VSlot $x *] [VSlot $y *]]<----+
                                                        |            |
                                                        V            V
                                            [VStore int 1]  [VStore int 3]

在上面的示例中,$a 的 VStore 被初始化为包含一个指向数组 HStore 的句柄,该数组包含两个元素,其中一个元素是整数,另一个是指向对象 HStore 的句柄。

现在考虑以下值赋值 $b = $a。符合规范的实现必须以以下方式之一实现数组的值赋值:(1)急切复制,其中实现会在值赋值期间复制 $a 的数组,并将 $b 的 VSlot 更改为指向该副本;或者(2)延迟复制,其中实现使用满足某些要求的延迟复制机制。本节描述急切复制,紧随其后的部分描述 延迟复制

为了描述急切复制的语义,让我们从考虑值赋值 $b = $a 开始

[VSlot $a *]-->[VStore array *]-->[HStore Array [VSlot 0 *] [VSlot 'B' *]]
                                                         |             |
[VSlot $b *]-->[VStore array *]                          V             V
                             |                  [VStore int 10]  [VStore object *]
                             V                                                  |
[HStore Array [VSlot 0 *] [VSlot 'B' *]]                                        |
                       |             |                                          |
             +---------+   +---------+                                          |
             V             V                                                    |
[VStore int 10] [VStore object *]-->[HStore Point [VSlot $x *] [VSlot $y *]]<---+
                                                            |            |
                                                            V            V
                                                 [VStore int 1]  [VStore int 3]

值赋值 $b = $a 复制了 $a 的数组。请注意,$b 的 VSlot 指向与 $a 的 VSlot 不同的 VStore,并且 $b 的 VStore 指向与 $a 的 VStore 不同的 HStore。每个源数组元素使用成员复制赋值 =* 进行复制,其定义如下

   $destination =* $source
  • 如果 $source 的 VStore 的引用计数等于 1,则引擎使用值赋值(destination = $source)复制数组元素。
  • 如果 $source 的 VStore 的引用计数大于 1,则引擎使用实现定义的算法来决定是使用值赋值($destination = $source)还是通过引用赋值($destination =& $source)来复制元素。

请注意,成员复制赋值 =* **不是** PHP 语言中的运算符或语言结构,而是用于本文中描述引擎的数组复制和其他操作的行为。

对于上面的特定示例,成员复制赋值表现出与所有符合规范的实现的值赋值相同的语义,因为所有数组元素的 VStore 的引用计数都等于 1。$a 的数组和 $b 的数组中的第一个元素 VSlot 指向不同的 VStore,每个 VStore 都包含整数 10 的不同副本。$a 的数组和 $b 的数组中的第二个元素 VSlot 指向不同的 VStore,每个 VStore 都包含指向同一个对象 HStore 的句柄。

让我们考虑另一个示例

$x = 123;
$a = array(array(&$x, 'hi'));
$b = $a;

急切复制会产生两种可能的结果,具体取决于实现。以下是第一个可能的结果

[VSlot $a *]---->[VStore array *]---->[HStore Array [VSlot 0 *]]
                                                             |
[VSlot $x *]-------------------------+   [VStore array *]<---+
                                     |                 |
[VSlot $b *]-->[VStore array *]      |                 V
                             |       |  [HStore Array [VSlot 0 *][VSlot 1 *]]
                             V       |                         |          |
         [HStore Array [VSlot 0 *]]  |                         V          |
                                |    +---------------->[VStore int 123]   |
                                V                          ^              V
                     [VStore array *]                      |   [VStore string 'hi']
                                   |        +--------------+
                                   V        |
                     [HStore Array [VSlot 0 *] [VSlot 1 *]]
                                                        |
                                                        V
                                                     [VStore string 'hi']

以下是第二个可能的结果

[VSlot $a *]---->[VStore array *]---->[HStore Array [VSlot 0 *]]
                                                             |
[VSlot $x *]-------------------------+  [VStore array *]<----+
                                     |                |
[VSlot $b *]-->[VStore array *]      |                V
                             |       |  [HStore Array [VSlot 0 *] [VSlot 1 *]]
                             V       |                         |           |
         [HStore Array [VSlot 0 *]]  |                         V           |
                                |    +---------------->[VStore int 123]    |
                                V                                          V
                     [VStore array *]                            [VStore string 'hi']
                                   |
                                   V
                    [HStore Array [VSlot 0 *] [VSlot 1 *]]
                                           |           |
                                           V           V
                                  [VStore int 123]  [VStore string 'hi']

在这两种可能的结果中,使用急切复制的值赋值都会复制 $a 的数组,使用成员复制赋值(在本例中将表现出与所有实现的值赋值相同的语义)复制数组的单个元素,这反过来会复制 $a 的数组内的内部数组,使用成员复制赋值复制内部数组的元素。内部数组的第一个元素 VSlot 指向一个引用计数大于 1 的 VStore,因此使用实现定义的算法来决定是使用值赋值还是通过引用赋值。上面显示的第一个可能结果演示了如果实现选择执行通过引用赋值会发生什么,上面显示的第二个可能结果演示了如果实现选择执行值赋值会发生什么。内部数组的第二个元素 VSlot 指向一个引用计数等于 1 的 VStore,因此对于所有使用急切复制的符合规范的实现,都使用值赋值来复制内部数组的第二个元素。

虽然本节中的示例只使用了一个或两个元素的数组,但该模型同样适用于所有数组,即使它们可以具有任意数量的元素。关于 HStore 如何容纳所有这些元素,在抽象模型中没有规定,也不重要。

延迟数组复制

正如 上一节 中提到的,实现可以选择使用延迟复制机制,而不是在数组的值赋值期间急切地进行复制。只要符合本节中介绍的抽象模型对延迟数组复制机制的描述,实现可以使用任何所需的延迟复制机制。

由于数组的内容可以任意大,因此在值赋值期间急切地复制整个数组的内容可能很昂贵。在实践中,用 PHP 编写的应用程序可能会依赖于数组的值赋值相对便宜(以便提供可接受的性能),因此,实现通常使用延迟数组复制机制来降低数组的值赋值成本。

之前讨论的符合规范的延迟字符串复制机制必须产生与急切字符串复制相同的可观察行为不同,延迟数组复制机制在某些情况下允许表现出与急切数组复制明显不同的行为。因此,为了完整性,本节描述了如何用抽象内存模型来模拟延迟数组复制,以及符合规范的延迟数组复制机制必须如何运行。

符合规范的延迟数组复制机制的工作方式是在值赋值期间不进行数组复制,而是允许目标 VStore 与源 VStore 共享一个数组 HStore,并在必要时延迟复制数组 HStore。抽象模型通过用特殊的“Arr-D”类型标签标记目标 VStore,以及在源 VStore 和目标 VStore 之间共享同一个数组 HStore 来表示延迟数组复制关系。请注意,源 VStore 的类型标签保持不变。在本抽象模型中,“Arr-D”类型标签在所有方面都被认为与 array 类型相同,除非另有说明。

为了说明这一点,让我们看看在假设实现延迟复制数组的情况下,前面的示例将如何在抽象模型中表示

$x = 123;
$a = array(array(&$x, 'hi'));
$b = $a;
[VSlot $a *]--->[VStore array *]--->[HStore Array [VSlot 0 *]]
                                      ^                    |
                                      | [VStore array *]<--+
[VSlot $b *]--->[VStore Arr-D *]------+               |
                                                      V
                                        [HStore Array [VSlot 0 *] [VSlot 1 *]]
                                                               |           |
                                                               V           |
[VSlot $x *]------------------------------------------>[VStore int 123]    |
                                                                           V
                                                               [VStore string 'hi']

正如我们所看到的,$a 的 VStore(源 VStore)和 $b 的 VStore(目标 VStore)都指向同一个数组 HStore。请注意,抽象模型中延迟数组复制的表示方式是不对称的。在上面的示例中,源 VStore 的类型标签在值赋值后保持不变,而目标 VStore 的类型标签被更改为“Arr-D”。

当引擎即将对一个标记为“Arr”的参与延迟数组复制关系的 VStore 或一个标记为“Arr-D”的 VStore 执行数组修改操作时,引擎必须首先采取某些操作,这些操作涉及复制数组(在下一段中描述),然后执行数组修改操作。数组修改操作是可以添加或删除数组元素、覆盖现有数组元素、更改数组内部游标的状态,或导致一个或多个数组元素 VStore 或子元素 VStore 的引用计数从 1 增加到大于 1 的任何操作。这种在对参与延迟数组复制关系的 VStore 执行数组修改操作之前必须采取某些操作的要求通常被称为写时复制要求。

当即将对一个标记为“array”的参与延迟数组复制关系的给定 VStore X 执行数组修改操作时,引擎必须找到所有指向与 VStore X 指向的同一个数组 HStore 的标记为“Arr-D”的 VStore,复制数组(使用 成员复制赋值来复制数组的元素,并将所有这些标记为“Arr-D”的 VStore 更新为指向新创建的副本(请注意,VStore X 保持不变)。当即将对一个标记为“Arr-D”的给定 VStore X 执行数组修改操作时,引擎必须 复制数组,将 VStore X 更新为指向新创建的副本,并将 VStore X 的类型标签更改为“array”。引擎在某些时候必须对 VStore 执行的这些特定操作,以满足写时复制要求,统称为数组分离分离 VStore。据说数组修改操作触发了数组分离。

请注意,对于任何参与延迟数组复制关系的标记为“array”的 VStore,或任何标记为“Arr-D”的 VStore,符合规范的实现可以选择在任何时候出于任何原因分离 VStore,只要写时复制要求得到满足。

继续前面的示例,考虑数组修改操作 $b[1]++。根据实现,这可以产生三种可能的结果之一。以下是一种可能的结果

[VSlot $a *]---->[VStore array *]---->[HStore Array [VSlot 0 *]]
                                                             |
[VSlot $b *]-->[VStore array *]            [VStore Arr *]<---+
                             |                         |
      +----------------------+              +----------+
      V                                     V
  [HStore Array [VSlot 0 *] [VSlot 1 *]]  [HStore Array [VSlot 0 *] [VSlot 1 *]]
                         |           |       ^                   |           |
                         |           V       |                   V           |
                         |   [VStore int 1]  |            [VStore int 123]   |
                         V                   |             ^                 V
                       [VStore Arr-D *]------+             |   [VStore string 'hi']
                                                           |
 [VSlot $x *]----------------------------------------------+

如上所示结果中,$b 的 VStore 被数组分离,现在 $a 的 VStore 和 $b 的 VStore 指向不同的数组 HStore。对 $b 的 VStore 执行数组分离是为了满足写时复制要求。$a 的数组保持不变,$x$a[0][0] 之间仍然存在别名关系。对于这个特定示例,符合要求的实现需要保留 $a 数组的内容,并保留 $x$a[0][0] 之间的别名关系。最后,请注意 $a[0]$b[0] 在上述结果中彼此之间具有延迟复制关系。对于这个特定示例,符合要求的实现不需要对 $b[0] 的 VStore 执行数组分离,上述结果演示了当不执行 $b[0] 的 VStore 的数组分离时会发生什么。但是,如果需要,实现可以选择在任何时间对 $b[0] 的 VStore 执行数组分离。以下显示的另外两种可能的结果演示了如果实现选择对 $b[0] 的 VStore 执行数组分离,可能发生的情况。这是第二个可能的结果。

[VSlot $a *]---->[VStore array *]---->[HStore Array [VSlot 0 *]]
                                                             |
[VSlot $b *]-->[VStore array *]          [VStore array *]<---+
                             |                         |
                             V                         V
  [HStore Array [VSlot 0 *] [VSlot 1 *]]  [HStore Array [VSlot 0 *] [VSlot 1 *]]
                         |           |                           |           |
       +-----------------+           V                           |           |
       |                     [VStore int 1]                 +----+           |
       V                                                    |                V
  [VStore Arr-D *]-->[HStore Array [VSlot 0 *] [VSlot 1 *]] | [VStore string 'hi']
                                            |           |   |
                                    +-------+           |   |
                                    |                   V   |
                                    | [VStore string 'hi']  |
                                    V                       |
 [VSlot $x *]--------------------->[VStore int 123]<--------+

这是第三个可能的结果。

[VSlot $a *]---->[VStore array *-]---->[HStore Array [VSlot 0 *]]
                                                              |
[VSlot $b *]-->[VStore array *]           [VStore array *]<---+
                             |                          |
                             V                          V
 [HStore Array [VSlot 0 *] [VSlot 1 *]]  [HStore Array [VSlot 0 *] [VSlot 1 *]]
                        |           |                           |           |
       +----------------+           V                           |           |
       |                     [VStore int 1]                  +--+           |
       V                                                     |              V
   [VStore Arr-D *]-->[HStore Array [VSlot 0 *] [VSlot 1 *]] | [VStore string 'hi']
                                             |           |   |
                     [VStore int 123]<-------+           |   |
                                                         V   |
                                       [VStore string 'hi']  |
                                                             |
 [VSlot $x *]--------------------->[VStore int 123]<---------+

第二个和第三个可能结果显示了如果实现选择对 $b[0] 的 VStore 执行数组分离,可能发生的情况。在第二个结果中,$b[0][0]$x$a[0][0] 存在别名关系。在第三个结果中,$b[0][0] 不存在别名关系,尽管 $x$a[0][0] 仍然彼此之间存在别名关系。第二个和第三个结果之间的差异反映了当引擎使用成员复制赋值将 $a[0] 的数组的元素复制到 $b[0] 的数组时,不同的可能性。

最后,让我们简要考虑另一个示例。

$x = 0;
$a = array(&$x);
$b = $a;
$x = 2;
unset($x);
$b[1]++;
$b[0]++;
echo $a[0], ' ', $b[0];

对于上述示例,符合要求的实现可以输出“2 1”、“2 3”或“3 3”,具体取决于它如何实现数组的值赋值。

为了可移植性,一般建议用 PHP 编写的程序应该避免执行值为数组的赋值操作,其中该数组有一个或多个元素或子元素存在别名关系。

实现说明: 为了通用性和简洁性,抽象模型以一种更开放和表面上与 php.net 实现模型不同的方式来表示延迟数组复制机制,php.net 实现模型使用了一种对称的延迟复制机制,其中单个 zval 包含指向给定哈希表的唯一指针,延迟数组复制表示为指向同一个单个 zval 的多个槽,该 zval 包含数组。尽管存在这种表面上的差异,但 php.net 的实现产生的行为与抽象模型对延迟数组复制机制的定义兼容。

一般值赋值

到目前为止,以上各节已经描述了将值赋值给局部变量的机制。对非变量的可修改左值(如数组元素或对象属性)的赋值与局部变量赋值类似,只是表示变量的 VSlot 被替换为表示目标左值的 VSlot。如果需要,将创建这样的 VSlot。

例如,假设 Point 的定义如前几节所示,并进一步假设所有实例属性都是公有的,则以下代码

$a = new Point(1, 3);
$b = 123;
$a->x = $b;

将导致

[VSlot $a *]-->[VStore object *]-->[HStore Point [VSlot $x *] [VSlot $y *]]
                                                           |            |
                                                           V            V
                                                  [VStore int 123] [VStore int 3]
[VSlot $b *]-->[VStore int 123]

如果需要,将作为包含 VStore 的一部分创建新的 VSlot,例如

$a = new Point(1, 3);
$b = 123;
$a->z = $b;

将导致

[VSlot $a *]-->[VStore object *]-->[HStore Point [VSlot $x *] [VSlot $y *] [VSlot $z *]]
                                                           |            |            |
                                                           V            V            V
                                                  [VStore int 1] [VStore int 3] [VStore int 123]
[VSlot $b *]-->[VStore int 123]

数组元素也是如此

$a = array('hello', 'world');
$b = 'php';
$a[1] = $b;
$a[2] = 'World!';

将导致

[VSlot $a *]-->[VStore array *]-->[HStore Array [VSlot 0 *]  [VSlot 1 *]  [VSlot 2 *]]
                                                         |            |            |
                                                         V            V            V
                                    [VStore string 'hello'] [VStore string 'php'] [VStore string 'World!']
[VSlot $b *]-->[VStore string 'php']

其中,索引为 2 的第三个 VSlot 是由赋值创建的。

请注意,任何数组元素和实例属性(包括非现有属性的指定)都被视为可修改左值,引擎将自动创建 VSlot 并将其添加到相应的 HStore 中。静态类属性也被视为可修改左值,但不会自动创建新的静态属性。

一般按引用赋值

到目前为止,以上各节已经描述了使用局部变量进行按引用赋值的机制。对非变量的可修改左值(如数组元素或对象属性)进行按引用赋值与局部变量赋值类似,只是表示变量的 VSlot 被替换为表示目标左值的 VSlot。如果需要,将创建这样的 VSlot 并将其添加到相应的 HStore 中。

例如

$a = new Point(1, 3);
$b = 123;
$a->z =& $b;

将导致

[VSlot $a *]-->[VStore object *]-->[HStore Point [VSlot $x *] [VSlot $y *] [VSlot $z *]]
                                                           |            |            |
                                                           V            V            |
                                                  [VStore int 1] [VStore int 3]      |
[VSlot $b *]---------------->[VStore int 123]<---------------------------------------+

参数传递

参数传递定义为 简单赋值按引用赋值,具体取决于参数的声明方式。也就是说,将参数传递给具有相应参数的函数,就像将该参数赋值给该参数一样。涉及缺少参数或未定义变量参数的函数调用情况将在描述 函数调用运算符 的部分中讨论。

返回值

从函数返回值定义为 简单赋值按引用赋值,具体取决于函数的声明方式。也就是说,从函数返回值给其调用者,就像将该值赋值给调用者的返回值的用户一样。涉及缺少返回值的函数返回情况将在描述 函数调用运算符 的部分中讨论。

请注意,为了实现按引用赋值语义,函数返回和返回值的赋值都应该是按引用的。例如

function &counter()
{
  static $c = 0;
  $c++;
  echo $c." ";
  return $c;
}

$cnt1 = counter();
$cnt1++; // this does not influence counter
$cnt2 =& counter();
$cnt2++; // this does influence counter
counter();

这个示例输出 1 2 4,因为第一个赋值没有产生按引用语义,即使函数返回是按引用声明的。如果函数没有声明为按引用返回,则无论其如何赋值,其返回都不会产生按引用语义。

将函数的返回值传递给另一个函数,被视为与将该值赋值给相应函数的参数相同,其中按引用参数被视为按引用赋值。

克隆对象

当分配对象实例时,运算符 new 返回指向该对象的句柄。如 上面 所述,将句柄赋值给对象不会复制对象 HStore 本身。相反,它会创建句柄的副本。HStore 本身的复制是通过 运算符 clone 执行的。

为了演示 clone 运算符的工作原理,请考虑以下情况,其中 Widget 类的实例包含两个实例属性:$p1 的整数值为 10,$p2 是指向某种类型(或多种类型)元素数组或指向其他类型实例的句柄。

[VSlot $a *]-->[VStore object *]-->[HStore Widget [VSlot $p1 *][VSlot $p2 *]]
                                                             |            |
                                                             V            V
                                               [VStore int 10] [VStore object *]
                                                                              |
                                                        [HStore ...]<---------+

让我们考虑 $b = clone $a 的结果

[VSlot $a *]-->[VStore object *]-->[HStore Widget [VSlot $p1 *][VSlot $p2 *]]
                                                             |            |
[VSlot $b *]-->[VStore object *]                             V            V
                             |                  [VStore int 10] [VStore object *]
     +-----------------------+                                                 |
     V                                                                         |
   [HStore Widget [VSlot $p1 *] [VSlot $p2 *]]              +--->[HStore ...]<-+
                             |             |                |
                             V             V                |
                 [VStore int 10] [VStore object *]----------+

clone 运算符将创建另一个与原始对象类相同的类对象 HStore,并使用 成员复制赋值 复制 $a 的对象的实例属性。对于上面显示的示例,使用值赋值将指向新创建的 HStore 的句柄存储到 $b 中。请注意,clone 运算符不会递归地克隆 $a 的实例属性中包含的对象;因此,clone 运算符执行的对象复制通常被称为浅拷贝。如果需要对象的深拷贝,则程序员必须使用 方法 __clone 手动完成,该方法在执行初始浅拷贝后调用。

作用域

同一个名称可以在程序的不同位置代表不同的内容。对于名称代表的每个不同内容,该名称仅在其称为该名称的作用域的一部分中可见。

PHP 中存在多种作用域类型

  • 变量作用域 - 定义了非限定变量(如 $foo)所引用的内容的作用域。在一个变量作用域中定义的变量在另一个变量作用域中不可见。
  • 类作用域 - 定义方法和属性的可见性以及 selfparent 等关键字解析的作用域。类作用域包含 该类的主体 及其所有派生类。
  • 命名空间作用域 - 定义非限定和非完全限定的类和函数名称(例如 foo()new Bar())所引用的内容的作用域。命名空间作用域规则在 命名空间章节 中定义。

对于变量作用域,可以区分以下作用域

  • 全局作用域是脚本的最顶层作用域,包含全局变量,包括预定义的变量和在任何其他作用域之外定义的变量。
  • 函数作用域,表示从声明/首次初始化开始到 函数主体 结束的范围。

启动脚本 具有全局变量作用域。包含 的脚本具有与执行包含运算符的位置的作用域相匹配的变量作用域。

在函数内部声明或首次初始化的变量具有函数作用域;否则,该变量具有与封闭脚本相同的变量作用域。

全局变量 可以使用 global 关键字引入当前作用域。超级全局变量 存在于全局变量作用域中,但是它们也可以在任何作用域中访问;它们不需要显式声明。

每个函数都有其自己的函数作用域。匿名函数 有其自己的作用域,该作用域与定义该匿名函数的任何函数的作用域分开。

参数的变量作用域是声明该参数的函数的主体。

命名标签 的作用域是定义该标签的函数的主体。

在类类型 C 中声明或继承的 类成员 m 的类作用域是 C 的主体。

在接口类型 I 中声明或继承的 接口成员 m 的类作用域是 I 的主体。

特征 被类或接口使用时,特征的成员 将采用该类或接口的成员的类作用域。

存储时长

变量的生存期是指程序执行期间保证为该变量分配存储空间的时间。这种生存期称为变量的存储时长,它有三种类型:自动、静态和分配。

具有自动存储时长的变量在声明时或首次使用时(如果它没有声明)会产生并被初始化。它的生存期由封闭的 作用域 限定。自动变量的生存期在该作用域结束时结束。自动变量适合存储在堆栈上,在那里它们可以帮助支持参数传递和递归。局部变量(包括 函数参数)具有自动存储时长。

具有静态存储时长的变量在首次使用之前会产生并被初始化,并一直存在到程序关闭。以下类型的变量具有静态存储时长:常量函数静态变量全局变量静态属性 以及类和接口的 常量

具有**分配存储期**的变量是通过使用new 运算符或工厂函数根据程序逻辑创建的。通常,一旦不再需要此类存储,引擎会通过其垃圾收集过程和使用析构函数自动回收它们。以下类型的变量具有分配存储期:数组元素实例属性

虽然所有三种存储期都有默认的生存期结束,但它们的生存期可以通过使用unset 语句缩短,该语句会销毁任何给定的变量集。

以下示例演示了三种存储期

class Point { ... }

$av1 = new Point(0, 1);       // auto variable $av1 created and initialized
static $sv1 = ...;          // static variable $sv1 created and initialized

function doit($p1)
{
  $av2 = ...;           // auto variable $av2 created and initialized
  static $sv2 = ...;        // static variable $sv2 created and initialized
  if ($p1)
  {
    $av3 = ...;         // auto variable $av3 created and initialized
    static $sv3 = ...;    // static variable $sv3 created and initialized
    ...
  }
  global $av1;
  $av1 = new Point(2, 3);   // Point(0,1) is eligible for destruction
  ...
}                   // $av2 and $av3 are eligible for destruction

doit(TRUE);

// At end of script, $av1, $sv1, $sv2, and $sv3 are eligible for destruction

注释指示每个变量的生存期开始和结束。对于初始分配的 Point 变量(其句柄存储在 $av1 中),它的生存期在 $av1 指向另一个 Point 时结束。

如果函数 doit 被多次调用,每次调用时都会创建和初始化其自动变量,而其静态变量会保留来自先前调用的值。

考虑以下递归函数

function factorial($i)
{
  if ($i > 1) return $i * factorial($i - 1);
  else if ($i == 1) return $i;
  else return 0;
}

factorial 首次被调用时,局部变量参数 $i 被创建并使用调用中的参数值进行初始化。然后,如果此函数调用自身,则每次调用都会重复相同的过程。具体来说,每次 factorial 调用自身时,都会创建一个新的局部变量参数 $i 并使用调用中的参数值进行初始化。

只要需要,引擎就可以延长任何 VStore 或 HStore 的生存期。从概念上讲,VStore 的生存期在其不再被任何 VSlots 指向时结束。从概念上讲,HStore 的生存期在没有 VStore 拥有其句柄时结束。