面向對象的三大特性(封裝、繼承、多態),其中封裝是一個非常重要的特性。封裝隱藏了對象內部的細節和實現,使對象能夠集中而完整的描述并對應一個具體的事物,只提供對外的訪問接口,這樣可以在不改變接口的前提下改變實現細節,而且能使對象自我完備。除此之外,封裝還可以增強安全性和簡化編程。在面向對象的語言中一般是通過訪問控制來實現封裝的特性。PHP提供了public、protected及private三個層次訪問控制。這和其他面向對象的語言中對應的關鍵字語義一樣。這幾個關鍵字都用于修飾類的成員:
- private 用于禁止除類本身以外(包括繼承也屬于非類本身)對成員的訪問,用于隱藏類的內部數據和實現。
- protected 用于禁止除本類以及繼承該類的類以外的任何訪問。同樣用于封裝類的實現,同時給予類一定的擴展能力,因為子類還是可以訪問到這些成員。
- public 最好理解,被public修飾的成員可以被任意的訪問。
> 如果沒有設置訪問控制關鍵字,則類的成員方法和成員變量會被設置成默認的 public。
這三個關鍵字在語法解析時分別對應三種訪問控制的標記:
member_modifier:
T_PUBLIC { Z_LVAL($$.u.constant) = ZEND_ACC_PUBLIC; }
| T_PROTECTED { Z_LVAL($$.u.constant) = ZEND_ACC_PROTECTED; }
| T_PRIVATE { Z_LVAL($$.u.constant) = ZEND_ACC_PRIVATE; }
這三種訪問控制的標記是PHP內核中定義的三個常量,在Zend/zend_compile.h中,其定義如下:
#define ZEND_ACC_PUBLIC 0x100
#define ZEND_ACC_PROTECTED 0x200
#define ZEND_ACC_PRIVATE 0x400
#define ZEND_ACC_PPP_MASK (ZEND_ACC_PUBLIC | ZEND_ACC_PROTECTED | ZEND_ACC_PRIVATE)
> 我們經常使用16進制的數字表標示狀態,例如上面的訪問控制常量, 0x100使用二進制表示就為 0001 0000 0000 0x200為0010 0000 0000 0x400為0100 0000 0000 我們通過二進制的某個位來表示特定的意義,至于為什么ZEND_ACC_PUBLIC這幾個常量后面多兩個0, 這是因為0x01和0x10已經被占用了,使用和其他不同意義的常量值不一樣的值可以避免誤用。 通過簡單的二進制&即可的除某個數值是否表示特定的意義,例如:某個常量為0011 0000 0000,這個數值和 0001 0000 0000 做&, 如果結果為0則說明這個位上的值不為1,在上面的例子中就是這個訪問控制不具有public的級別。 當然PHP中不允許使用多個訪問控制修飾符修飾同一個成員。這種處理方式在很多語言中都很常見。
在前面有提到當我們沒有給成員方法或成員變量設置訪問控制時,其默認值為public。與常規的訪問控制實現一樣,也是在語法解析階段進行的。
method_modifiers:
/* empty */
{ Z_LVAL($$.u.constant) = ZEND_ACC_PUBLIC; }
| non_empty_member_modifiers { $$ = $1;
if (!(Z_LVAL($$.u.constant) & ZEND_ACC_PPP_MASK))
{ Z_LVAL($$.u.constant) |= ZEND_ACC_PUBLIC; } }
;
雖然是在語法解析時就已經設置了訪問控制,但其最終還是要存儲在相關結構中。在上面的語法解析過程中,訪問控制已經存儲在編譯節點中,在編譯具體的類成員時會傳遞給相關的結構。此變量會作為一個參數傳遞給生成中間代碼的函數。如在解析成員方法時,PHP內核是通過調用zend_do_begin_function_declaration函數實現,此函數的第五個參數表示訪問控制,在具體的代碼中,
// ...省略
fn_flags = Z_LVAL(fn_flags_znode->u.constant);
// ... 省略
?
op_array.fn_flags |= fn_flags;
// ...省略
如此,就將訪問控制的相關參數傳遞給了將要執行的中間代碼。 假如我們先現在有下面一段代碼:
class Tipi{
private static function t() {
echo 1;
}
}
?
Tipi::t();
這個還是上一小節中我們說明靜態成員方法的示例,只是,這里我們將其訪問控制從public變成了private。執行這段代碼會報錯:Fatal error: Call to private method Tipi::t() from context '' in...
根據前一節的內容我們知道,如果要執行一個靜態成員變量需要先獲得類,再獲得類的方法,最后執行訪方法。而是否有訪問權限的檢測的實現過程在獲取類的方法過程中,即在zend_std_get_static_method函數中。此函數在獲取了類的方法后,會執行訪問控制的檢查過程。
if (fbc->op_array.fn_flags & ZEND_ACC_PUBLIC) {
//公有方法,可以訪問
} else if (fbc->op_array.fn_flags & ZEND_ACC_PRIVATE) {
// 私有方法,報錯
} else if ((fbc->common.fn_flags & ZEND_ACC_PROTECTED)) {
// 保護方法,報錯
}
> 見前面有關訪問控制常量的討論,這是使用的是 fbc->op_array.fn_flags & ZEND_ACC_PUBLIC 而不是使用==來判斷訪問控制類型, 通過這種方式,op_array.fn_flags中可以保存不止訪問控制的信息,所以flag使用的是復數。
對于成員函數來說,其對于訪問控制存儲在函數結構體中的fn_flags字段中,不管是函數本身的common結構體中的fn_flags,還是函數包含所有中間代碼的代碼集合op_array中的fn_flags。
## 訪問控制的小漏洞[]()
先看一個小例子吧:
<?php
?
class A {
private $money = 10000;
public function doSth($anotherA) {
$anotherA->money = 10000000000;
}
?
public function getMoney() {
return $this->money;
}
}
?
$b = new A();
echo $b->getMoney(); // 10000
?
$a = new A();
$a->doSth($b);
echo $b->getMoney(); // 10000000000;
在$a變量的doSth()方法中我們直接修改了$b變量的私有成員money,當然我們不太可能這樣寫代碼,從封裝的角度來看,這也是不應該的行為,從PHP實現的角度來看,這并不是一個功能,在其他語言中并不是這樣表現的。這也是PHP面向對象不純粹的表現之一。
下面我們從實現上面來看看是什么造就了這樣的行為。以下函數為驗證某個屬性能否被訪問的驗證方法:
[static](http://www.php.net/static) int zend_verify_property_access(zend_property_info *property_info, zend_class_entry *ce TSRMLS_DC) /* {{{ */
{
switch (property_info->flags & ZEND_ACC_PPP_MASK) {
case ZEND_ACC_PUBLIC:
return 1;
case ZEND_ACC_PROTECTED:
return zend_check_protected(property_info->ce, EG(scope));
case ZEND_ACC_PRIVATE:
if ((ce==EG(scope) || property_info->ce == EG(scope)) && EG(scope)) {
return 1;
} else {
return 0;
}
break;
}
return 0;
}
在doSth()方法中,我們要訪問$b對象的屬性money,這是Zend引擎檢查我們能否訪問$b對象的這個屬性,這是Zend贏取獲取$b對象的類,以及要訪問的屬性信息,首先要看看這個屬性是否為public,公開的話直接訪問就好了。如果是protected的則繼續調用zend_check_protected()函數檢查,因為涉及到該類的父類,這里不繼續跟這個函數了,看看是private的情況下是什么情況,在函數doSth()執行的時候,這時的EG(scope)指向的正是類A,ce變量值得就是變量$b的類,而$b的類就是類A,這樣檢查就判斷成功返回1,也就表示可以訪問。
至于成員函數的檢查規則類似,就留給讀者自己去探索了。
- 第一章 準備工作和背景知識
- 第一節 環境搭建
- 第二節 源碼結構、閱讀代碼方法
- 第三節 常用代碼
- 第四節 小結
- 第二章 用戶代碼的執行
- 第一節 生命周期和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中文手冊