PHP数组是PHP语言中最灵活的数据结构之一,它既可以当作普通数组使用,也可以作为哈希映射、队列、栈等结构使用,这一切都源于其底层基于哈希表的实现逻辑。PHP的数组底层并非简单的连续内存存储,而是通过哈希表将键名映射到对应的存储位置,同时维护额外的结构来保证数组的遍历顺序。

PHP数组的底层结构定义
在PHP的源码中,数组的核心结构是HashTable,每个数组实例都对应一个HashTable结构体,其简化定义如下:
typedef struct _hashtable {
uint32_t nTableSize; // 哈希表的大小,总是2的幂次
uint32_t nTableMask; // 掩码,用于计算哈希索引,值为nTableSize-1
uint32_t nNumUsed; // 已使用的Bucket数量(包含已删除的)
uint32_t nNumOfElements; // 实际有效的元素数量
uint32_t nNextFreeElement; // 下一个可用的数字索引
Bucket *arData; // 存储元素的数组,连续内存
// 其他辅助字段省略
} HashTable;
其中Bucket是存储单个数组元素的结构,简化定义如下:
typedef struct _Bucket {
zval val; // 元素的值,zval是PHP的变量容器
zend_ulong h; // 键名的哈希值,数字键直接使用数字本身
zend_string *key; // 字符串键名,数字键时为NULL
// 其他辅助字段省略
} Bucket;
哈希函数的工作原理
哈希表的核心是通过哈希函数将键名转换为对应的存储索引,PHP的哈希函数设计兼顾了性能和分布均匀性:
- 对于数字键,直接使用键名作为哈希值,不需要额外计算
- 对于字符串键,使用DJBX33A哈希算法计算哈希值,该算法计算速度快,哈希分布较为均匀
得到哈希值后,通过nTableMask计算最终索引,公式为idx = h & ht->nTableMask,因为nTableSize是2的幂次,所以nTableMask的二进制是全1,按位与操作可以快速得到0到nTableSize-1之间的索引值。
哈希冲突的解决方式
不同的键名可能计算出相同的哈希索引,这种情况就是哈希冲突。PHP的哈希表采用链地址法解决哈希冲突,具体实现方式是在arData数组之外维护一个索引数组,每个索引位置对应一个冲突链表的头节点。
当发生冲突时,新的元素会被插入到对应索引的冲突链表头部,遍历时沿着链表依次查找即可。这种方式的优势是插入和删除操作的时间复杂度都是O(1),不需要移动大量元素。
冲突链表的查找逻辑示例
假设要查找键名为test的元素,步骤如下:
- 计算
test的哈希值h - 计算索引
idx = h & nTableMask - 从
arData[idx]开始,沿着冲突链表遍历,对比每个Bucket的h和key,找到匹配的元素
数组顺序的维护机制
PHP数组支持按照插入顺序遍历元素,这是普通哈希表不具备的特性。PHP的实现方式是将arData数组同时作为元素存储区和顺序维护区:
- 所有有效元素都按顺序存储在
arData的连续位置中,遍历时直接遍历arData的前nNumUsed个位置即可 - 哈希索引指向的是
arData中的位置,冲突链表也是通过arData中的位置指针串联
当元素被删除时,对应的Bucket会被标记为已删除,不会立即从arData中移除,避免打乱顺序,同时nNumUsed不会减少,nNumOfElements会减少。
哈希表的扩容与重建
当哈希表的元素数量过多时,冲突概率会上升,性能会下降,此时会触发扩容操作:
- 当
nNumOfElements大于nTableSize的负载因子(默认是1)时,会将nTableSize扩大为原来的2倍 - 扩容后会重新计算所有元素的哈希索引,将元素移动到新的
arData数组中,这个过程称为重建索引 - 重建索引后,冲突链表会重新构建,元素的顺序依然保持插入顺序
如果数组中已删除的元素过多,还会触发缩容操作,将nTableSize缩小,减少内存占用。
简单PHP数组操作的底层逻辑示例
以下是一段简单的PHP数组操作代码,对应底层的哈希表操作:
<?php $arr = []; // 插入字符串键元素,底层计算哈希值,找到索引,插入到arData和冲突链表 $arr['name'] = 'php'; // 插入数字键元素,直接使用数字作为哈希值,计算索引插入 $arr[1] = 'test'; // 访问元素,计算哈希值,查找冲突链表,找到对应Bucket返回zval echo $arr['name']; ?>
通过理解PHP数组底层的哈希表实现,我们可以更好地使用数组:比如尽量避免过长的字符串键名减少哈希计算开销,了解数组遍历的顺序特性,合理预估数组大小减少扩容次数等,从而写出性能更优的PHP代码。