0

0

PHP内核探索之变量(四)- 数组操作

php中文网

php中文网

发布时间:2016-06-13 12:12:17

|

929人浏览过

|

来源于php中文网

原创

PHP内核探索之变量(4)- 数组操作

上一节(php内核探索之变量(3)- hash table),我们已经知道,数组在php的底层实际上是hashtable(链接法解决冲突),本文将对最常用的函数系列-数组操作的相关函数做进一步的跟踪。

本文主要内容:

  1. PHP中提供的数组操作函数
  2. 数组操作函数的实现
  3. 结语参考文献

一、PHP中提供的数组操作函数

可以说,数组是PHP中使用最广泛的数据结构之一,正因如此,PHP为开发者提供了丰富的数组操作函数(参见http://cn2.php.net/manual/en/ref.array.php ), 大约有80个,这对于绝大多数的数组操作而言,已经足够了。如果按照数组操作的类别来分,这些函数大致可以分为如下几类(不完全分类):

  1. 数组遍历相关函数:如prev, next, current, end,reset, each等
  2. 数组排序相关:如sort, rsort, asort, arsort, ksort, krsort, uasort, uksort
  3. 数组查找相关: 如in_array, array_search, array_key_exists等
  4. 数组分割、合并相关: array_slice, array_splice, implode, array_chunk, array_combine等
  5. 数组交并差:如array_merge, array_diff, array_diff_*, array_intersect, array_intersect_*
  6. 作为stack/queue容器的数组: 如array_push, array_pop, array_shift
  7. 其他的数组操作:array_fill, array_flip, array_sum, array_reverse等

PHP中,数组相关的操作有如下特点:

  1. 数组操作函数是通过扩展的形式(ext/standard/array.c)提供的,因此也会经历扩展的MINIT, RINIT, RSHUTDOWN, MSHUTDOWN等过程。
  2. 在底层,定义PHP函数的方式是PHP_FUNCTION(function_name),例如数组操作函数array_merge在底层是PHP_FUNCTION(array_merge)
  3. 由于数组的底层实现是HashTable,因而数组的绝大多数操作实际上都是针对HashTable的操作,这是通过HashTable API实现的。

接下来,我们以几个具体的函数为例,深入探索PHP中数组函数的实现。

立即学习PHP免费学习笔记(深入)”;

二、数组操作的实现

由于数组的操作实际上是对HashTable的相关操作,因而,我们再次贴出HashTable的结构和结构图,以便参考。

HashTable的结构:

typedef struct _hashtable {    uint nTableSize;    uint nTableMask;    uint nNumOfElements;    ulong nNextFreeElement;    Bucket *pInternalPointer;   /* Used for element traversal */    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;

对应的结构图:

 

接下来,我们以几个数组操作函数为例,来查看具体的操作实现。

1.  数组定义和初始化

在高级语言中,一条简单的语句往往需要在底层中经过很多的操作步骤才能实现,对于数组的操作亦是如此,例如:$arr = array(1, 2, 3);这样的赋值语句,实际上会经历数组初始化(array_init)、添加数组元素(ADD_ARRAY_ELEMENT)、赋值这些步骤才会实现。
(1)数组的初始化
这是通过array_init来实现的,实际上是调用_array_init来完成数组的初始化:
ZEND_API int _array_init(zval *arg, uint size ZEND_FILE_LINE_DC){    ALLOC_HASHTABLE_REL(Z_ARRVAL_P(arg));       _zend_hash_init(Z_ARRVAL_P(arg), size, NULL, ZVAL_PTR_DTOR, 0 ZEND_FILE_LINE_RELAY_CC);    Z_TYPE_P(arg) = IS_ARRAY;    return SUCCESS;}

其中zval *arg即为我们要初始化的数组,第一句ALLOC_HASHTABLE_REL(Z_ARRVAL_P(arg));宏展开后,实际上是:

(*arg).value.ht = (HashTable *) emalloc_rel(sizeof(HashTable));

之后则通过_zend_hash_init函数实现初始化HashTable,并把arg的zval类型设置为IS_ARRAY:

Z_TYPE_P(arg) = IS_ARRAY;

(2)  zend_hash_init 上一节已经介绍过,这里不再赘述

2.  数组遍历 prev, next和current

在PHP中,我们可以使用prev, next,current等完成对数组的访问,例如:

$traverse = array('one', 'after', 'another');$cur = current($traverse);echo "cur:", $cur.PHP_EOL;$next = next($traverse);echo "next: ", $next.PHP_EOL;$nextnext = next($traverse);echo "nextnext: ", $nextnext.PHP_EOL;$prev = prev($traverse);echo "prev: ", $prev.PHP_EOL;

我们知道,HashTable结构体中,有一个成员pInternalPointer, 这个成员便是控制数组的访问指针的。以prev函数为例,对HashTable的遍历实现如下:

(1)将访问指针移动一步

这是通过zend_hash_move_backwards(array);来实现的,具体来说,先找到数组的当前位置或指针:

HashPosition *current = pos ? pos : &ht->pInternalPointer

然后访问这个指针的pListLast找到上一个元素:

*current = (*current)->pListLast;

移动指针的过程如下(可以看出,在不传递pos参数时,实际上移动的是ht-> pInternalPointer这个指针):

ZEND_API int zend_hash_move_backwards_ex(HashTable *ht, HashPosition *pos){         HashPosition *current = pos ? pos : &ht->pInternalPointer;    IS_CONSISTENT(ht);      if (*current) {        *current = (*current)->pListLast;        return SUCCESS;    } else        return FAILURE;}

(2)如果需要返回值,由于访问指针已经移动到了适当的位置,则直接获取当前指针指向的元素

if (return_value_used) {  if (zend_hash_get_current_data(array, (void **) &entry) == FAILURE) {	RETURN_FALSE;  }  RETURN_ZVAL(*entry, 1, 0);}

获取当前指针指向的元素是通过zend_hash_get_current_data来实现的:

#define zend_hash_get_current_data(ht, pData) \    zend_hash_get_current_data_ex(ht, pData, NULL)ZEND_API int zend_hash_get_current_data_ex(HashTable *ht, void **pData, HashPosition *pos){         Bucket *p;	    /* 获取当前指针 */     p = pos ? (*pos) : ht->pInternalPointer;    IS_CONSISTENT(ht);    if (p) {        *pData = p->pData;        return SUCCESS;    } else {        return FAILURE;    }}

知道了prev函数的原理,我们不难想象next, current, reset等函数的实现机制。

prev函数的源码:

PHP_FUNCTION(prev){    HashTable *array;    zval **entry;    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "H", &array) == FAILURE) {        return;    }    zend_hash_move_backwards(array);    if (return_value_used) {        if (zend_hash_get_current_data(array, (void **) &entry) == FAILURE) {            RETURN_FALSE;        }        RETURN_ZVAL(*entry, 1, 0);    }}

3.  数组排序 asort,arsort,ksort等

php中提供了大量的函数用于数组的排序,如用于普通排序的sort函数,用于逆序排序的rsort函数,用于按照键名排序的函数ksortkrsort, 用于自定义比较函数的usortuksort等,可以说非常丰富。我们以sort函数的实现为例,探索PHP中排序算法的实现。

sort函数的签名为:

bool sort ( array &$array [, int $sort_flags = SORT_REGULAR ] )

其中sort_flags会影响排序的结果,该值可以是:SORT_REGULARSORT_NUMERICSORT_STRINGSORT_LOCALE_STRINGSORT_NATURAL

( http://cn2.php.net/manual/zh/function.sort.php )

sort函数的实现过程如下:

(1)由于sort_flags会影响比较函数的行为,因此首先需要根据sort_type确定用于元素比较的函数(自然排序,整数排序,还是字符串排序,区分大小写还是不区分)。这是通过php_set_compare_func来实现的:

static void php_set_compare_func(int sort_type TSRMLS_DC){          switch (sort_type & ~PHP_SORT_FLAG_CASE) {        case PHP_SORT_NUMERIC:            ARRAYG(compare_func) = numeric_compare_function;            break;        case PHP_SORT_STRING:            ARRAYG(compare_func) = sort_type & PHP_SORT_FLAG_CASE ? 
string_case_compare_function : string_compare_function; break; case PHP_SORT_NATURAL: ARRAYG(compare_func) = sort_type & PHP_SORT_FLAG_CASE ?
string_natural_case_compare_function : string_natural_compa re_function; break;#if HAVE_STRCOLL case PHP_SORT_LOCALE_STRING: ARRAYG(compare_func) = string_locale_compare_function; break;#endif case PHP_SORT_REGULAR: default: ARRAYG(compare_func) = compare_function;//默认使用compare_function break; }}

switch (sort_type & ~PHP_SORT_FLAG_CASE)这是什么意思呢?首先,PHP针对排序设置的sort_type常量有:

#define PHP_SORT_REGULAR                0#define PHP_SORT_NUMERIC                1#define PHP_SORT_STRING                 2#define PHP_SORT_DESC                   3#define PHP_SORT_ASC                    4#define PHP_SORT_LOCALE_STRING          5#define PHP_SORT_NATURAL                6#define PHP_SORT_FLAG_CASE              8

其次,sort函数的第二个参数可以设置为SORT_NATURAL | SORT_FLAG_CASE或者SORT_STRING | SORT_FLAG_CASE. 因此sort_type & ~PHP_SORT_FLAG_CASE的含义为:排除PHP_SORT_FLAG_CASE标志之后的值,得到的值可以是PHP_SORT_NUMERIC,PHP_SORT_STRING,PHP_SORT_NATURAL,PHP_SORT_LOCALE_STRING,PHP_SORT_REGULAR。而在PHP_SORT_STRING和PHP_SORT_NATURAL中,还需要通过sort_type & PHP_SORT_FLAG_CASE来判断是否是不区分大小写的排序(即是否使用了SORT_FLAG_CASE标志)。

(2) 设置完sort_type之后,调用zend_hash_sort完成实际的排序:

 zend_hash_sort(Z_ARRVAL_P(array), zend_qsort, php_array_data_compare, 1 TSRMLS_CC);

zend_hash_sort的函数签名是:

ZEND_API int zend_hash_sort(HashTable *ht, sort_func_t sort_func, compare_func_t compar, int renumber TSRMLS_DC);

其中:

  1. HashTable * ht  指向HashTable的指针
  2. Sort_func_t sort_func  用于排序的函数,因此,实际上是调用zend_qsort来完成排序。
  3. Compare_func_t compar: 用于排序的比较函数,前一步骤已经设置。

我们首先跟踪zend_hash_sort的基本过程,而后再追踪zend_qsort的具体实现。

由于数组排序并不会改变数组中的元素,而只是改变了数组中元素的位置,因而,对底层而言,实际上只是对全局的双链表进行排序,这显然需要n个额外的空间(n是数组元素个数):

arTmp = (Bucket **) pemalloc(ht->nNumOfElements * sizeof(Bucket *), ht->persistent);

然后遍历双链表,将双链表的每个节点存储到临时空间(c数组,每个元素是个bucket *)中:

Interior AI
Interior AI

AI室内设计,上传室内照片自动帮你生成多种风格的室内设计图

下载
p = ht->pListHead;i = 0;while (p) {    arTmp[i] = p;    p = p->pListNext;    i++;}

现在,可以调用排序函数对数组进行排序了:

(*sort_func)((void *) arTmp, i, sizeof(Bucket *), compar TSRMLS_CC);

实际上是:

zend_qsort((void *) arTmp, i, sizeof(Bucket *), compar TSRMLS_CC);

排序之后,双链表中节点的位置发生了变化,因而需要调整指针的指向。首先调整pListHead,并设置pListTail为NULL:

ht->pListHead = arTmp[0];ht->pListTail = NULL;

然后遍历数组,分别设置每一个节点的pListLast和pListNext:

arTmp[0]->pListLast = NULL;if (i > 1) {    arTmp[0]->pListNext = arTmp[1];    for (j = 1; j < i-1; j++) {        arTmp[j]->pListLast = arTmp[j-1];        arTmp[j]->pListNext = arTmp[j+1];    }    arTmp[j]->pListLast = arTmp[j-1];    arTmp[j]->pListNext = NULL;} else {    arTmp[0]->pListNext = NULL;}

最后设置HashTable的pListTail:

ht->pListTail = arTmp[i-1];

排序过程如下所示:

 

排序之后,调整指针走向之后的HashTable:

 

现在,已经知道zend_hash_sort的基本过程了,我们接着跟踪一下zend_qsort的实现(函数位于Zend/zend_qsort.c),该函数的签名为:

ZEND_API void zend_qsort(void *base, size_t nmemb, size_t siz, compare_func_t compare TSRMLS_DC);

这实际上是Zend实现的快速排序算法,主要包括两个部分:

1. _zend_qsort_swap(void *a, void *b, size_t siz) 用于交换任意类型的两个值,与我们经常使用的swap(int *a ,int *b), 或者swap(char *a, char *b), _zend_qsort_swap有更好的通用性,因而它的实现也略微复杂, 具体交换过程为:

(1) . 以sizeof(int)为步长, 交换指针指向的值:

for (i = sizeof(int); i <= siz; i += sizeof(int)) {    t_i = *tmp_a_int;    *tmp_a_int++ = *tmp_b_int;    *tmp_b_int++ = t_i;}

这个循环执行完毕后,有两种可能的情况:一种是siz刚好是sizeof(int)的整倍数,那么交换就已经完成了,因为指针a和指针b指向的内存空间的值已经完全得到了交换。另一种情况是, siz并不是sizeof(int)的整倍数,那么实际上上述交换步骤多交换了一些字节的值(例如对于sizeof(int)=4的情况,可能多交换了1,2,3个字节的内存的值),那么对于这多交换出来的一部分,还需要交换回去。怎么做呢?

(2). 使用char指针一个一个字节的交换:

tmp_a_char = (char *) tmp_a_int;tmp_b_char = (char *) tmp_b_int;for (i = i - sizeof(int) + 1; i <= siz; ++i) {//i控制交换次数    t_c = *tmp_a_char;    *tmp_a_char++ = *tmp_b_char;    *tmp_b_char++ = t_c;}

这样就完成了交换。

2. zend_qsort(void *base, size_t nmemb, size_t siz, compare_func_t compare TSRMLS_DC). 快速排序算法,与常见的快速排序算法不同,这是非递归版本的快速排序。算法的基本思想是:使用QSORT_STACK_SIZE大小的(实际上是数组,不过每次都取数组的末尾元素,当做栈使用)存储快排的开始索引和结束索引(指针),从而将递归的快排过程转换为非递归的。

综上,我们可以得出PHP排序函数的一般特点:

  a. 需要额外的空间,空间复杂度是O(n), 因而应该尽量避免对很大的数组排序.

  b. 底层使用快速排序,平均时间复杂度是O(n*lgn)

zend_qsort的 实现代码(有兴趣的童鞋可以研究一下实现细节):

ZEND_API void zend_qsort(void *base, size_t nmemb, size_t siz, compare_func_t compare TSRMLS_DC){	/* 存储开始和结束指针的栈 */	void           *begin_stack[QSORT_STACK_SIZE];	void           *end_stack[QSORT_STACK_SIZE];	register char  *begin;	register char  *end;	register char  *seg1;	register char  *seg2;		/* partition index */	register char  *seg2p;	register int    loop;		/* pivot index */	uint            offset;		begin_stack[0] = (char *) base;	end_stack[0]   = (char *) base + ((nmemb - 1) * siz);	for (loop = 0; loop >= 0; --loop) {		begin = begin_stack[loop];		end   = end_stack[loop];				/* partition的过程 */		while (begin < end) {		  offset = (end - begin) >> 1;		  _zend_qsort_swap(begin, begin + (offset - (offset % siz)), siz);		  seg1 = begin + siz;		  seg2 = end;		  while (1) {			/* 从左向右找 */			for (; seg1 < seg2 && compare(begin, seg1 TSRMLS_CC) > 0;		       seg1 += siz);							  /* 从右向左找 */			  for (; seg2 >= seg1 && compare(seg2, begin TSRMLS_CC) > 0;			    seg2 -= siz);							  if (seg1 >= seg2)				break;							  /* 交换seg1和seg2指向的值 */			  _zend_qsort_swap(seg1, seg2, siz);							  /* 指针移动,每次都是siz步长 */			  seg1 += siz;			  seg2 -= siz;			}			_zend_qsort_swap(begin, seg2, siz);			seg2p = seg2;						/* 右半部分 */			if ((seg2p - begin) <= (end - seg2p)) {				if ((seg2p + siz) < end) {				  begin_stack[loop] = seg2p + siz;				  end_stack[loop++] = end;				}				end = seg2p - siz;			}			else { /* 左半部分 */				if ((seg2p - siz) > begin) {					begin_stack[loop] = begin;					end_stack[loop++] = seg2p - siz;				}				begin = seg2p + siz;			}		}	}}

4.  数组合并 array_merge

array_merge用于合并两个或者多个数组(实际上,array_merge可以仅传入一个数组参数如array_merge($a)  )例如:

$a = array('index' => "a",1 =>'a');$b = array('index' => "b",1 =>'b');print_r(array_merge($a, $b));

结果是:

Array(    [index] => b    [0] => a    [1] => b)

那么,对于array_merge, PHP底层是如何处理字符串索引和数字索引的呢?

PHP_FUNCTION(array_merge){    php_array_merge_or_replace_wrapper(INTERNAL_FUNCTION_PARAM_PASSTHRU, 0, 0);}

因此,实际上是通过php_array_merge_or_replace_wrapper来完成的,继续查看php_array_merge_or_replace_wrapper的实现:

static void php_array_merge_or_replace_wrapper(INTERNAL_FUNCTION_PARAMETERS, int recursive, int replace);

注意传入的参数,recursive=0, replace=0 ( 不递归merge,数字索引不替换 ) ,而INTERNAL_FUNCTION_PARAMETERS是:

#define INTERNAL_FUNCTION_PARAMETERS int ht, zval *return_value, zval **return_value_ptr, zval *this_ptr, int return_value_used     TSRMLS_DC

array_merge的基本过程是:

(1)     确定初始化数组的大小(使用元素最多的数组的大小作为结果数组的初始大小),初始化数组:

for (i = 0; i < argc; i++) {      /* 不是数组 */    if (Z_TYPE_PP(args[i]) != IS_ARRAY) {        php_error_docref(NULL TSRMLS_CC, E_WARNING, "Argument #%d is not an array", i + 1);        efree(args);        RETURN_NULL();    } else {        int num = zend_hash_num_elements(Z_ARRVAL_PP(args[i]));                    		/* 使用元素最多的数组的大小作为init_size的大小 */        if (num > init_size) {            init_size = num;        }    }}array_init_size(return_value, init_size);

return_value是个zval *, 它指向返回值的zval

(2)     对array_merge参数中的每个数组,依次执行php_array_merge(由于replace=0和recursive=0), 我们只看第一个分支:

for (i = 0; i < argc; i++) {SEPARATE_ZVAL(args[i]);if (!replace) {        php_array_merge(Z_ARRVAL_P(return_value), Z_ARRVAL_PP(args[i]), recursive TSRMLS_CC);    }}

SEPARATE_ZVAL用于创建一个与原始数据相同的zval,避免在操作的过程中修改参数的值(参数是非引用传递的情况下)。而真正的merge过程是通过php_array_merge来实现的。

(3)     merge的过程

由于PHP数组中包含字符串索引和数字索引,对于这两类不同的索引,merge的处理是不同的(replace=0, recursive=0,只看对应的分支):

switch (zend_hash_get_current_key_ex(src, &string_key, &string_key_len, &num_key, 0, &pos)){    case HASH_KEY_IS_STRING:        Z_ADDREF_PP(src_entry);		zend_hash_update(dest, string_key, string_key_len, src_entry, sizeof(zval *), NULL);	break;	case HASH_KEY_IS_LONG:		Z_ADDREF_PP(src_entry);		zend_hash_next_index_insert(dest, src_entry, sizeof(zval *), NULL);	break;}

上述代码表明:对于字符串索引,PHP在执行array_merge的时候,会更新字符串索引的值,其结果就是参数靠后数组的值会覆盖靠前的数组的值。而对于数字型索引,PHP执行的zend_hash_next_index_insert操作,也就是插入一个新的元素,这同时也更改了键(例如原来的key=2, array_merge之后,可能变成了0)。这也解释了最开始array_merge脚本的输出:

$a = array('index' => "a",1 =>'a');$b = array('index' => "b",1 =>'b');print_r(array_merge($a, $b));

更多的数组操作函数我们不再一一介绍,只要知道了HashTable的结构,要理解这些实现,并不困难。

由于写作匆忙,本文难免会有错误之处,敬请批评指正。

ps: 近期正在补习C语言/操作系统的相关基础,尤其是指针/内存管理这一块,有一起的同学,欢迎交流。

   三、参考文献

  1. http://blog.csdn.net/a600423444/article/details/7073854
  2. http://www.nowamagic.net/librarys/veda/detail/1455
  3. http://www.nowamagic.net/librarys/veda/detail/1474
  4. http://www.phppan.com/2010/01/php-source-code5-array/

相关文章

PHP速学教程(入门到精通)
PHP速学教程(入门到精通)

PHP怎么学习?PHP怎么入门?PHP在哪学?PHP怎么学才快?不用担心,这里为大家提供了PHP速学教程(入门到精通),有需要的小伙伴保存下载就能学习啦!

下载

相关标签:

php

本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

相关专题

更多
Java 桌面应用开发(JavaFX 实战)
Java 桌面应用开发(JavaFX 实战)

本专题系统讲解 Java 在桌面应用开发领域的实战应用,重点围绕 JavaFX 框架,涵盖界面布局、控件使用、事件处理、FXML、样式美化(CSS)、多线程与UI响应优化,以及桌面应用的打包与发布。通过完整示例项目,帮助学习者掌握 使用 Java 构建现代化、跨平台桌面应用程序的核心能力。

37

2026.01.14

php与html混编教程大全
php与html混编教程大全

本专题整合了php和html混编相关教程,阅读专题下面的文章了解更多详细内容。

19

2026.01.13

PHP 高性能
PHP 高性能

本专题整合了PHP高性能相关教程大全,阅读专题下面的文章了解更多详细内容。

37

2026.01.13

MySQL数据库报错常见问题及解决方法大全
MySQL数据库报错常见问题及解决方法大全

本专题整合了MySQL数据库报错常见问题及解决方法,阅读专题下面的文章了解更多详细内容。

19

2026.01.13

PHP 文件上传
PHP 文件上传

本专题整合了PHP实现文件上传相关教程,阅读专题下面的文章了解更多详细内容。

16

2026.01.13

PHP缓存策略教程大全
PHP缓存策略教程大全

本专题整合了PHP缓存相关教程,阅读专题下面的文章了解更多详细内容。

6

2026.01.13

jQuery 正则表达式相关教程
jQuery 正则表达式相关教程

本专题整合了jQuery正则表达式相关教程大全,阅读专题下面的文章了解更多详细内容。

3

2026.01.13

交互式图表和动态图表教程汇总
交互式图表和动态图表教程汇总

本专题整合了交互式图表和动态图表的相关内容,阅读专题下面的文章了解更多详细内容。

45

2026.01.13

nginx配置文件详细教程
nginx配置文件详细教程

本专题整合了nginx配置文件相关教程详细汇总,阅读专题下面的文章了解更多详细内容。

9

2026.01.13

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
PHP函数之array数组函数视频讲解
PHP函数之array数组函数视频讲解

共76课时 | 25.9万人学习

PHP课程
PHP课程

共137课时 | 8.6万人学习

JavaScript ES5基础线上课程教学
JavaScript ES5基础线上课程教学

共6课时 | 7万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号