码迷,mamicode.com
首页 > Windows程序 > 详细

52-使用哈希表API

时间:2016-04-29 17:00:28      阅读:253      评论:0      收藏:0      [点我收藏+]

标签:

52-使用哈希表API

Zend把与HashTable有关的API分成了好几类以便于我们寻找,这些API的返回值大多都是常量SUCCESS或者FAILURE。

创建HashTable

下面在介绍函数原型的时候都使用了ht名称,但是我们在编写扩展的时候,一定不要使用这个名称,因为一些PHP宏展开后会声明这个名称的变量,进而引发命名冲突。

创建并初始化一个HashTable非常简单,只要使用zend_hash_init函数即可,它的定义如下:

int zend_hash_init(
    HashTable *ht,
    uint nSize,
    hash_func_t pHashFunction,
    dtor_func_t pDestructor,
    zend_bool persistent
);

*ht是指针,指向一个HashTable,我们即可以&一个已存在的HashTable变量,也可以通过emalloc()、pemalloc()等函数来直接申请一块内存,不过最常用的方法还是用ALLOC_HASHTABLE(ht)宏来让内核自动的替我们完成这项工作。ALLOC_HASHTABLE(ht)所做的工作相当于ht = emalloc(sizeof(HashTable));

nSize代表着这个HashTable可以拥有的元素的最大数量(HashTable能够包含任意数量的元素,这个值只是为了提前申请好内存,提高性能,省的不停的进行rehash操作)。在我们添加新的元素时,这个值会根据情况决定是否自动增长,有趣的是,这个值永远都是2的次方,如果你给它的值不是一个2的次方的形式,那它将自动调整成大于它的最小的2的次方值。它的计算方法就像这样:nSize = pow(2, ceil(log(nSize, 2)));

pHashFunction是早期的Zend Engine中的一个参数,为了兼容没有去掉它,但它已经没有用处了,所以我们直接赋成NULL就可以了。在原来,它其实是一个钩子,用来让用户自己hook一个散列函数,替换php默认的DJBX33A算法实现。

pDestructor也代表着一个回调函数,当我们删除或者修改HashTable中其中一个元素时候便会调用,它的函数原型必须是这样的:void method_name(void *pElement);这里的*pElement是一个指针,指向HashTable中那么将要被删除或者修改的那个数据,而数据的类型往往也是个指针。

persistent是最后一个参数,它的含义非常简单。如果它为true,那么这个HashTable将永远存在于内存中,而不会在RSHUTDOWN阶段自动被注销掉。此时第一个参数ht所指向的地址必须是通过pemalloc()函数申请的。

举个例子,PHP内核在每个Request请求的头部都调用了这个函数来初始化symbol_table。

zend_hash_init(&EG(symbol_table), 50, NULL, ZVAL_PTR_DTOR, 0);

//#define ZVAL_PTR_DTOR (void (*)(void *)) zval_ptr_dtor_wrapper

因为50不是2的整数幂形式,所以它会在函数执行时被调成成64。

添加与修改

我们有四个常用的函数来完成这项操作,它们的原型分别如下:

int zend_hash_add(
    HashTable *ht,      //待操作的ht
    char *arKey,            //索引,如"my_key"
    uint nKeyLen,       //字符串索引的长度,如6
    void **pData,       //要插入的数据,注意它是void **类型的。int *p,i=1;p=&i,pData=&p;。
    uint nDataSize,
    void *pDest         //如果操作成功,则pDest=*pData;
);

int zend_hash_update(
    HashTable *ht,
    char *arKey,
    uint nKeyLen,
    void *pData,
    uint nDataSize,
    void **pDest
);

int zend_hash_index_update(
    HashTable *ht,
    ulong h,
    void *pData,
    uint nDataSize,
    void **pDest
);

int zend_hash_next_index_insert(
    HashTable *ht,
    void *pData,
    uint nDataSize,
    void **pDest
);

前两个函数用户添加带字符串索引的数据到HashTable中,就像我们在PHP中使用的那样:$foo[‘bar’] = ‘baz’;用C来完成便是:

zend_hash_add(fooHashTbl, "bar", sizeof("bar"), &barZval, sizeof(zval*), NULL);

zend_hash_add()和zend_hash_update()唯一的区别就是如果这个key已经存在了,那么zend_hash_add()将返回FAILURE,而不会修改原有数据。

接下来的两个函数用于像HT中添加数字索引的数据,zend_hash_next_index_insert()函数则不需要索引值参数,而是自己直接计算出下一个数字索引值。

但是如果我们想获取下一个元素的数字索引值,也是有办法的,可以使用zend_hash_next_free_element()函数:

ulong nextid = zend_hash_next_free_element(ht);
zend_hash_index_update(ht, nextid, &data, sizeof(data), NULL);

所有这些函数中,如果pDest不为NULL,内核便会修改其值为被操作的那个元素的地址。在下面的代码中这个参数也有同样的功能。

查找

因为HashTable中有两种类型的索引值,所以需要两个函数来执行find操作。

int zend_hash_find(HashTable *ht, char *arKey, uint nKeyLength,void **pData);
int zend_hash_index_find(HashTable *ht, ulong h, void **pData);

第一种就是我们处理PHP语言中字符串索引数组时使用的,第二种是我们处理PHP语言中数字索引数组使用的。

void hash_sample(HashTable *ht, sample_data *data1)
{
    sample_data *data2;
    ulong targetID = zend_hash_next_free_element(ht);
    if (zend_hash_index_update(ht, targetID,
            data1, sizeof(sample_data), NULL) == FAILURE) {
            /* Should never happen */
            return;
    }
    if(zend_hash_index_find(ht, targetID, (void **)&data2) == FAILURE) {
        /* Very unlikely since we just added this element */
        return;
    }
    /* data1 != data2, however *data1 == *data2 */
}

除了读取,我们还需要检测某个key是否存在:

int zend_hash_exists(HashTable *ht, char *arKey, uint nKeyLen);
int zend_hash_index_exists(HashTable *ht, ulong h);

这两个函数返回SUCCESS或者FAILURE,分别代表着是否存在:

if( zend_hash_exists(EG(active_symbol_table),"foo", sizeof("foo")) == SUCCESS )
{
    /* $foo is set */
}
else
{
    /* $foo does not exist */
}

提速

ulong zend_get_hash_value(char *arKey, uint nKeyLen);

当我们需要对同一个字符串的key进行许多操作时候,比如先检测又没,然后插入,然后修改等等,这时我们便可以使用zend_get_hash_value函数来对我们的操作进行加速!这个函数的返回值可以和quick系列函数使用,达到加速的目的(就是不再重复计算这个字符串的散列值,而直接使用已准备好的)!

int zend_hash_quick_add(
    HashTable *ht,
    char *arKey,
    uint nKeyLen,
    ulong hashval,
    void *pData,
    uint nDataSize,
    void **pDest
);

int zend_hash_quick_update(
    HashTable *ht,
    char *arKey,
    uint nKeyLen,
    ulong hashval,
    void *pData,
    uint nDataSize,
    void **pDest
);

int zend_hash_quick_find(
    HashTable *ht,
    char *arKey,
    uint nKeyLen,
    ulong hashval,
    void **pData
);

int zend_hash_quick_exists(
    HashTable *ht,
    char *arKey,
    uint nKeyLen,
    ulong hashval
);

虽然很意外,但你还是要接受没有zend_hash_quick_del()这个函数呢。quick类函数会在下面这种场合中用到:

void php_sample_hash_copy(HashTable *hta, HashTable *htb,char *arKey, uint nKeyLen TSRMLS_DC)
{
    ulong hashval = zend_get_hash_value(arKey, nKeyLen);
    zval **copyval;

    if (zend_hash_quick_find(hta, arKey, nKeyLen,hashval, (void**)?val) == FAILURE)
    {
        //标明不存在这个索引
        return;
    }

    //这个zval已经被其它的Hashtable使用了,这里我们进行引用计数操作。
    (*copyval)->refcount__gc++;
    zend_hash_quick_update(htb, arKey, nKeyLen, hashval,copyval, sizeof(zval*), NULL);
}

复制与合并

在PHP语言中,我们经常需要进行数组间的Copy与Merge操作,所以php语言中的数组在C语言中的实现HashTable也肯定会经常碰到这种情况。为了简化这一类操作,内核中早已准备好了相应的API供我们使用。

void zend_hash_copy(
    HashTable *target,
    HashTable *source,
    copy_ctor_func_t pCopyConstructor,
    void *tmp,
    uint size
);
  • *source中的所有元素都会通过pCopyConstructor函数Copy到*target中去,我们还是以PHP语言中的数组举例,pCopyConstructor这个hook使得我们可以在copy变量的时候对他们的ref_count进行加一操作。target中原有的与source中索引位置的数据会被替换掉,而其它的元素则会被保留,原封不动。
  • tmp参数是为了兼容PHP4.0.3以前版本的,现在赋值为NULL即可。
  • size参数代表每个元素的大小,对于PHP语言中的数组来说,这里的便是sizeof(zval*)了。

    void zend_hash_merge(
    HashTable *target,
    HashTable *source,
    copy_ctor_func_t pCopyConstructor,
    void *tmp,
    uint size,
    int overwrite
    );

zend_hash_merge()与zend_hash_copy唯一的不同便是多了个int类型的overwrite参数,当其值非0的时候,两个函数的工作是完全一样的;如果overwrite参数为0,则zend_hash_merge函数就不会对target中已有索引的值进行替换了。

typedef zend_bool (*merge_checker_func_t)(HashTable *target_ht,void *source_data, zend_hash_key *hash_key, void *pParam);
void zend_hash_merge_ex(
    HashTable *target,
    HashTable *source,
    copy_ctor_func_t pCopyConstructor, 
    uint size,
    merge_checker_func_t pMergeSource,
    void *pParam
);

这个函数又繁琐了些,与zend_hash_copy相比,其多了两个参数,多出来的pMergeSoure回调函数允许我们选择性的进行merge,而不是全都merge。

遍历

在PHP语言中,我们有很多方法来遍历一个数组,对于数组的本质HashTable,我们也有很多办法来对其进行遍历操作。首先最简单的一种办法便是使用一种与PHP语言中forech语句功能类似的函数——zend_hash_apply,它接收一个回调函数,并将HashTable的每一个元素都传递给它。

typedef int (*apply_func_t)(void *pDest TSRMLS_DC);
void zend_hash_apply(HashTable *ht,apply_func_t apply_func TSRMLS_DC);

下面是另外一种遍历函数:

typedef int (*apply_func_arg_t)(void *pDest,void *argument TSRMLS_DC);
void zend_hash_apply_with_argument(HashTable *ht,apply_func_arg_t apply_func, void *data TSRMLS_DC);

通过上面的函数可以在执行遍历时向回调函数传递任意数量的值,这在一些diy操作中非常有用。

上述函数对传给它们的回调函数的返回值有一个共同的约定,详细介绍下下表:

回调函数的返回值

名称 备注
ZEND_HASH_APPLY_KEEP 结束当前请求,进入下一个循环。与PHP语言forech语句中的一次循环执行完毕或者遇到continue关键字的作用一样。
ZEND_HASH_APPLY_STOP 跳出,与PHP语言forech语句中的break关键字的作用一样。
ZEND_HASH_APPLY_REMOVE 删除当前的元素,然后继续处理下一个。相当于在PHP语言中:unset(foo[key]);continue;

我们来一下PHP语言中的forech循环:

<?php
foreach($arr as $val) {
    echo "The value is: $val\n";
}
?>

那我们的回调函数在C语言中应该这样写:

int php_sample_print_zval(zval **val TSRMLS_DC)
{
    //重新copy一个zval,防止破坏原数据
    zval tmpcopy = **val;
    zval_copy_ctor(&tmpcopy);

    //转换为字符串
    INIT_PZVAL(&tmpcopy);
    convert_to_string(&tmpcopy);

    //开始输出
    php_printf("The value is: ");
    PHPWRITE(Z_STRVAL(tmpcopy), Z_STRLEN(tmpcopy));
    php_printf("\n");

    //毁尸灭迹
    zval_dtor(&tmpcopy);

    //返回,继续遍历下一个~
    return ZEND_HASH_APPLY_KEEP;
}

遍历我们的HashTable:

//生成一个名为arrht、元素为zval*类型的HashTable
zend_hash_apply(arrht, php_sample_print_zval TSRMLS_CC);

保存在HashTable中的元素并不是真正的最终变量,而是指向它的一个指针。我们的上面的遍历函数接收的是一个zval**类型的参数。

typedef int (*apply_func_args_t)(void *pDest,int num_args, va_list args, zend_hash_key *hash_key);
void zend_hash_apply_with_arguments(HashTable *ht,apply_func_args_t apply_func, int numargs, ...);

为了能在遍历时同时接收索引的值,我们必须使用第三种形式的zend_hash_apply!就像PHP语言中这样的功能:

<?php
foreach($arr as $key => $val)
{
    echo "The value of $key is: $val\n";
}
?>

为了配合zend_hash_apply_with_arguments()函数,我们需要对我们的遍历执行函数做一下小小的改动,使其接受索引作为一个参数:

int php_sample_print_zval_and_key(zval **val,int num_args,va_list args,zend_hash_key *hash_key)
{
    //重新copy一个zval,防止破坏原数据
    zval tmpcopy = **val;
    /* tsrm_ls is needed by output functions */
    TSRMLS_FETCH();
    zval_copy_ctor(&tmpcopy);
    INIT_PZVAL(&tmpcopy);

    //转换为字符串
    convert_to_string(&tmpcopy);

    //执行输出
    php_printf("The value of ");
    if (hash_key->nKeyLength)
    {
        //如果是字符串类型的key
        PHPWRITE(hash_key->arKey, hash_key->nKeyLength);
    }
    else
    {
        //如果是数字类型的key
        php_printf("%ld", hash_key->h);
    }

    php_printf(" is: ");
    PHPWRITE(Z_STRVAL(tmpcopy), Z_STRLEN(tmpcopy));
    php_printf("\n");

    //毁尸灭迹
    zval_dtor(&tmpcopy);
    /* continue; */
    return ZEND_HASH_APPLY_KEEP;
}

执行遍历:

zend_hash_apply_with_arguments(arrht,php_sample_print_zval_and_key, 0);

这个函数通过C语言中的可变参数特性来接收参数。This particular example required no arguments to be passed; for information on extracting variable argument lists from va_list args, see the POSIX documentation pages for va_start(), va_arg(), and va_end().

当我们检查这个hash_key是字符串类型还是数字类型时,是通过nKeyLength属性来检测的,而不是arKey属性。这是因为内核有时候会留在arKey属性里些脏数据,但nKeyLength属性是安全的,可以安全的使用。甚至对于空字符串索引,它也照样能处理。比如:$foo[”] =”Bar”;索引的值是NULL字符,但它的长度却是包括最后这个NULL字符的,所以为1。

向前遍历HashTable

有时我们希望不用回调函数也能遍历一个数组的数据,为了实现这个功能,内核特意的为每个HashTable加了个属性:The internal pointer(内部指针)。我们还是以PHP语言中的数组举例,有以下函数来处理它所对应的那个HashTable的内部指针:reset(), key(), current(), next(), prev(), each(), and end()。

<?php
    $arr = array(‘a‘=>1, ‘b‘=>2, ‘c‘=>3);
    reset($arr);
    while (list($key, $val) = each($arr)) {
        /* Do something with $key and $val */
    }
    reset($arr);
    $firstkey = key($arr);
    $firstval = current($arr);
    $bval = next($arr);
    $cval = next($arr);
?>

ZEND内核中有一组操作HashTable的功能与以上函数功能类似的函数:

/* reset() */
void zend_hash_internal_pointer_reset(HashTable *ht);

/* key() */
int zend_hash_get_current_key(HashTable *ht,char **strIdx, unit *strIdxLen,ulong *numIdx, zend_bool duplicate);

/* current() */
int zend_hash_get_current_data(HashTable *ht, void **pData);

/* next()/each() */
int zend_hash_move_forward(HashTable *ht);

/* prev() */
int zend_hash_move_backwards(HashTable *ht);

/* end() */
void zend_hash_internal_pointer_end(HashTable *ht);

/* 其他的...... */
int zend_hash_get_current_key_type(HashTable *ht);
int zend_hash_has_more_elements(HashTable *ht);

PHP语言中的next()、prev()、end()函数在移动完指针之后,都通过调用zend_hash_get_current_data()函数来获取当前所指的元素并返回。而each()虽然和next()很像,却是使用zend_hash_get_current_key()函数的返回值来作为它的返回值。

现在我们用另外一种方法来实现上面的forech:

void php_sample_print_var_hash(HashTable *arrht)
{

    for(
        zend_hash_internal_pointer_reset(arrht);
        zend_hash_has_more_elements(arrht) == SUCCESS;
        zend_hash_move_forward(arrht))
    {
        char *key;
        uint keylen;
        ulong idx;
        int type;
        zval **ppzval, tmpcopy;

        type = zend_hash_get_current_key_ex(arrht, &key, &keylen,&idx, 0, NULL);
        if (zend_hash_get_current_data(arrht, (void**)&ppzval) == FAILURE)
        {
            /* Should never actually fail
             * since the key is known to exist. */
            continue;
        }

        //重新copy一个zval,防止破坏原数据
        tmpcopy = **ppzval;
        zval_copy_ctor(&tmpcopy);
        INIT_PZVAL(&tmpcopy);

        convert_to_string(&tmpcopy);

        /* Output */
        php_printf("The value of ");
        if (type == HASH_KEY_IS_STRING)
        {
            /* String Key / Associative */
            PHPWRITE(key, keylen);
        } else {
            /* Numeric Key */
            php_printf("%ld", idx);
        }
        php_printf(" is: ");
        PHPWRITE(Z_STRVAL(tmpcopy), Z_STRLEN(tmpcopy));
        php_printf("\n");
        /* Toss out old copy */
        zval_dtor(&tmpcopy);
    }
}

上面的代码你应该都能看懂了,唯一还没接触到的可能是zend_hash_get_current_key()函数的返回值。

在我们遍历一个HashTable时,一般是很难陷入死循环的。

void php_sample_print_var_hash(HashTable *arrht)
{
    HashPosition pos;
    for(zend_hash_internal_pointer_reset_ex(arrht, &pos);
    zend_hash_has_more_elements_ex(arrht, &pos) == SUCCESS;
    zend_hash_move_forward_ex(arrht, &pos)) {
        char *key;
        uint keylen;
        ulong idx;
        int type;

        zval **ppzval, tmpcopy;

        type = zend_hash_get_current_key_ex(arrht,
                                &key, &keylen,
                                &idx, 0, &pos);
        if (zend_hash_get_current_data_ex(arrht,
                    (void**)&ppzval, &pos) == FAILURE) {
            /* Should never actually fail
             * since the key is known to exist. */
            continue;
        }
        /* Duplicate the zval so that
         * the original‘s contents are not destroyed */
        tmpcopy = **ppzval;
        zval_copy_ctor(&tmpcopy);
        /* Reset refcount & Convert */
        INIT_PZVAL(&tmpcopy);
        convert_to_string(&tmpcopy);
        /* Output */
        php_printf("The value of ");
        if (type == HASH_KEY_IS_STRING) {
            /* String Key / Associative */
            PHPWRITE(key, keylen);
        } else {
            /* Numeric Key */
            php_printf("%ld", idx);
        }
        php_printf(" is: ");
        PHPWRITE(Z_STRVAL(tmpcopy), Z_STRLEN(tmpcopy));
        php_printf("\n");
        /* Toss out old copy */
        zval_dtor(&tmpcopy);
    }
}


<p>With these very slight additions, the HashTable‘s true internal pointer is preserved in whatever state it was initially in on entering the function. When it comes to working with internal pointers of userspace variable HashTables (that is, arrays), this extra step will very likely make the difference between whether the scripter‘s code works as expected.</p>

<h3>删除</h3>

<p>内核中一共预置了四个删除HashTable元素的函数,头两个是用户删除某个确定索引的数据:</p>
<code c>
int zend_hash_del(HashTable *ht, char *arKey, uint nKeyLen);
int zend_hash_index_del(HashTable *ht, ulong h);

它们两个分别用来删除字符串索引和数字索引的数据,操作完成后都返回SUCCESS或者FAILURE表示成功or失败。回顾一下最上面的叙述,当一个元素被删除时,会激活HashTable的destructor回调函数。

void zend_hash_clean(HashTable *ht);
void zend_hash_destroy(HashTable *ht);

前者用于将HashTable中的元素全部删除,而后者是将这个HashTable自身也毁灭掉。

现在让我们来完整的回顾一下HashTable的创建、添加、删除操作。

int sample_strvec_handler(int argc, char **argv TSRMLS_DC)
{
    HashTable *ht;

    //分配内存
    ALLOC_HASHTABLE(ht);

    //初始化
    if (zend_hash_init(ht, argc, NULL,ZVAL_PTR_DTOR, 0) == FAILURE) {
        FREE_HASHTABLE(ht);
        return FAILURE;
    }

    //填充数据
    while (argc) {
        zval *value;
        MAKE_STD_ZVAL(value);
        ZVAL_STRING(value, argv[argc], 1);
        argv++;
        if (zend_hash_next_index_insert(ht, (void**)&value,
                            sizeof(zval*)) == FAILURE) {
            /* Silently skip failed additions */
            zval_ptr_dtor(&value);
        }
    }

    //完成工作
    process_hashtable(ht);

    //毁尸灭迹
    zend_hash_destroy(ht);

    //释放ht 为什么不在destroy里free呢,求解释!
    FREE_HASHTABLE(ht);
    return SUCCESS;
}

排序、比较

针对HashTable操作的Zend Api中有很多都需要回调函数。首先让我们来处理一下对HashTable中元素大小比较的问题:

typedef int (*compare_func_t)(void *a, void *b TSRMLS_DC);

这很像PHP语言中usort函数需要的函数,它将比较两个值*a与*b,如果*a>*b,则返回1,相等则返回0,否则返回-1。下面是zend_hash_minmax函数的声明,它就需要我们上面声明的那个类型的函数作为回调函数: int zend_hash_minmax(HashTable *ht, compare_func_t compar,int flag, void **pData TSRMLS_DC); 这个函数的功能我们从它的名称中便能肯定,它用来比较HashTable中的元素大小。如果flag==0则返回最小值,否则返回最大值!

下面让我们来利用这个函数来对用户端定义的所有函数根据函数名找到最大值与最小值(大小写不敏感~)。

//先定义一个比较函数,作为zend_hash_minmax的回调函数。
int fname_compare(zend_function *a, zend_function *b TSRMLS_DC)
{
    return strcasecmp(a->common.function_name, b->common.function_name);
}

void php_sample_funcname_sort(TSRMLS_D)
{
    zend_function *fe;
    if (zend_hash_minmax(EG(function_table), fname_compare,0, (void **)&fe) == SUCCESS)
    {
        php_printf("Min function: %s\n", fe->common.function_name);
    }
    if (zend_hash_minmax(EG(function_table), fname_compare,1, (void **)&fe) == SUCCESS)
    {
        php_printf("Max function: %s\n", fe->common.function_name);
    }
}

zend_hash_compare()也许要回调函数,它的功能是将HashTable看作一个整体与另一个HashTable做比较,如果前者大于后者返回1,相等返回0,否则返回-1。

int zend_hash_compare(HashTable *hta, HashTable *htb,compare_func_t compar, zend_bool ordered TSRMLS_DC);

默认情况下它往往是先判断各个HashTable元素的个数,个数多的最大! 如果两者的元素一样多,然后就比较它们各自的第一个元素。另外一个重要的需要回调函数的API便是排序函数,它需要的回调函数形式是这样的:

typedef void (*sort_func_t)(void **Buckets, size_t numBuckets,size_t sizBucket, compare_func_t comp TSRMLS_DC);

最后一个参数如果为处,则会抛弃HashTable中原有的索引-键关系,将对排列号的新值赋予新的数字键值。PHP语言中的sort函数实现如下:

zend_hash_sort(target_hash, zend_qsort,array_data_compare, 1 TSRMLS_CC);

array_data_compare是一个返回compare_func_t类型数据的函数,它将按照HashTable中zval*值的大小进行排序。

52-使用哈希表API

标签:

原文地址:http://blog.csdn.net/ghostlv/article/details/51249500

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!