PHP内核--数据类型

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.1121)2)。

一个浮点数(Value)可以表示为:
Value = sign * exponent * fraction

  • sign 是符号位,表示浮点数的正负,占一位二进制位。
  • exponent 是二进制科学计数法的指数位(俗称指数域)。在单精度float标准下占8位二进制位,在双精度浮点数在占11位二进制位。以指数偏移量的形式存储,固定偏移值为 2 e − 1 − 1 2^{e-1}-1 2e11,在单精度下为 2 8 − 1 − 1 = 127 2^{8-1}-1 = 127 2811=127。如指数实际值为 1 2 ( 10 ) 12_{(10)} 12(10),则在单精度浮点数中的指数域偏码值为 12 + 127 = 139 12 + 127 = 139 12+127=139
  • fraction 是浮点数中的尾数。分数 (fraction) 部分最高有效位(即整数字)是 1 1 1。如上面的例子,在浮点数的尾数域中只保存小数点之后的二进制位。

所以上面的 1.11 ∗ 2 − 1 1.11 * 2^{-1} 1.1121可以实际表示为:

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 126127=1

尾数域为 11000000000000000000000 11000000000000000000000 11000000000000000000000,实际可以表示为 1.11000000000000000000000 1.11000000000000000000000 1.11000000000000000000000

二进制小数
二进制小数

以上是十进制的小数转换成二进制的科学计数法的计算过程。最后都可以用 1. p ∗ 2 ( 2 ) e 1.p*2^{e}_{(2)} 1.p2(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";
>

如上面这段代码,在内核编译时,testhelloabcbye!这些字符串都是可以直接确定,而$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;}"),可以正常反序列化。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值