踩一坑,採一金之php數據類型那點“破”事

   

    學海無涯,乘舟以渡之~

    php邊學邊寫差不多一年多點,php這種弱類型語言與之前接觸的c、java、as3等語言還是挺不一樣的,現在覺得很慶幸的是從c開始學編程,無論數據類型還是指針也好,至少有個基礎的概念。

    在php數據類型上踩了不少坑,也學到了一些東西,在這裏分享一下,看源碼可能會很枯燥,不過了解一些底層實現就好,後面不要再踩坑。

序、

    之前在網上看到有比較熱的帖子說:PHPip2longbug,請慎用?於是看了下描述,大致如下

<?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有前導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的。

     注:Ip2long對於部分ip32位會溢出,所以使用時一般使用sprintf(“%u”,),注意一下就好了

一、intval

    最大的值取決於操作系統。32位系統最大帶符號的integer範圍是-21474836482147483647。舉例,在這樣的系統上,intval('1000000000000') 會返回214748364764位系統上,最大帶符號的integer值是9223372036854775807
$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");
   我很好奇的是這兩個字符串比較爲什麼位true,當然在32位和64位機器結果不同,顯然與轉整型有關,在網上沒看到其他人有解釋,於是搜尋了下源碼相關。大致如下:

   ==這個比較操作符,在比較兩個字符串的時候,核心調用方法爲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是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 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的那位不太一樣,結合上面,這就是爲什麼
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));

   輸出是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++的基礎,研究其他語言還是會容易很多,能夠揣摩到一些底層實現原理,當然底層原理還是要再深入的學習。




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章