PHP内核探索:类的结构和实现
2011-06-11 00:00
1736 查看
面向对象编程中我们的编程都是围绕类和对象进行的。那在PHP内部类是怎么实现的呢? 它的内存布局以及存储是怎么样的呢?继承、封装和多态又是怎么实现的呢?
类和函数类似,PHP内置及PHP扩展均可以实现自己的内部类,也可以由用户使用PHP代码进行定义。 当然我们在编写代码时通常是自己定义。
使用上,我们使用class关键字进行定义,后面接类名,类名可以是任何非PHP保留字的名字。 在类名后面紧跟着一对花括号,里面是类的实体,包括类所具有的属性,这些属性是对象的状态的抽象, 其表现为PHP中支持的数据类型,也可以包括对象本身,通常我们称其为成员变量。 除了类的属性, 类的实体中也包括类所具有的操作,这些操作是对象的行为的抽象,其表现为用操作名和实现该操作的方法, 通常我们称其为成员方法或成员函数。看类示例的代码:
这里定义了一个父类ParentClass,一个接口Ifce,一个子类Tipi。子类继承父类ParentClass, 实现接口Ifce,并且有一个静态变量$sa,一个类常量 CA,一个公用方法,一个私有方法和一个公用静态方法。 这些结构在Zend引擎内部是如何实现的?类的方法、成员变量是如何存储的?访问控制,静态成员是如何标记的?
首先,我们看看类的内部存储结构:
取上面这个结构的部分字段,我们分析文章最开始的那段PHP代码在内核中的表现。 如下表所示:
类的结构中,type有两种类型,数字标记为1和2。分别为一下宏的定义,也就是说用户定义的类和模块或者内置的类也是保存在这个结构里的:
对于父类和接口,都是保存在struct _zend_class_entry结构体中。这表示接口也是以类的形式存储, 而实现是一样的,并且在继承等操作时有与类操作的不同的处理。常规的成员方法存放在函数结构体的哈希表中, 而魔术方法则单独保存。 如在类定义中的 union _zend_function *constructor; 定义就是类的构造魔术方法, 它是以函数的形式存在于类结构中,并且与常规的方法分隔开来了。在初始化时,这些魔术方法都会被设置为NULL。
上面的class_entry_type语法说明在语法分析阶段将类分为三种类型:常规类(T_CLASS), 抽象类(T_ABSTRACT T_CLASS)和final类(T_FINAL T_CLASS )。 他们分别对应的类型在内核中为:
常规类(T_CLASS) 对应的type=0
抽象类(T_ABSTRACT T_CLASS) 对应type=ZEND_ACC_EXPLICIT_ABSTRACT_CLASS
final类(T_FINAL T_CLASS) 对应type=ZEND_ACC_FINAL_CLASS
除了上面的三种类型外,类还包含有另外两种类型没有加abstract关键字的抽象类和接口:
没有加abstract关键字的抽象类,它对应的type=ZEND_ACC_IMPLICIT_ABSTRACT_CLASS。 由于在class前面没有abstract关键字,在语法分析时并没有分析出来这是一个抽象类,但是由于类中拥有抽象方法, 在函数注册时判断成员函数是抽象方法或继承类中的成员方法是抽象方法时,会将这个类设置为此种抽象类类型。
接口,其type=ZEND_ACC_INTERFACE。接口类型的区分是在interface关键字解析时设置,见interface_entry:对应的语法说明。
这五种类型在Zend/zend_complie.h文件中定义如下:
常规类为0,在这里没有定义,并且在程序也是直接赋值为0。
语法解析完后就可以知道一个类是抽象类还是final类,普通的类,又或者接口。 定义类时调用了zend_do_begin_class_declaration和zend_do_end_class_declaration函数, 从这两个函数传入的参数,zend_do_begin_class_declaration函数用来处理类名,类的类别和父类, zend_do_end_class_declaration函数用来处理接口和类的中间代码 这两个函数在Zend/zend_complie.c文件中可以找到其实现。
在zend_do_begin_class_declaration中,首先会对传入的类名作一个转化,统一成小写,这也是为什么类名不区分大小的原因,如下代码:
运行时程序报错: Fatal error: Cannot redeclare class tipi。 这个错误会在运行生成中间的代码时触发。 此错误的判断过程在后面中间代码生成时说明。而关于类的名称的判断则是通过 T_STRING token, 在语法解析时做的判断, 但是这只能识别出类名是一个字符串。假如类名为一些关键字, 如下代码:
运行, 程序会显示: Fatal error: Cannot use 'self' as class name as it is reserved in...
以上的错误程序判断定义在 zend_do_begin_class_declaration 函数。 与self关键字一样, 还有parent, static两个关键字的判断在同一个地方。 当这个函数执行完后,我们会得到类声明生成的中间代码为:ZEND_DECLARE_CLASS 。 当然,如果我们是声明内部类的话,则生成的中间代码为: ZEND_DECLARE_INHERITED_CLASS。
根据生成的中间代码,我们在Zend/zend_vm_execute.h文件中找到其对应的执行函数 ZEND_DECLARE_CLASS_SPEC_HANDLER。 这个函数通过调用 do_bind_class 函数将此类加入到 EG(class_table) 。 在添加到列表的同时,也判断该类是否存在,如果存在,则添加失败,报我们之前提到的类重复声明错误,只是这个判断在编译开启时是不会生效的。
类相关的各个结构均保存在struct _zend_class_entry 结构体中。这些具体的类别在语法分析过程中进行区分。 识别出类的类别,类的类名等,并将识别出来的结果存放到类的结构中。
PHP内核探索:从SAPI接口开始
PHP内核探索:一次请求的开始与结束
PHP内核探索:一次请求生命周期
PHP内核探索:单进程SAPI生命周期
PHP内核探索:多进程/线程的SAPI生命周期
PHP内核探索:Zend引擎
PHP内核探索:再次探讨SAPI
PHP内核探索:Apache模块介绍
PHP内核探索:通过mod_php5支持PHP
PHP内核探索:Apache运行与钩子函数
PHP内核探索:嵌入式PHP
PHP内核探索:PHP的FastCGI
PHP内核探索:如何执行PHP脚本
PHP内核探索:PHP脚本的执行细节
PHP内核探索:操作码OpCode
PHP内核探索:PHP里的opcode
PHP内核探索:解释器的执行过程
PHP内核探索:变量概述
PHP内核探索:变量存储与类型
PHP内核探索:PHP中的哈希表
PHP内核探索:理解Zend里的哈希表
PHP内核探索:PHP哈希算法设计
PHP内核探索:翻译一篇HashTables文章
PHP内核探索:哈希碰撞攻击是什么?
PHP内核探索:常量的实现
PHP内核探索:变量的存储
PHP内核探索:变量的类型
PHP内核探索:变量的值操作
PHP内核探索:变量的创建
PHP内核探索:预定义变量
PHP内核探索:变量的检索
PHP内核探索:变量的类型转换
PHP内核探索:弱类型变量的实现
PHP内核探索:静态变量的实现
PHP内核探索:变量类型提示
PHP内核探索:变量的生命周期
PHP内核探索:变量赋值与销毁
PHP内核探索:变量作用域
PHP内核探索:诡异的变量名
PHP内核探索:变量的value和type存储
PHP内核探索:全局变量Global
PHP内核探索:变量类型的转换
PHP内核探索:内存管理开篇
PHP内核探索:Zend内存管理器
PHP内核探索:PHP的内存管理
PHP内核探索:内存的申请与销毁
PHP内核探索:引用计数与写时复制
PHP内核探索:PHP5.3的垃圾回收机制
PHP内核探索:内存管理中的cache
PHP内核探索:写时复制COW机制
PHP内核探索:数组与链表
PHP内核探索:使用哈希表API
PHP内核探索:数组操作
PHP内核探索:数组源码分析
PHP内核探索:函数的分类
PHP内核探索:函数的内部结构
PHP内核探索:函数结构转换
PHP内核探索:定义函数的过程
PHP内核探索:函数的参数
PHP内核探索:zend_parse_parameters函数
PHP内核探索:函数返回值
PHP内核探索:形参return value
PHP内核探索:函数调用与执行
PHP内核探索:引用与函数执行
PHP内核探索:匿名函数及闭包
PHP内核探索:面向对象开篇
PHP内核探索:类的结构和实现
PHP内核探索:类的成员变量
PHP内核探索:类的成员方法
PHP内核探索:类的原型zend_class_entry
PHP内核探索:类的定义
PHP内核探索:访问控制
PHP内核探索:继承,多态与抽象类
PHP内核探索:魔术函数与延迟绑定
PHP内核探索:保留类与特殊类
PHP内核探索:对象
PHP内核探索:创建对象实例
PHP内核探索:对象属性读写
PHP内核探索:命名空间
PHP内核探索:定义接口
PHP内核探索:继承与实现接口
PHP内核探索:资源resource类型
PHP内核探索:Zend虚拟机
PHP内核探索:虚拟机的词法解析
PHP内核探索:虚拟机的语法分析
PHP内核探索:中间代码opcode的执行
PHP内核探索:代码的加密与解密
PHP内核探索:zend_execute的具体执行过程
PHP内核探索:变量的引用与计数规则
PHP内核探索:新垃圾回收机制说明
类的结构
首先我们看看类是什么。类是用户定义的一种抽象数据类型,它是现实世界中某些具有共性事物的抽象。 有时我们也可以理解其为对象的类别。类也可以看作是一种复合型的结构,其需要存储多元化的数据, 如属性、方法、以及自身的一些性质等。类和函数类似,PHP内置及PHP扩展均可以实现自己的内部类,也可以由用户使用PHP代码进行定义。 当然我们在编写代码时通常是自己定义。
使用上,我们使用class关键字进行定义,后面接类名,类名可以是任何非PHP保留字的名字。 在类名后面紧跟着一对花括号,里面是类的实体,包括类所具有的属性,这些属性是对象的状态的抽象, 其表现为PHP中支持的数据类型,也可以包括对象本身,通常我们称其为成员变量。 除了类的属性, 类的实体中也包括类所具有的操作,这些操作是对象的行为的抽象,其表现为用操作名和实现该操作的方法, 通常我们称其为成员方法或成员函数。看类示例的代码:
class ParentClass { } interface Ifce { public function iMethod(); } final class Tipi extends ParentClass implements Ifce { public static $sa = 'aaa'; const CA = 'bbb'; public function __constrct() { } public function iMethod() { } private function _access() { } public static function access() { } }
这里定义了一个父类ParentClass,一个接口Ifce,一个子类Tipi。子类继承父类ParentClass, 实现接口Ifce,并且有一个静态变量$sa,一个类常量 CA,一个公用方法,一个私有方法和一个公用静态方法。 这些结构在Zend引擎内部是如何实现的?类的方法、成员变量是如何存储的?访问控制,静态成员是如何标记的?
首先,我们看看类的内部存储结构:
struct _zend_class_entry { char type; // 类型:ZEND_INTERNAL_CLASS / ZEND_USER_CLASS char *name;// 类名称 zend_uint name_length; // 即sizeof(name) - 1 struct _zend_class_entry *parent; // 继承的父类 int refcount; // 引用数 zend_bool constants_updated; zend_uint ce_flags; // ZEND_ACC_IMPLICIT_ABSTRACT_CLASS: 类存在abstract方法 // ZEND_ACC_EXPLICIT_ABSTRACT_CLASS: 在类名称前加了abstract关键字 // ZEND_ACC_FINAL_CLASS // ZEND_ACC_INTERFACE HashTable function_table; // 方法 HashTable default_properties; // 默认属性 HashTable properties_info; // 属性信息 HashTable default_static_members;// 类本身所具有的静态变量 HashTable *static_members; // type == ZEND_USER_CLASS时,取&default_static_members; // type == ZEND_INTERAL_CLASS时,设为NULL HashTable constants_table; // 常量 struct _zend_function_entry *builtin_functions;// 方法定义入口 union _zend_function *constructor; union _zend_function *destructor; union _zend_function *clone; /* 魔术方法 */ union _zend_function *__get; union _zend_function *__set; union _zend_function *__unset; union _zend_function *__isset; union _zend_function *__call; union _zend_function *__tostring; union _zend_function *serialize_func; union _zend_function *unserialize_func; zend_class_iterator_funcs iterator_funcs;// 迭代 /* 类句柄 */ zend_object_value (*create_object)(zend_class_entry *class_type TSRMLS_DC); zend_object_iterator *(*get_iterator)(zend_class_entry *ce, zval *object, intby_ref TSRMLS_DC); /* 类声明的接口 */ int(*interface_gets_implemented)(zend_class_entry *iface, zend_class_entry *class_type TSRMLS_DC); /* 序列化回调函数指针 */ int(*serialize)(zval *object, unsignedchar**buffer, zend_uint *buf_len, zend_serialize_data *data TSRMLS_DC); int(*unserialize)(zval **object, zend_class_entry *ce, constunsignedchar*buf, zend_uint buf_len, zend_unserialize_data *data TSRMLS_DC); zend_class_entry **interfaces; // 类实现的接口 zend_uint num_interfaces; // 类实现的接口数 char *filename; // 类的存放文件地址 绝对地址 zend_uint line_start; // 类定义的开始行 zend_uint line_end; // 类定义的结束行 char *doc_comment; zend_uint doc_comment_len; struct _zend_module_entry *module; // 类所在的模块入口:EG(current_module) };
取上面这个结构的部分字段,我们分析文章最开始的那段PHP代码在内核中的表现。 如下表所示:
字段名 | 字段说明 | ParentClass类 | Ifce接口 | Tipi类 |
---|---|---|---|---|
name | 类名 | ParentClass | Ifce | Tipi |
type | 类别 | 2 | 2 | 2 |
parent | 父类 | 空 | 空 | ParentClass类 |
refcount | 引用计数 | 1 | 1 | 2 |
ce_flags | 类的类型 | 0 | 144 | 524352 |
function_table | 函数列表 | 空 | function_name=iMethod | type=2 | fn_flags=258 | function_name=__construct | type=2 | fn_flags=8448 function_name=iMethod | type=2 | fn_flags=65800 function_name=_access | type=2 | fn_flags=66560 function_name=access | type=2 | fn_flags=257 |
interfaces | 接口列表 | 空 | 空 | Ifce接口 接口数为1 |
filename | 存放文件地址 | /tipi.php | /tipi.php | /ipi.php |
line_start | 类开始行数 | 15 | 18 | 22 |
line_end | 类结束行数 | 16 | 20 | 38 |
#define ZEND_INTERNAL_CLASS 1 #define ZEND_USER_CLASS 2
对于父类和接口,都是保存在struct _zend_class_entry结构体中。这表示接口也是以类的形式存储, 而实现是一样的,并且在继承等操作时有与类操作的不同的处理。常规的成员方法存放在函数结构体的哈希表中, 而魔术方法则单独保存。 如在类定义中的 union _zend_function *constructor; 定义就是类的构造魔术方法, 它是以函数的形式存在于类结构中,并且与常规的方法分隔开来了。在初始化时,这些魔术方法都会被设置为NULL。
类的实现
类的定义是以class关键字开始,在Zend/zend_language_scanner.l文件中,找到class对应的token为T_CLASS。 根据此token,在Zend/zend_language_parser.y文件中,找到编译时调用的函数:unticked_class_declaration_statement: class_entry_type T_STRING extends_from { zend_do_begin_class_declaration(&$1, &$2, &$3 TSRMLS_CC); } implements_list '{' class_statement_list '}' { zend_do_end_class_declaration(&$1, &$2 TSRMLS_CC); } | interface_entry T_STRING { zend_do_begin_class_declaration(&$1, &$2, NULL TSRMLS_CC); } interface_extends_list '{' class_statement_list '}' { zend_do_end_class_declaration(&$1, &$2 TSRMLS_CC); } ; class_entry_type: T_CLASS { $$.u.opline_num = CG(zend_lineno); $$.u.EA.type = 0; } | T_ABSTRACT T_CLASS { $$.u.opline_num = CG(zend_lineno); $$.u.EA.type = ZEND_ACC_EXPLICIT_ABSTRACT_CLASS; } | T_FINAL T_CLASS { $$.u.opline_num = CG(zend_lineno); $$.u.EA.type = ZEND_ACC_FINAL_CLASS; } ;
上面的class_entry_type语法说明在语法分析阶段将类分为三种类型:常规类(T_CLASS), 抽象类(T_ABSTRACT T_CLASS)和final类(T_FINAL T_CLASS )。 他们分别对应的类型在内核中为:
常规类(T_CLASS) 对应的type=0
抽象类(T_ABSTRACT T_CLASS) 对应type=ZEND_ACC_EXPLICIT_ABSTRACT_CLASS
final类(T_FINAL T_CLASS) 对应type=ZEND_ACC_FINAL_CLASS
除了上面的三种类型外,类还包含有另外两种类型没有加abstract关键字的抽象类和接口:
没有加abstract关键字的抽象类,它对应的type=ZEND_ACC_IMPLICIT_ABSTRACT_CLASS。 由于在class前面没有abstract关键字,在语法分析时并没有分析出来这是一个抽象类,但是由于类中拥有抽象方法, 在函数注册时判断成员函数是抽象方法或继承类中的成员方法是抽象方法时,会将这个类设置为此种抽象类类型。
接口,其type=ZEND_ACC_INTERFACE。接口类型的区分是在interface关键字解析时设置,见interface_entry:对应的语法说明。
这五种类型在Zend/zend_complie.h文件中定义如下:
#define ZEND_ACC_IMPLICIT_ABSTRACT_CLASS 0x10 #define ZEND_ACC_EXPLICIT_ABSTRACT_CLASS 0x20 #define ZEND_ACC_FINAL_CLASS 0x40 #define ZEND_ACC_INTERFACE 0x80
常规类为0,在这里没有定义,并且在程序也是直接赋值为0。
语法解析完后就可以知道一个类是抽象类还是final类,普通的类,又或者接口。 定义类时调用了zend_do_begin_class_declaration和zend_do_end_class_declaration函数, 从这两个函数传入的参数,zend_do_begin_class_declaration函数用来处理类名,类的类别和父类, zend_do_end_class_declaration函数用来处理接口和类的中间代码 这两个函数在Zend/zend_complie.c文件中可以找到其实现。
在zend_do_begin_class_declaration中,首先会对传入的类名作一个转化,统一成小写,这也是为什么类名不区分大小的原因,如下代码:
<?php class TIPI { } class tipi { } ?>
运行时程序报错: Fatal error: Cannot redeclare class tipi。 这个错误会在运行生成中间的代码时触发。 此错误的判断过程在后面中间代码生成时说明。而关于类的名称的判断则是通过 T_STRING token, 在语法解析时做的判断, 但是这只能识别出类名是一个字符串。假如类名为一些关键字, 如下代码:
class self { }
运行, 程序会显示: Fatal error: Cannot use 'self' as class name as it is reserved in...
以上的错误程序判断定义在 zend_do_begin_class_declaration 函数。 与self关键字一样, 还有parent, static两个关键字的判断在同一个地方。 当这个函数执行完后,我们会得到类声明生成的中间代码为:ZEND_DECLARE_CLASS 。 当然,如果我们是声明内部类的话,则生成的中间代码为: ZEND_DECLARE_INHERITED_CLASS。
根据生成的中间代码,我们在Zend/zend_vm_execute.h文件中找到其对应的执行函数 ZEND_DECLARE_CLASS_SPEC_HANDLER。 这个函数通过调用 do_bind_class 函数将此类加入到 EG(class_table) 。 在添加到列表的同时,也判断该类是否存在,如果存在,则添加失败,报我们之前提到的类重复声明错误,只是这个判断在编译开启时是不会生效的。
类相关的各个结构均保存在struct _zend_class_entry 结构体中。这些具体的类别在语法分析过程中进行区分。 识别出类的类别,类的类名等,并将识别出来的结果存放到类的结构中。
延伸阅读
此文章所在专题列表如下:PHP内核探索:从SAPI接口开始
PHP内核探索:一次请求的开始与结束
PHP内核探索:一次请求生命周期
PHP内核探索:单进程SAPI生命周期
PHP内核探索:多进程/线程的SAPI生命周期
PHP内核探索:Zend引擎
PHP内核探索:再次探讨SAPI
PHP内核探索:Apache模块介绍
PHP内核探索:通过mod_php5支持PHP
PHP内核探索:Apache运行与钩子函数
PHP内核探索:嵌入式PHP
PHP内核探索:PHP的FastCGI
PHP内核探索:如何执行PHP脚本
PHP内核探索:PHP脚本的执行细节
PHP内核探索:操作码OpCode
PHP内核探索:PHP里的opcode
PHP内核探索:解释器的执行过程
PHP内核探索:变量概述
PHP内核探索:变量存储与类型
PHP内核探索:PHP中的哈希表
PHP内核探索:理解Zend里的哈希表
PHP内核探索:PHP哈希算法设计
PHP内核探索:翻译一篇HashTables文章
PHP内核探索:哈希碰撞攻击是什么?
PHP内核探索:常量的实现
PHP内核探索:变量的存储
PHP内核探索:变量的类型
PHP内核探索:变量的值操作
PHP内核探索:变量的创建
PHP内核探索:预定义变量
PHP内核探索:变量的检索
PHP内核探索:变量的类型转换
PHP内核探索:弱类型变量的实现
PHP内核探索:静态变量的实现
PHP内核探索:变量类型提示
PHP内核探索:变量的生命周期
PHP内核探索:变量赋值与销毁
PHP内核探索:变量作用域
PHP内核探索:诡异的变量名
PHP内核探索:变量的value和type存储
PHP内核探索:全局变量Global
PHP内核探索:变量类型的转换
PHP内核探索:内存管理开篇
PHP内核探索:Zend内存管理器
PHP内核探索:PHP的内存管理
PHP内核探索:内存的申请与销毁
PHP内核探索:引用计数与写时复制
PHP内核探索:PHP5.3的垃圾回收机制
PHP内核探索:内存管理中的cache
PHP内核探索:写时复制COW机制
PHP内核探索:数组与链表
PHP内核探索:使用哈希表API
PHP内核探索:数组操作
PHP内核探索:数组源码分析
PHP内核探索:函数的分类
PHP内核探索:函数的内部结构
PHP内核探索:函数结构转换
PHP内核探索:定义函数的过程
PHP内核探索:函数的参数
PHP内核探索:zend_parse_parameters函数
PHP内核探索:函数返回值
PHP内核探索:形参return value
PHP内核探索:函数调用与执行
PHP内核探索:引用与函数执行
PHP内核探索:匿名函数及闭包
PHP内核探索:面向对象开篇
PHP内核探索:类的结构和实现
PHP内核探索:类的成员变量
PHP内核探索:类的成员方法
PHP内核探索:类的原型zend_class_entry
PHP内核探索:类的定义
PHP内核探索:访问控制
PHP内核探索:继承,多态与抽象类
PHP内核探索:魔术函数与延迟绑定
PHP内核探索:保留类与特殊类
PHP内核探索:对象
PHP内核探索:创建对象实例
PHP内核探索:对象属性读写
PHP内核探索:命名空间
PHP内核探索:定义接口
PHP内核探索:继承与实现接口
PHP内核探索:资源resource类型
PHP内核探索:Zend虚拟机
PHP内核探索:虚拟机的词法解析
PHP内核探索:虚拟机的语法分析
PHP内核探索:中间代码opcode的执行
PHP内核探索:代码的加密与解密
PHP内核探索:zend_execute的具体执行过程
PHP内核探索:变量的引用与计数规则
PHP内核探索:新垃圾回收机制说明
相关文章推荐
- PHP内核探索 —— 变量的创建:通过zval结构来实现
- PHP内核探索:弱类型变量的实现
- PHP内核探索:静态变量的实现
- PHP内核探索 —— 变量概述:变量在PHP的内部如何实现
- PHP内核探索 —— PHP中的哈希表:哈希表是PHP实现中尤为关键的数据结构
- PHP内核探索 —— Zend引擎:Zend引擎是PHP实现的核心
- PHP内核探索 —— 哈希碰撞攻击是什么:攻击的原理及实现
- PHP内核探索 —— 常量的实现
- PHP内核探索 —— 变量的类型:PHP弱类型变量特性是如何实现?
- PHP内核探索:常量的实现
- PHP内核探索:继承与实现接口
- PHP内核探索:函数的内部结构
- PHP内核探索:函数结构转换
- PHP内核原理(一)Zvals基本结构
- C语言的结构和联合,以及PHP是怎么实现弱类型的
- PHP内核探索:Apache模块介绍
- PHP内核探索:一次请求的开始与结束
- [PHP]算法-队列结构的PHP实现
- PHP内核探索:新垃圾回收机制说明
- PHP内核探索 —— PHP脚本的执行细节:PHP、C、汇编、机器码