PHP所有数组本质上都是哈希表,具体为zend_array(PHP 7+中对HashTable的封装),其底层由哈希表(arData)与双向链表(pListHead/pListTail)协同构成,既支持O(1)键查找,又严格保持插入顺序。

PHP数组不是“普通数组”,而是带有序遍历能力的哈希表——zend_array(PHP 7+ 中对旧 HashTable 的重命名封装),它同时维护哈希索引与插入链表,这是理解所有数组行为的关键。
为什么 foreach 遍历顺序 = 插入顺序?
因为 PHP 的哈希表底层有两个并行结构:arData(连续 Bucket 数组)和双向链表指针(pListHead/pListTail)。每个 Bucket 不仅存键值对,还包含 next 和 prev 指针。遍历时走的是链表,不是哈希槽顺序。
-
foreach、current()、next()等函数操作的是链表指针pInternalPointer,跟哈希分布无关 - 删除元素(如
unset($arr['key']))只标记为IS_UNDEF,不破坏链表结构,所以遍历顺序不变 - 只有当扩容或重建哈希表时(比如大量
unset后再array_values()),链表才可能被重排
哈希冲突怎么解决?用的是拉链法还是开放寻址?
PHP 使用「链地址法(拉链法)」,但实现上不是传统单向链表,而是通过 arBuckets 指向每个槽位的首个 Bucket,该 Bucket 再通过 next 指针串起同槽位的其他元素。
-
nTableMask是关键:它等于nTableSize - 1,用于快速计算哈希槽位 —— 实际槽位是h & nTableMask,本质是位运算取模(要求nTableSize必须是 2 的幂) - 字符串键的哈希由内核函数
zend_string_hash_func()计算,结果是ulong类型;数字键直接作为哈希值(但会做符号处理) - 同一个槽位里多个
Bucket形成单向链(从arBuckets[i]开始),查找时需遍历该链,最坏 O(n),但平均仍是 O(1)
数组动态扩容时发生了什么?
扩容不是简单 realloc,而是一次完整的哈希重建:分配新 arData,重新计算每个有效元素的哈希槽位,并按插入顺序重新链接到新链表中。
立即学习“PHP免费学习笔记(深入)”;
- 触发条件:当
nNumOfElements > nTableSize * 0.75(负载因子阈值),或显式调用array_pad/array_merge等导致元素数超限时 - 新
nTableSize总是 2 的幂,最小为 8,每次翻倍(8 → 16 → 32…),nTableMask相应更新 - 已标记
IS_UNDEF的Bucket在重建时被跳过,所以扩容也顺便“清理”了被删元素(但不会立即释放内存) - 注意:
array_push()或$arr[] =可能触发扩容,但array_pop()不会缩容 —— PHP 不自动收缩哈希表
数字键和字符串键在底层存储有区别吗?
有,但只影响哈希计算方式和是否参与 nNextFreeElement 更新,存储结构完全一致。
- 纯数字字符串键(如
'123')会被转为整型键存储,h字段存整数值,key字段为NULL - 非数字字符串键(如
'name')保留原始字符串,h存哈希值,key指向zend_string结构 - 整型键(如
123)直接存为h,无key字段;且会更新nNextFreeElement(除非显式指定键) - 所有键类型最终都映射到同一套
Bucket结构和哈希逻辑,不存在“两种数组类型”的底层分叉
真正容易被忽略的点是:哈希表的「有序性」来自链表,「高效查找」依赖哈希,二者靠 Bucket 中冗余字段(h、key、val、next、prev)硬性耦合。改任何一个字段的语义,都会打破整个数组契约 —— 这也是为什么你不能安全地用 C 扩展直接操作 arData 而不维护链表一致性。



















