PHP作为“世界上最好的语言”,我们都知道php是弱类型语言,即在使用过程中,可以任意改变变量的类型。这对于代码中的灵活性有极大的方便。php底层是由c语言去实现的,那么c语言作为强类型语言,是怎么实现php的这些特性?
变量的定义
我们先从变量的定义开始。在百度百科上,变量来源于数学,是计算机语言中能储存计算结果或能表示值抽象概念。变量可以通过变量名访问。意思就是说,变量是来存储值的。那么我们来看看在php中,变量的定义。
//php
//定义了变量名为a 变量值为 整数 1
$a=1;
//输出$a的值
echo $a;
//将变量值 字符串 aaaa 赋给了变量名a
$a='aaaa';
在php中,变量会在首次为其赋值时被创建,我们在使用中,可以随意改变变量的类型。示例中的代码,简单两行就完成了,变量的定义赋值和重新赋值。而在c语言中,变量则需要先创建才能使用,并且需要严格定义类型。
在php中变量的值由zval来表示
php7中,zval的结构定义如下。zval由,value、u1和u2组成。
struct _zval_struct {
zend_value value; /* value */
union {
struct {
ZEND_ENDIAN_LOHI_4(
zend_uchar type, //存储具体类型
zend_uchar type_flags,
zend_uchar const_flags,
zend_uchar reserved) /* call info for EX(This) */
} v;
uint32_t type_info;
} u1;
union {
uint32_t next; /* hash collision chain */
uint32_t cache_slot; /* literal cache slot */
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 access_flags; /* class constant access flags */
uint32_t property_guard; /* single property guard */
uint32_t extra; /* not further specified */
} u2;
};
typedef union _zend_value {
zend_long lval; //整型
double dval; //浮点型
zend_refcounted *counted; //引用计数
zend_string *str; //字符串类型
zend_array *arr; //数组类型
zend_object *obj; //对象类型
zend_resource *res; //资源类型
zend_reference *ref; //引用类型
zend_ast_ref *ast; //抽象语法树
zval *zv; //zval类型
void *ptr; //指针类型
zend_class_entry *ce; //class类型
zend_function *func; //function类型
struct {
uint32_t w1;
uint32_t w2;
} ww;
} zend_value;
变量弱类型的实现
在zval中,value存储具体的值或者值的指针,u1存储变量类型的相关信息,u2则存储优化信息。php根据u1.type的值的变化,取value中对应类型的值的指针,从而完成变量类型的表达。
在上述php代码中
- $a=1;php生成新的zval,将zval.u1.v.type定义为整型,将1写入到zval.value.lval中。
- echo $a;PHP在变量哈希表中,找到变量名为a的键,拿到对应的zval,根据zval.u1.v.type的类型读取对应zval.value中的值或者是对应的指针类型。
- $a='aaa';php拿到变量a的zval,根据要赋予变量的值,来修改zval.u1.v.type的值,同时将zval.value中对应的存储字段进行赋值。
变量类型的转换
在php内核中定义了几个函数,当需要将变量转换成相同类型时,则会取调用对应的函数,进行变量的转换。比如在比较整型和字符串时,会根据规则,将整型转成字符串,或者是将字符串转成整型,在进行比较。
这里贴一下_zval_get_string_func 转换成字符串函数的源码
ZEND_API zend_string* ZEND_FASTCALL _zval_get_string_func(zval *op) /* {{{ */
{
try_again:
switch (Z_TYPE_P(op)) {
case IS_UNDEF:
case IS_NULL:
case IS_FALSE:
return ZSTR_EMPTY_ALLOC();//被初始化成空字符串
case IS_TRUE:
if (CG(one_char_string)['1']) {
return CG(one_char_string)['1'];
} else {
return zend_string_init("1", 1, 0);
}
case IS_RESOURCE: {//返回对应的资源号ID
char buf[sizeof("Resource id #") + MAX_LENGTH_OF_LONG];
int len;
len = snprintf(buf, sizeof(buf), "Resource id #" ZEND_LONG_FMT, (zend_long)Z_RES_HANDLE_P(op));
return zend_string_init(buf, len, 0);
}
case IS_LONG: {
return zend_long_to_str(Z_LVAL_P(op));
}
case IS_DOUBLE: {
return zend_strpprintf(0, "%.*G", (int) EG(precision), Z_DVAL_P(op));
}
case IS_ARRAY:
zend_error(E_NOTICE, "Array to string conversion");
return zend_string_init("Array", sizeof("Array")-1, 0);
case IS_OBJECT: {
zval tmp;
if (Z_OBJ_HT_P(op)->cast_object) {
if (Z_OBJ_HT_P(op)->cast_object(op, &tmp, IS_STRING) == SUCCESS) {
return Z_STR(tmp);
}
} else if (Z_OBJ_HT_P(op)->get) {
zval *z = Z_OBJ_HT_P(op)->get(op, &tmp);
if (Z_TYPE_P(z) != IS_OBJECT) {
zend_string *str = zval_get_string(z);
zval_ptr_dtor(z);
return str;
}
zval_ptr_dtor(z);
}
zend_error(EG(exception) ? E_ERROR : E_RECOVERABLE_ERROR, "Object of class %s could not be converted to string", ZSTR_VAL(Z_OBJCE_P(op)->name));
return ZSTR_EMPTY_ALLOC();
}
case IS_REFERENCE:
op = Z_REFVAL_P(op);
goto try_again;
case IS_STRING:
return zend_string_copy(Z_STR_P(op));
EMPTY_SWITCH_DEFAULT_CASE()
}
return NULL;
}
当你在php代码中,在一个值前(string) strval()或者是使用echo,将变量当作字符串处理时,就会用到这个函数。
- 当变量类型是未定义undefined、空null或者是布尔false时,返回一个初始化的空字符串。
- 当变量类型是布尔true,返回'1'
- 当变量类型是资源,则返回 'Resource id #'+资源id
- 当变量类型为整型,则返回这一串数字的字符串
- 当变量类型为浮点数,则返回这一串数字的字符串
- 当变量类型为数组,则返回字符串'Array'
- 当变量类型为对象,不可转换,并报错 'Object of class %s could not be converted to string'
- 当变量类型为引用,取出引用的zval,在继续转换
- 当变量类型为字符串,则输出字符串。
类似的函数还有 zval_get_double、zval_get_long
变量的存储
php的变量存储在hashtable上,当调用一个函数或者类的时候,会创建新的hashtable,这也就是为什么没法直接在函数内使用外部定义的变量。因为它们分属两个作用域,一个是当前的(局部变量),一个是全局的(全局变量)。在函数内怎么使用全局变量呢,通过globals 关键字,在创建新的hashtable的时候,将局部变量表的值链接全局变量表,从而达到了,在函数内使用全局变量。
变量的赋值和引用
赋值
对于整型、浮点型、布尔和NULL,由于占用空间小,在zval中直接存储。直接在进行赋值时,会创建2个zval。
字符串、数组、资源类型和对象会在赋值时,指向同一个value,等到变量的值被改变时,才会申请变量值的内存空间。
$a='1234';
$b=$a;
当将b时,变量b指向同一个字符串,并且zend_string的refcount 会记录当前被引用的次数。
引用
在php7中,引入了zend_reference来处理。使得即使变量同时被引用和赋值,在内存中只存有一份字符串。
通过在zend_reference中的zval来进行一次引用的转发。
struct _zend_reference{
zend_refcounted_h gc;
zval val;
}
$a = '1234';//$a->zend_string(type=IS_STRING,recount_gc=1,is_ref_gc=0);
$c=$a;// $c,$a-> zend_string (type=IS_STRING,recount_gc=2,is_ref_gc=0);
$b=&$a;// $b,$a-> zval (type=IS_REFERENCE,recount_gc=2);
//$c-> zend_string (type=IS_STRING,recount_gc=2,);