PHP Arrays
PHP 数组是一种复杂的数据结构。它们可以表示一个有序的 map,其中的整数和字符串键指向任何 PHP 值(zval
)。
在内部,PHP 数组是作为一种采用型(adoptive)数据结构来实现的,运行时可能会根据存储的数据改变其内部表示和行为。例如,如果脚本在数组中存储的元素有排序和相近的数字索引(如 [0=>1,1=>2,3=>3]
),那么它将表示为普通数组。我们将此类数组命名为打包(packed)数组。打包数组的元素通过偏移量访问,速度与 C 数组接近。一旦一个 PHP 数组获得了带有字符串(或 "坏的" 数字)键(例如 [0=>1,1=>3,3=>3,"ops"=>4]
)的新元素,它就会自动转换成一个真正的哈希表,并解决冲突。
下面的示例解释了在 PHP 中如何对键进行逻辑组织:
-
$a = [1, 2, 3];
// packed array -
$a = [0=>1, 1=>2, 3=>3];
//packed array with a "hole" -
$a = [0=>1, 2=>3, 1=>2];
// hash table (because of ordering) without conflicts -
$a = [0=>1, 1=>2, 256 =>3];
// hash table (because of density) with conflicts -
$a = [0=>1, 1=>2, "x"=>3];
// hash table (because of string keys)
数值总是以有序的普通数组形式存储。它们可以简单地自上而下迭代,也可以反向迭代。实际上,这是一个 Buckets
数组,内嵌 zvals
和一些附加信息。
在打包数组中,值索引与数字键相同。偏移量的计算方法是 key * sizeof(Bucket)
。
HashTables 使用额外的索引数组(Hash)。它将为数字或字符串键值计算的哈希函数值重新映射到值索引。当一些数组键具有相同的哈希函数值时,可能会发生碰撞。这些碰撞可以通过具有相同哈希值的元素的链接列表来解决。
内部 PHP 数组表示
现在,让我们来看看 PHP 内部的数组表示法。IS_ARRAY
类型的 zval
的值域保存了一个指向 zend_array
结构的指针。它 "继承" 自 zend_refcounted
,后者定义了第一个 64 位字的引用计数器格式。
其他字段是 zend_array
或 HashTable
特有的。其中最重要的是 arData
,它是一个指向依赖数据结构的指针。实际上,它们是作为一个内存块分配的两个数据结构。
由 arData
指向的地址上方是 "哈希" 部分(如上所述)。同一地址的下方是 "有序值" 部分。哈希部分是一个由 32 位 Bucket 偏移量组成的向下数组,以散列值为索引。打包数组 可能会漏掉这一部分,在这种情况下,可以直接通过数字索引访问 Bucket。
有序值 部分是一个 Bucket 数组。每个 Bucket 包含嵌入式 zval,字符串键由指向 zend_string 的指针表示(对于数字键,它为 NULL),以及数字键(或字符串键的字符串 hash_value)。Zvlas 中的保留空间用于组织冲突元素的链表。它包含下一个具有相同 hash_value 的元素的索引。

从历史上看,PHP 5 对数组和 HashTable 结构有明确的区分(HashTable 不包含引用计数器)。然而,在 PHP 7 中,这些结构被合并并成为别名。
PHP 数组可以是不可变的。这与内部字符串非常相似。这种数组不需要引用计数,其行为与标量值相同。
PHP 数组 API
使用以下宏从 zval 检索 zend_array (也可使用 _P
后缀表示指向 zval 的指针):
-
Z_ARR(zv) – 返回 zval 的 zend_array 值(类型必须是 IS_ARRAY)。
-
Z_ARRVAL(zv) – Z_ARR(zv) 的历史别名。
使用以下宏和函数来处理 zval 表示的数组:
-
ZVAL_ARR(zv, arr) - 使用给定的 zend_array 初始化 PHP 数组 zval。
-
array_init(zv) - 创建一个新的空数组。
-
array_init_size(zv, count) - 创建一个新的空的 PHP 数组,并为 "count" 元素保留内存。
-
add_next_index_null(zval *arr) - 用下一个索引插入新的 NULL 元素。
-
add_next_index_bool(zval *ar, int b) - 插入新的 IS_BOOL 元素,其值为 "b",并插入下一个索引。
-
add_next_index_long(zval *arr, zend_long val) - 插入新的 IS_LONG 元素,其值为 "val",并插入下一个索引。
-
add_next_index_double(zval *arr, double val) - 插入值为 "val" 的新 IS_DOUBLE 元素和下一个索引。
-
add_next_index_str(zval *arr, zend_string *zstr) - 插入值为 "zstr" 的新 IS_DOUBLE 元素和下一个索引。
-
add_next_index_string(zval *arr, char *cstr) - 从 0 结尾的 C 字符串 "cstr" 创建 PHP 字符串,并插入下一个索引。
-
add_next_index_stringl(zval *arr, char *cstr, size_t len) - 从长度为 "len" 的 C 字符串 "cstr" 创建 PHP 字符串,并插入下一个索引。
-
add_next_index_zval(zval *arr, zval *val) - 将给定的 zval 以下一个索引插入数组。注意,插入值的引用计数器不会改变。你应该自己关注引用计数(例如调用 Z_TRY_ADDREF_P(val))。所有其他 add_next_index_…() 函数都是通过该函数实现的。
-
add_index_…(zval *arr, zend_ulong idx, …) - 以给定的数字 "idx" 插入值的另一系列函数。与上述 add_next_index_…() 系列具有类似后缀和参数的变体。
-
add_assoc_…(zval *arr, char *key, …) - 以给定的字符串 "key" 插入数值的另一系列函数,由零结尾的 C 字符串定义。
-
add_assoc_…_ex(zval *arr, char *key, size_t key_len, …) - 另一系列函数,通过给定的字符串 "key" 插入数值,由 C 字符串及其长度定义。
下面是一些可以直接使用 zend_array 的函数:
-
zend_new_arra(count) - 创建并返回新数组(为 "count" 元素保留内存)。
-
zend_array_destroy(zend_array *arr) - 释放为数组及其所有元素分配的内存。
-
zend_array_count(zend_array *arr) - 返回数组中元素的个数。
-
zend_array_dup(zend_array *arr) - 创建另一个与给定数组相同的数组。
zend_array 和 HashTable 的表示方法相同。每个 zend_array 也是一个 HashTable,但不能反过来。通用哈希表可以保存指向任何数据结构的指针。从技术上讲,这可以用一种特殊的 zval 类型 IS_PTR 来表示。HashTable API 的内容相当广泛,因此我们在此仅作简要介绍:
-
zend_hash_init() – 初始化哈希表。HashTable 本身可以嵌入到另一个结构中,在堆栈上分配或通过 malloc()/emalloc() 分配。该函数的参数之一是析构函数回调,它将针对从 HashTable 中删除的每个元素执行。对于 zend_arrays 来说,这是 zval_ptr_dtor()。
-
zend_hash_clean() – 删除 HashTable 的所有元素。
-
zend_hash_destroy() – 释放 HashTable 及其所有元素分配的内存。
-
zend_hash_copy() – 将给定哈希表的所有元素复制到另一个哈希表中。
-
zend_hash_num_elements() – 返回HashTable 中的元素数量。
-
zend_hash_[str_|index_]find[_ptr|_deref|_ind]() – 查找并返回具有给定字符串或数字键的 HashTable 元素。如果键不存在,则返回 NULL。
-
zend_hash_[str_|index_]exists[_ind]() – 检查哈希表中是否存在具有给定字符串或数字键的元素。
-
zend_hash_[str_|index_](add|update)[_ptr|_ind]() – 使用给定的字符串或数字键添加新元素或更新 HashTable 的现有元素。如果具有相同键的元素已经存在,"zend_hash…add" 函数返回 NULL。"zend_hash…update" 函数插入新元素(如果之前不存在)。
-
zend_hash_[str_|index_]del[_ind]() – 从 HashTable 中删除具有给定字符串或数字键的元素。
-
zend_symtable_[str_]find[_ptr|_deref|_ind]() – 与 zend_hash_find…() 类似,但给定的键始终表示为字符串。它可能包含数字字符串。在这种情况下,它被转换为数字并调用 zend_hash_index_find…() 。
-
zend_symtable_[str_|]exists[_ind]() - 与 zend_hash_exists…() 类似,但给定的键始终表示为字符串。它可能包含数字字符串。在这种情况下,它被转换为数字并调用 zend_hash_index_exists…() 。
-
zend_symtable_[str_](add|update)[_ptr|_ind]() – 与 zend_hash_add/update…() 类似,但给定的键始终表示为字符串。它可能包含数字字符串。在这种情况下,它被转换为数字并调用 zend_hash_index_add/update…() 。
-
zend_symtable_[str_|]del[_ind]() – 与 zend_hash_del…() 类似,但给定的键始终表示为字符串。它可能包含数字字符串。在本例中,它被转换为数字并调用 zend_hash_index_del…()。
还有多种方法可以迭代 HashTable:
-
ZEND_HASH_FOREACH_KEY_VAL(ht, num_key, str_key, zv) – 在哈希表"ht" 的所有元素上启动迭代循环的宏。将为每个元素调用嵌套的 C 代码块。C 变量"num_key"、"str_key" 和 "zv" 将使用数字键、字符串键和指向元素 zval 的指针进行初始化。对于带有数字键的元素,"str_key" 将为 NULL。还有更多类似的宏仅适用于值、键等。还有类似的宏以相反的顺序进行迭代。该宏的用法将在下一个示例中演示。
-
ZEND_HASH_FOREACH_END() – 结束迭代循环的宏。
-
zend_hash_[_reverse]apply() – 为 HashTable 的每个元素调用给定的回调函数。
-
zend_hash_apply_with_argument[s]() – 使用附加参数为给定 HashTable 的每个元素调用给定回调函数。
请参阅 Zend/zend_hash.h 中的更多信息。
在我们的示例扩展中使用 PHP 数组
让我们扩展 test_scale() 函数,使其支持数组。让它返回另一个具有保留键和缩放值的数组。
由于数组的元素可能是另一个数组(递归更深),我们必须将 scale 逻辑分离到一个单独的递归函数 do_scale() 中。IS_LONG、IS_DOUBLE 和 IS_STRING 的逻辑保持不变,只是我们的函数现在向调用者报告 SUCCESS 或 FAILURE,因此我们必须将 RETURN_…() 宏替换为 RETVAL_…() 和 返回 SUCCESS。
static int do_scale(zval *return_value, zval *x, zend_long factor)
{
if (Z_TYPE_P(x) == IS_LONG) {
RETVAL_LONG(Z_LVAL_P(x) * factor);
} else if (Z_TYPE_P(x) == IS_DOUBLE) {
RETVAL_DOUBLE(Z_DVAL_P(x) * factor);
} else if (Z_TYPE_P(x) == IS_STRING) {
zend_string *ret = zend_string_safe_alloc(Z_STRLEN_P(x), factor, 0, 0);
char *p = ZSTR_VAL(ret);
while (factor-- > 0) {
memcpy(p, Z_STRVAL_P(x), Z_STRLEN_P(x));
p += Z_STRLEN_P(x);
}
*p = '\000';
RETVAL_STR(ret);
} else if (Z_TYPE_P(x) == IS_ARRAY) {
zend_array *ret;
zend_ulong idx;
zend_string *key;
zval *val, tmp;
ALLOC_HASHTABLE(ret);
zend_hash_init(ret, zend_array_count(Z_ARR_P(x)), NULL, ZVAL_PTR_DTOR, 0);
ZEND_HASH_FOREACH_KEY_VAL(Z_ARR_P(x), idx, key, val) {
if (do_scale(&tmp, val, factor) != SUCCESS) {
return FAILURE;
}
if (key) {
zend_hash_add(ret, key, &tmp);
} else {
zend_hash_index_add(ret, idx, &tmp);
}
} ZEND_HASH_FOREACH_END();
RETVAL_ARR(ret);
} else {
php_error_docref(NULL, E_WARNING, "unexpected argument type");
return FAILURE;
}
return SUCCESS;
}
PHP_FUNCTION(test_scale)
{
zval *x;
zend_long factor = TEST_G(scale); // default value
ZEND_PARSE_PARAMETERS_START(1, 2)
Z_PARAM_ZVAL(x)
Z_PARAM_OPTIONAL
Z_PARAM_LONG(factor)
ZEND_PARSE_PARAMETERS_END();
do_scale(return_value, x, factor);
}
IS_ARRAY 参数的新代码会创建一个空的结果数组(保留与源数组相同的元素数)。然后遍历源数组元素,并对每个元素调用相同的 do_scale()
函数,将临时结果存储在 tmp zval 中。然后将临时值添加到结果数组中的相同字符串键或数字索引下。
让我们测试一下新功能…
$ php -r 'var_dump(test_scale([2, 2.0, "x" => ["2"]], 3));'
array(3) {
[0]=>
int(6)
[1]=>
float(6)
["x"]=>
array(1) {
[0]=>
string(3) "222"
}
}
运行正常,但实际上,我们的函数有一个错误。它可能会在某些边缘条件下泄漏内存。