php数组既是“数组”又是“字典”因其底层hashtable融合双向链表(保插入顺序)与哈希桶数组(o(1)查找),统一存储zval并支持整数/字符串键,结合写时复制与惰性缩容机制。

PHP 数组底层是用 哈希表(HashTable) 实现的,它同时支持整数下标和字符串键名,本质是一个“有序的、可重复索引的、支持混合键类型的映射结构”。这不是传统意义的 C 数组,也不是纯哈希容器,而是 PHP 自研的高效混合结构。
为什么 PHP 数组既是“数组”又是“字典”?
因为底层 HashTable 内部维护了一个 双向链表 + 哈希桶数组 的组合结构:
- 哈希桶数组(buckets)负责 O(1) 平均查找:根据 key 的 hash 值定位槽位,解决键名查找问题;
- 双向链表(arData)保持插入顺序:每个元素按写入顺序串在链表中,所以
foreach遍历一定按插入顺序; - 所有元素(zval)统一存放在连续内存块中(arData),通过索引快速访问,支撑整数下标操作(如
$arr[0]); - 整数 key 会尝试转为“packed array”优化路径(如全为连续非负整数时,跳过哈希计算,直接按偏移访问)。
数组扩容与 rehash 是怎么发生的?
HashTable 有容量(nTableSize)和实际元素数(nNumOfElements)。当插入导致负载因子(nNumOfElements / nTableSize)超过阈值(通常是 0.75),就会触发扩容:
- 分配新桶数组,大小翻倍(如从 8→16),且一定是 2 的幂(便于位运算取模);
- 对所有已有元素重新计算 hash,再插入新桶中(即 rehash);
- 注意:rehash 不改变元素在双向链表中的顺序,只更新哈希桶指针;
- 删除元素不会缩容,但 PHP 8.0+ 引入了惰性缩容机制(如连续多次删除后可能触发)。
zval 和数组如何协同工作?
PHP 7+ 中,数组内部存储的是 zval 的值拷贝或引用(非指针),而 zval 本身包含类型、值、引用计数(refcount)和是否为垃圾(gc_info)等字段:
立即学习“PHP免费学习笔记(深入)”;
- 赋值
$b = $a时,不立即复制整个 HashTable,而是共享底层数据,仅增加 refcount; - 当某一方发生写操作(如
$b[] = 1),才触发“写时复制(Copy On Write)”,分离出独立副本; - 空数组(
[])初始化时,HashTable 结构已存在,但 arData 为空,桶数组最小为 8; - unset 某个 key 后,该位置在链表中被标记为“空洞(empty slot)”,仍占链表节点,但桶中指针置空,后续插入可能复用。
常见误区提醒
面试中容易答偏的点:
- ❌ “PHP 数组是用红黑树实现的” —— 错,是哈希表 + 双向链表;
- ❌ “键名顺序由哈希值决定” —— 错,遍历顺序由链表决定,和哈希无关;
- ❌ “整数键和字符串键存储在不同区域” —— 错,统一存于同一套 arData + HashTable 中,只是 hash 计算方式不同;
- ✅ 关键结论:PHP 数组 = 有序性(链表) + 快速查找(哈希) + 内存友好(连续 zval 块) + 写时复制(COW)。











