PHP中的变量如何实现的?
Mission Start!
一、PHP变量的实现---zval结构
我们都知道在PHP中一个变量可以用来存储任意的数据类型,但实现PHP的底层C语言却是强类型语言,那么在PHP中是如何实现这样一个可以改变数据类型的变量的呢?
在PHP内核中是通过一个叫zval的结构体来存储变量的,其定义在Zend/zend.h头文件中,其代码结构为:
PHP5
struct _zval_struct {
zvalue_value value; /* 变量的值 */
zend_uint refcount__gc; /* 引用计数 */
zend_uchar type; /* 变量的数据类型 */
zend_uchar is_ref__gc; /* 是否引用 */
};
typedef struct _zval_struct zval;
首先看代码中的zend_uint和zend_uchar类型,它们定义在Zend/zend_types.h中,分别是unsigned int、unsigned char:
typedef unsigned int zend_uint; // zend_uint => unsigned int
typedef unsigned char zend_uchar; // zend_uchar => unsigned char
而变量的值value的数据类型为zvalue_value,它是一个联合体(union,联合体详情请看第四节==>传送门),在Zend/zend.h中定义:
typedef union _zvalue_value {
long lval; /* long value */
double dval; /* double value */
struct {
char *val;
int len;
} str;
HashTable *ht; /* hash table value */
zend_object_value obj;
} zvalue_value;
- 其中lval用于存放整型
- dval用于存放浮点数据
- str是一个结构体,用于存放字符串,包括字符串的值和字符串的长度
- ht存放哈希表的指针(PHP中数组就是用哈希表实现的)
- obj是存放对象类型的
PHP7
struct _zval_struct {
zend_value value; // 变量值
union {
struct {
ZEND_ENDIAN_LOTH_4(
zend_uchar type,
zend_uchar type_flags,
zend_uchar const_flags,
zend_uchar reserverd)
} v;
uint32_t type_info;
} u1;
union {
uint32_t var_flags;
uint32_t next;
uint32_t cache_slot;
uint32_t lineno;
uint32_t num_args;
uint32_t fe_pos;
uint32_t fe_iter_idx;
} u2;
};
联合体u1:由一个结构体v和一个32位无符号整型type_info组成。ZEND_ENDIAN_LOTH_4宏用于解决字节序问题,忽略。v中定义了type用于表示value类型,type_flags是类型掩码,用于变量的内存管理。type_info实际上是将v结构的4各成员组合到一起,每个字节对应一个成员,共4个字节。
联合体u2:纯粹用于辅助功能,zval结构的value和u1共占用了12types,而系统会进行字节对齐,共占用16types,所以定义了辅助结构u2将4types进行利用。
typedef union _zend_value {
zend_long lval; // 整型
double dval; // 浮点型
zend_refcounted *counted; // 获取不同类型的gc头部
zend_string *str; // 字符串
zend_array *arr; // 数组
zend_object *obj; // 对象
zend_resource *res; // 资源类型
zend_reference *ref; // 引用类型
// 以下几个都是给内核使用的value
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;
二、变量的类型
PHP中有8大数据类型,分别为PHP中有8大数据类型,分别为null、布尔、整型、浮点型、字符串、数组、对象、资源(resource)。在PHP内核中是使用特定的常量对zval实例进行标识,以实现区分变量的不同类型:
常量 | 描述 |
---|---|
IS_NULL | 第一次使用的变量若未被初始化,则自动被赋予IS_NULL。这个类型的值只有一个,就是NULL |
IS_BOOL | 布尔类型的变量有两个值,true或者false。在PHP语言中,while、if等语句会自动的把表达式的值转成这个类型的。(在PHP7中BOOL类型直接使用type类型IS_TRUE、IS_FALSE来区分true、false,而在PHP旧版本中BOOL类型也是通过整型来进行区分的) |
IS_LONG | PHP语言中的整型,在内核中是通过所在操作系统的signed long数据类型来表示的。在最常见的32位操作系统中,它可以存储从-2147483648 到 +2147483647范围内的任一整数。有一点需要注意的是,如果PHP语言中的整型变量超出最大值或者最小值,它并不会直接溢出,而是会被内核转换成IS_DOUBLE类型的值然后再参与计算。再者,因为使用了signed long来作为载体,所以也就解释了为什么PHP语言中的整型数据都是带符号的了。 |
IS_DOUBLE | PHP中的浮点数据是通过C语言中的signed double型变量来存储的, 这最终取决与所在操作系统的浮点型实现。 我们做为程序猿,应该知道计算机是无法精准的表示浮点数的, 而是采用了科学计数法来保存某个精度的浮点数。 用科学计数法,计算机只用8位便可以保存2.225x10(-308)~~1.798x10308之间的浮点数。 用计算机来处理浮点数简直就是一场噩梦,十进制的0.5转成二进制是0.1, 0.8转换后是0.1100110011....。 但是当我们从二进制转换回来的时候,往往会发现并不能得到0.8。 我们用1除以3这个例子来解释这个现象:1/3=0.3333333333.....,它是一个无限循环小数, 但是计算机可能只能精确存储到0.333333,当我们再乘以三时, 其实计算机计算的数是0.333333*3=0.999999,而不是我们平时数学中所期盼的1.0. |
IS_STRING | PHP中最常用的数据类型——字符串,在内存中的存储和C差不多, 就是一块能够放下这个变量所有字符的内存,并且在这个变量的zval实现里会保存着指向这块内存的指针。 与C不同的是,PHP内核还同时在zval结构里保存着这个字符串的实际长度, 这个设计使PHP可以在字符串中嵌入‘\0’字符,也使PHP的字符串是二进制安全的, 可以安全的存储二进制数据!本着艰苦朴素的作风,内核只会为字符串申请它长度+1的内存, 最后一个字节存储的是‘\0’字符,所以在不需要二进制安全操作的时候, 我们可以像通常C语言的方式那样来使用它。 |
IS_ARRAY | 数组是一个非常特殊的数据类型,它唯一的功能就是聚集别的变量。 在C语言中,一个数组只能承载一种类型的数据,而PHP语言中的数组则灵活的多, 它可以承载任意类型的数据,这一切都是HashTable的功劳, 每个HashTable中的元素都有两部分组成:索引与值, 每个元素的值都是一个独立的zval(确切的说应该是指向某个zval的指针)。 |
IS_OBJECT | 和数组一样,对象也是用来存储复合数据的,但是与数组不同的是, 对象还需要保存以下信息:方法、访问权限、类常量以及其它的处理逻辑。 |
IS_RESOURCE | 有一些数据的内容可能无法直接呈现给PHP用户的, 比如与某台mysql服务器的链接,或者直接呈现出来也没有什么意义。 但用户还需要这类数据,因此PHP中提供了一种名为Resource(资源)的数据类型。 |
PHP内核提供三个宏来获取一个变量的数据类型:Z_TYPE()、Z_TYPE_P()、Z_TYPE_PP(),它们的参数分别为:zval、*zval、**zval。这三个宏定义在Zend/zend_operators.h中。
三、变量的值
PHP内核提供了三个基础宏来获取变量的值(与上述类型获取的宏参数一样):
类型 | 宏 |
---|---|
IS_BOOL | Z_BVAL、Z_BVAL_P、Z_BVAL_PP |
IS_DOUBLE | Z_DVAL、Z_DVAL_P、Z_DVAL_PP |
IS_STRING | 1. 获取值:Z_STRVAL、Z_STRVAL_P、Z_STRVAL_PP 2. 获取字符串(返回字符串地址):Z_STRLEN、Z_STRLEN_P、Z_STRLEN_PP |
IS_ARRAY | Z_ARRVAL、Z_ARRVAL_P、Z_ARRVAL_PP |
IS_OBJECT | 对象是一个复杂的结构体(zend_object_value结构体),不仅存储属性的定义、属性的值,还存储着访问权限、方法等信息。 内核中定义了以下组合宏让我们方便的操作对象: OBJ_HANDLE:返回handle标识符, OBJ_HT:handle表, OBJCE:类定义, OBJPROP:HashTable的属性, OBJ_HANDLER:在OBJ_HT中操作一个特殊的handler方法。 |
IS_RESOURCE | 资源型变量的值其实就是一个整数,可以用RESVAL组合宏来访问它,我们把它的值传给zend_fetch_resource函数,便可以得到这个资源的操作句柄。 |
四、变量符号表与作用域
变量符号表
PHP的变量符号表与zval值的映射,是通过HashTable实现的。如:
<?php
$var = 'test';
var的类型和值的zval结构存储在哈希表中。内核通过变量符号表与zval地址的哈希映射来实现PHP变量的存取。
作用域
按照作用域可将PHP变量分为全局变量和局部变量,每种作用域PHP都会维护一个符号表的HashTable。当在PHP中创建一个函数或者类时,zend engine就会创建一个新的符号表,表名函数或类中的变量是局部变量,这样就实现了局部变量的保护。当用户创建一个PHP变量时,zend engine会分配一个zval,并设置相应type和初始值,并将改变量加入到当前作用域的符号表。