类的实现
前面介绍了 PHP 7 的类的种类和常用特性。从本节开始,会依次介绍类在 PHP 7 中的存储数据结构和类的静态属性、常量、方法、接口和特性的源码实现。最后,以继承的源码实现的分析结束本节。
类的结构
面向对象的核心是类,先来看看 PHP 7 中存储类的数据结构 zend_class_entry
:
struct _zend_class_entry {
char type;
zend_string *name;
struct _zend_class_entry *parent;
int refcount;
uint32_t ce_flags;
int default_properties_count;
int default_static_members_count;
zval *default_properties_table;
zval *default_static_members_table;
zval *static_members_table;
HashTable function_table;
HashTable properties_info;
HashTable constants_table;
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 *__callstatic;
union _zend_function *__tostring;
union _zend_function *__debugInfo;
union _zend_function *serialize_func;
union _zend_function *unserialize_func;
zend_class_iterator_funcs iterator_funcs;
/* handlers */
zend_object* (*create_object)(zend_class_entry *class_type);
zend_object_iterator *(*get_iterator)(zend_class_entry *ce, zval *object, int
by_ref);
int (*interface_gets_implemented)(zend_class_entry *iface, zend_class_entry
*class_type); /* a class implements this interface */
union _zend_function *(*get_static_method)(zend_class_entry *ce, zend_string*
method);
/* serializer callbacks */
int (*serialize)(zval *object, unsigned char **buffer, size_t *buf_len, zend_
serialize_data *data);
int (*unserialize)(zval *object, zend_class_entry *ce, const unsigned char
*buf, size_t buf_len, zend_unserialize_data *data);
uint32_t num_interfaces;
uint32_t num_traits;
zend_class_entry **interfaces;
zend_class_entry **traits;
zend_trait_alias **trait_aliases;
zend_trait_precedence **trait_precedences;
union {
struct {
zend_string *filename;
uint32_t line_start;
uint32_t line_end;
zend_string *doc_comment;
} user;
struct {
const struct _zend_function_entry *builtin_functions;
struct _zend_module_entry *module;
} internal;
} info;
};
此结构体的主要字段有以下几个。
-
type
:类的类型,共有两种——1 代表内置的类,2 代表用户自定义的类。#define ZEND_INTERNAL_CLASS //内置类 #define ZEND_USER_CLASS //用户自定义的类
-
name
:类名。 -
parent
:继承的父类指针。 -
refcount
:引用计数。 -
ce_flags
:位组合标记。其中0x10
表示此类有抽象方法,0x20
表示此类为抽象类,0x40
表示接口,0x80
表示特性,0x100
表示匿名类,0x400
表示其为某个类的子类。定义如下:#define ZEND_ACC_IMPLICIT_ABSTRACT_CLASS 0x10 #define ZEND_ACC_EXPLICIT_ABSTRACT_CLASS 0x20 #define ZEND_ACC_INTERFACE 0x40 #define ZEND_ACC_TRAIT 0x80 #define ZEND_ACC_ANON_CLASS 0x100 #define ZEND_ACC_ANON_BOUND 0x200 #define ZEND_ACC_INHERITED 0x400
-
default_properties_count
:默认普通属性个数。 -
default_static_members_count
:默认静态属性个数。 -
default_properties_table
:默认普通属性值数组。 -
default_static_members_table
:默认静态属性值数组。 -
static_members_table
:静态属性成员。 -
constructor
:构造方法。 -
destructor
:析构方法。 -
clone
:克隆方法。 -
__get
:魔术方法__get
。 -
__set
:魔术方法__set
。 -
__unset
:魔术方法__unset
。 -
__isset
:魔术方法__isset
。 -
__call
:魔术方法__call
。 -
__callstatic
:魔术方法__callstatic
。 -
__tostring
:魔术方法__tostring
。 -
__debugInfo
:魔术方法__debugInfo
。 -
serialize_func
:对象序列化方法。 -
unserialize_func
:对象反序列化方法。 -
iterator_funcs
:PHP 5 开始,支持接口并且内置了Iterator
接口,其为此接口的相关操作方法。 -
create_object
:实例化对象时调用的方法,默认为函数zend_objects_new
,可以通过扩展或修改源码来改变此值。 -
serialize
:序列化方法回调指针。 -
unserialize
:反序列化方法回调指针。 -
num_interfaces
:类implements
的接口个数。 -
num_traits
:类use
的特性个数。 -
interfaces
:类implements
的接口指针。 -
traits
:类use
的traits
指针。 -
trait_aliases
:类use
的特性方法的别名。 -
trait_precedences
:类use
的特性方法的优先级(用于多个特性有相同名称的方法时,解决冲突,6.1.4节中有代码示例)。 -
info
:记录类的其他信息,比如类所在的文件、注释之类。结合 PHP 代码说明特性相关的字段,示例为如下代码:
trait Win
{
public function exec(){
echo "I am Win! \n";
}
}
trait Mac {
public function exec(){
echo "I am Mac! \n";
}
}
trait Config {
public function filename() {
echo "php.ini\n";
}
}
class Php {
use Config {
Config::filename as configName;
}
use Win, Mac {
Win::exec insteadof Mac;
}
}
上面代码定义了 PHP
类。
-
它用了 3 个特性(分别为
Win
、Mac
、Config
),所以ce->num_traits
为 3,3 个特性的结构体分别为ce->traits[0]
、ce->traits[1]
、ce->traits[2]
。 -
代码
“Config::filename as configName”
添加了一条别名信息,存储在ce->trait_aliases[0]
。 -
代码
“Win::exec insteadof Mac”
添加了一条优先级信息,存储在ce->trait_precedences[0]
。
PHP 的类在编译(通过函数 zend_compile_class_decl()
)时生成。每个类对应着一个结构体 struct _zend_class_entry
,存储在一个以类名字(全部转为小写)为 key
的 HashTable
中,也就是全局变量 EG(class_table)
中。
介绍完了类的存储结构,再来介绍存储属性和方法的相关数据结构 zend_property_info
:
typedef struct _zend_property_info {
uint32_t offset; /* property offset for object properties or
property index for static properties */
uint32_t flags;
zend_string *name;
zend_string *doc_comment;
zend_class_entry *ce;
} zend_property_info;
各字段含义如下。
-
offset
:当查找普通属性时,此值为地址偏移量;查找静态属性时,此值为索引。可能大家很难理解,为什么如此相近的属性,却一个用地址偏移量,另一个用索引?因为普通属性存储在对象结构体struct_zend_object
的柔性数组properties_table
中(详见6.4.1),而静态属性存储在类结构体struct_zend_class_entry
的静态属性指针default_static_members_table
指向的内存块中。一个结构体中只能有一个柔性数组,而对象结构中,只有这一个字段
properties_table
是变长的数组,所以把此字段放在结构体最后面,用柔性数组即可。而类结构中,有三个字段(default_properties_table
、default_static_members_table
、static_members_table
)是变长数组,所以只能通过指针的方式实现。 -
flags:属性的访问权限以及是否是静态属性。
#define ZEND_ACC_PUBLIC 0x100 #define ZEND_ACC_PROTECTED 0x200 #define ZEND_ACC_PRIVATE 0x400 #define ZEND_ACC_STATIC 0x01
-
name:属性名称。
-
doc_comment:注释。
-
ce:所属的类指针。
仍然以示例说明:
class Php{
const VERSION_5 = 5; //
const VERSION_7 = 7; //
protected $_version; //
public function version(){
return $this->_version;
}
}
class Php7 extends Php{
protected $_ast; //
public function ast(){
return $this->_ast;
}
}
PHP 通过 zend_compile_class_decl()
函数将 AST 树转成 struct _zend_class_entry
结构体,通过 gdb 打印这段代码生成的两个 struct _zend_class_entry
结构。
查看 PHP 类的名称:
(gdb) p *compiler_globals.class_table.arData[157].val.value.ce.name.val@3
$2 = "Php"
示例代码中 PHP 类在源码中的数据结构存在的地址如下:
(gdb) p compiler_globals.class_table.arData[157].val.value.ce
$19 = (zend_class_entry *) 0x7ffff7c03018
类的数据结构示意图如图6-1所示。

大家知道,一个类的静态属性和静态方法是和类相关的,而普通属性是和对象相关的,所以下节先详细介绍静态属性、常量和方法。
静态属性、常量和方法
-
静态属性:如前文所述,静态属性存储在
properties_info
和default_static_members_table
中。properties_info
是一个HashTable
,当访问一个静态属性时,以变量名为key
,在properties_info
中找到对应的value
,再取结构体struct _zend_property_info
中的字段offset
。而default_static_members_table
是一个数组,所以default_static_members_table[offset]
即为目标属性值。类的静态属性查找示意图如图 6-2 所示。
Figure 2. 图6-2 类的静态属性查找示意图 -
常量:类常量存储在
HashTable
类型的constants_table
字段中。 -
方法:类的方法(包括类的静态方法和类的普通方法)和普通方法(非成员方法)编译后生成的
zend_op_array
基本没有区别。唯一区别就是类的方法编译后生成的zend_op_array
是存在于类结构体的function_table
中,不像普通方法,编译后存储在全局变量CG(function_table)
中。
类成员方法的访问权限(private
、protected
、public
)以及是否是静态方法等信息,存储在 zend_op_array
中的 fn_flags
字段里。
类的普通方法调用与静态方法调用基本无异。区别在于普通方法可以使用 $this
变量获取到当前所在对象。因此,在 ZEND_INIT_METHOD_CALL
操作中,最后初始化调用栈的时候会将当前对象当成参数 $this
传入。
因此,如果在一个类的普通方法的实现中,没有用到 $this
变量,那么把普通方法当成静态方法调用是没有问题的,否则会报语法错误。这和 C++
语言的实现基本上一样。
class Php
{
protected $_version;
public function version1()
{
echo "7.1.1\n";
}
public function version2()
{
$this->_version = "7.1.0";
echo $this->_version;
}
}
Php::version1();
Php::version2();
因为 PHP 没有指针的概念,为了更加深刻地理解类的普通方法的 this
变量,可通过一段神奇的 C++
代码来横向对比一下。
#include<iostream>
using namespace std;
class Php{
protected:
std::string _version;
public:
void version()
{
std::cout << "7.1.0" << endl;
this->_version = "7.1.0"; //会报错
}
};
int main(int argc, char* argv[])
{
Php* php = (Php*)0;
php->version();
return 0;
}
上面的代码并没有实例化一个 PHP 对象,而是直接使用了 0
这个指针,但通过这个空指针调用这个类的普通方法,程序仍然可以正常运行,和 PHP 的原理一样,因为我们并没有使用 this
指针,也没有修改非法内存。
接口和特性
(1)接口
PHP 只支持单一继承,也就是一个子类只能有一个父类。而为了实现类似于 C++
的多重继承的功能,PHP 引入了接口的概念。接下来介绍接口的实现。
在一个类初始化时,已经确定了此类的接口个数,而关联一个类与其所实现的接口,是通过函数 zend_do_implement_interface()
来实现的。
关联接口和类时,根据 PHP 关于接口的语法,可猜测进行了哪些操作。
-
ce->num_interfaces
加 1,将此接口的结构体指针赋值给ce->interfaces[ce->num_interfaces-1]
。 -
遍历接口中的
constants_table
,并依次插入到ce->constants_table
。如果类和接口有相同名字的常量,则报错。 -
遍历接口中的
function_table
,根据继承的逻辑,判断是否可以插入到类的function_table
中。如果可以,则继承此方法。否则不进行任何操作。 -
将接口中的
interfaces
按顺序拷贝到类的interfaces
后。
(2)特性
前文已介绍了特性的具体定义和代码示例,现在来介绍特性的具体实现。
特性与类进行关联通过方法 zend_do_bind_traits()
来实现:
ZEND_API void zend_do_bind_traits(zend_class_entry *ce) /* {{{ */
{
if (ce->num_traits <= 0) {
return;
}
/* complete initialization of trait strutures in ce */
zend_traits_init_trait_structures(ce);
/* first care about all methods to be flattened into the class */
zend_do_traits_method_binding(ce);
/* Aliases which have not been applied indicate typos/bugs. */
zend_do_check_for_inconsistent_traits_aliasing(ce);
/* then flatten the properties into it, to, mostly to notfiy developer about problems */
zend_do_traits_property_binding(ce);
/* verify that all abstract methods from traits have been implemented */
zend_verify_abstract_class(ce);
/* Emit E_DEPRECATED for PHP 4 constructors */
zend_check_deprecated_constructor(ce);
/* now everything should be fine and an added ZEND_ACC_IMPLICIT_ABSTRACT_CLASS
should be removed */
if (ce->ce_flags & ZEND_ACC_IMPLICIT_ABSTRACT_CLASS) {
ce->ce_flags -= ZEND_ACC_IMPLICIT_ABSTRACT_CLASS;
}
}
主要完成了如下操作。
-
完成类结构体指针
ce
的特性初始化。遍历ce
的特性优先级变量ce->trait_precedences
(假如遍历中的当前变量叫作cur_pre
)。校验优先级最高的类和方法的语法,如果优先级中类cur_pre->exclude_from_classes
还未编译,则进行编译(类似include
的编译);然后保证优先级中用到的特性类cur_pre->trait_method->ce
存在于变量ce->traits
中,且优先级特性中的方法cur_pre->trait_method->method_name
在特性类的方法表cur_pre->trait_method->ce->function_table
中,再遍历被排除的类和方法的变量cur_pre->exclude_from_classes
,校验其相关语法。遍历类结构体中的特性别名变量ce->trait_aliases
,和上面的逻辑一样,编译未编译的类,校验相关语法。 -
将特性的方法拷贝到类中。遍历类结构体中的变量
ce->traits
,将其方法拷贝到ce->function_table
。在拷贝之前执行判断:先将ce->trait_precedences
中因为优先级被排除的类的方法排除掉,如果有方法存在别名,则将以别名为新名的方法拷贝到ce->function_table
。 -
校验别名的相关语法。遍历类结构体中的特性别名变量
ce->trait_aliases
,如发现仍然有别名所属的类未找到,则抛出语法错误。 -
拷贝特性的属性到类中。遍历
ce->num_traits
,即遍历每个特性的属性properties_info
,如果此属性在类中存在,且是继承于父类,则将此属性删除,然后将特性中的属性拷贝到类中;若不是继承自父类,则继续遍历。如果此属性在类中不存在,则将特性中的属性拷贝到类中。 -
校验类是否已实现所有抽象方法,没有则报错。
-
校验类是否存在与类同名的方法来做构造方法,有则提示。
继承
继承划分了类的层次,父类代表的是更一般、更泛化的类,而子类则更为具体、细化。继承是实现代码重用、扩展软件功能的重要手段。子类中与父类完全相同的属性和方法不必重写,只需写出新增或改写的内容,不必一切从零开始。
PHP 只支持单一继承,实现相对简单。PHP 父类和子类是分别编译的。编译完成后,再对父类和子类进行继承。继承操作在函数 do_bind_inherited_class()
中完成。
继承属性
普通属性和静态属性的继承是先后完成的。在类结构中二者的存储十分相近,继承的操作也十分相近,这里只介绍普通属性继承的实现。
-
申请一个元素类型是
zval
的数组table
,大小为父类的普通属性个数(parent_ce->default_properties_count
)和子类的普通属性个数(ce->default_properties_count
)之和。 -
将父类的普通属性中
parent_ce->default_properties_table
的元素拷贝到数组table
。 -
将子类的普通属性中的
ce->default_properties_table
的ce->default_properties_count
个元素拷贝到table+parent_ce->default_properties_count
。 -
释放子类的普通属性指针
ce->default_properties_table
,将table
赋值给ce->default_properties_table
。
这样就完成了普通属性的合并,请看如图6-3所示的示意图。

类的静态属性也如此完成合并。
可以看出,子类的静态属性和普通属性在元素中的位置,相对于合并前都有偏移,所以要对其在 HashTable 中的偏移进行重置,重置的大致步骤如下。
-
遍历
properties_info
:如果元素是静态属性,则对offset
加parent_ce->default_static_members_count
。 -
如果元素是普通属性,则对
offset
加parent_ce->default_properties_count *sizeof(val)
。 -
接下来进行子类的
properties_info
和父类的properties_info
的合并。
由于不同的属性可能拥有不同的权限,例如父类和子类有重复的属性,甚至重复属性的类型也不同(这里的类型指普通属性和静态属性),所以这两个 HashTable 的合并会有很复杂的逻辑,但是基于以上讲的数据结构,实现起来并不复杂,对 PHP 语法特别熟稔的同学,完全可以自己实现这段代码,这里就不啰嗦了。
继承常量
常量存储是用 HashTable 实现的,两个 HashTable 的合并比较简单,无非就是遍历父类的 constants_table
。
-
如果子类中存在此常量,则不进行任何操作。
-
如果子类中不存在此常量,则把此
key->val
键值对插入到子类的constants_table
中。
if (zend_hash_num_elements(&parent_ce->constants_table)) {
zend_class_constant *c;
zend_hash_extend(&ce->constants_table,
zend_hash_num_elements(&ce->constants_table) +
zend_hash_num_elements(&parent_ce->constants_table), 0);
ZEND_HASH_FOREACH_STR_KEY_PTR(&parent_ce->constants_table, key, c) {
do_inherit_class_constant(key, c, ce);
} ZEND_HASH_FOREACH_END();
}
继承方法
与常量继承的实现类似,方法的继承也是遍历父类的 function_table
,然后将结果插入到子类的 function_table
中。不同的是,可能存在方法为 private
、abstract
或 final
特性,或者同一个方法在父类中为静态方法,而在子类中为普通方法等特殊情况。
if (zend_hash_num_elements(&parent_ce->function_table)) {
zend_hash_extend(&ce->function_table,
zend_hash_num_elements(&ce->function_table) +
zend_hash_num_elements(&parent_ce->function_table), 0);
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();
}