PHP内核–数据类型
简介:在PHP语言中一共拥有整形、浮点型、字符串、引用、资源、实例和数组
,一共7种数据类型。其中数组实现其他语言(如python
)中的元组、列表和字典等功能。本文将对PHP内核中对数据类型的一些简单内容进行讲解。
PHP内核中保存数据的结构体
结构体zval是保存PHP代码中变量的值,而zend_value则是保存了实际的值。结构体定义在zend_types.h头文件中。
typedef union _zend_value {
zend_long lval; /* long value */
double dval; /* double value */
zend_refcounted *counted;
zend_string *str;
zend_array *arr;
zend_object *obj;
zend_resource *res;
zend_reference *ref;
zend_ast_ref *ast;
zval *zv;
void *ptr;
zend_class_entry *ce;
zend_function *func;
struct {
uint32_t w1;
uint32_t w2;
} ww;
} zend_value;
struct _zval_struct {
zend_value value; /* value */
union {
uint32_t type_info;
struct {
ZEND_ENDIAN_LOHI_3(
zend_uchar type, /* active type */
zend_uchar type_flags,
union {
uint16_t extra; /* not further specified */
} u)
} v;
} u1;
union {
uint32_t next; /* hash collision chain */
uint32_t cache_slot; /* cache slot (for RECV_INIT) */
uint32_t opline_num; /* opline number (for FAST_CALL) */
uint32_t lineno; /* line number (for ast nodes) */
uint32_t num_args; /* arguments number for EX(This) */
uint32_t fe_pos; /* foreach position */
uint32_t fe_iter_idx; /* foreach iterator index */
uint32_t property_guard; /* single property guard */
uint32_t constant_flags; /* constant flags */
uint32_t extra; /* not further specified */
} u2;
};
zval
struct zval
一共有三个成员变量(value,u1,u2
),大小为16Bytes。
value
保存了变量中实际的值。u1
用于标识数据类型u2
用于额外的操作需要(如用于hash冲突链表的next,用于函数参数的初始化cache_slot,快速调用FAST_CALL的opline_num,用于抽象语法树节点行号的lineno,用于执行的参数个数,用于foreach的位置,用于foreach的迭代器索引,单一类型保护,常量标识)
zend_value
是一个共同体,大小是8Bytes,可以保存整形,浮点型数据。还有引用计数,字符串、数组、实例、资源、引用等指针。还包括内核自用的类结构体和函数结构体。
1.整形(integer)
在PHP内核中整形保存在zend_value中的zend_long,为有符号的64位数字。对于整形来说没有引用计数,在内核编译的过程中和执行过程中,整形可以直接释放。
2.浮点型(float|double)
浮点型根据服务器所采用的标准(IEEE 754标准
)不同有不同的取值。与整形不同的是,浮点型采用的是二进制科学计数法进行表示(
(
+
0.875
)
10
=
(
+
1.11
∗
2
−
1
)
2
(+0.875)_{10} = (+1.11 * 2 ^{-1})_{2}
(+0.875)10=(+1.11∗2−1)2)。
一个浮点数(Value
)可以表示为:
Value = sign * exponent * fraction
sign
是符号位,表示浮点数的正负,占一位二进制位。exponent
是二进制科学计数法的指数位(俗称指数域
)。在单精度float标准下占8位二进制位,在双精度浮点数在占11位二进制位。以指数偏移量的形式存储,固定偏移值为 2 e − 1 − 1 2^{e-1}-1 2e−1−1,在单精度下为 2 8 − 1 − 1 = 127 2^{8-1}-1 = 127 28−1−1=127。如指数实际值为 1 2 ( 10 ) 12_{(10)} 12(10),则在单精度浮点数中的指数域偏码值为 12 + 127 = 139 12 + 127 = 139 12+127=139fraction
是浮点数中的尾数。分数 (fraction) 部分最高有效位(即整数字)是 1 1 1。如上面的例子,在浮点数的尾数域中只保存小数点之后的二进制位。
所以上面的 1.11 ∗ 2 − 1 1.11 * 2^{-1} 1.11∗2−1可以实际表示为:
0 ∣ 01111110 ∣ 11000000000000000000000 0|01111110|11000000000000000000000 0∣01111110∣11000000000000000000000
符号域为 0 0 0表示正数
指数域为 01111110 01111110 01111110 = 12 6 ( 10 ) 126_{(10)} 126(10),实际的值为 126 − 127 = − 1 126 - 127 = -1 126−127=−1
尾数域为 11000000000000000000000 11000000000000000000000 11000000000000000000000,实际可以表示为 1.11000000000000000000000 1.11000000000000000000000 1.11000000000000000000000
以上是十进制的小数转换成二进制的科学计数法的计算过程。最后都可以用 1. p ∗ 2 ( 2 ) e 1.p*2^{e}_{(2)} 1.p∗2(2)e来表示。
但是不可避免的,对于浮点数的存储位有限,对于有无限位数尾数的情况,必定会产生一定精度的损失。
浮点数的有效位
单精度和双精浮点数的有效数字分别是有存储的23位和52个位,加上最左边没有存储的第1个位,即是24和53个位。
l o g 2 24 = 7.22 log2^{24} = 7.22 log224=7.22
l o g 2 53 = 15.95 log2^{53} = 15.95 log253=15.95
由以上计算,单精和双精浮点数可以保证7位和15位十进制有效数字。
十进制精度
9位十进制浮点数才能保证近似转化为32比特浮点再近似转化回9位十进制浮点后保持值不变,双精浮点数则为17位。
3.字符串(string)
字符串结构体
struct _zend_string {
zend_refcounted_h gc;
zend_ulong h; /* hash value */
size_t len;
char val[1];
};
成员变量:
- gc:引用计数,保存了字符串被引用的次数和字符串本身的数据类型type。
- h:hash值,通过time33算法计算出的字符串的hash值,大部分用于数组操作。
- len:字符串长度,在字符串操作中不需要要再重复计算字符串的长度。
- val[1]:字符串数组占位符,用来表示字符串开始的位置。
在内核初始化时,会生成一份hashTable用来保存内部字符串。内部字符串是在程序编译时可以确认的字符串,如类名、函数名、变量名和常量字符串等。在初始化时就已经加入了1-256的数字字符串。
<?php
class test{
public $info;
public function hello(){
}
}
$a = new test();
$b = "bye";
$c = $b."!";
$d = "test";
>
如上面这段代码,在内核编译时,test
、hello
、a
、b
、c
、bye
、!
这些字符串都是可以直接确定,而$c所指向的字符串bye!
则需要在执行时才能确认。
zend内核将这两种情况产生的字符串分开处理,内部字符串在编译时会加入到全局字符串表interned_strings
中,相同的字符串指向相同的内存地址,如类test的类名与变量$d所指向的字符串是相同,所以他们指向的是同一个内存地址。
内部字符串在程序结束才会释放。
json_encode(str,option,depth)
在option的值为JSON_UNESCAPED_UNICODE时才会解析unicode字符,不然都是暴露出除ASCII码外的unicode字符,且该范围只包括基本平面,不包括增补平面。
utf8和UTF-8
uft8是MYSQL里的字符集,表示的是使用三个字节UTF-8进行编码的unicode字符集,只能表示基本平面,即65536个字符。包括几乎所有国家和地区的文字,不包括大部分制表符、特殊符号和EMOJI表情包。
UTF-8是UNICODE字符集的编码方式,unicode字符分为0-16一共17个平面,每个平面包含65536个编码,每个字符对应一个编码。平面0称为字符集的基本面,保存了几乎所有文字。而增补平面(1-16)通常用来标识制表符、特殊符号和EMOJI表情包。
最近发布的Unicode 14.0,共计144,697个字符。
utf8和utf8mb4
utf8和utf8mb4都是MYSQL的字符集。utf8mb4支持四字节UTF-8编码的unicode字符,所以utf8mb4支持增补平面。但utf8mb4是否支持对应的unicode字符由机器所使用的unicode版本决定。
在MYSQL5.6版本中,如果sql语句包含不支持的unicode字符(如emoji表情,特殊字符等),会在该字符处截断sql语句。而在MYSQL5.7及以上版本中,会报ERROR 1366(HY000):INCORRECT STRING VALUE :
。如emoji表情🈶,用unicode来表示为U+1F236
,如果MYSQL所在的机器的unicode版本不支持这个emoji字符,则会报错。
二进制安全
在C语言中,字符串是以’\0’结尾。所以在一串字符串中如果存在’\0’,在对这个字符串计算长度时会以第一个’\0’为界限。
如字符串’abc\0abc\0’,字符串的长度为3而不是8。这样在字符串的操作(如拼接、查找)中都是不安全的。
而在结构体中记录字符串的长度可以避免这个问题。
数组(hashTable)
typedef struct _Bucket {
zval val;
zend_ulong h; /* hash value (or numeric index) */
zend_string *key; /* string key or NULL for numerics */
} Bucket;
typedef struct _zend_array HashTable;
struct _zend_array {
zend_refcounted_h gc;
union {
struct {
ZEND_ENDIAN_LOHI_4(
zend_uchar flags,
zend_uchar _unused,
zend_uchar nIteratorsCount,
zend_uchar _unused2)
} v;
uint32_t flags;
} u;
uint32_t nTableMask;
union {
uint32_t *arHash; /* hash table (allocated above this pointer) */
Bucket *arData; /* array of hash buckets */
zval *arPacked; /* packed array of zvals */
};
uint32_t nNumUsed;
uint32_t nNumOfElements;
uint32_t nTableSize;
uint32_t nInternalPointer;
zend_long nNextFreeElement;
dtor_func_t pDestructor;
};
PHP用数组实现了其他语言中(元组,列表,字典等)数据结构的功能。尤其是在PHP8.0中,ZEND内核为数组增加了一个flag成员变量用来标识数组是否需要通过hash取值。
PHP数组的一个重要的特点是,在数据存储上是连续。对于hashTable来说,如何解决hash冲突是非常关键的,我们不仅需要在hash算法中,尽量使索引具有足够的离散性,还需要对产生hash冲突的数据进行处理。通常的解决办法是通脱开放地址和拉链式。拉链式是解决冲突的比较好的方法,但是会造成数据存储上的不连续。而开放地址法虽然能够让数据存储在连续的内存上,但是相对拉链式会降低取值的效率。
PHP为了实现内存的连续采取了个折中的办法。通过额外的数组来保存拉链的起始索引。这样做的好处是内存释放的时候只要调用一次free就够了。而对应拉链式需要遍历每一个元素单独进行释放。
数组重构,当数组的元素满的时候(指的是占用了最后一个数组位置
),会申请一份本身内存的两倍的内存用于数组的扩容并rehash。
实例(Object)
struct _zend_object {
zend_refcounted_h gc;
uint32_t handle; // TODO: may be removed ???
zend_class_entry *ce;
const zend_object_handlers *handlers;
HashTable *properties;
zval properties_table[1];
};
成员变量包括引用计数gc,类的编译结构ce,实例操作函数表(获取方法,获取类名等)handlers,类的成员变量数组properties。
序列化与反序列化
<?php
class a{
public $c = 12;
protected $e = 10;
private $d = 11;
public function test(){
echo "111";
}
public function __sleep(){
return ["c","e"];
}
}
$a = new a();
var_dump($a);
$b = serialize($a);
$c = unserialize($b);
var_dump($b);
var_dump($c);
$c->test();
serialize()函数将变量转化成字符串的形式。以字母开头来标识变量的类型,例如:N(表示变量是空的),R:(表示变量是引用类型),b:0(表示false),b:1(表示true),d:(表示double),s:(表示字符串变量),E:(表示枚举类),O:(表示实例);
在PHP7以上中,在序列化时会截断序列化字符串("O:1:"a":2:{s:1:"c";i:12;s:4:"
),这是因为在类的编译结构中,存储private和protected变量时,会在变量名的头部插入"\000"。在反序列化时为空实例。可以通过__sleep()魔术方法返回公共变量来规避这个问题。字符串变为("O:1:"a":1:{s:4:"var1";i:12}"
),unserialize()之后为正常的实例。
在PHP5.4则可以正常显示为("O:1:"a":2:{s:1:"c";i:12;s:4:"\000*\000e";i:10;}"
),可以正常反序列化。