在上一小節,我們介紹了類的結構和聲明過程,從而,我們知道了類的存儲結構,接口抽象類等類型的實現方式。在本小節,我們將介紹類的成員變量和成員方法。首先,我們看一下,什么是成員變量,什么是成員方法。
類的成員變量在PHP中本質上是一個變量,只是這些變量都歸屬于某個類,并且給這些變量是有訪問控制的。類的成員變量也稱為成員屬性,它是現實世界實體屬性的抽象,是可以用來描述對象狀態的數據。
類的成員方法在PHP中本質上是一個函數,只是這個函數以類的方法存在,它可能是一個類方法也可能是一個實例方法,并且在這些方法上都加上了類的訪問控制。類的成員方法是現實世界實體行為的抽象,可以用來實現類的行為。
## 成員變量[]()
在第三章介紹過變量,不過那些變量要么是定義在全局范圍中,叫做全局變量,要么是定義在某個函數中,叫做局部變量。成員變量是定義在類里面,并和成員方法處于同一層次。如下一個簡單的PHP代碼示例,定義了一個類,并且這個類有一個成員變量。
class Tipi {
public $var;
}
類的結構在PHP內核中的存儲方式我們已經在上一小節介紹過了。現在,我們要討論類的成員變量的存儲方式。假如我們需要直接訪問這個變量,整個訪問過程是什么?當然,以這個示例來說,訪問這個成員變量是通過對象來訪問,關于對象的相關知識我們將在后面的小節作詳細的介紹。
當我們用VLD擴展查看以上代碼生成的中間代碼時,我們發現,并沒有相關的中間代碼輸出。這是因為成員變量在編譯時已經注冊到了類的結構中,那注冊的過程是什么? 成員變量注冊的位置在哪?
我們從上一小節知道,在編譯時類的聲明編譯會調用zend_do_begin_class_declaration函數。此函數用來初始化類的基本信息,其中包括類的成員變量。其調用順序為:[zend_do_begin_class_declaration] --> [zend_initialize_class_data] --> [zend_hash_init_ex]
zend_hash_init_ex(&ce->default_properties, 0, NULL, zval_ptr_dtor_func, persistent_hashes, 0);
因為類的成員變量是保存在HashTable中,所以,其數據的初始化使用zend_hash_init_ex函數來進行。
在聲明類的時候初始化了類的成員變量所在的HashTable,之后如果有新的成員變量聲明時,在編譯時**zend_do_declare_property**。函數首先檢查成員變量不允許的一些情況:
- 接口中不允許使用成員變量
- 成員變量不能擁有抽象屬性
- 不能聲明成員變量為final
- 不能重復聲明屬性
如果在上面的PHP代碼中的類定義中,給成員變量前面添加final關鍵字:
class Tipi {
public final $var;
}
運行程序將報錯:Fatal error: Cannot declare property Tipi::$var final, the final modifier is allowed only for methods and classes in .. 這個錯誤由zend_do_declare_property函數拋出:
if (access_type & ZEND_ACC_FINAL) {
zend_error(E_COMPILE_ERROR, "Cannot declare property %s::$%s final, the final modifier is allowed only for methods and classes",
CG(active_class_entry)->name, var_name->u.constant.value.str.val);
}
在定義檢查沒有問題之后,函數會進行成員變量的初始化操作。
ALLOC_ZVAL(property); // 分配內存
?
if (value) { // 成員變量有初始化數據
*property = value->u.constant;
} else {
INIT_PZVAL(property);
Z_TYPE_P(property) = IS_NULL;
}
在初始化過程中,程序會先分配內存,如果這個成員變量有初始化的數據,則將數據直接賦值給該屬性,否則初始化ZVAL,并將其類型設置為IS_NULL。在初始化過程完成后,程序通過調用 **zend_declare_property_ex**函數將此成員變量添加到指定的類結構中。
以上為成員變量的初始化和注冊成員變量的過程,常規的成員變量最后都會注冊到類的 **default_properties** 字段。在我們平時的工作中,可能會用不到上面所說的這些過程,但是我們可能會使用get_class_vars()函數來查看類的成員變量。此函數返回由類的默認屬性組成的關聯數組,這個數組的元素以 varname => value 的形式存在。其實現核心代碼如下:
if (zend_lookup_class(class_name, class_name_len, &pce TSRMLS_CC) == FAILURE) {
RETURN_FALSE;
} else {
array_init(return_value);
zend_update_class_constants(*pce TSRMLS_CC);
add_class_vars(*pce, &(*pce)->default_properties, return_value TSRMLS_CC);
add_class_vars(*pce, CE_STATIC_MEMBERS(*pce), return_value TSRMLS_CC);
}
首先調用zend_lookup_class函數查找名為class_name的類,并將賦值給pce變量。這個查找的過程最核心是一個HashTable的查找函數zend_hash_quick_find,它會查找EG(class_table)。判斷類是否存在,如果存在則直接返回。如果不存在,則需要判斷是否可以自動加載,如果可以自動加載,則會加載類后再返回。如果不能找到類,則返回FALSE。如果找到了類,則初始化返回的數組,更新類的靜態成員變量,添加類的成員變量到返回的數組。這里針對類的靜態成員變量有一個更新的過程,關于這個過程我們在下面有關于靜態成員變量中做相關介紹。
## 靜態成員變量[]()
類的靜態成員變量是所有實例共用的,它歸屬于這個類,因此它也叫做類變量。在PHP的類結構中,類本身的靜態變量存放在類結構的 **default_static_members** 字段中。
與普通成員變量不同,類變量可以直接通過類名調用,這也體現其稱作類變量的特別。一個PHP示例:
class Tipi {
public static $var = 10;
}
?
Tipi::$var;
這是一個簡單的類,它僅包括一個公有的靜態變量$var。通過VLD擴展查看其生成的中間代碼:
function name: (null)
number of ops: 6
compiled vars: !0 = $var
line # * op fetch ext return operands
--------------------------------------------------------------------------------
-
2 0 > EXT_STMT
1 NOP
6 2 EXT_STMT
3 ZEND_FETCH_CLASS :1 'Tipi'
4 FETCH_R static member 'var'
5 > RETURN 1
?
branch: # 0; line: 2- 6; sop: 0; eop: 5
path #1: 0,
Class Tipi: [no user functions]
這段生成的中間代碼僅與Tipi::$var;這段調用對應,它與前面的類定義沒有多大關系。根據前面的內容和VLD生成的內容,我們可以知道PHP代碼:Tipi::$var; 生成的中間代碼包括ZEND_FETCH_CLASS和FETCH_R。這里只是一個靜態變量的調用,但是它卻生成了兩個中間代碼,什么原因呢?很直白的解釋:我們要調用一個類的靜態變量,當然要先找到這個類,然后再獲取這個類的變量。從PHP源碼來看,這是由于在編譯時其調用了zend_do_fetch_static_member函數,而在此函數中又調用了zend_do_fetch_class函數,從而會生成ZEND_FETCH_CLASS中間代碼。它所對應的執行函數為 **ZEND_FETCH_CLASS_SPEC_CONST_HANDLER**。此函數會調用zend_fetch_class函數(Zend/zend_execute_API.c)。而zend_fetch_class函數最終也會調用 **zend_lookup_class_ex** 函數查找類,這與前面的查找方式一樣。
找到了類,接著應該就是查找類的靜態成員變量,其最終調用的函數為:zend_std_get_static_property。這里由于第二個參數的類型為 ZEND_FETCH_STATIC_MEMBER。這個函數最后是從 **static_members** 字段中查找對應的值返回。而在查找前會和前面一樣,執行zend_update_class_constants函數,從而更新此類的所有靜態成員變量,其程序流程如圖5.1所示:

圖5.1 靜態變量更新流程圖
## 成員方法[]()
成員方法從本質上來講也是一種函數,所以其存儲結構也和常規函數一樣,存儲在zend_function結構體中。對于一個類的多個成員方法,它是以HashTable的數據結構存儲了多個zend_function結構體。和前面的成員變量一樣,在類聲明時成員方法也通過調用zend_initialize_class_data方法,初始化了整個方法列表所在的HashTable。在類中我們如果要定義一個成員方法,格式如下:
class Tipi{
public function t() {
[echo](http://www.php.net/echo) 1;
}
}
除去訪問控制關鍵字,一個成員方法和常規函數是一樣的,從語法解析中調用的函數一樣(都是zend_do_begin_function_declaration函數),但是其調用的參數有一些不同,第三個參數is_method,成員方法的賦值為1,表示它作為成員方法的屬性。在這個函數中會有一系統的編譯判斷,比如在接口中不能聲明私有的成員方法。看這樣一段代碼:
interface Ifce {
private function method();
}
如果直接運行,程序會報錯:Fatal error: Access type for interface method Ifce::method() must be omitted in 這段代碼對應到zend_do_begin_function_declaration函數中的代碼,如下:
if (is_method) {
if (CG(active_class_entry)->ce_flags & ZEND_ACC_INTERFACE) {
if ((Z_LVAL(fn_flags_znode->u.constant) & ~(ZEND_ACC_STATIC|ZEND_ACC_PUBLIC))) {
zend_error(E_COMPILE_ERROR, "Access type for interface method %s::%s() must be omitted",
CG(active_class_entry)->name, function_name->u.constant.value.str.val);
}
Z_LVAL(fn_flags_znode->u.constant) |= ZEND_ACC_ABSTRACT; /* propagates to the rest of the parser */
}
fn_flags = Z_LVAL(fn_flags_znode->u.constant); /* must be done *after* the above check */
} else {
fn_flags = 0;
}
在此程序判斷后,程序將方法直接添加到類結構的function_talbe字段,在此之后,又是若干的編譯檢測。比如接口的一些魔術方法不能被設置為非公有,不能被設置為static,如__call()、__callStatic()、__get()等。如果在接口中設置了靜態方法,如下定義的一個接口:
interface ifce {
public [static](http://www.php.net/static) function __get();
}
若運行這段代碼,則會顯示Warning:Warning: The magic method __get() must have public visibility and cannot be static in
這段編譯檢測在zend_do_begin_function_declaration函數中對應的源碼如下:
if (CG(active_class_entry)->ce_flags & ZEND_ACC_INTERFACE) {
if ((name_len == sizeof(ZEND_CALL_FUNC_NAME)-1) && (!memcmp(lcname, ZEND_CALL_FUNC_NAME, sizeof(ZEND_CALL_FUNC_NAME)-1))) {
if (fn_flags & ((ZEND_ACC_PPP_MASK | ZEND_ACC_STATIC) ^ ZEND_ACC_PUBLIC)) {
zend_error(E_WARNING, "The magic method __call() must have public visibility and cannot be static");
}
} else if() { // 其它魔術方法的編譯檢測
}
}
同樣,對于類中的這些魔術方法,也有同樣的限制,如果在類中定義了靜態的魔術方法,則顯示警告。如下代碼
class Tipi {
public [static](http://www.php.net/static) function __get($var) {
?
}
}
運行這段代碼,則會顯示: Warning: The magic method __get() must have public visibility and cannot be static in
與成員變量一樣,成員方法也有一個返回所有成員方法的函數--get_class_methods()。此函數返回由指定的類中定義的方法名所組成的數組。 從 PHP 4.0.6 開始,可以指定對象本身來代替指定的類名。它屬于PHP內建函數,整個程序流程就是一個遍歷類成員方法列表,判斷是否為符合條件的方法,如果是,則將這個方法作為一個元素添加到返回數組中。
## 靜態成員方法[]()
類的靜態成員方法通常也叫做類方法。 與靜態成員變量不同,靜態成員方法與成員方法都存儲在類結構的function_table 字段。
類的靜態成員方法可以通過類名直接訪問。
class Tipi{
public static function t() {
echo 1;
}
}
?
Tipi::t();
以上的代碼在VLD擴展下生成的部分中間代碼如如下:
number of ops: 8
compiled vars: none
line # * op fetch ext return operands
---------------------------------------------------------------------------------
2 0 > EXT_STMT
1 NOP
8 2 EXT_STMT
3 ZEND_INIT_STATIC_METHOD_CALL 'Tipi','t'
4 EXT_FCALL_BEGIN
5 DO_FCALL_BY_NAME 0
6 EXT_FCALL_END
9 7 > RETURN 1
?
branch: # 0; line: 2- 9; sop: 0; eop: 7
path #1: 0,
Class Tipi:
Function t:
Finding entry points
Branch analysis from position: 0
從以上的內容可以看出整個靜態成員方法的調用是一個先查找方法,再調用的過程。而對于調用操作,對應的中間代碼為 ZEND_INIT_STATIC_METHOD_CALL。由于類名和方法名都是常量,于是我們可以知道中間代碼對應的函數是ZEND_INIT_STATIC_METHOD_CALL_SPEC_CONST_CONST_HANDLER。在這個函數中,它會首先調用zend_fetch_class函數,通過類名在EG(class_table)中查找類,然后再執行靜態方法的獲取方法。
if (ce->get_static_method) {
EX(fbc) = ce->get_static_method(ce, function_name_strval, function_name_strlen TSRMLS_CC);
} else {
EX(fbc) = zend_std_get_static_method(ce, function_name_strval, function_name_strlen TSRMLS_CC);
}
如果類結構中的get_static_method方法存在,則調用此方法,如果不存在,則調用zend_std_get_static_method。在PHP的源碼中get_static_method方法一般都是NULL,這里我們重點查看zend_std_get_static_method函數。此函數會查找ce->function_table列表,在查找到方法后檢查方法的訪問控制權限,如果不允許訪問,則報錯,否則返回函數結構體。關于訪問控制,我們在后面的小節中說明。
## 方法(Function)與函數(Method)的異同[]()
在前面的章節里,筆者介紹了函數的實現,函數與方法的本質是比較相似的,都是將一系列的邏輯放到一個集合里執行, 但二者在使用中也存在很多的不同,這里我們討論一下二者的實現。從實現的角度來看,二者內部代碼都被最終解釋為op_array,其執行是沒有區別的(除非使用了$this/self等對象特有的變法或方法),而二者的不同體現在兩個方面:一是定義(注冊)的實現;二是調用的實現;
### 定義(注冊)方式的實現[]()
函數和方法都是在編譯階段注冊到compiler_globals變量中的,二者都使用相同的內核處理函數**zend_do_begin_function_declaration()**和**zend_do_end_function_declaration()**來完成這一過程。二者的內部內容會被最終解釋并存儲為一個op_codes數組,但編譯后“掛載”的位置不同,如下圖:

圖5.2 PHP中函數與方法的注冊位置
> 使用vld等擴展查看OPCODES時,會發現函數和方法定義時的OPCODE都是**ZEND_NOP** 。 這是Zend引擎的一個優化,即在編譯時進行已經完成定義,不需要在執行時再次定義。
### 調用方式的實現[]()
定義位置的不同,以及性質的不同,決定了方法比函數要進行更多的驗證工作,方法的調用比函數的調用多一個名為**ZEND_INIT_METHOD_CALL**的OPCODE,其作用是把方法注冊到execute_data.fbc , 然后就可以使用與函數相同的處理函數**ZEND_DO_FCALL_BY_NAME**進行處理。
在**ZEND_DO_FCALL_BY_NAME()**處理函數中,絕大部分的處理沒有區別,只在一些細節上使用了類似于if (EX(object)){...}來處理一些方法的特性,有興趣的讀者可以看一個PHP源碼 $PHP_SOURCE/Zend/zend_vm_execute.h
中的相關代碼。
## 靜態方法和實例方法的小漏洞[]()
細心的讀者應該注意到前面提到靜態方法和實例方法都是保存在類結構體zend_class_entry.function_table中,那這樣的話,Zend引擎在調用的時候是怎么區分這兩類方法的,比如我們靜態調用實例方法或者實例調用靜態方法會怎么樣呢?
可能一般人不會這么做,不過筆者有一次錯誤的這樣調用了,而代碼沒有出現任何問題,在review代碼的時候意外發現筆者像實例方法那樣調用的靜態方法,而什么問題都沒有發生(沒有報錯)。在理論上這種情況是不應發生的,類似這這樣的情況在PHP中是非常的多的,例如前面提到的create_function方法返回的偽匿名方法,后面介紹訪問控制時還會介紹訪問控制的一些瑕疵,PHP在現實中通常采用Quick and Dirty的方式來實現功能和解決問題,這一點和Ruby完整的面向對象形成鮮明的對比。我們先看一個例子:
<?php
?
error_reporting(E_ALL);
?
class A {
public static function staticFunc() {
echo "static";
}
?
public function instanceFunc() {
echo "instance";
}
}
?
A::instanceFunc(); // instance
$a = new A();
$a->staticFunc(); // static
上面的代碼靜態的調用了實例方法,程序輸出了instance,實例調用靜態方法也會正確輸出static,這說明這兩種方法本質上并沒有卻別。唯一不同的是他們被調用的上下文環境,例如通過實例方法調用方法則上下文中將會有$this這個特殊變量,而在靜態調用中將無法使用$this變量。
不過實際上Zend引擎是考慮過這個問題的,將error_reporting的級別增加E_STRICT,將會出出現E_STRICT錯誤:
Strict Standards: Non-static method A::instanceFunc() should not be called statically
這只是不建議將實例方法靜態調用,而對于實例調用靜態方法沒有出現E_STRICT錯誤,有人說:某些事情可以做并不代表我們要這樣做。
PHP在實現新功能時通常采用漸進的方式,保證兼容性,在具體實現上通常采用打補丁的方式,這樣就造成有些”邊界“情況沒有照顧到。
- 第一章 準備工作和背景知識
- 第一節 環境搭建
- 第二節 源碼結構、閱讀代碼方法
- 第三節 常用代碼
- 第四節 小結
- 第二章 用戶代碼的執行
- 第一節 生命周期和Zend引擎
- 第二節 SAPI概述
- Apache模塊
- 嵌入式
- FastCGI
- 第三節 PHP腳本的執行
- 詞法分析和語法分析
- opcode
- opcode處理函數查找
- 第四節 小結
- 第三章 變量及數據類型
- 第一節 變量的結構和類型
- 哈希表(HashTable)
- PHP的哈希表實現
- 鏈表簡介
- 第二節 常量
- 第三節 預定義變量
- 第四節 靜態變量
- 第五節 類型提示的實現
- 第六節 變量的生命周期
- 變量的賦值和銷毀
- 變量的作用域
- global語句
- 第七節 數據類型轉換
- 第八節 小結
- 第四章 函數的實現
- 第一節 函數的內部結構
- 函數的內部結構
- 函數間的轉換
- 第二節 函數的定義,傳參及返回值
- 函數的定義
- 函數的參數
- 函數的返回值
- 第三節 函數的調用和執行
- 第四節 匿名函數及閉包
- 第五節 小結
- 第五章 類和面向對象
- 第一節 類的結構和實現
- 第二節 類的成員變量及方法
- 第三節 訪問控制的實現
- 第四節 類的繼承,多態及抽象類
- 第五節 魔術方法,延遲綁定及靜態成員
- 第六節 PHP保留類及特殊類
- 第七節 對象
- 第八節 命名空間
- 第九節 標準類
- 第十節 小結
- 第六章 內存管理
- 第一節 內存管理概述
- 第二節 PHP中的內存管理
- 第三節 內存使用:申請和銷毀
- 第四節 垃圾回收
- 新的垃圾回收
- 第五節 內存管理中的緩存
- 第六節 寫時復制(Copy On Write)
- 第七節 內存泄漏
- 第八節 小結
- 第七章 Zend虛擬機
- 第一節 Zend虛擬機概述
- 第二節 語法的實現
- 詞法解析
- 語法分析
- 實現自己的語法
- 第三節 中間代碼的執行
- 第四節 PHP代碼的加密解密
- 第五節 小結
- 第八章 線程安全
- 第二節 線程,進程和并發
- 第三節 PHP中的線程安全
- 第九章 錯誤和異常處理
- 第十章 輸出緩沖
- 第十六章 PHP語言特性的實現
- 第一節 循環語句
- foreach的實現
- 第二十章 怎么樣系列(how to)
- 附錄
- 附錄A PHP及Zend API
- 附錄B PHP的歷史
- 附錄C VLD擴展使用指南
- 附錄D 怎樣為PHP貢獻
- 附錄E phpt測試文件說明
- 附錄F PHP5.4新功能升級解析
- 附錄G:re2c中文手冊