Home  >  Article  >  Backend Development  >  PHP数组/Hash表的实现、操作

PHP数组/Hash表的实现、操作

WBOY
WBOYOriginal
2016-06-20 12:38:541087browse

catalogue

1. PHP Hash表1. PHP数组定义

1. PHP Hash表

0x1: 基本概念

哈希表在实践中使用的非常广泛,例如编译器通常会维护的一个符号表来保存标记,很多高级语言中也显式的支持哈希表。 哈希表通常提供查找(Search),插入(Insert),删除(Delete)等操作,这些操作在最坏的情况

下和链表的性能一样为O(n)。 不过通常并不会这么坏,合理设计的哈希算法能有效的避免这类情况,通常哈希表的这些操作时间复杂度为O(1)。 这也是它被钟爱的原因

正是因为哈希表在使用上的便利性及效率上的表现,目前大部分动态语言的实现中都使用了哈希表

哈希表是一种通过哈希函数,将特定的键映射到特定值的一种数据结构,它维护键和值之间一一对应关系

1. 键(key): 用于操作数据的标示,例如PHP数组中的索引,或者字符串键等等 2. 槽(slot/bucket): 哈希表中用于保存数据的一个单元,也就是数据真正存放的容器 3. 哈希函数(hash function): 将key映射(map)到数据应该存放的slot所在位置的函数 4. 哈希冲突(hash collision): 哈希函数将两个不同的key映射到同一个索引的情况 

哈希表可以理解为数组的扩展或者关联数组,数组使用数字下标来寻址,如果关键字(key)的范围较小且是数字的话, 我们可以直接使用数组来完成哈希表,而如果关键字范围太大,如果直接使用数组我们需要为所有可能的key申请空间。 很多情况下这是不现实的。即使空间足够,空间利用率也会很低,这并不理想。同时键也可能并不是数字, 在PHP中尤为如此,所以人们使用一种映射函数(哈希函数)来将key映射到特定的域中

h(key) -> index

通过合理设计的哈希函数,我们就能将key映射到合适的范围,因为我们的key空间可以很大(例如字符串key), 在映射到一个较小的空间中时可能会出现两个不同的key映射被到同一个index上的情况, 这就是我们所说的出现了冲突。 目前解决hash冲突的方法主要有两种:链接法和开放寻址法

1. 冲突解决: 链接法

链接法通过使用一个链表来保存slot值的方式来解决冲突,也就是当不同的key映射到一个槽中的时候使用链表来保存这些值。 所以使用链接法是在最坏的情况下,也就是所有的key都映射到同一个槽中了,这样哈希表就退化成了一个链表, 这样的话操作链表的时间复杂度则成了O(n),这样哈希表的性能优势就没有了, 所以选择一个合适的哈希函数是最为关键的

由于目前大部分的编程语言的哈希表实现都是开源的,大部分语言的哈希算法都是公开的算法, 虽然目前的哈希算法都能良好的将key进行比较均匀的分布,而这个假使的前提是key是随机的,正是由于算法的确定性, 这就导致了别有用心的黑客能利用已知算法的可确定性来构造一些特殊的key,让这些key都映射到同一个槽位导致哈希表退化成单链表,导致程序的性能急剧下降,从而造成一些应用的吞吐能力急剧下降, 尤其是对于高并发的应用影响很大,通过大量类似的请求可以让服务器遭受DoS(服务拒绝攻击)

哈希冲突攻击利用的哈希表最根本的弱点是:

开源算法和哈希实现的确定性以及可预测性, 这样攻击者才可以利用特殊构造的key来进行攻击。要解决这个问题的方法则是让攻击者无法轻易构造 能够进行攻击的key序列 

目前PHP中HashTable的哈希冲突解决方法就是链接法

2. 冲突解决: 开放寻址法

通常还有另外一种解决冲突的方法:开放寻址法。使用开放寻址法是槽本身直接存放数据,在插入数据时如果key所映射到的索引已经有数据了,这说明发生了冲突,这是会寻找下一个槽, 如果该槽也被占用了则继续寻找下一个槽,直到寻找到没有被占用的槽,在查找时也使用同样的策略来进行 由于开放寻址法处理冲突的时候占用的是其他槽位的空间,这可能会导致后续的key在插入的时候更加容易出现 哈希冲突,所以采用开放寻址法的哈希表的装载因子不能太高,否则容易出现性能下降

装载因子是哈希表保存的元素数量和哈希表容量的比1. 通常采用链接法解决冲突的哈希表的装载,因子最好不要大于1(等于1意味着Hash表已经存满了,接下来开始保存的键值都会导致冲突发生即链表增长,而链表的效率是低于Hash表的)2. 而采用开放寻址法的哈希表最好不要大于0.5 

0x2: 哈希表的实现

实现一个哈希表也很容易,主要需要完成的工作只有三点

1. 实现哈希函数2. 冲突的解决3. 操作接口的实现

在开始学习PHP原生内核的Hash表实现前,我们自己可以先手工实现一个简易版的Hash表

1. 基础数据结构定义

#ifndef _HASH_TABLE_H_#define _HASH_TABLE_H_ 1typedef struct _Bucket{    char *key;    void *value;    struct _Bucket *next;} Bucket;typedef struct _HashTable{    int size;    //HashTable size/lines    int elem_num;    //total elements count    Bucket** buckets;} HashTable;#endif

2. 哈希函数实现

哈希函数需要尽可能的将不同的key映射到不同的槽(slot或者bucket)中,首先我们采用一种最为简单的哈希算法实现: 将key字符串的所有字符加起来,然后以结果对哈希表的大小取模,这样索引就能落在数组索引的范围之内了

//hashtable.c#include <stdio.h>#include <stdlib.h>#include <string.h>#include "hashtable.h" int hash_str(char *key); int hash_str(char *key){    int hash = 0;    char *cur = key;    while(*cur != '\0')        {        hash += *cur;        ++cur;        }        return hash;}//hashtable.h#define HASH_INDEX(ht, key) (hash_str((key)) % (ht)->size)

3. 操作接口的实现

为了操作哈希表,实现了如下几个操作接口函数

int hash_init(HashTable *ht);                               // 初始化哈希表int hash_lookup(HashTable *ht, char *key, void **result);   // 根据key查找内容int hash_insert(HashTable *ht, char *key, void *value);     // 将内容插入到哈希表中int hash_remove(HashTable *ht, char *key);                  // 删除key所指向的内容int hash_destroy(HashTable *ht);

4. 完整源代码

hashtable.c

#include <stdio.h>#include <stdlib.h>#include <string.h>#include "hashtable.h"static void resize_hash_table_if_needed(HashTable *ht);static int hash_str(char *key);int hash_init(HashTable *ht){    ht->size         = HASH_TABLE_INIT_SIZE;    ht->elem_num     = 0;    ht->buckets        = (Bucket **)calloc(ht->size, sizeof(Bucket *));    if(ht->buckets == NULL) return FAILED;    LOG_MSG("[init]\tsize: %i\n", ht->size);    return SUCCESS;}int hash_lookup(HashTable *ht, char *key, void **result){    int index = HASH_INDEX(ht, key);    Bucket *bucket = ht->buckets[index];    if(bucket == NULL) goto failed;    while(bucket)    {        if(strcmp(bucket->key, key) == 0)        {            LOG_MSG("[lookup]\t found %s\tindex:%i value: %p\n",                key, index, bucket->value);            *result = bucket->value;                return SUCCESS;        }        bucket = bucket->next;    }failed:    LOG_MSG("[lookup]\t key:%s\tfailed\t\n", key);    return FAILED;}int hash_insert(HashTable *ht, char *key, void *value){    // check if we need to resize the hashtable    resize_hash_table_if_needed(ht);    int index = HASH_INDEX(ht, key);    Bucket *org_bucket = ht->buckets[index];    Bucket *tmp_bucket = org_bucket;    // check if the key exits already    while(tmp_bucket)    {        if(strcmp(key, tmp_bucket->key) == 0)        {            LOG_MSG("[update]\tkey: %s\n", key);            tmp_bucket->value = value;            return SUCCESS;        }        tmp_bucket = tmp_bucket->next;    }    Bucket *bucket = (Bucket *)malloc(sizeof(Bucket));    bucket->key      = key;    bucket->value = value;    bucket->next  = NULL;    ht->elem_num += 1;    if(org_bucket != NULL)    {        LOG_MSG("[collision]\tindex:%d key:%s\n", index, key);        bucket->next = org_bucket;    }    ht->buckets[index]= bucket;    LOG_MSG("[insert]\tindex:%d key:%s\tht(num:%d)\n",        index, key, ht->elem_num);    return SUCCESS;}int hash_remove(HashTable *ht, char *key){    int index = HASH_INDEX(ht, key);    Bucket *bucket  = ht->buckets[index];    Bucket *prev    = NULL;    if(bucket == NULL) return FAILED;    // find the right bucket from the link list     while(bucket)    {        if(strcmp(bucket->key, key) == 0)        {            LOG_MSG("[remove]\tkey:(%s) index: %d\n", key, index);            if(prev == NULL)            {                ht->buckets[index] = bucket->next;            }            else            {                prev->next = bucket->next;            }            free(bucket);            return SUCCESS;        }        prev   = bucket;        bucket = bucket->next;    }    LOG_MSG("[remove]\t key:%s not found remove \tfailed\t\n", key);    return FAILED;}int hash_destroy(HashTable *ht){    int i;    Bucket *cur = NULL;    Bucket *tmp = NULL;    for(i=0; i < ht->size; ++i)    {        cur = ht->buckets[i];        while(cur)        {            tmp = cur;            cur = cur->next;            free(tmp);        }    }    free(ht->buckets);    return SUCCESS;}static int hash_str(char *key){    int hash = 0;    char *cur = key;    while(*cur != '\0')    {        hash +=    *cur;        ++cur;    }    return hash;}static int hash_resize(HashTable *ht){    // double the size    int org_size = ht->size;    ht->size = ht->size * 2;    ht->elem_num = 0;    LOG_MSG("[resize]\torg size: %i\tnew size: %i\n", org_size, ht->size);    Bucket **buckets = (Bucket **)calloc(ht->size, sizeof(Bucket **));    Bucket **org_buckets = ht->buckets;    ht->buckets = buckets;    int i = 0;    for(i=0; i < org_size; ++i)    {        Bucket *cur = org_buckets[i];        Bucket *tmp;        while(cur)         {            // rehash: insert again            hash_insert(ht, cur->key, cur->value);            // free the org bucket, but not the element            tmp = cur;            cur = cur->next;            free(tmp);        }    }    free(org_buckets);    LOG_MSG("[resize] done\n");    return SUCCESS;}// if the elem_num is almost as large as the capacity of the hashtable// we need to resize the hashtable to contain enough elementsstatic void resize_hash_table_if_needed(HashTable *ht){    if(ht->size - ht->elem_num < 1)    {        hash_resize(ht);        }}

hashtable.h

#ifndef _HASH_TABLE_H_#define _HASH_TABLE_H_ 1#define HASH_TABLE_INIT_SIZE 6#define HASH_INDEX(ht, key) (hash_str((key)) % (ht)->size)#if defined(DEBUG)#  define LOG_MSG printf#else#  define LOG_MSG(...)#endif#define SUCCESS 0#define FAILED -1typedef struct _Bucket{    char *key;    void *value;    struct _Bucket *next;} Bucket;typedef struct _HashTable{    int size;        // 哈希表的大小    int elem_num;    // 已经保存元素的个数    Bucket **buckets;} HashTable;int hash_init(HashTable *ht);int hash_lookup(HashTable *ht, char *key, void **result);int hash_insert(HashTable *ht, char *key, void *value);int hash_remove(HashTable *ht, char *key);int hash_destroy(HashTable *ht);#endif

main.c

#include <stdio.h>#include <stdlib.h>#include <assert.h>#include <string.h>#include "hashtable.h"#define TEST(tcase) printf(">>> [START CASE] " tcase "<<<\n")#define PASS(tcase) printf(">>> [PASSED] " tcase " <<<\n")int main(int argc, char **argv){    HashTable *ht = (HashTable *)malloc(sizeof(HashTable));    int result = hash_init(ht);    assert(result == SUCCESS);    /* Data */    int  int1 = 10;    int  int2 = 20;    char str1[] = "Hello TIPI";    char str2[] = "Value";    /* to find data container */    int *j = NULL;    char *find_str = NULL;    /* Test Key insert */    TEST("Key insert");    hash_insert(ht, "KeyInt", &int1);    hash_insert(ht, "asdfKeyStrass", str1);    hash_insert(ht, "K13eyStras", str1);    hash_insert(ht, "KeyStr5", str1);    hash_insert(ht, "KeyStr", str1);    PASS("Key insert");    /* Test key lookup */    TEST("Key lookup");    hash_lookup(ht, "KeyInt", (void **)&j);    hash_lookup(ht, "KeyStr", (void **)&find_str);    assert(strcmp(find_str, str1) == 0);    assert(*j = int1);    PASS("Key lookup");    /* Test Key update */    TEST("Test key update");    hash_insert(ht, "KeyInt", &int2);    hash_lookup(ht, "KeyInt", (void **)&j);    assert(*j = int2);    PASS("Test key update");    TEST(">>>     Test key not found        <<< ");    result = hash_lookup(ht, "non-exits-key", (void **)&j);    assert(result == FAILED);    PASS("non-exist-key lookup");    TEST("Test key not found after remove");    char strMyKey[] = "My-Key-Value";    find_str = NULL;    hash_insert(ht, "My-Key", &strMyKey);    result = hash_remove(ht, "My-Key");    assert(result == SUCCESS);    result = hash_lookup(ht, "My-Key", (void **)&find_str);    assert(find_str == NULL);    assert(result == FAILED);    PASS("Test key not found after remove");    PASS(">>>     Test key not found        <<< ");    TEST("Add many elements and make hashtable rehash");    hash_insert(ht, "a1", &int2);    hash_insert(ht, "a2", &int1);    hash_insert(ht, "a3", &int1);    hash_insert(ht, "a4", &int1);    hash_insert(ht, "a5", &int1);    hash_insert(ht, "a6", &int1);    hash_insert(ht, "a7", &int1);    hash_insert(ht, "a8", str2);    hash_insert(ht, "a9", &int1);    hash_insert(ht, "a10", &int1);    hash_insert(ht, "a11", &int1);    hash_insert(ht, "a12", &int1);    hash_insert(ht, "a13", &int1);    hash_insert(ht, "a14", &int1);    hash_insert(ht, "a15", &int1);    hash_insert(ht, "a16", &int1);    hash_insert(ht, "a17", &int1);    hash_insert(ht, "a18", &int1);    hash_insert(ht, "a19", &int1);    hash_insert(ht, "a20", &int1);    hash_insert(ht, "a21", &int1);    hash_insert(ht, "a22", &int1);    hash_insert(ht, "a23", &int1);    hash_insert(ht, "a24", &int1);    hash_insert(ht, "a24", &int1);    hash_insert(ht, "a24", &int1);    hash_insert(ht, "a25", &int1);    hash_insert(ht, "a26", &int1);    hash_insert(ht, "a27", &int1);    hash_insert(ht, "a28", &int1);    hash_insert(ht, "a29", &int1);    hash_insert(ht, "a30", &int1);    hash_insert(ht, "a31", &int1);    hash_insert(ht, "a32", &int1);    hash_insert(ht, "a33", &int1);    hash_lookup(ht, "a23", (void **)&j);    assert(*j = int1);    hash_lookup(ht, "a30", (void **)&j);    assert(*j = int1);    PASS("Add many elements and make hashtable rehash");    hash_destroy(ht);    free(ht);    printf("Woohoo, It looks like HashTable works properly\n");    return 0;}

编译运行

gcc -g -Wall -DDEBUG -o a.out main.c hashtable.c

0x3: 数据结构

在PHP中所有的数据,变量、常量、类、属性、数组都用Hash表来实现\php-5.6.17\Zend\zend_hash.h

typedef struct bucket {    ulong h;                        /* Used for numeric indexing */    uint nKeyLength;                //key长度    void *pData;                    //指向 Bucke保存的数据 指针    void *pDataPtr;                    //指针数据    struct bucket *pListNext;        //下一个元素指针    struct bucket *pListLast;        //上一个元素指针    struct bucket *pNext;    struct bucket *pLast;    const char *arKey;} Bucket;typedef struct _hashtable {    uint nTableSize;            //HashTable的大小    uint nTableMask;            //等于nTableSize-1    uint nNumOfElements;        //对象个数    ulong nNextFreeElement;        //指向下一个空元素位置 nTableSize+1    Bucket *pInternalPointer;    /* Used for element traversal 保存当前遍历的指针 */    Bucket *pListHead;            //头元素指针    Bucket *pListTail;            //尾元素指针    Bucket **arBuckets;            //存储hash数组数据    dtor_func_t pDestructor;    //类似于析构函数    zend_bool persistent;        //用哪种方法分配内存空间 PHP统一管理内存还是用普通的malloc    unsigned char nApplyCount;    //当前hash bucket被访问的次数,是否遍历过数据,防止无限递归循环    zend_bool bApplyProtection;#if ZEND_DEBUG    int inconsistent;#endif} H

Relevant Link:

http://www.imsiren.com/archives/6http://www.php-internals.com/book/?p=chapt03/03-01-01-hashtable https://github.com/reeze/tipi/tree/master/book/sample/chapt03/03-01-01-hashtable 

1. PHP数组定义

PHP中的数组实际上是一个有序映射。映射是一种把 values 关联到 keys 的类型。此类型在很多方面做了优化,因此可以把它当成

1. 真正的数组2. 列表(向量)3. 散列表(是映射的一种实现)4. 字典5. 集合6. 栈7. 队列以及更多可能性

数组元素的值也可以是另一个数组。树形结构和多维数组也是允许的,PHP中经常使用数组,使用数组最大的好处便是速度!读写都可以在O(1)内完成,因为它每个元素的大小都是一致的,只要知道下标,便可以瞬间计算出其对应的元素在内存中的位置,从而直接取出或者写入

PHP大部分功能,都是通过HashTable来实现,其中就包括数组

HashTable即具有双向链表的优点,PHP中的定义的变量保存在一个符号表里,而这个符号表其实就是一个HashTable,它的每一个元素都是一个zval*类型的变量。不仅如此,保存用户定义的函数、类、资源等的容器都是以HashTable的形式在内核中实现的

因此,PHP的数组读写都可以在O(1)内完成,这是非常高效的,因此开销和C++、Java相比也就是hashtable的创建了,我们看一下PHP定义数组

<?php    $array = array();    $array["key"] = "values";?>

在内核中使用宏来实现

0x1:  数组初始化

Zend/zend_vm_execute.h

static int ZEND_FASTCALL  ZEND_INIT_ARRAY_SPEC_CV_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS){    USE_OPLINE    array_init(&EX_T(opline->result.var).tmp_var);    if (IS_CV == IS_UNUSED) {        ZEND_VM_NEXT_OPCODE();#if 0 || IS_CV != IS_UNUSED    } else {        return ZEND_ADD_ARRAY_ELEMENT_SPEC_CV_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU);#endif    }}

\php-5.6.17\Zend\zend_API.c

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, ZVAL_PTR_DTOR, 0 ZEND_FILE_LINE_RELAY_CC);    Z_TYPE_P(arg) = IS_ARRAY;    return SUCCESS;}

\php-5.6.17\Zend\zend_hash.c

ZEND_API int _zend_hash_init(HashTable *ht, uint nSize, dtor_func_t pDestructor, zend_bool persistent ZEND_FILE_LINE_DC){    uint i = 3;    SET_INCONSISTENT(HT_OK);    if (nSize >= 0x80000000) {        /* prevent overflow */        //HASH表大小大于0x80000000则初始化为0x80000000        ht->nTableSize = 0x80000000;    } else {        while ((1U << i) < nSize) {            i++;        }        //申请的数组Size空间调整为2的n次方,这样有利于内存对齐,i=3,nTableSize最小值为8        ht->nTableSize = 1 << i;    }    ht->nTableMask = 0;    /* 0 means that ht->arBuckets is uninitialized */    ht->pDestructor = pDestructor;    //一个函数指针,当HashTable发生增,删,改时调用    ht->arBuckets = (Bucket**)&uninitialized_bucket;    ht->pListHead = NULL;    ht->pListTail = NULL;    ht->nNumOfElements = 0;    ht->nNextFreeElement = 0;    ht->pInternalPointer = NULL;    ht->persistent = persistent;    //如果persisient为TRUE,则使用操作系统本身的内存分配函数为Bucket分配内存,否则使用PHP的内存分配函数    ht->nApplyCount = 0;    ht->bApplyProtection = 1;    return SUCCESS;}ZEND_API int _zend_hash_init_ex(HashTable *ht, uint nSize, dtor_func_t pDestructor, zend_bool persistent, zend_bool bApplyProtection ZEND_FILE_LINE_DC){    int retval = _zend_hash_init(ht, nSize, pDestructor, persistent ZEND_FILE_LINE_CC);    ht->bApplyProtection = bApplyProtection;    return retval;}

0x2: 数组添加键值

0x3: 操作PHP数组的API

//初始化PHP数组array_init(zval *arg);array_init_size(zval *arg, uint size); //关联数组赋值的操作函数,等同于$array[$stringKey] = $value;add_assoc_null(zval *aval, char *key);add_assoc_bool(zval *aval, char *key, zend_bool bval);add_assoc_long(zval *aval, char *key, long lval);add_assoc_double(zval *aval, char *key, double dval);add_assoc_string(zval *aval, char *key, char *strval, int dup);add_assoc_stringl(zval *aval, char *key,char *strval, uint strlen, int dup);add_assoc_zval(zval *aval, char *key, zval *value);//上述函数均为宏函数,都是对add_assoc_*_ex函数的封装 //数字索引数组赋值的操作函数,等效于$array[$numKey] = $value;ZEND_API int add_index_long(zval *arg, ulong idx, long n);ZEND_API int add_index_null(zval *arg, ulong idx);ZEND_API int add_index_bool(zval *arg, ulong idx, int b);ZEND_API int add_index_resource(zval *arg, ulong idx, int r);ZEND_API int add_index_double(zval *arg, ulong idx, double d);ZEND_API int add_index_string(zval *arg, ulong idx, const char *str, int duplicate);ZEND_API int add_index_stringl(zval *arg, ulong idx, const char *str, uint length, int duplicate);ZEND_API int add_index_zval(zval *arg, ulong index, zval *value); //使用内置数字索引的数组赋值的操作函数,等效于$array[] = $value;ZEND_API int add_next_index_long(zval *arg, long n);ZEND_API int add_next_index_null(zval *arg);ZEND_API int add_next_index_bool(zval *arg, int b);ZEND_API int add_next_index_resource(zval *arg, int r);ZEND_API int add_next_index_double(zval *arg, double d);ZEND_API int add_next_index_string(zval *arg, const char *str, int duplicate);ZEND_API int add_next_index_stringl(zval *arg, const char *str, uint length, int duplicate);ZEND_API int add_next_index_zval(zval *arg, zval *value); //数组元素赋值并返回,等效于{ $array[$key] = $value; return $value; }ZEND_API int add_get_assoc_string_ex(zval *arg, const char *key, uint key_len, const char *str, void **dest, int duplicate);ZEND_API int add_get_assoc_stringl_ex(zval *arg, const char *key, uint key_len, const char *str, uint length, void **dest, int duplicate);#define add_get_assoc_string(__arg, __key, __str, __dest, __duplicate) add_get_assoc_string_ex(__arg, __key, strlen(__key)+1, __str, __dest, __duplicate)#define add_get_assoc_stringl(__arg, __key, __str, __length, __dest, __duplicate) add_get_assoc_stringl_ex(__arg, __key, strlen(__key)+1, __str, __length, __dest, __duplicate)ZEND_API int add_get_index_long(zval *arg, ulong idx, long l, void **dest);ZEND_API int add_get_index_double(zval *arg, ulong idx, double d, void **dest);ZEND_API int add_get_index_string(zval *arg, ulong idx, const char *str, void **dest, int duplicate);ZEND_API int add_get_index_stringl(zval *arg, ulong idx, const char *str, uint length, void **dest, int duplicate);

Relevant Link:

http://thiniki.sinaapp.com/?p=155http://www.imsiren.com/archives/250http://www.cnblogs.com/ohmygirl/p/internal-4.htmlhttp://weizhifeng.net/write-php-extension-part2-1.htmlhttp://blog.csdn.net/a600423444/article/details/7073854

Copyright (c) 2016 Little5ann All rights reserved

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Previous article:php几个版本区别Next article:php PDO实例化一次