经常有人问,如果使用PHP数组来访问,遍历的顺序是不是固定的?应该按照什么顺序来遍历?
比如: $val) { //结果是什么? }
另一个例子:
$val) { //现在结果又是什么? }
为了彻底理解这个问题,我认为我们应该首先了解PHP数组的内部实现结构...
PHP 数组
在PHP中,数组是使用HASH结构()实现的。PHP使用一些机制来实现O(1)时间复杂度的数组添加和删除,并且同时支持线性遍历和随机访问。
上一篇文章也讨论了PHP的HASH算法,在此基础上我们再进行进一步的扩展。
在了解之前我们先来看一下结构体定义,为了让大家更容易理解我添加了注释:
typedef struct _hashtable { uint nTableSize; /* 散列表大小, Hash值的区间 */ uint nTableMask; /* 等于nTableSize -1, 用于快速定位 */ uint nNumOfElements; /* HashTable中实际元素的个数 */ ulong nNextFreeElement; /* 下个空闲可用位置的数字索引 */ Bucket * pInternalPointer; /* 内部位置指针, 会被reset, current这些遍历函数使用 */ Bucket * pListHead; /* 头元素, 用于线性遍历 */ Bucket * pListTail; /* 尾元素, 用于线性遍历 */ Bucket ** arBuckets; /* 实际的存储容器 */ dtor_func_t pDestructor; /* 元素的析构函数(指针) */ zend_bool persistent; unsigned char nApplyCount; /* 循环遍历保护 */ zend_bool bApplyProtection; #if ZEND_DEBUG int inconsistent; #endif } HashTable;
我们可以通过一个例子来理解的含义:
设置该字段是为了防止循环引用导致的无限循环。
查看上面的结构体,我们可以看到关键元素是 ,也就是实际的存储容器,我们来看一下它的结构定义:
typedef struct bucket { ulong h; /* 数字索引/hash值 */ uint nKeyLength; /* 字符索引的长度 */ void *pData; /* 数据 */ void *pDataPtr; /* 数据指针 */ struct bucket *pListNext; /* 下一个元素, 用于线性遍历 */ struct bucket *pListLast; /* 上一个元素, 用于线性遍历 */ struct bucket *pNext; /* 处于同一个拉链中的下一个元素 */ struct bucket *pLast; /* 处于同一拉链中的上一个元素 */ char arKey[1]; /* 节省内存,方便初始化的技巧 */ } Bucket;我们注意到最后一个元素是一个数组trick,可以节省内存,也方便初始化,有兴趣的朋友可以使用数组。
h 是元素的哈希值。对于具有数字索引的元素,h 是直接索引值(使用 =0 表示数字索引)。对于字符串索引,索引值存储在 arKey 中,索引长度存储在 中。
在 中,实际的数据是存放在 pData 指针指向的内存块中的,这个内存块一般由系统分配。但有一个例外,就是当存储的数据是指针时,系统不会请求分配空间来存储这个指针,而是直接将指针保存在 中,然后 pData 会指向这个结构体成员的地址。这样可以提高效率,减少内存碎片。从中可以看出 PHP 设计的精妙之处,如果 中的数据不是指针,则为 NULL(此段来自《Zend 详解》)
结合上面的结构,我们来解释一下下面的整体结构图:
有点粗糙
指向线性列表形式中第一个元素的指针,在上图中它是元素1,指向最后一个元素的指针0。对于每个元素,它是红线绘制的线性结构的下一个元素,以及前一个元素。
指向当前内部指针位置。顺序遍历数组时,该指针指向当前元素。
线性(顺序)遍历的时候,我们从最开始,按照中间的/,移动,实现线性遍历所有元素。
比如对于 ,我们查看它生成的序列可以发现,在之前,会对数组内部的指针进行一次重置,也就是(详细信息参见深入理解PHP原理),然后每次都进行递增,从而实现顺序遍历。
同样的,我们在使用each/next系列函数遍历的时候,也是通过移动数组内部指针来实现顺序遍历的,这里就存在一个问题,比如:
知道了刚才介绍的,问题就很明确了,因为会自动重置,而while语句块不会重置,所以结束之后就指向了数组的末尾,while语句块是无法访问到的。解决办法就是在each之前重置一下数组的内部指针。
在进行随机访问时,通过hash值确定hash数组中头指针的位置,再通过pNext/pLast寻找特征元素。
添加元素时,会将其插入到同一个 Hash 元素链的头部和线性列表的尾部。也就是说,在线性遍历时,元素会按照插入的顺序进行遍历。这种特殊的设计使得在 PHP 中,当使用数字索引时,元素的顺序由添加顺序决定,而不是索引顺序。
也就是说 PHP 中数组的遍历顺序是和元素添加的顺序有关的,现在我们清楚的知道了文章开头那道题的输出是:
huixinchen 2007 2008因此,如果你想按索引大小迭代数值索引数组,则应使用 for 而不是
for($i=0,$l=count($arr); $i<$l; $i++) { //这个时候,不能认为是顺序遍历(线性遍历) }
扫一扫在手机端查看
我们凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设、网站改版、域名注册、主机空间、手机网站建设、网站备案等方面的需求,请立即点击咨询我们或拨打咨询热线: 13761152229,我们会详细为你一一解答你心中的疑难。