深入理解PHP传参原理_PHP教程
首先说下今天想到的一个问题。在编写php扩展的时候,似乎参数(即传给zend_parse_parameters的变量)是不需要free的。举例:
<span PHP_FUNCTION(test) { </span><span char</span>*<span str; </span><span int</span><span str_len; </span><span if</span> (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, <span "</span><span s</span><span "</span>, &str, &str_len) ==<span FAILURE) { RETURN_FALSE; } php_printf(str);<br /> <br /> </span><span //</span><span 无需free(str) </span> }
运行正常:
test("Hello World"); <span //</span><span 打印Hello World</span>
这里不用担心test函数会发生内存泄露,php会自动帮我们回收这些用于保存参数的变量。
那php究竟是如何做到的呢?要解释这个问题,还是得看php是怎么传递参数的。
EG(argument_stack)简介
简单来讲,在php中的EG中保存了一个专门用于存放参数的栈,名为argument_stack。每当发生函数调用的时候,php会将传入的参数压进EG(argument_stack)。一旦函数调用结束,则EG(argument_stack)被清理,并且等待下一次的函数调用。
关于EG(argument_stack)的struct结构、用途,php5.2和5.3实现有一些区别。本文主要以5.2为例,5.3+的变化后面抽空再说。
上图是5.2中argument_stack的大概示意图,看起来简单明了。其中,栈顶和栈底固定为NULL。函数接收的参数按照从左到右的顺序,依次被压入栈。注意,最后会被额外压入一个long型的值,表示栈里的参数个数(上图中为10)。
那被压入argument_stack的参数究竟是什么呢?其实是一个个zval类型的指针。它们指向的zva有可能是CV变量,有可能是is_ref=1的变量,还有可能是一个常量数字,或者常量字符串。
EG(argument_stack)在php5.2中被具体实现为zend_ptr_stack类型:
typedef <span struct</span><span _zend_ptr_stack { </span><span int</span><span top; // 栈中当前元素的个数 </span><span int</span><span max; // 栈中最多存放元素的个数 </span><span void</span> **<span elements; // 栈底 </span><span void</span> **<span top_element; // 栈顶 } zend_ptr_stack;</span>
初始化argument_stack
初始化argument_stack的工作是发生在php处理具体的请求之前,更准确说是处于php解释器的启动过程之中。
在init_executor函数里我们发现如下2行:
zend_ptr_stack_init(&<span EG(argument_stack)); zend_ptr_stack_push(</span>&EG(argument_stack), (<span void</span> *) NULL);
这2行分别代表着,初始化EG(argument_stack),紧接着压入一个NULL。由于EG是个全局变量,因此在实际调用zend_ptr_stack_init之前,EG(argument_stack)中的所有数据全部为0。
zend_ptr_stack_init实现很简单。
ZEND_API <span void</span> zend_ptr_stack_init(zend_ptr_stack *<span stack) { stack</span>->top_element = stack->elements = (<span void</span> **) emalloc(<span sizeof</span>(<span void</span> *)*<span PTR_STACK_BLOCK_SIZE); stack</span>->max =<span PTR_STACK_BLOCK_SIZE; // 栈的大小被初始化成64 stack</span>->top = <span 0</span><span ; // 当前元素个数为0 }</span>
一旦argument_stack被初始化完,则立即会被压入NULL。这里无须深究,这个NULL其实没有任何的含义。
NULL入栈之后,整个argument_stack的实际内存分布如下:
参数入栈
在压入第一个NULL之后,一旦再有参数入栈,则argument_stack会发生如下动作:
stack->top++<span ; </span>*(stack->top_element++) =<span 参数;</span>
我们用一段简单的php代码来说明问题:
<span function</span> foo( <span $str</span><span ){ </span><span print_r</span>(<span 123</span><span ); } foo(</span>"hello world");
上述代码在调用foo的时候,传入了一个字符串常量。因此,实际上被压入栈的是一个指向存储“hello world”的zval。用vld来查看编译之后的opcode:
line # * op fetch ext return operands --------------------------------------------------------------------------------- 3 0 > NOP 6 1 SEND_VAL OP1[ IS_CONST (458754) 'hello world' ] 2 DO_FCALL 1 OP1[ IS_CONST (458752) 'foo' ] 15 3 > RETURN OP1[ IS_CONST (0) 1 ]
SEND_VAL指令实际上做的事情就是将“hello world”压入argument_stack。
<span int</span><span ZEND_SEND_VAL_SPEC_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS) { ……</span><span zval </span>*<span valptr, </span>*<span value; value </span>= &opline-><span op1.u.constant; ALLOC_ZVAL(valptr); INIT_PZVAL_COPY(valptr, value); </span><span if</span> (!<span 0</span><span ) { zval_copy_ctor(valptr); }<br /><br /> <span // 入栈,<span valptr</span>指向存放hello world的zval</span> zend_ptr_stack_push(</span>&<span EG(argument_stack), valptr); <span ……</span> }</span>
入栈完成之后的argument_stack为:
参数个数
前文说到,实际上并非把所有参数入栈就完事了。php还会额外压入一个数字,表示参数的个数,这个工作并非发生在SEND_XXX指令的时候。实际上,在真正执行函数之前,php会将参数个数入栈。
继续沿用上面的例子,DO_FCALL 指令用于调用foo函数。在调用foo之前,php会自动填入argument_stack最后一块。
<span static</span> <span int</span><span zend_do_fcall_common_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS) { …… </span><span //</span><span 在argument_stack中压入2个值 </span><span //</span><span 一个是参数个数(即opline->extended_value) </span><span //</span><span 一个是标识栈顶的NULL</span> zend_ptr_stack_2_push(&EG(argument_stack), (<span void</span> *)(zend_uintptr_t)opline-><span extended_value, NULL); …… </span><span if</span> (EX(function_state).function->type ==<span ZEND_INTERNAL_FUNCTION) { …… } </span><span else</span> <span if</span> (EX(function_state).function->type ==<span ZEND_USER_FUNCTION) { …… </span><span //</span><span 调用foo函数</span> <span zend_execute(EG(active_op_array) TSRMLS_CC); } </span><span else</span> { <span /*</span><span ZEND_OVERLOADED_FUNCTION </span><span */</span><span …… } …… </span><span //</span><span 清理<span argument_stack</span></span> <span zend_ptr_stack_clear_multiple(TSRMLS_C); …… ZEND_VM_NEXT_OPCODE(); }</span>
压入参数个数和NULL之后,用于foo调用的整个argument_stack已然完成。
获取参数
继续跟进上面的例子。让我们深入到foo函数,看看foo的opcode是什么样子的。
line # * op fetch ext return operands --------------------------------------------------------------------------------- 3 0 > RECV OP1[ IS_CONST (0) 1 ] 4 1 SEND_VAL OP1[ IS_CONST (5) 123 ] 2 DO_FCALL 1 OP1[ IS_CONST (459027) 'print_r' ] 5 3 > RETURN OP1[ IS_CONST (0) null ]
第一条指令是RECV,从字面上理解便是用于获取栈中参数的。实际上,SEND_VAL和RECV有点对应的感觉。每次函数调用之前SEND_VAL,在函数内部进行RECV。为什么不说是完全对应,实际上RECV指令并非一定需要。只有当用户定义的函数被调用是,才会产生RECV。我们编写的扩展函数,php自带的内建函数,都不会有RECV。
需要额外指出的是,每次SEND_VAL和RECV 均只能处理一个参数。也就是说如果传参的过程中有多个参数,那么会产生若干SEND_VAL以及若干RECV。这里引出一个很有趣的话题,传入参数和获取参数的顺序是怎样的呢?
答案是,SEND_VAL会将参数从左至右的进行压栈,而RECV一样的从左至右获取参数。
<span static</span> <span int</span><span ZEND_RECV_SPEC_HANDLER(ZEND_OPCODE_HANDLER_ARGS) { ……</span><span //</span><span param拿参数的顺序是沿着栈顶-->栈底</span> <span if</span> (zend_ptr_stack_get_arg(arg_num, (<span void</span> **) ¶m TSRMLS_CC)==<span FAILURE) { …… } </span><span else</span><span { zend_free_op free_res; zval </span>**<span var_ptr; </span><span //</span><span 验证参数</span> zend_verify_arg_type((zend_function *) EG(active_op_array), arg_num, *<span param TSRMLS_CC); var_ptr </span>= get_zval_ptr_ptr(&opline->result, EX(Ts), &<span free_res, BP_VAR_W); </span><span //</span><span 获取参数</span> <span if</span> (PZVAL_IS_REF(*<span param)) { zend_assign_to_variable_reference(var_ptr, param TSRMLS_CC); } </span><span else</span><span { zend_receive(var_ptr, </span>*<span param TSRMLS_CC); } } ZEND_VM_NEXT_OPCODE(); }</span>
zend_assign_to_variable_reference 和 zend_receive 都会完成“获取参数” 。“获取参数”不太好理解,实际它究竟是做哪些事情呢?
说到底很简单,“获取参数”就是将这个参数添加到当前函数执行期间的“符号表”中,具体对应为EG(current_execute_data)->symbol_table。本示例中,RECV完成之后,函数体内的symbol_table中有了一个符号‘str’,它的值为“hello world”。
但argument_stack并没有发生一丝变化,因为RECV仅仅是读取参数,而不会对栈产生类似pop操作。
清理argument_stack
foo内部的print_r也是一个函数调用,因此也会产生压栈-->清栈的操作。因此print_r执行之前的argument_stack为:
print_r执行之后argument_stack又回到了foo刚RECV完的状态。
具体调用print_r的过程并非本文阐述的重点。我们关心的是当调用foo结束之后,php是如何清理argument_stack的。
上面展示的do_fcall代码片段中可以看出,清理工作由
<span static</span> inline <span void</span><span zend_ptr_stack_clear_multiple(TSRMLS_D) { </span><span void</span> **p = EG(argument_stack).top_element-<span 2</span><span ; </span><span //</span><span 取栈顶保存的参数个数</span> <span int</span> delete_count = (<span int</span>)(zend_uintptr_t) *<span p; EG(argument_stack).top </span>-= (delete_count+<span 2</span><span ); </span><span //</span><span 从上至下,依次清理</span> <span while</span> (--delete_count>=<span 0</span><span ) { zval </span>*q = *(zval **)(--<span p); </span>*p =<span NULL; zval_ptr_dtor(</span>&<span q); } EG(argument_stack).top_element </span>=<span p; }</span>
注意这里清理栈中zval指针,使用的是zval_ptr_dtor。zval_ptr_dtor会将refcount减1,一旦refcount减为0,则保存该变量的内存区域会被真正的回收掉。
在本文示例中,foo调用完毕之后,保存“hello world”的zval状态为:
value "hello world" refcount 1 type 6 is_ref 0
由于refcount只剩1,因此,zval_ptr_dtor会将“hello world”真正从内存中销毁。
消栈完毕之后的argument_stack内存状态为:

熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

Video Face Swap
使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

熱工具

記事本++7.3.1
好用且免費的程式碼編輯器

SublimeText3漢化版
中文版,非常好用

禪工作室 13.0.1
強大的PHP整合開發環境

Dreamweaver CS6
視覺化網頁開發工具

SublimeText3 Mac版
神級程式碼編輯軟體(SublimeText3)

PHP和Python各有優勢,選擇依據項目需求。 1.PHP適合web開發,尤其快速開發和維護網站。 2.Python適用於數據科學、機器學習和人工智能,語法簡潔,適合初學者。

PHP在電子商務、內容管理系統和API開發中廣泛應用。 1)電子商務:用於購物車功能和支付處理。 2)內容管理系統:用於動態內容生成和用戶管理。 3)API開發:用於RESTfulAPI開發和API安全性。通過性能優化和最佳實踐,PHP應用的效率和可維護性得以提升。

PHP是一種廣泛應用於服務器端的腳本語言,特別適合web開發。 1.PHP可以嵌入HTML,處理HTTP請求和響應,支持多種數據庫。 2.PHP用於生成動態網頁內容,處理表單數據,訪問數據庫等,具有強大的社區支持和開源資源。 3.PHP是解釋型語言,執行過程包括詞法分析、語法分析、編譯和執行。 4.PHP可以與MySQL結合用於用戶註冊系統等高級應用。 5.調試PHP時,可使用error_reporting()和var_dump()等函數。 6.優化PHP代碼可通過緩存機制、優化數據庫查詢和使用內置函數。 7

PHP和Python各有優勢,選擇應基於項目需求。 1.PHP適合web開發,語法簡單,執行效率高。 2.Python適用於數據科學和機器學習,語法簡潔,庫豐富。

PHP仍然具有活力,其在現代編程領域中依然佔據重要地位。 1)PHP的簡單易學和強大社區支持使其在Web開發中廣泛應用;2)其靈活性和穩定性使其在處理Web表單、數據庫操作和文件處理等方面表現出色;3)PHP不斷進化和優化,適用於初學者和經驗豐富的開發者。

PHP適合web開發,特別是在快速開發和處理動態內容方面表現出色,但不擅長數據科學和企業級應用。與Python相比,PHP在web開發中更具優勢,但在數據科學領域不如Python;與Java相比,PHP在企業級應用中表現較差,但在web開發中更靈活;與JavaScript相比,PHP在後端開發中更簡潔,但在前端開發中不如JavaScript。

PHP和Python各有優劣,選擇取決於項目需求和個人偏好。 1.PHP適合快速開發和維護大型Web應用。 2.Python在數據科學和機器學習領域佔據主導地位。

PHP主要是過程式編程,但也支持面向對象編程(OOP);Python支持多種範式,包括OOP、函數式和過程式編程。 PHP適合web開發,Python適用於多種應用,如數據分析和機器學習。
