### 3.4.3 繼承
繼承是面向對象編程技術的一塊基石,它允許創建分等級層次的類,它允許子類繼承父類所有公有或受保護的特征和行為,使得子類對象具有父類的實例域和方法,或子類從父類繼承方法,使得子類具有父類相同的行為。
繼承對于功能的設計和抽象是非常有用的,而且對于類似的對象增加新功能就無須重新再寫這些公用的功能。
PHP中通過`extends`關鍵詞繼承一個父類,一個類只允許繼承一個父類,但是可以多級繼承。
```php
class 父類 {
}
class 子類 extends 父類 {
}
```
前面的介紹我們已經知道,類中保存著成員屬性、方法、常量等,父類與子類之間通過`zend_class_entry.parent`建立關聯,如下圖所示。

問題來了:每個類都有自己獨立的常量、成員屬性、成員方法,那么繼承類父子之間的這些信息是如何進行關聯的呢?接下來我們將帶著這個疑問再重新分析一下類的編譯過程中是如何處理繼承關系的。
3.4.1.5一節詳細介紹了類的編譯過程,這里再簡單回顧下:首先為類分配一個zend_class_entry結構,如果沒有繼承類則生成一條類聲明的opcode(ZEND_DECLARE_CLASS),有繼承類則生成兩條opcode(ZEND_FETCH_CLASS、ZEND_DECLARE_INHERITED_CLASS),然后再繼續編譯常量、成員屬性、成員方法注冊到zend_class_entry中,最后編譯完成后調用`zend_do_early_binding()`進行 __父子類關聯__ 以及 __注冊到EG(class_table)符號表__。
如果父類在子類之前定義的,那么父子類之間的關聯就是在`zend_do_early_binding()`中完成的,這里不考慮子類在父類前定義的情況,實際兩者沒有本質差別,區別在于在哪一個階段執行。有繼承類的情況在`zend_do_early_binding()`中首先是查找父類,然后調用`do_bind_inherited_class()`處理,最后將`ZEND_FETCH_CLASS`、`ZEND_DECLARE_INHERITED_CLASS`兩條opcode刪除,這些過程前面已經介紹過了,下面我們重點看下`do_bind_inherited_class()`的處理過程。
```c
ZEND_API zend_class_entry *do_bind_inherited_class(
const zend_op_array *op_array, //這個是定義類的地方的
const zend_op *opline, //類聲明的opcode:ZEND_DECLARE_INHERITED_CLASS
HashTable *class_table, //CG(class_table)
zend_class_entry *parent_ce, //父類
zend_bool compile_time) //是否編譯時
{
zend_class_entry *ce;
zval *op1, *op2;
if (compile_time) {
op1 = CT_CONSTANT_EX(op_array, opline->op1.constant);
op2 = CT_CONSTANT_EX(op_array, opline->op2.constant);
}else{
...
}
...
//父子類關聯
zend_do_inheritance(ce, parent_ce);
//注冊到CG(class_table)
...
}
```
上面這個函數的處理與注冊非繼承類的`do_bind_class()`幾乎完全相同,只是多了一個`zend_do_inheritance()`一步,此函數輸入很直觀,只一個類及父類。
```c
//zend_inheritance.c #line:758
ZEND_API void zend_do_inheritance(zend_class_entry *ce, zend_class_entry *parent_ce)
{
zend_property_info *property_info;
zend_function *func;
zend_string *key;
zval *zv;
//interface、trait、final類檢查
...
ce->parent = parent_ce;
zend_do_inherit_interfaces(ce, parent_ce);
//下面就是繼承屬性、常量、方法
}
```
下面的操作我們根據一個示例逐個來看。
```php
//示例
class A {
const A1 = 1;
public $a1 = array(1);
private $a2 = 120;
public function get() {
echo "A::get()";
}
}
class B extends A {
const B1 = 2;
public $b1 = "ddd";
public function get() {
echo "B::get()";
}
}
```
#### 3.4.3.1 繼承屬性
前面我們已經介紹過:屬性按靜態、非靜態分別保存在兩個數組中,各屬性按照定義的先后順序編號(offset),同時按照這個編號順序存儲排列,而這些編號信息通過`zend_property_info`結構保存,全部靜態、非靜態屬性的`zend_property_info`保存在一個以屬性名為key的HashTable中,所以檢索屬性時首先根據屬性名找到此屬性的`zend_property_info`,然后拿到其屬性值的offset,再根據靜態、非靜態分別到`default_static_members_count`、`default_properties_table`數組中取出屬性值。
當類存在繼承關系時,操作方式是:__將屬性從父類復制到子類__ 。子類會將父類的公共、受保護的屬性值數組全部合并到子類中,然后將全部屬性的`zend_property_info`哈希表也合并到子類中。
合并的步驟:
__(1)合并非靜態屬性default_properties_table:__ 首先申請一個父類+子類非靜態屬性大小的數組,然后先將父類非靜態屬性復制到新數組,然后再將子類的非靜態數組接著父類屬性的位置復制過去,子類的default_properties_table指向合并后的新數組,default_properties_count更新為新數組的大小,最后將子類舊的數組釋放。
```c
if (parent_ce->default_properties_count) {
zval *src, *dst, *end;
...
zval *table = pemalloc(sizeof(zval) * (ce->default_properties_count + parent_ce->default_properties_count), ...);
ce->default_properties_table = table;
//復制父類、子類default_properties_table
do {
...
}while(dst != end);
//更新default_properties_count為合并后的大小
ce->default_properties_count += parent_ce->default_properties_count;
}
```
示例合并后的情況如下圖。

__(2)合并靜態屬性default_static_members_table:__ 與非靜態屬性相同,新申請一個父類+子類靜態屬性大小的數組,依次將父類、子類靜態屬性復制到新數組,然后更新子類default_static_members_table指向新數組。
__(3)更新子類屬性offset:__ 因為合并后原子類屬性整體向后移了,所以子類屬性的編號offset需要加上前面父類屬性的總大小。
```c
ZEND_HASH_FOREACH_PTR(&ce->properties_info, property_info) {
if (property_info->ce == ce) {
if (property_info->flags & ZEND_ACC_STATIC) {
//靜態屬性offset為數組下標,直接加上父類default_static_members_count即可
property_info->offset += parent_ce->default_static_members_count;
} else {
//非靜態屬性offset為內存偏移值,按zval大小遞增
property_info->offset += parent_ce->default_properties_count * sizeof(zval);
}
}
} ZEND_HASH_FOREACH_END();
```
__(4)合并properties_info哈希表:__ 這也是非常關鍵的一步,上面只是將父類的屬性值合并到了子類,但是索引屬性用的是properties_info哈希表,所以需要將父類的屬性索引表與子類的索引表合并。在合并的過程中就牽扯到父子類屬性的繼承、覆蓋問題了,各種情況具體處理如下:
* __父類屬性不與子類沖突 且 父類屬性是私有:__ 即父類屬性為private,且子類中沒有重名的,則將此屬性插入子類properties_info,但是更新其flag為ZEND_ACC_SHADOW,這種屬性將不能被子類使用;
* __父類屬性不與子類沖突 且 父類屬性是公有:__ 這種比較簡單,子類可以繼承使用,直接插入子類properties_info;
* __父類屬性與子類沖突 且 父類屬性為私有:__ 不繼承父類的,以子類原屬性為準,但是打上`ZEND_ACC_CHANGED`的flag,這種屬性父子類隔離,互不干擾;
* __父類屬性與子類沖突 且 父類屬性是公有或受保護的:__
* __父子類屬性一個是靜態一個是非靜態:__ 編譯錯誤;
* __父子類屬性都是非靜態:__ 用父類的offset,但是值用子類的,父子類共享;
* __父子類屬性都是靜態:__ 不繼承父類屬性,以子類原屬性為準,父子類隔離,互不干擾;
這個地方相對比較復雜,具體的合并策略在`do_inherit_property()`中,這里不再羅列代碼。
所以,繼承類實際上是把父類的屬性、常量、方法合并到了子類里面,上一節介紹實例化時會將普通成員屬性值復制到對象中去,這樣在實例化時子類就與普通的類的操作沒有任何差別了。
#### 3.4.3.2 繼承常量
常量的合并策略比較簡單,如果父類與子類沖突時用子類的,不沖突時則將父類的常量合并到子類。
```c
static void do_inherit_class_constant(zend_string *name, zval *zv, zend_class_entry *ce, zend_class_entry *parent_ce)
{
//父類定義的常量在子類中沒有定義
if (!zend_hash_exists(&ce->constants_table, name)) {
...
_zend_hash_append(&ce->constants_table, name, zv);
}
}
```
#### 3.4.3.3 繼承方法
與屬性一樣,子類可以繼承父類的公有、受保護的方法,方法的繼承比較復雜,因為會有訪問控制、抽象類、接口、Trait等多種限制條件。實現上與前面幾種相同,即父類的function_table合并到子類的function_table中。
首先是將子類function_table擴大,以容納父子類全部方法,然后遍歷父類function_table,逐個判斷是否可被子類繼承,如果可被繼承則插入到子類function_table中。
```c
if (zend_hash_num_elements(&parent_ce->function_table)) {
//擴展子類的function_table哈希表大小
zend_hash_extend(&ce->function_table,
zend_hash_num_elements(&ce->function_table) +
zend_hash_num_elements(&parent_ce->function_table), 0);
//遍歷父類function_table,檢查是否可被子類繼承
ZEND_HASH_FOREACH_STR_KEY_PTR(&parent_ce->function_table, key, func) {
zend_function *new_func = do_inherit_method(key, func, ce);
if (new_func) {
_zend_hash_append_ptr(&ce->function_table, key, new_func);
}
} ZEND_HASH_FOREACH_END();
}
```
在合并的過程中需要對父類的方法進行一系列檢查,最簡單的情況就是父類中定義的方法在子類中不存在,這種情況比較簡單,直接將父類的zend_function復制一份給子類。
```c
static zend_function *do_inherit_method(zend_string *key, zend_function *parent, zend_class_entry *ce)
{
zval *child = zend_hash_find(&ce->function_table, key);
if(child){
//方法與子類沖突
...
}
//父子類方法不沖突,直接復制
return zend_duplicate_function(parent, ce);
}
```
當然這里不完全是復制:如果繼承的父類是內部類則會硬拷貝一份zend_function結構(此結構的指針成員不復制);如果父類是用戶自定義的類,且繼承的方法沒有靜態變量則不會硬拷貝,而是增加zend_function的引用計數(zend_op_array.refcount)。
```c
//func是父類成員方法,ce是子類
static zend_function *zend_duplicate_function(zend_function *func, zend_class_entry *ce)
{
zend_function *new_function;
if (UNEXPECTED(func->type == ZEND_INTERNAL_FUNCTION)) {
//內部函數
//如果子類也是內部類則會調用malloc分配內存(不會被回收),否則在zend內存池分配
...
}else{
if (func->op_array.refcount) {
(*func->op_array.refcount)++;
}
if (EXPECTED(!func->op_array.static_variables)) {
return func;
}
//硬拷貝
new_function = zend_arena_alloc(&CG(arena), sizeof(zend_op_array));
memcpy(new_function, func, sizeof(zend_op_array));
}
}
```
合并時另外一個比較復雜的情況是父類與子類中的方法沖突了,即子類重寫了父類的方法,這種情況需要對父子類以及要合并的方法進行一系列檢查,這一步在`do_inheritance_check_on_method()`中完成,具體情況如下:
```c
static void do_inheritance_check_on_method(zend_function *child, zend_function *parent)
{
uint32_t child_flags;
uint32_t parent_flags = parent->common.fn_flags;
...
}
```
__(1)抽象子類的抽象方法與抽象父類的抽象方法沖突:__ 無法重寫,Fatal錯誤。
```php
abstract class B extends A {
abstract function test();
}
abstract class A
{
abstract function test();
}
============================
PHP Fatal error: Can't inherit abstract function A::test() (previously declared abstract in B)
```
判斷邏輯:
```c
//do_inheritance_check_on_method():
if ((parent->common.scope->ce_flags & ZEND_ACC_INTERFACE) == 0 //父類非接口
&& parent->common.fn_flags & ZEND_ACC_ABSTRACT //父類方法為抽象方法
&& parent->common.scope != (child->common.prototype ? child->common.prototype->common.scope : child->common.scope)
&& child->common.fn_flags & (ZEND_ACC_ABSTRACT|ZEND_ACC_IMPLEMENTED_ABSTRACT) //子類方法為抽象或實現了抽象方法
) {
zend_error_noreturn(E_COMPILE_ERROR, "Can't inherit abstract function %s::%s() (previously declared abstract in %s)",...);
}
```
__(2)父類方法為final:__ Fatal錯誤,final成員方法不得被重寫。
判斷邏輯:
```c
//do_inheritance_check_on_method():
if (UNEXPECTED(parent_flags & ZEND_ACC_FINAL)) {
zend_error_noreturn(E_COMPILE_ERROR, "Cannot override final method %s::%s()", ...);
}
```
__(3)父子類方法靜態屬性不一致:__ 父類方法為非靜態而子類的是靜態(或相反),Fatal錯誤。
```php
class A {
public function test(){}
}
class B extends A {
static public function test(){}
}
============================
PHP Fatal error: Cannot make non static method A::test() static in class B
```
判斷邏輯:
```c
//do_inheritance_check_on_method():
if (UNEXPECTED((child_flags & ZEND_ACC_STATIC) != (parent_flags & ZEND_ACC_STATIC))) {
zend_error_noreturn(E_COMPILE_ERROR,...);
}
```
__(4)抽象子類的抽象方法覆蓋父類非抽象方法:__ Fatal錯誤。
```php
class A {
public function test(){}
}
abstract class B extends A {
abstract public function test();
}
============================
PHP Fatal error: Cannot make non abstract method A::test() abstract in class B
```
判斷邏輯:
```c
//do_inheritance_check_on_method():
if (UNEXPECTED((child_flags & ZEND_ACC_ABSTRACT) > (parent_flags & ZEND_ACC_ABSTRACT))) {
zend_error_noreturn(E_COMPILE_ERROR, "Cannot make non abstract method %s::%s() abstract in class %s",...);
}
```
__(5)子類方法限制父類方法訪問權限:__ Fatal錯誤,不允許派生類限制父類方法的訪問權限,如父類方法為public,而子類試圖重寫為protected/private。
```php
class A {
public function test(){}
}
class B extends A {
protected function test(){}
}
============================
PHP Fatal error: Access level to B::test() must be public (as in class A)
```
判斷邏輯:
```c
//do_inheritance_check_on_method():
//ZEND_ACC_PPP_MASK = (ZEND_ACC_PUBLIC | ZEND_ACC_PROTECTED | ZEND_ACC_PRIVATE)
if (UNEXPECTED((child_flags & ZEND_ACC_PPP_MASK) > (parent_flags & ZEND_ACC_PPP_MASK))) {
zend_error_noreturn(E_COMPILE_ERROR, "Access level to %s::%s() must be %s (as in class %s)%s", ...);
} else if (((child_flags & ZEND_ACC_PPP_MASK) < (parent_flags & ZEND_ACC_PPP_MASK))
&& ((parent_flags & ZEND_ACC_PPP_MASK) & ZEND_ACC_PRIVATE)) {
child->common.fn_flags |= ZEND_ACC_CHANGED;
}
```
__(6)剩余檢查情況:__ 除了上面5中情形下無法重寫方法,剩下還有一步對函數參數的檢查,這個過程我們整體看一下。
```c
//do_inheritance_check_on_method():
if (UNEXPECTED(!zend_do_perform_implementation_check(child, parent))) {
...
zend_error(error_level, "Declaration of %s %s be compatible with %s", ZSTR_VAL(child_prototype), error_verb, ZSTR_VAL(method_prototype));
zend_string_free(child_prototype);
zend_string_free(method_prototype);
}
```
實際上`zend_do_perform_implementation_check()`這個函數是用來檢查一個方法是否實現了某抽象方法的,繼承的時候遵循的也是這個規則,所以這里可以將父類方法理解為抽象方法,只有子類方法實現了該"抽象方法"才能重寫父類方法。
```c
static zend_bool zend_do_perform_implementation_check(const zend_function *fe, const zend_function *proto)
{
...
//如果檢查的方法是__construct且父類方法不是interface和abstract則子類__construct覆蓋父類的
if ((fe->common.fn_flags & ZEND_ACC_CTOR)
&& ((proto->common.scope->ce_flags & ZEND_ACC_INTERFACE) == 0
&& (proto->common.fn_flags & ZEND_ACC_ABSTRACT) == 0)) {
return 1;
}
//如果父類方法為私有方法則子類方法可以覆蓋
if (proto->common.fn_flags & ZEND_ACC_PRIVATE) {
return 1;
}
//如果父類方法必傳參數小于子類的或者父類的總參數大于子類的則不能覆蓋
//如:
// 父類 public function test($a, $b = 3){}
// 子類 public function test($a, $b){}
if (proto->common.required_num_args < fe->common.required_num_args
|| proto->common.num_args > fe->common.num_args) {
return 0;
}
//可變函數,暫未理解這里的可變函數指哪類,忽略
...
//如果有定義的參數檢查參數類型是否匹配,如果顯式聲明了參數類型則父子類方法必須匹配
for (i = 0; i < num_args; i++) {
zend_arg_info *fe_arg_info = &fe->common.arg_info[i];
if (!zend_do_perform_type_hint_check(fe, fe_arg_info, proto, proto_arg_info)) {
return 0;
}
//是否引用也必須一致
if (fe_arg_info->pass_by_reference != proto_arg_info->pass_by_reference) {
return 0;
}
}
//如果父類方法聲明了返回值類型則子類方法必須聲明且類型一致,相反如果子類聲明了而父類無要求則可以
if (proto->common.fn_flags & ZEND_ACC_HAS_RETURN_TYPE) {
if (!(fe->common.fn_flags & ZEND_ACC_HAS_RETURN_TYPE)) {
return 0;
}
if (!zend_do_perform_type_hint_check(fe, fe->common.arg_info - 1, proto, proto->common.arg_info - 1)) {
return 0;
}
}
}
```
這個判斷過程還是比較復雜的,有些地方很難理解為什么設計,想了解完整過程的可以自行翻下代碼。
- 前言
- 第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超時控制的思考