踩一坑,采一金之php数据类型那点“破”事

学海无涯,乘舟以渡之~

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转换的结果都不正确。

这货真的懂编程语言,真的懂数据类型么?

源码不贴了,在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对于部分ip在32位会溢出,所以使用时一般使用sprintf(“%u”,),注意一下就好了

一、intval

最大的值取决于操作系统。32位系统最大带符号的integer范围是-2147483648到2147483647。举例,在这样的系统上,intval(‘1000000000000‘)
会返回2147483647。64位系统上,最大带符号的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++的基础,研究其他语言还是会容易很多,能够揣摩到一些底层实现原理,当然底层原理还是要再深入的学习。

踩一坑,采一金之php数据类型那点“破”事

时间: 2024-10-12 22:25:51

踩一坑,采一金之php数据类型那点“破”事的相关文章

初用react容易踩的坑

此文章同步连接: https://github.com/p2227/p2227.github.io/issues/3 # 初用react容易踩的坑 ## 自定义组件忘记大写第一个字母 ```javascript var myComp = React.createClass({ render: function() { return <div>Hello world</div>; } }); ReactDOM.render(<myComp />, mountNode);

Ubuntu中安装FTP 服务器自己踩得坑

12点多了,擦!做个码农真不容易呀! 系统:Ubuntu16.04 安装:FTP 步骤: 1.不管有没有一上来我先卸载: sudo apt-get purge vsftpd 2.再安装:sudo apt-get install vsftpd 3.创建ftp用户: 创建用户目录 sudo mkdir /home/uftp 创建用户 sudo useradd -d /home/uftp -s /bin/bash uftp 修改密码 sudo passwd uftp#这里会提示你输入二次密码 4配置v

使用CCNode作为容器容易踩的坑

Cocos2dx中CCNode经常作为一个父容器,里面装一些UI控件,最后组成一个复杂的自定义的UI控件,但是在使用别人的自定义控件和自己写自定义问题的时候会踩一些坑. 首先拿到一个自定义的UI控件一定要明确他的position是在控件的什么位置,即锚点位置,这样才能知道改如何取设置position. 其次知道父容器区域是不是全包含子容器的,因为ccnode里面的子节点的范围是可以超出你设定的ccnode的区域的.原因在于ccnode可以没有区域,这个跟winform桌面编程的Panel有差别,

Asp.Net Core中使用Swagger,你不得不踩的坑

很久不来写blog了,换了新工作后很累,很忙.每天常态化加班到21点,偶尔还会到凌晨,加班很累,但这段时间,也确实学到了不少知识,今天这篇文章和大家分享一下:Asp.Net Core中使用Swagger,你不得不踩的坑. 这篇文章着重讲几点: swagger 跨层注释问题 swagger Get请求传多个参数的问题 swagger Enum 注释问题 swagger api文档版本控制 第一步:搭建一个webapi项目或者mvc项目,引入swagger nuget 我创建项目,习惯性的先创建一个

必读丨新手程序员最容易踩的“坑”,你踩过几个?

人生,就是一边踩"坑",一边上升的过程.而程序员的一生,不仅要改无数的BUG,也要越过很多的"坑".今天,小千为大家分享一些开发人员常见的"坑",希望同学们能够从中受益. 1.重新实现API中已有的代码 大多数开发人员都会利用某种框架来减轻工作的负担.对于没有使用该框架经验的开发人员来说,掌握框架的API提供的所有功能非常困难. 因此,他们常常会重新实现API中已有的某些代码.没有经验的开发人员更有可能踩这个坑的原因有两个:? 第一,由于缺乏经验

mobile web曾经的踩过坑

兼容性一直是前端工程师心中永远的痛.手机浏览器,因为基本是webkit(blink)内核当道,很多公司,不用考虑IE系的浏览器,所以感觉兼容性上的问题可能会少一些. 但是手机端,虽然出了很多工具,但是调试依然比PC端麻烦很多.而且很多坑是因为手机浏览器本身的bug,一旦出现,相应的解决方案很难根据以前的经验进行推测.只能寄希望于谷歌 + 猜. 这里记录一下我做手机端浏览器曾经踩过的坑.之所以用”曾经”,随着版本的更新,有些问题没了. 另外我(我司)没有足够的人力和物力对很多手机浏览器进行测试,我

从剑桥走出的创业公司,要踩多少坑才能成长为估值10亿的公司

张以弛34岁,高个儿,瘦削,眼镜下一张干净的娃娃脸.创业8年,这张脸偶尔会给他带来困扰.他到政府部门去汇报工作时,对方总会问:你们这个是大学生创业吧? 对方大概是觉得娃娃脸不够成熟.但这问题又没毛病.这确实是一个"学生"创业项目.张以弛在剑桥大学攻读语义.逻辑和编程博士学位,中途肄业,创业至今,做的项目还跟教育有关.公司名叫"校宝在线",专门为中国的教育培训机构及全日制学校提供信息化解决方案.从去年末到今年年中,它已经拿到超2亿元的C轮融资,领投方是蚂蚁金服,公司估

Markdown编辑器开发记录(一):开发的初衷和初期踩的坑

先说下选择Markdown编辑器的原因,我们进行平台开发,需要很多的操作手册和API文档,要在网站中展示出来就需要是HTML格式的文件,但是由于内容很多,不可能全部由技术人员进行文档的编写,如果是只有文档操作经验的人来做就会出现很麻烦的情况. 最初,我们先用试着用word来写,再转换成HTML文件保存,但是这样存在几个问题:1.转换出来的文件标签和样式十分的杂乱和冗余,有太多无用的标签,后期要修改样式也十分不容易:2.图片无法保存,在word文档中插入的图片路径是固定的物理路径,或是与文档一起存

redhat 7.2更新yum源时踩的坑

一.update yum 1.先查看redhat7.2中yum的包版本 [[email protected] jiayimeng]#  rpm -qa | grep yum yum-rhn-plugin-2.0.1-5.el7.noarch yum-metadata-parser-1.1.4-10.el7.x86_64 yum-langpacks-0.4.2-4.el7.noarch yum-utils-1.1.31-34.el7.noarch yum-3.4.3-132.el7.noarch