php数组底层是融合有序链表、开放寻址哈希、引用计数与内存池优化的zend_array结构,支持packed与hash两种模式自动切换,通过ardata线性存储和arhash索引实现o(1)访问及插入顺序遍历。

PHP 7.4.29 中数组的底层实现基于 HashTable(哈希表),但不是传统意义上的纯哈希结构,而是融合了有序链表 + 开放寻址哈希 + 引用计数 + 内存池优化的复合数据结构。它的核心目标是兼顾:
✅ 索引数组([0], [1])的连续访问效率
✅ 关联数组(['name' => 'Tom'])的键值查找性能
✅ 内存紧凑性与写时复制(Copy-on-Write)支持
数组本质是一个 zend_array 结构体
在 PHP 7.4 源码中(Zend/zend_types.h 和 Zend/zend_hash.h),数组类型对应的是 zend_array,其关键字段包括:
-
uint32_t nTableSize:哈希桶(bucket)总数,总是 2 的幂(如 8、16、32…),用于快速位运算取模 -
uint32_t nNumUsed:已使用的 bucket 数量(含空洞) -
uint32_t nNumOfElements:实际存储的有效元素个数(即count($arr)) -
Bucket *arData:指向连续内存块的指针,所有 bucket 按插入顺序线性排列 -
uint32_t *arHash:哈希索引表,长度为nTableSize,每个元素存的是arData中对应键的下标(或INVALID_IDX) -
zend_uchar nFlags:标记是否为 packed array(纯整数索引且从 0 连续)、是否引用计数等
?
Bucket是真正存数据的单元,定义在Zend/zend_hash.h:typedef struct _Bucket { zval val; // 实际值(zval 联合体,支持类型复用) zend_ulong h; // 哈希值(对字符串键计算,整数键直接用) zend_string *key; // 字符串键(若为关联键),否则为 NULL } Bucket;
索引数组 vs 关联数组:同一结构,不同行为
| 类型 | 特征 | 底层表现 |
|---|---|---|
| Packed array(紧凑数组) | 键全为连续非负整数(如 [0,1,2,3]),且无空洞 |
nFlags & HASH_FLAG_PACKED 为真;arHash 不参与查找,arData[i].val 直接按序访问,O(1) 随机读取 |
| Hash array(哈希数组) | 含字符串键,或整数键不连续/含负数(如 [-1], [5], ['name']) |
使用 arHash 查找:h & (nTableSize - 1) 得桶号 → 查 arHash[桶号] → 定位 arData[下标] → 比对 key 或 h
|
✅ PHP 7.4 会自动识别并切换模式:
$a = []; $a[] = 1; $a[] = 2;→ 初始为 packed;一旦$a['name'] = 3;→ 升级为 hash array(触发重哈希)
zval:值的统一容器
每个 Bucket.val 是一个 zval(Zend/zend_types.h),它仅占 16 字节(64 位系统),通过类型标签 + 联合体 + 引用计数实现高效复用:
-
u1.v.type:当前类型(IS_LONG,IS_STRING,IS_ARRAY,IS_REFERENCE等) -
u2.guard/u1.v.gc:用于垃圾回收和引用计数(refcount__gc) -
value:联合体,根据 type 动态解释为long,double,zend_string*,zend_array*,zend_object*等
⚠️ 注意:PHP 7+ 的数组本身是 zval,但
zend_array*是堆分配对象;当$a = $b时,仅复制zval(含指针),refcount加 1,真正修改时才分离(COW)
哈希冲突处理:线性探测(Linear Probing)
PHP 7 不用链地址法,而用开放寻址 + 线性探测:
- 插入键
k时,计算h = zend_string_hash_val(k) % nTableSize - 若
arHash[h] == INVALID_IDX→ 直接填入 - 否则检查
arHash[h+1],arHash[h+2]… 直到找到空位(循环至末尾) - 查找时同样线性探测,直到命中或遇到
INVALID_IDX
✅ 优势:缓存友好(内存连续)、无指针跳转;缺点:负载因子过高时性能下降快(PHP 默认
nTableSize至少为nNumOfElements * 2)
实际影响与开发者须知
-
foreach遍历始终按插入顺序(因arData是顺序存储的),与键无关 -
array_keys()/array_values()不改变原数组结构,只是提取视图 -
unset($arr[$k])不立即收缩内存,只标记该 bucket 为“已删除”(IS_UNDEF),后续插入可能复用 - 大量删除后可显式
array_values($arr)强制重建 packed array 提升性能
不复杂但容易忽略
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











