ホームページ >バックエンド開発 >PHPチュートリアル >罠にはまって、PHP データ型の「壊れた」点を見つけてください。
学習に制限はありません。ただボートに乗って渡ってください~
私はほぼ 1 年にわたって PHP を学び、書いてきました。PHP は弱い型付け言語ですが、それでも C 言語とはまったく異なります。 Java、AS3、およびこれまでに接してきた他の言語も同様に、C からプログラミングを学び始めたことがとても幸運だと感じています。データ型やポインターに関係なく、少なくとも基本的な概念は持っています。
私は PHP データ型で多くの落とし穴を経験し、いくつかのことを学びました。ソース コードを見るのは退屈かもしれませんが、基礎となる実装のいくつかを理解するのは良いことです。後で再び落とし穴に足を踏み入れないでください。
まえがき、
インターネット上で比較的人気のある次のような投稿を目にしました: PHP の ip2long にはバグがあるので、注意して使用してください。そこで説明を読んでみたところ、おおよそ次のような内容です
<?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 変換の結果は正しくありません。
この男は本当にプログラミング言語とデータ型を理解していますか?
ext/standard/basic_functions.c ファイル (5.3.28) では、c 関数 inet_pton または inet_addr を直接呼び出してから、ntohl を呼び出してバイト順序を変換するだけで、ソース コードは公開されなくなりました。言うまでもなく、011 には 8 進数を表すために先頭に 0 が付いているため、011 は 10 進数の 9 になります。したがって、58.99.11.1 は 58.99.011.1 とは異なります。8 進数であるため、8 が現れるはずがないため、058.99 .11.1 は不正です。そしてもちろんlongに変換する方法はありません。invalidはfalseを返し、echo falseは当然空と表示されるとマニュアルに書いてありますが、falseですよ~なのでバグはありません。
注: Ip2long は一部の IP では 32 ビットでオーバーフローするため、使用する場合は一般的に sprintf("%u",) が使用されます。注意してください。 1. intval
$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;ここでは、さまざまな種類のデータ変換の結果をより明確に確認できます。二重と文字列。タイプが IS_DOUBLE の場合、zend_dval_to_lval マクロが使用されます。このマクロは zend _operators.h で定義されています。実際には、このマクロには他の分岐もありますが、意味はほぼ同じです。 long 型は強制的に long に変換され、結果は c のオーバーフローと同じになります。
型が IS_STRING の場合、C 関数 strtol を直接呼び出します。この関数の機能は次のとおりです。文字列内の整数値が倍長整数の表現範囲 (オーバーフローまたはアンダーフロー) を超える場合、strtol は表現できる最大値を返します。 (または最小の) 整数。したがって、PHP の intval にもこれらの動作があります。
2. ==
# define zend_dval_to_lval(d) ((long) (d))上記は、PHP の弱い型についての例として、32 ビットと 64 ビットの結果を追加しました。
まず、それぞれは基本的にPHP比較時の型変換に基づいており、これは比較的基礎的な知識です。これらの結果を見て、多くの人は少し感情的になるでしょう~
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"2 つの文字列が true と比較される理由が非常に興味深いです。 もちろん、32 ビット マシンと 64 ビット マシンでは結果が異なるのは明らかです。整数への変換に関する情報はインターネット上になかったので、関連するソースコードを検索しました。それはおおよそ次のとおりです:
次に、zendi_smart_strcmp のソース コードを貼り付けます。あまり長くありません
var_dump("111111111111111111" == "111111111111111112");
zend.h にはこのマクロ定義があります
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))); }}
32 ビット マシンの場合、long 型は 4 バイト、64 ビット マシンの場合、long 型は 8 バイトであることがわかります。違いはここにあることがわかりました!もちろん、11 と 20 というあらかじめ定義された長さもあり、これは魔法の数字だと思います。
さて、上記の 1 がたくさんある文字列は、32 ビット マシンでは明らかに IS_DOUBLE です。次に、それが制限された値であるかどうかを判断するブランチ zend_finite があります。実際、これらはそれほど重要ではありません。文は
#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ここで、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.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会成为按照自然数重新排列
例如
<?php$a = array(5 => 5, 7 => 4);$b = array(1 => 1, 9 => 9);var_dump(array_merge($a, $b));
源码实现比较简单,我也看过,就是碰到整数就使用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++的基础,研究其他语言还是会容易很多,能够揣摩到一些底层实现原理,当然底层原理还是要再深入的学习。