php边学边写差不多一年多点,php这种弱类型语言与之前接触的c、java、as3等语言还是挺不一样的,现在觉得很庆幸的是从c开始学编程,无论数据类型还是指针也好,至少有个基础的概念。
在php数据类型上踩了不少坑,也学到了一些东西,在这里分享一下,看源码可能会很枯燥,不过了解一些底层实现就好,后面不要再踩坑。
序、
之前在网上看到有比较热的帖子说:php的ip2long有bug,请慎用?于是看了下描述,大致如下
is_equal_function=>compare_function=>zendi_smart_strcmp
然后贴下zendi_smart_strcmp的源码,不是很长
zend_api void zendi_smart_strcmp(zval *result, zval *s1, zval *s2) /* {{{ */{ int ret1, ret2; long lval1, lval2; double dval1, dval2; if ((ret1=is_numeric_string(z_strval_p(s1), z_strlen_p(s1), &lval1, &dval1, 0)) && (ret2=is_numeric_string(z_strval_p(s2), z_strlen_p(s2), &lval2, &dval2, 0))) { if ((ret1==is_double) || (ret2==is_double)) { if (ret1!=is_double) { dval1 = (double) lval1; } else if (ret2!=is_double) { dval2 = (double) lval2; } else if (dval1 == dval2 && !zend_finite(dval1)) { /* both values overflowed and have the same sign, * so a numeric comparison would be inaccurate */ goto string_cmp; } z_dval_p(result) = dval1 - dval2; zval_long(result, zend_normalize_bool(z_dval_p(result))); } else { /* they both have to be long's */ zval_long(result, lval1 > lval2 ? 1 : (lval1
其中is_numeric_string是zend_operators.h中的一个inline函数,判断字符串是不是数字,并且返回is_long或者is_double类型,其中决定是long还是double比较关键的点是源码中的digits >= max_length_of_long,那么max_length_of_long又是个什么东西?
在zend.h中有这个宏定义
#if sizeof_long == 4#define max_length_of_long 11static const char long_min_digits[] = 2147483648;#elif sizeof_long == 8#define max_length_of_long 20static const char long_min_digits[] = 9223372036854775808;#else#error unknown sizeof_long#endif
大致明白了,对于32位机器long型是4字节,64位机器long型是8字节,原来差别在这里!当然也预定义了个长度,11和20两个我觉得挺magic的number。
好,上面那个那么多个1的字符串在32位机器上显然就是is_double了,接下来有个分支zend_finite判断是否是有限值,其实这些现在看都不是很重要,最重要的一句话是
z_dval_p(result) = dval1 - dval2;zval_long(result, zend_normalize_bool(z_dval_p(result)));
其中zend_normalize_bool宏是用来标准化bool值的
#define zend_normalize_bool(n) \ ((n) ? (((n)>0) ? 1 : -1) : 0)
好,dval1-dval2究竟是什么呢,这时要想到double型的有效位数了,c里double型有效位数大概16位,上面那个字符串是18个1,已经超出了有效位数,做减法已经不会准确了,这里不想去深究double型的表示,简单用c语言展示一下。
#include int main() {double a = 11111 11111 11111 12.0l;double b = 11111111111111111.0l;double c= 11111111111111114.0l;printf(%lf , a-b);printf(%d , a-b == 0);printf(%lf , c-b);printf(%d , c-b == 0);}
对于这样一个c程序,输出结果为
0.00000012.0000000
在32位机器与64位机器上相同,因为double型都是8字节。
可以试一下,尾数1、2、3相减都是0,到了尾数为4才会发生变化,结果也不精确,下面看下内存中表示:
double c = 11111111111111111.0l;double d = 11111111111111112.0l;double e = 11111111111111113.0l;double f = 11111111111111114.0l;double *p = &c;printf(%x, %x\n , ((int *)p)[0], ((int *)p)[1]);p = &d;printf(%x, %x\n , ((int *)p)[0], ((int *)p)[1]);p = &e;printf(%x, %x\n , ((int *)p)[0], ((int *)p)[1]);p = &f;printf(%x, %x\n , ((int *)p)[0], ((int *)p)[1]);
其实就是将double型强转位int数组,然后转16进制输出,结果为:
936b38e4, 4343bcbf936b38e4, 4343bcbf936b38e4, 4343bcbf936b38e5, 4343bcbf
可以看到尾数为4的那位不太一样,结合上面,这就是为什么
var_dump(111111111111111111 == 111111111111111112);
在32位机器结果为true的原因,4字节溢出转成double,然后相减不精确了,变成了0,导致相等。64位机器因为没溢出,所以为false。
三、array_flip
在32位机器上,使用企业qq号码做关联数组key的时候,需要注意大于21亿的问题
32位$a = array(2355199999 => 1, 2355199998 => 1);var_dump($a);array(2) { [-1939767297]=> int(1) [-1939767298]=> int(1) } $b = array(2355199999, 2355199998);var_dump($b);array(2) { [0]=> float(2355199999) [1]=> float(2355199998) } var_dump(array_flip($b));warning: array_flip() can only flip string and integer values!$c = array();foreach($b as $key => $value) { $c[$value] = $key;}var_dump($c);
因为key只能为string或者interger,在32位机器上,大于21亿就成为了float,所以如果强行拿float去做key,会溢出变成类似负数等等~这里如果将大于21亿的数加上引号才可以
四、array_merge
简单说下,array_merge在文档上有写明,如果key为整数,merge后key会成为按照自然数重新排列
例如
5, 7 => 4);$b = array(1 => 1, 9 => 9);var_dump(array_merge($a, $b));
输出是array(4) { [0]=> int(5) [1]=> int(4) [2]=> int(1) [3]=> int(9)}
源码实现比较简单,我也看过,就是碰到整数就使用nextindex,碰到字符串就正常insert。
于是在32位机器上,如果key大于21亿的话,array_merge不会将key使用nextindex变成自然数重新排,在64位机上当然大于21亿也没有用~
所以如果key为整数,合并数组的时候可以使用array+array这样代替。
array_merge($a, $b)的时候如果字符串key相同,$b会覆盖$a,如果key为32位或者64位long整数范围内,则不会覆盖,因为实现的时候是简单的遍历覆盖插入hashtable。
array+array如果key相同,是保留前者,抛弃后者。
结、
我很庆幸第一门语言学的是c语言,虽然本科懵懂的简单代码写的挺溜,各种技术了解比较少,但是有了c语言及一些c++的基础,研究其他语言还是会容易很多,能够揣摩到一些底层实现原理,当然底层原理还是要再深入的学习。