PHP是一门入门容易,使用范围广泛的语言,以其灵活性以及web后端开发被很多人熟知,也被很多人戏称“PHP是世界上最好的语言”。本人是一名“忠实”的PHPer,相信用过PHP的程序员都会体会到PHP数组的灵活性,相对传统的C语言,使用起来很是方便,拥有关联数组(key值可以是字符串),不需要预定义数组空间大小,关联数组,不需要指定key的快速索引赋值等等便利方法,这段时间研究了一下PHP数组的底层结构,并总结分析,里面含有一些我自己的猜想,如有错误请指出。

1.PHP的数组底层结构

  哈希结构是一种非常重要的数据结构,他是一种通过key映射到value的结构,由于其特性,可以在大部分的情况下让查找和插入的效率达到O(1),在很多语言或者系统里面都有显性得体现出来,具体的实现思路有很多种。详细的介绍可以看我的博客数据结构之哈希结构

  PHP的数组是用链地址法的哈希结构去实现的,链表是双向链表,这样既可以动态分配数组空间,也可以通过key值去计算hash值去访问对应的元素,是一种非常高效的数据结构。
  下面是PHP  Bucket的结构,Bucket是一个基本结点的结构,Bucket是以存放基本元素的容器,可以简单理解为数组元素的房子。
typedef struct Bucket{
ulong h;//哈希值
uint nKeyLength; //key的长度,如果key是整形,则此项不需要赋值
Bucket* pNext; //该桶后面的桶,冲突处理的桶
Bucket* pLast; //该桶前面的桶,冲突处理的桶
Bucket* pListNext; //用以记录数组的顺序,该元素前一个元素。
Bucket* pListLast; //用以记录数组的顺序,该元素后一个元素。
const char * pData; //模拟记录PHP数据,原来是void *pData和 void *pDataPtr
char arKey[] //记录key,之所以是[1]是因为这是柔性成员,具体可以百度C99柔性成员
}Bucket;

  下面是PHP  HashTable结构,HashTable是用以存储Bucket数组和Bucket信息的哈希表结构,采用双向链表的拉链法结构。

typedef struct HashTable{
uint nTableSize; //哈希表的大小
uint nTableMask; //哈希表掩码,用以矫正过长的哈希值
ulong nNumOfElements; //记录当前哈希表存储了多少个元素,用count($arr)其实就是取出hash表的这个数据
ulong NextFreeELement; //记录下一个空闲位置的索引位置,$arr[]=$value里的$value就会放到该空间。
Bucket* pListHead; //记录PHP数组的第一个元素
Bucket* pLstTail; //记录PHP数组的最后一个元素
Bucket* pInternalPointer; //记录当前哈希表指向的Bucket,在foreach,current,next,prev等等会用到,
Bucket** arBuckets; //指向存储实际Hash数组的指针的指针。
}

  可能首次去看数据结构可能会觉得有点难受,密密麻麻的一堆东西,下面我会一个个分析数据字段。

2.Bucket结构体

  1.h(哈希值)

  通过key映射的哈希值(未经过纠正)h,为了让不同key值均匀分配到哈希表的各个位置,必须要有一个好的哈希函数,而PHP选用的是time33算法,也就是下面的算法(简化版)。

ulong hash(const char* key){
ulong hash;
for(int i=;key[i];i++){
hash=hash*+key[i];
}
return hash;
}

  当然啦在PHP的具体实现细节又会有点不同,但是原理是差不多的。

  2.nKeyLength(字符的个数)

  如果使用的是关联索引,那么此处nKeyLength就是字符的个数,比如说$arr['key']='value' ,那么这个值就为3,如果是索引数组,此字段就不会用上。

  3.pNext pLast (记录该桶的前后桶)

  继续引用百度的图,类似于下面的哈希表,拿元素337来说,他的pNext指向353的位置,pLast指向1的位置,只不过下面是单向链表,没有看到当前元素指向前一个元素。

  4.pListNext(记录该桶在数据上的后元素)

  这个字段从命名意思就可以看出,是链表的指向后继元素的指针。比如说作如下赋值。guangdong的pListNext指向beijing,beijing的pListNext指向shanghai.....

$arr[2]="guangdong";
$arr[1]="beijing";
$arr[3]="shanghai";
$arr[4]="zhejiang";

  所以你如果用foreach去遍历数组,会发现一个很有趣的现象。输出的结果如下,居然不是按照数组下标1,2,3,4顺序去输出,其实只要你理解了PHP的存储数组的数据结构你就很明白了。

2 => "guangdong"
1 => "beijing"
3 => "shanghai"
4 => "zhejiang"

  他的数据结构如下图显示,第一个元素是guangdong,然后来个元素beijing,于是guangdong的pListLast指向beijing,后面的元素同理。而foreach遍历会从第一个元素(也就是pListHead指向的Bucket,详看下文HashTable的介绍)去输出,然后再指向下一个元素,因此输出的顺序不是按照下标来的,而是按照赋值顺序来的,这也是为什么foreach遍历数组要比for遍历要快的原因,因为for每次查找元素都要去做一次哈希映射查找对应下标的Bucket,而foreach只需要遍历Bucket链表就好了。pListLast与pListNext同理,只是指向前一个数组元素。

  5.arKey(用以存储key值)

    这是一个c99柔性成员,如果需要深究可以百度查查c的柔性成员,如果这一个关联数组,这个arKey就是存储对应的key值。$arr['abc']='value';那么arKey存储的就是abc。

3.HashTable结构体

  1.nTableSize(哈希表的大小)

  这是哈希表的分配Bucket空间的大小,默认会分配8个Bucket空间,当存储元素个数大于8个就会存储16个,如此下去,存储的个数为2x大小,即8,16,32,64...

  2.nTableMask(纠正掩码)

  用以纠正过长的哈希值,值为nTableSize-1,比如说一个我有一个字符经过哈希函数得出值为9,但是nTableSize为8,那该怎么办呢,存放到第1个位置吧,计算方法就是9   mod 8,但是在计算机里面下标是从0开始,因此我们会使用&运算得出结果,9&7=1。

  3.nNumOfElements(数组元素个数)

  用以统计数组元素的个数,PHP的count()元素其实就是获取这个值。

  4.NextFreeElement(下一个空闲的元素)

  用以存储下一个空闲的元素的值。当你的数组是索引数组,用到$arr[]=value赋值就会用到,如果你上次赋值的元素下标是100,那么NextFreeELement就为101了。无关你的元素个数。

  5.pListHead(链表的头部元素)

  这个Bucket指针从名字就可以看出来,用以指向链表的头部元素,例如你给一个数组第一次附上一个值$arr[]=value1,那么这个指针就是指向value1。

  6.pListTail(链表的尾部元素)

  原理同上,只是指向尾部元素,每次来一个新的数组元素,pListTail就会指向它。

  7.nInternalPointer(用以指向内部指向的元素)

  如果我们用foreach遍历数组,这个指针就会指向当前遍历的元素,用以保存当前指向记录。用到此项的还有current(),next(),prev()函数。

  8.arBuckets(用以存储Bucket在C的内部数组)

  此项为指针的指针,可以用于操作Bucket数组。

  下面列出一副图来说明PHP的数组结构(为版面清晰忽略了两种指向前一个Bucket的指针:pListLast,pLast)。

  最后我大概猜想一下foreach函数的执行过程,首先是将nInternalPointer指向HashTable的第一个Buckets,也就是pListHead,如果不为空则输出该元素,然后nInternaPointer指向该Bucket的下一个元素,也就是pListNext,如此循环下去。

void foreach_print(HashTable *ht){
// 指向数组的头元素
ht->pInternalPointer=ht->pListHead;
// 如果不空则循环遍历下去
while(ht->pInternalPointer){
printf("[%s][%s]\n", ht->pInternalPointer->arKey,ht->pInternalPointer->pData);
// 然后指向下一个元素
ht->pInternalPointer = ht->pInternalPointer->pListNext;
}
}

  最后附上自己的关联数组实现方法,各位有兴趣的可以下载来看看。

  点击下载

剖析PHP底层数组是如何实现的的更多相关文章

  1. WCF技术剖析之二十二: 深入剖析WCF底层异常处理框架实现原理[中篇]

    原文:WCF技术剖析之二十二: 深入剖析WCF底层异常处理框架实现原理[中篇] 在[上篇]中,我们分别站在消息交换和编程的角度介绍了SOAP Fault和FaultException异常.在服务执行过 ...

  2. go语言基础之切片和底层数组关系

    1.切片和底层数组关系 示例: package main //必须有个main包 import "fmt" func main() { a := []int{0, 1, 2, 3, ...

  3. 全方位深度剖析PHP7底层源码(已完结)

    第1章 课程介绍本章主要介绍课程要讲的知识点,以及课程要求等. 第2章 PHP7的新特性本章主要介绍PHP7的新特性,做基准测试,与PHP5对比验证PHP7的性能提升程度,引出对PHP7源码学习的必要 ...

  4. Go语言中底层数组和切片的关系以及数组扩容规则

    Go语言中底层数组和切片的关系以及数组扩容规则 demo package main import ( "fmt" ) func main() { // 声明一个底层数组,长度为10 ...

  5. 『随笔』.Net 底层 数组[] 的 基本设计探秘 512 子数组

    static void Main(string[] args) { Console.ReadKey(); //初始化数组 不会立即开辟内存字节, 只有实际给数组赋值时 才会开辟内存 // //猜测数组 ...

  6. [GO]切片和底层数组的关系

    package main import "fmt" func main() { a := [], , , , , , , , , } s1 := a[:] s1[] = fmt.P ...

  7. C语言深度剖析-----多维数组和多维指针

    多维数组和多维指针 指向指针的指针 指针变量同样也有传址调用和传值调用 case1:估算要5个字节的空间,实际只用前面3个字节,设计释放空的2字节 case2:扩充到10字节 二维数组与二维指针 二维 ...

  8. 彻底剖析RMI底层源码 、手写轻量级RMI框架

    https://blog.csdn.net/Z0157/article/details/82049975 User public class User { private int age; publi ...

  9. 破败之王杀人戒bug原理剖析(从底层存储来解释)

    今儿看到了破败之王的bug,一级团杀了人变成了对面,然后送塔,戒指就变成了很夸张的层数. 视频如下: https://www.bilibili.com/video/BV1yr4y1A7Mo 一开始我也 ...

随机推荐

  1. Error configuring application listener of class org.springframework.web.util

    解决方案: 1.打开工程属性对话框,到Deployment Assembly页面,点击Add 2. 选择Jave Build Path Entries 3. 把程序用于的Library加入进来 4.重 ...

  2. (转)android import library switch语句报错case expressions must be constant expressions

    今天当我从github上下载一个工程,并把它的库文件导入eclipse中,发现switch语句报错case expressions must be constant expressions : 解决方 ...

  3. 用TSQL从sqlserve 发布订阅链中删除一张或几张表

    一个简单的存储过程,用来实现从一个SQLSERVE 发布订阅链中删除一张或几张表. /* 1.停日志读取代理 2.exec usp_从复制订阅中删除表 'dbtestPub','test1' 3.开日 ...

  4. 数据库-mysql语句-查-WEB服务器

    (1)MySQL中的查询 (2)WEB服务器 Order:  订单.排序 Set:设置.集合 Table:表.桌子 1.MySQL中的简单查询 —— 查询结果的排序 示例:查询出所有员工信息,要求按工 ...

  5. idea执行mapreduce报错 Could not locate Hadoop executable: C:\hadoop-3.1.1\bin\winutils.exe

    window执行mapreduce报错 Exception in thread "main" java.lang.RuntimeException: java.io.FileNot ...

  6. 加盟阿里!贾扬清被曝从Facebook离职,任阿里硅谷研究院VP

    3 月 2 日傍晚,知乎上爆出一则 AI 人事变动大消息——Caffe 作者贾扬清将从 Facebook 离职. 短短数小时,就有近 10 万人浏览这个问题.不仅如此,据 AI 前线爆料,贾扬清离开 ...

  7. 2019swpuj2ee作业一:C/S,B/S的应用的区别

    1.硬件环境不同: C/S 一般建立在专用的网络上, 小范围里的网络环境, 局域网之间再通过专门服务器提供连接和数据交换服务.B/S 建立在广域网之上的, 不必是专门的网络硬件环境,例与电话上网,  ...

  8. 20155326刘美岑 《网络对抗》Exp1 PC平台逆向破解

    20155326刘美岑 <网络对抗>逆向及Bof基础实践 1.1 实践目标 本次实践的对象是一个名为pwn1的linux可执行文件. 该程序正常执行流程是:main调用foo函数,foo函 ...

  9. 解决eclipse部署maven时,src/main/resources里面配置文件加载不到webapp下classes路径下的问题

    解决eclipse部署maven时,src/main/resources里面配置文件加载不到webapp下classes路径下的问题. 有时候是src/main/resources下面的,有时候是sr ...

  10. Django Admin 专题

    Django admin Django强大的功能之一就是提供了Admin后台管理界面,简单配置就可以对数据库内容做管理 创建ModelAdmin并注册 from django.contrib impo ...