php 边学边写差不多一年多点,php 这种弱类型语言与之前接触的 c、java、as3 等语言还是挺不一样的,现在觉得很庆幸的是从 c 开始学编程,无论数据类型还是指针也好,至少有个基础的概念。
在 php 数据类型上踩了不少坑,也学到了一些东西,在这里分享一下,看源码可能会很枯燥,不过了解一些底层实现就好,后面不要再踩坑。
之前在网上看到有比较热的帖子说:PHP 的 ip2long 有 bug,请慎用?于是看了下描述,大致如下
- <?php
- echo ip2long('58.99.11.1'),"<br/>"; //输出是979569409
- echo ip2long('58.99.011.1'),"<br/>"; //输出是979568897
- echo ip2long('058.99.11.1'),"<br/>";
- //输出是空
看上面看似 "一样" 的 IP 地址,输出的结果 "竟然" 不一样。于是那个帖子得出结论:在 PHP 4.x,5.x 中, 有前导零的 ip 转换的结果都不正确。
这货真的懂编程语言,真的懂数据类型么?
源码不贴了,在
文件中(5.3.28),无非就是直接调用 c 函数
- ext/standard/basic_functions.c
或者
- inet_pton
,然后调用 ntohl 转换一下字节序。不用多说,011 有前导 0 表示 8 进制,于是 011 就变成了十进制 9,所以 58.99.11.1 与 58.99.011.1 是不一样的,既然是 8 进制,绝不可能出现 8 吧,所以 058.99.11.1 不合法,当然也没办法转换为 long,手册里写了,invalid 会返回 false,echo false 当然显示为空,但是人家是 false~ 所以没 bug 的。
- inet_addr
最大的值取决于操作系统。32 位系统最大带符号的 integer 范围是 - 2147483648 到 2147483647。举例,在这样的系统上,
会返回 2147483647。64 位系统上,最大带符号的 integer 值是 9223372036854775807。
- intval('1000000000000')
- $i = intval('2355200853');
- $j = intval(2355200853);
- var_dump($i);
- var_dump($j);
- int(2147483647) int(-1939766443)
intval 源码最终调用的是
函数,简单贴下部分源码(
- convert_to_long_base
):
- Zend/zend_operators.c
- switch (Z_TYPE_P(op)) {
- case IS_NULL:
- Z_LVAL_P(op) = 0;
- break;
- case IS_RESOURCE: {
- TSRMLS_FETCH();
- zend_list_delete(Z_LVAL_P(op));
- }
- /* break missing intentionally */
- case IS_BOOL:
- case IS_LONG:
- break;
- case IS_DOUBLE:
- Z_LVAL_P(op) = zend_dval_to_lval(Z_DVAL_P(op));
- break;
- case IS_STRING:
- {
- char *strval = Z_STRVAL_P(op);
- Z_LVAL_P(op) = strtol(strval, NULL, base);
- STR_FREE(strval);
- }
- break;
- case IS_ARRAY:
- tmp = (zend_hash_num_elements(Z_ARRVAL_P(op))?1:0);
- zval_dtor(op);
- Z_LVAL_P(op) = tmp;
- break;
可以比较清晰的看到各种类型数据转换的结果,这里关注下 double 和 string。如果类型是 IS_DOUBLE 使用了
宏,这个宏在
- zend_dval_to_lval
中定义了,主要的含义就是
- zend _operators.h
- # define zend_dval_to_lval(d) ((long) (d))
实际上这个宏还有其他分支,不过意思大致如此,对于 long 型已经溢出的 double 强转为 long,结果与 c 中一样,溢出了。
如果类型是 IS_STRING,直接调用 c 函数 strtol,这个函数功能是:如果字符串中的整数值超出 longint 的表示范围(上溢或下溢),则 strtol 返回它所能表示的最大(或最小)整数。所以 php 的 intval 也就拥有了这些行为。
- var_dump(in_array(0, array('s')));
- var_dump(0 == "string");
- var_dump("1111" == "1112");
- var_dump("111111111111111111" == "111111111111111112");
- $str = 'string';
- var_dump($str['aaa']);
- 32位bool(true) bool(true) bool(false) bool(true) string(1) "s"
- 64位bool(true)bool(true)bool(false)bool(false)string(1) "s"
上面是很多人会对 php 弱类型举的一些例子,我加上了 32 位和 64 位的结果。
首先,每个基本上都基于 php 比较时的类型转换,是比较基础的知识。很多人看到这些结果也都会有点感慨~
- var_dump("111111111111111111" == "111111111111111112");
== 这个比较操作符,在比较两个字符串的时候,核心调用方法为
- ZEND_IS_EQUAL=>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 < lval2 ? -1 : 0));
- }
- } else {
- string_cmp: Z_LVAL_P(result) = zend_binary_zval_strcmp(s1, s2);
- ZVAL_LONG(result, ZEND_NORMALIZE_BOOL(Z_LVAL_P(result)));
- }
- }
其中
是
- is_numeric_string
中的一个 inline 函数,判断字符串是不是数字,并且返回 IS_LONG 或者 IS_DOUBLE 类型,其中决定是 long 还是 double 比较关键的点是源码中的
- zend_operators.h
,那么
- digits >= MAX_LENGTH_OF_LONG
又是个什么东西? 在 zend.h 中有这个宏定义
- MAX_LENGTH_OF_LONG
- #if SIZEOF_LONG == 4
- #define MAX_LENGTH_OF_LONG 11
- static const char long_min_digits[] = "2147483648";
- #elif SIZEOF_LONG == 8
- #define MAX_LENGTH_OF_LONG 20
- static 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 <stdio.h>
- 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.000000
- 1
- 2.000000
- 0
在 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, 4343bcbf
- 936b38e4, 4343bcbf
- 936b38e4, 4343bcbf
- 936b38e5, 4343bcbf
可以看到尾数为 4 的那位不太一样,结合上面,这就是为什么
在 32 位机器结果为 true 的原因,4 字节溢出转成 double,然后相减不精确了,变成了 0,导致相等。64 位机器因为没溢出,所以为 false。
- var_dump("111111111111111111" == "111111111111111112");
在 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 在文档上有写明,如果 key 为整数,merge 后 key 会成为按照自然数重新排列
例如
- <?php
- $a = array(5 => 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 这样代替。
的时候如果字符串 key 相同,$b 会覆盖 $a,如果 key 为 32 位或者 64 位 long 整数范围内,则不会覆盖,因为实现的时候是简单的遍历覆盖插入 hashtable。
- array_merge($a, $b)
array+array 如果 key 相同,是保留前者,抛弃后者。
我很庆幸第一门语言学的是 c 语言,虽然本科懵懂的简单代码写的挺溜,各种技术了解比较少,但是有了 c 语言及一些 c++ 的基础,研究其他语言还是会容易很多,能够揣摩到一些底层实现原理,当然底层原理还是要再深入的学习。
来源: