## 4.3 循環結構
實際應用中有許多具有規律性的重復操作,因此在程序中就需要重復執行某些語句。循環結構是在一定條件下反復執行某段程序的流程結構,被反復執行的程序被稱為循環體。循環語句是由循環體及循環的終止條件兩部分組成的。
PHP中的循環結構有4種:while、for、foreach、do while,接下來我們分析下這幾個結構的具體的實現。
### 4.3.1 while循環
while循環的語法:
```php
while(expression)
{
statement;//循環體
}
```
while的結構比較簡單,由兩部分組成:expression、statement,其中expression為循環判斷條件,當expression為true時重復執行statement,具體的語法規則:
```c
statement:
...
| T_WHILE '(' expr ')' while_statement { $$ = zend_ast_create(ZEND_AST_WHILE, $3, $5); }
...
;
while_statement:
statement { $$ = $1; }
| ':' inner_statement_list T_ENDWHILE ';' { $$ = $2; }
;
```
從while語法規則可以看出,在解析時會創建一個`ZEND_AST_WHILE`節點,expression、statement分別保存在兩個子節點中,其AST如下:

while編譯的過程也比較簡單,比較特別的是while首先編譯的是循環體,然后才是循環判斷條件,更像是do while,編譯過程大致如下:
* __(1)__ 首先編譯一條ZEND_JMP的opcode,這條opcode用來跳到循環判斷條件expression的位置,由于while是先編譯循環體再編譯循環條件,所以此時還無法確定具體的跳轉值;
* __(2)__ 編譯循環體statement;編譯完成后更新步驟(1)中ZEND_JMP的跳轉值;
* __(3)__ 編譯循環判斷條件expression;
* __(4)__ 編譯一條ZEND_JMPNZ的opcode,這條opcode用于循環判斷條件執行完以后跳到循環體的,如果循環條件成立則通過此opcode跳到循環體開始的位置,否則繼續往下執行(即:跳出循環)。
具體的編譯過程:
```c
void zend_compile_while(zend_ast *ast)
{
zend_ast *cond_ast = ast->child[0];
zend_ast *stmt_ast = ast->child[1];
znode cond_node;
uint32_t opnum_start, opnum_jmp, opnum_cond;
//(1)編譯ZEND_JMP
opnum_jmp = zend_emit_jump(0);
zend_begin_loop(ZEND_NOP, NULL);
//(2)編譯循環體statement,opnum_start為循環體起始位置
opnum_start = get_next_op_number(CG(active_op_array));
zend_compile_stmt(stmt_ast);
//設置ZEND_JMP opcode的跳轉值
opnum_cond = get_next_op_number(CG(active_op_array));
zend_update_jump_target(opnum_jmp, opnum_cond);
//(3)編譯循環條件expression
zend_compile_expr(&cond_node, cond_ast);
//(4)編譯ZEND_JMPNZ,用于循環條件成立時跳回循環體開始位置:opnum_start
zend_emit_cond_jump(ZEND_JMPNZ, &cond_node, opnum_start);
zend_end_loop(opnum_cond);
}
```
編譯后opcode整體如下:

運行時首先執行`ZEND_JMP`,跳到while條件expression處開始執行,然后由`ZEND_JMPNZ`對條件的執行結果進行判斷,如果條件成立則跳到循環體statement起始位置開始執行,如果條件不成立則繼續向下執行,跳出while,第一次循環執行以后將不再執行`ZEND_JMP`,后續循環只有靠`ZEND_JMPNZ`控制跳轉,循環體執行完成后接著執行循環判斷條件,進行下一輪循環的判斷。
> __Note:__ 實際執行時可能會省略`ZEND_JMPNZ`這一步,這是因為很多while條件expression執行完以后會對下一條opcode進行判斷,如果是`ZEND_JMPNZ`則直接根據條件成立與否進行快速跳轉,不需要再由`ZEND_JMPNZ`判斷,比如:
>
> $a = 123;
> while($a > 100){
> echo "yes";
> }
> `$a > 100`對應的opcode:ZEND_IS_SMALLER,執行時發現$a與100類型可以直接比較(都是long),則直接就能知道循環條件的判斷結果,這種情況下將會判斷下一條opcode是否為ZEND_JMPNZ,是的話直接設置下一條要執行的opcode,這樣就不需要再單獨執行依次ZEND_JMPNZ了。
>
> 上面的例子如果`$a = '123';`就不會快速進行處理了,而是按照正常的邏輯調用ZEND_JMPNZ。
### 4.3.2 do while循環
do while與while非常相似,唯一的區別在于do while第一次執行時不需要判斷循環條件。
do while循環的語法:
```php
do{
statement;//循環體
}while(expression)
```
do while編譯過程與while的基本一致,不同的地方在于do while沒有`ZEND_JMP`這條opcode:
```c
void zend_compile_do_while(zend_ast *ast)
{
zend_ast *stmt_ast = ast->child[0];
zend_ast *cond_ast = ast->child[1];
znode cond_node;
uint32_t opnum_start, opnum_cond;
//(1)編譯循環體statement,opnum_start為循環體起始位置
opnum_start = get_next_op_number(CG(active_op_array));
zend_compile_stmt(stmt_ast);
//(2)編譯循環判斷條件expression
opnum_cond = get_next_op_number(CG(active_op_array));
zend_compile_expr(&cond_node, cond_ast);
//(3)編譯ZEND_JMPNZ
zend_emit_cond_jump(ZEND_JMPNZ, &cond_node, opnum_start);
}
```
編譯后的結果:

運行時首先執行循環體statement,然后執行循環判斷條件,如果條件成立跳到循環體起始位置,否則結束循環。
### 4.3.3 for循環
for循環語法:
```php
for (init expr; condition expr; loop expr){
statement
}
```
init expr在循環開始前無條件執行一次,后面循環不再執行;condition expr在每次循環開始前運算,是循環的判斷條件,如果值為true,則繼續循環,執行循環體,如果值為false,則終止循環;loop expr在每次循環體執行完以后被執行。
for的語法規則:
```c
statement:
...
| T_FOR '(' for_exprs ';' for_exprs ';' for_exprs ')' for_statement
{ $$ = zend_ast_create(ZEND_AST_FOR, $3, $5, $7, $9); }
...
;
```
從語法規則可以看出,for被編譯為`ZEND_AST_FOR`節點,包含4個子節點,分別為:expr1、expr2、expr3、statement。

for的編譯與while類似,只是多了init expr、loop expr兩部分,編譯過程大致如下:
* __(1)__ 首先編譯初始化表達式:init expr;
* __(2)__ 編譯一條`ZEND_JMP`的opcode,此opcode用于跳到條件expression位置,具體跳轉值需要后面才能確定;
* __(3)__ 編譯循環體statement;
* __(4)__ 編譯loop expr;然后設置步驟(2)中`ZEND_JMP`的跳轉值;
* __(5)__ 編譯循環條件:condition expr;
* __(6)__ 編譯一條`ZEND_JMPNZ`,此opcode用于循環條件成立時跳到循環體起始位置。
具體編譯過程:
```c
void zend_compile_for(zend_ast *ast)
{
zend_ast *init_ast = ast->child[0];
zend_ast *cond_ast = ast->child[1];
zend_ast *loop_ast = ast->child[2];
zend_ast *stmt_ast = ast->child[3];
znode result;
uint32_t opnum_start, opnum_jmp, opnum_loop;
//(1)編譯init expression
zend_compile_expr_list(&result, init_ast);
zend_do_free(&result);
//(2)編譯ZEND_JMP
opnum_jmp = zend_emit_jump(0);
//opnum_start是循環體起始位置
opnum_start = get_next_op_number(CG(active_op_array));
//(3)編譯循環體
zend_compile_stmt(stmt_ast);
//(4)編譯loop expression
opnum_loop = get_next_op_number(CG(active_op_array));
zend_compile_expr_list(&result, loop_ast);
zend_do_free(&result);
//設置ZEND_JMP跳轉值
zend_update_jump_target_to_next(opnum_jmp);
//(5)編譯循環條件expression
zend_compile_expr_list(&result, cond_ast);
zend_do_extended_info();
//(6)編譯ZEND_JMPNZ
zend_emit_cond_jump(ZEND_JMPNZ, &result, opnum_start);
}
```
最終編譯結果:

運行時首先執行初始化表達式:init expression,然后執行`ZEND_JMP`跳到循環條件expression處,如果條件成立則執行`ZEND_JMPNZ`跳到循環體起始位置依次執行循環體、loop expression,如果條件不成立則終止循環,第一次循環之后就是:`循環條件->ZEND_JMPNZ->循環體->loop expression`之間循環了。
### 4.3.4 foreach循環
foreach是PHP針對數組、對象提供的一種遍歷方式,foreach語法:
```php
foreach (array_expression as $key => $value){
statement
}
```
遍歷arraiy_expression時每次循環會把當前單元的值賦給$value,當前單元的鍵值賦給$key,其中$key可以省略,$value前也可以加"&"表示引用單元的值。
foreach的語法規則:
```c
statement:
...
//省略key的規則: foreach($array as $v){ ... }
| T_FOREACH '(' expr T_AS foreach_variable ')' foreach_statement
{ $$ = zend_ast_create(ZEND_AST_FOREACH, $3, $5, NULL, $7); }
//有key的規則: foreach($array as $k=>$v){ ... }
| T_FOREACH '(' expr T_AS foreach_variable T_DOUBLE_ARROW foreach_variable ')' foreach_statement
{ $$ = zend_ast_create(ZEND_AST_FOREACH, $3, $7, $5, $9); }
...
;
```
foreach在編譯階段解析為`ZEND_AST_FOREACH`節點,包含4個子節點,分別表示:遍歷的數組或對象、遍歷的value、遍歷的key以及循環體,生成的AST類似這樣:

如果value是指向數組或對象成員的引用,則value對應的節點類型為`ZEND_AST_REF`。
相對上面幾種常規的循環結構,foreach的實現略顯復雜:$key、$value實際就是兩個普通的局部變量,遍歷的過程就是對兩個局部變量不斷賦值、更新的過程,以數組為例,首先將數組拷貝一份用于遍歷(只拷貝zval,value還是指向同一份),從arData第一個元素開始,把Bucket.zval.value值賦值給$value,把Bucket.key(或Bucket.h)賦值給$key,然后更新迭代位置:將下一個元素的位置記錄在`zval.u2.fe_iter_idx`中,這樣下一輪遍歷時直接從這個位置開始,這也是遍歷前為什么要拷貝一份zval用于遍歷的原因,如果發現`zval.u2.fe_iter_idx`已經到達arData末尾了則結束遍歷,銷毀一開始拷貝的zval。舉個例子來看:
```php
$arr = array(1,2,3);
foreach($arr as $k=>$v){
echo $v;
}
```
局部變量對應的內存結構:

如果value是引用則在循環前首先將原數組或對象重置為引用類型,然后新分配一個zval指向這個引用,后面的過程就與上面的一致了,仍以上面的例子為例,如果是:`foreach($arr as $k=>&$v){ ... }`則:

了解了foreach的實現、運行機制我們再回頭看下其編譯過程:
* __(1)__ 編譯"拷貝"數組/對象操作的opcode:`ZEND_FE_RESET_R`,如果value是引用則是`ZEND_FE_RESET_RW`,執行時如果發現數組或對象屬性為空則直接跳出遍歷,所以這條opcode還需要知道跳出的位置,這個位置需要編譯完foreach以后才能確定;
* __(2)__ 編譯fetch數組/對象當前單元key、value的opcode:`ZEND_FE_FETCH_R`,如果是引用則是`ZEND_FE_FETCH_RW`,此opcode還需要知道當遍歷已經到達數組末尾時跳出遍歷的位置,與步驟(1)的opcode相同,另外還有一個關鍵操作,前面已經說過遍歷的key、value實際就是普通的局部變量,它們的內存存儲位置正是在這一步分配確定的,分配過程與普通局部變量的過程完全相同,如果value不是一個CV變量(比如:foreach($arr as $v["xx"]){...})則還會編譯其它操作的opcode;
* __(3)__ 如果foreach定義了key則編譯一條賦值opcode,此操作是對key進行賦值;
* __(4)__ 編譯循環體statement;
* __(5)__ 編譯跳回遍歷開始位置的opcode:`ZEND_JMP`,一次遍歷結束時會跳回步驟(2)編譯的opcode處進行下次遍歷;
* __(6)__ 設置步驟(1)、(2)兩條opcode跳過的opcode數;
* __(7)__ 編譯`ZEND_FE_FREE`,此操作用于釋放步驟(1)"拷貝"的數組。
最終編譯后的結構:

運行時的步驟:
* __(1)__ 執行`ZEND_FE_RESET_R`,過程上面已經介紹了;
* __(2)__ 執行`ZEND_FE_FETCH_R`,此opcode的操作主要有三個:檢查遍歷位置是否到達末尾、將數組元素的value賦值給$value、將數組元素的key賦值給一個臨時變量(注意與value不同);
* __(3)__ 如果定義了key則執行`ZEND_ASSIGN`,將key的值從臨時變量賦值給$key,否則跳到步驟(4);
* __(4)__ 執行循環體的statement;
* __(5)__ 執行`ZEND_JMPNZ`跳回步驟(2);
* __(6)__ 遍歷結束后執行`ZEND_FE_FREE`釋放數組。
PHP中還有幾個與遍歷相關的函數:
* current() - 返回數組中的當前單元
* each() - 返回數組中當前的鍵/值對并將數組指針向前移動一步
* end() - 將數組的內部指針指向最后一個單元
* next() - 將數組中的內部指針向前移動一位
* prev() - 將數組的內部指針倒回一位
- 前言
- 第1章 PHP基本架構
- 1.1 PHP簡介
- 1.2 PHP7的改進
- 1.3 FPM
- 1.3.1 概述
- 1.3.2 基本實現
- 1.3.3 FPM的初始化
- 1.3.4 請求處理
- 1.3.5 進程管理
- 1.4 PHP執行的幾個階段
- 第2章 變量
- 2.1 變量的內部實現
- 2.2 數組
- 2.3 靜態變量
- 2.4 全局變量
- 2.5 常量
- 第3章 Zend虛擬機
- 3.1 PHP代碼的編譯
- 3.1.1 詞法解析、語法解析
- 3.1.2 抽象語法樹編譯流程
- 3.2 函數實現
- 3.2.1 內部函數
- 3.2.2 用戶函數的實現
- 3.3 Zend引擎執行流程
- 3.3.1 基本結構
- 3.3.2 執行流程
- 3.3.3 函數的執行流程
- 3.3.4 全局execute_data和opline
- 3.4 面向對象實現
- 3.4.1 類
- 3.4.2 對象
- 3.4.3 繼承
- 3.4.4 動態屬性
- 3.4.5 魔術方法
- 3.4.6 類的自動加載
- 3.5 運行時緩存
- 3.6 Opcache
- 3.6.1 opcode緩存
- 3.6.2 opcode優化
- 3.6.3 JIT
- 第4章 PHP基礎語法實現
- 4.1 類型轉換
- 4.2 選擇結構
- 4.3 循環結構
- 4.4 中斷及跳轉
- 4.5 include/require
- 4.6 異常處理
- 第5章 內存管理
- 5.1 Zend內存池
- 5.2 垃圾回收
- 第6章 線程安全
- 6.1 什么是線程安全
- 6.2 線程安全資源管理器
- 第7章 擴展開發
- 7.1 概述
- 7.2 擴展的實現原理
- 7.3 擴展的構成及編譯
- 7.3.1 擴展的構成
- 7.3.2 編譯工具
- 7.3.3 編寫擴展的基本步驟
- 7.3.4 config.m4
- 7.4 鉤子函數
- 7.5 運行時配置
- 7.5.1 全局變量
- 7.5.2 ini配置
- 7.6 函數
- 7.6.1 內部函數注冊
- 7.6.2 函數參數解析
- 7.6.3 引用傳參
- 7.6.4 函數返回值
- 7.6.5 函數調用
- 7.7 zval的操作
- 7.7.1 新生成各類型zval
- 7.7.2 獲取zval的值及類型
- 7.7.3 類型轉換
- 7.7.4 引用計數
- 7.7.5 字符串操作
- 7.7.6 數組操作
- 7.8 常量
- 7.9 面向對象
- 7.9.1 內部類注冊
- 7.9.2 定義成員屬性
- 7.9.3 定義成員方法
- 7.9.4 定義常量
- 7.9.5 類的實例化
- 7.10 資源類型
- 7.11 經典擴展解析
- 7.8.1 Yaf
- 7.8.2 Redis
- 第8章 命名空間
- 8.1 概述
- 8.2 命名空間的定義
- 8.2.1 定義語法
- 8.2.2 內部實現
- 8.3 命名空間的使用
- 8.3.1 基本用法
- 8.3.2 use導入
- 8.3.3 動態用法
- 附錄
- break/continue按標簽中斷語法實現
- defer推遲函數調用語法的實現
- 一起線上事故引發的對PHP超時控制的思考