先直接来看一段展示:
# Psy Shell v0.3.3 (PHP 5.5.30 — cli) by Justin Hileman>>> ceil(-0.5)=> -0.0>>> max(-0.0, 0)=> 0.0>>> max(ceil(-0.5), 0)=> -0.0
上面的演示中,ceil 函数返回的是 -0.0,max 在将 ceil 函数调用的结果作为参数传入的时候,返回的也是一个 -0.0。
如果给 ceil 的结果赋值给变量,还是能得到 -0.0 的结果:
>>> $a = ceil(-0.5)=> -0.0>>> max($a, 0)=> -0.0
下面就来一一分析是哪些原因导致了这些结果的产生。
ceil 会返回 -0.0
首先我们来看一下为什么 ceil 函数会返回 -0.0。
ceil 函数的实现在 $PHP-SRC/ext/stardands/math.c ($PHP-SRC 指的是 PHP 解释器源码根目录)中,为了展示清楚我去掉了一些细节:
PHP_FUNCTION(ceil){ ... if (Z_TYPE_PP(value) == IS_DOUBLE) { RETURN_DOUBLE(ceil(Z_DVAL_PP(value))); } else if (Z_TYPE_PP(value) == IS_LONG) { convert_to_double_ex(value); RETURN_DOUBLE(Z_DVAL_PP(value)); } ...}
从这里可以看出来 ceil 函数做了两个事情:
如果参数类型是 double,则直接调用 C 语言的 ceil 函数并返回执行结果;
如果参数类型是 long,则转换成 double 然后直接返回。
所以 ceil 返回 -0.0 这个本身的原因还在于 C。写个函数测试一下:
#include <stdio.h>#include <math.h>int main(int argc, char const *argv[]){ printf("%f\n", ceil(-0.5)); return 0;}
以上代码在我机器上的执行结果是 -0.000000。至于为什么会是这个结果,这是 C 语言的问题,这里也不细说,有兴趣的可以看这里:http://www.wikiwand.com/zh/-0。
不能直接传入 -0.0
接下来讨论一下为什么执行 max(-0.0, 0) 却得不到相同的结果。
用 vld 扩展查看了一下只有以上一行代码的 php 文件看一下结果:
line #* E I O op fetch ext return operands-------------------------------------------------------------------------- 3 0 E > EXT_STMT 1 EXT_FCALL_BEGIN 2 SEND_VAL 0 3 SEND_VAL 0 4 DO_FCALL 2 'max' 5 EXT_FCALL_END 5 6 > RETURN 1
注意到需要为 2 的 SEND_VAL 操作,送进去的值是 0。也就说在词法分析阶段之后 -0.0 就被转换成 0 了。如何转换的呢?下面我们简单的分析一下的过程。
PHP 的词法分析器由 re2c 生成,语法分析器则是由 Bison 生成。在 zend_language_scanner.l ($PHP-SRC/Zend 目录下)中我们可以找到以下的语句:
LNUM [0-9]+DNUM ([0-9]*"."[0-9]+)|([0-9]+"."[0-9]*)EXPONENT_DNUM (({LNUM}|{DNUM})[eE][+-]?{LNUM})......<ST_IN_SCRIPTING>{DNUM}|{EXPONENT_DNUM} { zendlval->value.dval = zend_strtod(yytext, NULL); zendlval->type = IS_DOUBLE; return T_DNUMBER;}
LNUM 和 DNUM 后面都是简单的正则表达式。虽然在词法扫描中 0.0 会被标记成 DNUM,并且位于 zend_strtod.c zend_strtod 函数中的也有对于 加减号的处理,但是 - 符号并不和 DNUM 匹配(那既然这样为什么 zend_strtod 还要处理加减号呢?因为这个函数不只是在这里使用的)。这里最终返回一个 T_DNUMBER 的标记。
再看 zend_language_parser.y 中:
common_scalar: T_LNUMBER { $$ = $1; } | T_DNUMBER { $$ = $1; } ...;static_scalar: /* compile-time evaluated scalars */ common_scalar { $$ = $1; } ... | '+' static_scalar { ZVAL_LONG(&$1.u.constant, 0); add_function(&$2.u.constant, &$1.u.constant, &$2.u.constant TSRMLS_CC); $$ = $2; } | '-' static_scalar { ZVAL_LONG(&$1.u.constant, 0); sub_function(&$2.u.constant, &$1.u.constant, &$2.u.constant TSRMLS_CC); $$ = $2; }
同样我们去掉了一些细节,简单描述一下上面的语法分析的处理流程:
T_DNUMBER 是一个 common_scalar 语句;
common_scalar 是一个 static_scalar 语句;
static_scalar 语句前面存在减号时,将操作数 1 (op1)设定为 值为 0 的 ZVAL_LONG ,然后调用 sub_function 函数处理两个操作数。
sub_function 函数的实现位于 zend_operators.c 中,所做的操作很简单,就是用 op1 的值减去 op2 的值,所以就不会存在传入 -0.0 的情况。
直接调用或赋值给变量
既然如此,为什么直接使用函数调用做参数或者赋值给变量的方式又可以传入呢?闲来看一下 zend_language_parser.y 中对于函数参数的分析语句:
function_call_parameter_list: '(' ')' { Z_LVAL($$.u.constant) = 0; } | '(' non_empty_function_call_parameter_list ')' { $$ = $2; } | '(' yield_expr ')' { Z_LVAL($$.u.constant) = 1; zend_do_pass_param(&$2, ZEND_SEND_VAL, Z_LVAL($$.u.constant) TSRMLS_CC); };non_empty_function_call_parameter_list: expr_without_variable { Z_LVAL($$.u.constant) = 1; zend_do_pass_param(&$1, ZEND_SEND_VAL, Z_LVAL($$.u.constant) TSRMLS_CC); } | variable { Z_LVAL($$.u.constant) = 1; zend_do_pass_param(&$1, ZEND_SEND_VAR, Z_LVAL($$.u.constant) TSRMLS_CC); } | '&' w_variable { Z_LVAL($$.u.constant) = 1; zend_do_pass_param(&$2, ZEND_SEND_REF, Z_LVAL($$.u.constant) TSRMLS_CC); }...;
为了直观 non_empty_function_call_parameter_list 语句块后面我隐去了三行。后面三行的处理逻辑实际上是递归调用,并不影响我们分析。
通过 function_call_parameter_list 可以看出函数的参数基本情况包括三种:
没有参数
有参数列表
有 yield 表达式
这里我们只需要关注有参数列表的情况,参数列表中的每个参数也分三种情况:
不包含变量的表达式
变量
引用变量
上文中我们提到的直接传入 -0.0 时对应的是第一种情况,传入赋值后的 $a 对应的是第二种情况。参数最终都会交给 zend_do_pass_param 函数(zend_compile.c)去处理。
那么传入 ceil(-0.5) 作为参数呢?实际上也是对应第二种情况,这个问题单独分析起来也比较复杂,省事儿一点我们直接用 vld 看一下执行 max(ceil(-0.5), 0)过程:
line #* E I O op fetch ext return operands-------------------------------------------------------------------------- 5 0 E > EXT_STMT 1 EXT_FCALL_BEGIN 2 EXT_FCALL_BEGIN 3 SEND_VAL -0.5 4 DO_FCALL 1 $0 'ceil' 5 EXT_FCALL_END 6 SEND_VAR_NO_REF 6 $0 7 SEND_VAL 0 8 DO_FCALL 2 'max' 9 EXT_FCALL_END 6 10 > RETURN 1
序号为 4 的语句中,ceil 的执行结果是赋值给一个 $0 的变量,而在序号为 6 的执行中,执行的是 SEND_VAR_NO_REF 的语句,调用的 $0。SEND_VAR_NO_REF 的 Opcode 是在何时被指定的呢?也是在 zend_do_pass_param 函数中:
if (op == ZEND_SEND_VAR && zend_is_function_or_method_call(param)) { /* Method call */ op = ZEND_SEND_VAR_NO_REF; ...}
函数执行过程中使用 zend_parse_parameters 函数(zend_API.c)来获取参数。从参数的存储到获取中间还有很多处理过程,这里不再一一详解。但是需要知道一件事:函数在使用变量作为参数的时候是直接从已经存储的变量列表中读取的,没有经过过滤处理,所以变量 $a 或 ceil(-0.5) 才可以直接将 -0.0 传递给 max 函数使用。
最后的原因
既然以上都知道了,那还剩一个问题:为什么在 -0.0 和 0 中 max 函数会选择前者?
其实这个问题很简单,看一下 max 函数的实现($PHP-SRC/ext/standard/array.c)就知道真的就是在两值相等时选择了前者:
max = args[0];for (i = 1; i < argc; i++) { is_smaller_or_equal_function(&result, *args[i], *max TSRMLS_CC); if (Z_LVAL(result) == 0) { max = args[i]; }}
同样,min 函数也存在这个问题,区别就是 min 函数是调用的 is_smaller_function 来比较两个数值,两个值相等的时候返回前者。
所以要解决这个问题也很简单,只需要调换一下参数顺序即可:
# Psy Shell v0.3.3 (PHP 5.5.30 — cli) by Justin Hileman>>> max(0, ceil(-0.5))=> 0
后话
本文仅仅是管中窥豹,从一个小 “bug” 入口简单的梳理一下各个环节的处理过程,如果想要更深入的理解 PHP 的执行过程,还需要大量的精力和知识储备。
分析 PHP 源码的执行过程不仅是为了对 PHP 有更深刻的理解,也能帮助我们了解一门语言从代码到执行结果中间的各个环节和实现。
关于词法分析器与语法分析器,这里讲的并不多,希望后面有机会的话能够再深入探讨。re2c 的规则比较简单,关于 Bison,则有很多相关的书籍。
文中有粗浅的疏解,也留下有问题,如有错误,欢迎指正。
Stay foolish,stay humble; Keep questioning,keep learning.
私博地址:http://0x1.im

PHP와 Python은 각각 고유 한 장점이 있으며 선택은 프로젝트 요구 사항을 기반으로해야합니다. 1.PHP는 간단한 구문과 높은 실행 효율로 웹 개발에 적합합니다. 2. Python은 간결한 구문 및 풍부한 라이브러리를 갖춘 데이터 과학 및 기계 학습에 적합합니다.

PHP는 죽지 않고 끊임없이 적응하고 진화합니다. 1) PHP는 1994 년부터 새로운 기술 트렌드에 적응하기 위해 여러 버전 반복을 겪었습니다. 2) 현재 전자 상거래, 컨텐츠 관리 시스템 및 기타 분야에서 널리 사용됩니다. 3) PHP8은 성능과 현대화를 개선하기 위해 JIT 컴파일러 및 기타 기능을 소개합니다. 4) Opcache를 사용하고 PSR-12 표준을 따라 성능 및 코드 품질을 최적화하십시오.

PHP의 미래는 새로운 기술 트렌드에 적응하고 혁신적인 기능을 도입함으로써 달성 될 것입니다. 1) 클라우드 컴퓨팅, 컨테이너화 및 마이크로 서비스 아키텍처에 적응, Docker 및 Kubernetes 지원; 2) 성능 및 데이터 처리 효율을 향상시키기 위해 JIT 컴파일러 및 열거 유형을 도입합니다. 3) 지속적으로 성능을 최적화하고 모범 사례를 홍보합니다.

PHP에서, 특성은 방법 재사용이 필요하지만 상속에 적합하지 않은 상황에 적합합니다. 1) 특성은 클래스에서 다중 상속의 복잡성을 피할 수 있도록 수많은 방법을 허용합니다. 2) 특성을 사용할 때는 대안과 키워드를 통해 해결할 수있는 방법 충돌에주의를 기울여야합니다. 3) 성능을 최적화하고 코드 유지 보수성을 향상시키기 위해 특성을 과도하게 사용해야하며 단일 책임을 유지해야합니다.

의존성 주입 컨테이너 (DIC)는 PHP 프로젝트에 사용하기위한 객체 종속성을 관리하고 제공하는 도구입니다. DIC의 주요 이점에는 다음이 포함됩니다. 1. 디커플링, 구성 요소 독립적 인 코드는 유지 관리 및 테스트가 쉽습니다. 2. 유연성, 의존성을 교체 또는 수정하기 쉽습니다. 3. 테스트 가능성, 단위 테스트를 위해 모의 객체를 주입하기에 편리합니다.

SplfixedArray는 PHP의 고정 크기 배열로, 고성능 및 메모리 사용이 필요한 시나리오에 적합합니다. 1) 동적 조정으로 인한 오버 헤드를 피하기 위해 생성 할 때 크기를 지정해야합니다. 2) C 언어 배열을 기반으로 메모리 및 빠른 액세스 속도를 직접 작동합니다. 3) 대규모 데이터 처리 및 메모리에 민감한 환경에 적합하지만 크기가 고정되어 있으므로주의해서 사용해야합니다.

PHP는 $ \ _ 파일 변수를 통해 파일 업로드를 처리합니다. 보안을 보장하는 방법에는 다음이 포함됩니다. 1. 오류 확인 확인, 2. 파일 유형 및 크기 확인, 3 파일 덮어 쓰기 방지, 4. 파일을 영구 저장소 위치로 이동하십시오.

JavaScript에서는 NullCoalescingOperator (??) 및 NullCoalescingAssignmentOperator (?? =)를 사용할 수 있습니다. 1. 2. ??= 변수를 오른쪽 피연산자의 값에 할당하지만 변수가 무효 또는 정의되지 않은 경우에만. 이 연산자는 코드 로직을 단순화하고 가독성과 성능을 향상시킵니다.


핫 AI 도구

Undresser.AI Undress
사실적인 누드 사진을 만들기 위한 AI 기반 앱

AI Clothes Remover
사진에서 옷을 제거하는 온라인 AI 도구입니다.

Undress AI Tool
무료로 이미지를 벗다

Clothoff.io
AI 옷 제거제

AI Hentai Generator
AI Hentai를 무료로 생성하십시오.

인기 기사

뜨거운 도구

SecList
SecLists는 최고의 보안 테스터의 동반자입니다. 보안 평가 시 자주 사용되는 다양한 유형의 목록을 한 곳에 모아 놓은 것입니다. SecLists는 보안 테스터에게 필요할 수 있는 모든 목록을 편리하게 제공하여 보안 테스트를 더욱 효율적이고 생산적으로 만드는 데 도움이 됩니다. 목록 유형에는 사용자 이름, 비밀번호, URL, 퍼징 페이로드, 민감한 데이터 패턴, 웹 셸 등이 포함됩니다. 테스터는 이 저장소를 새로운 테스트 시스템으로 간단히 가져올 수 있으며 필요한 모든 유형의 목록에 액세스할 수 있습니다.

Dreamweaver Mac版
시각적 웹 개발 도구

MinGW - Windows용 미니멀리스트 GNU
이 프로젝트는 osdn.net/projects/mingw로 마이그레이션되는 중입니다. 계속해서 그곳에서 우리를 팔로우할 수 있습니다. MinGW: GCC(GNU Compiler Collection)의 기본 Windows 포트로, 기본 Windows 애플리케이션을 구축하기 위한 무료 배포 가능 가져오기 라이브러리 및 헤더 파일로 C99 기능을 지원하는 MSVC 런타임에 대한 확장이 포함되어 있습니다. 모든 MinGW 소프트웨어는 64비트 Windows 플랫폼에서 실행될 수 있습니다.

VSCode Windows 64비트 다운로드
Microsoft에서 출시한 강력한 무료 IDE 편집기

에디트플러스 중국어 크랙 버전
작은 크기, 구문 강조, 코드 프롬프트 기능을 지원하지 않음
