面向对象编程中我们的编程都是围绕类和对象进行的。那在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代码在内核中的表现。如表5.1所示:
字段名 | 字段说明 | ParentClass类 | Ifce接口 | Tipi类 |
---|---|---|---|---|
name | 类名 | ParentClass | Ifce | Tipi |
type | 类别 | 2(用户自定义) | 2 (用户自定义) | 2 (用户自定义,1为系统内置类) |
parent | 父类 | 空 | 空 | ParentClass类 |
refcount | 引用计数 | 1 | 1 | 2 |
ce_flags | 类的类型 | 0 | 144 | 524352 |
function_table | 函数列表 | 空 | function_name=iMethod | type=2 |
interfaces | 接口列表 | 空 | 空 | Ifce接口 接口数为1 |
filename | 存放文件地址 | /tipi.php | /tipi.php | /ipi.php |
line_start | 类开始行数 | 15 | 18 | 22 |
line_end | 类结束行数 | 16 | 20 | 38 |
类的结构中,type有两种类型,数字标记为1和2。分别为一下宏的定义,也就是说用户定义的类和模块或者内置的类也是保存在这个结构里的:
#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中本质上是一个变量,只是这些变量都归属于某个类,并且给这些变量是有访问控制的。类的成员变量也称为成员属性,它是现实世界实体属性的抽象,是可以用来描述对象状态的数据。
类的成员方法在PHP中本质上是一个函数,只是这个函数以类的方法存在,它可能是一个类方法也可能是一个实例方法,并且在这些方法上都加上了类的访问控制。类的成员方法是现实世界实体行为的抽象,可以用来实现类的行为。