面向对象是一种编程范式,它将对象作为程序的基本单元,将程序和数据封装起来,以此来提高程序的重用性、灵活性和可扩展性。
目前很多语言都支持面向对象编程,既然对象对象是一种范式,其实这就和具体的编程语言没有直接关系,只不过很多语言将这个范式作为语言的基本元素,使用C语言也能够进行面向对象编程。
面向对象的程序设计中包含:
类。类是具体事物的抽象。通常类定义了事物的属性和所能完成的工作。有一点需要注意,并不是所有的面向对象编程语言的类都具有class这个明确的实体。例如Javascript就不是基于类的。 Javascript中的类(Function)也具有类定义的特性。这也印证了面向对象只是一种编程范式。
对象。对象是类的实例。对象是具体的。
方法。方法是类定义对象可以做的事情。
继承性。继承是类的具体化,子类是比具备更多特性和行为的类。面向对象是对现实世界的一个抽象。在很多时候的关系并不一定是继承关系。能在一定程序上实现代码的重用。
封装性、抽象性。封装性能实现的复杂性隐藏,减少出错的可能。
从我们接触PHP开始,我们最先遇到的是函数:数组操作函数,字符串操作函数,文件操作函数等等。这些函数是我们使用PHP的基础,也是PHP自出生就支持的面向过程编程。面向过程将一个个功能封装,以一种模块化的思想解决问题。 面向对象听起来很美,但是现实中的编程语言中很少有纯粹的面向对象的语言,处于性能或者程序员的开发习惯,通常的编程语言都同时支持两种变编程方式。 PHP就是如此,从PHP4起开始支持面向对象编程。但PHP4的面向对象支持不太完善。从PHP5起,PHP引入了新的对象模型(Object Model),增加了许多新特性,包括访问控制、抽象类和final类、类方法、魔术方法、接口、对象克隆和类型提示等。并且在近期发布的PHP5.3版本中,针对面向对象编程增加了命名空间、延迟静态绑定(Late Static Binding)以及增加了两个魔术方法__callStatic()和__invoke()。 PHP中对象是按引用传递的,即对象进行赋值和操作的时候是按引用(reference)传递的,而不是整个对象的拷贝。 这一章我们从面向对象讲起,会说到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代码在内核中的表现。如表5.1所示:
类的结构中,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 )。 他们分别对应的类型在内核中为:
1. 常规类(T_CLASS) 对应的type=0
2. 抽象类(T_ABSTRACT T_CLASS) 对应type=ZEND_ACC_EXPLICIT_ABSTRACT_CLASS
final类(T_FINAL T_CLASS) 对应type=ZEND_ACC_FINAL_CLASS
除了上面的三种类型外,类还包含有另外两种类型没有加abstract关键字的抽象类和接口:
3. 没有加abstract关键字的抽象类,它对应的type=ZEND_ACC_IMPLICIT_ABSTRACT_CLASS。由于在class前面没有abstract关键字,在语法分析时并没有分析出来这是一个抽象类,但是由于类中拥有抽象方法,在函数注册时判断成员函数是抽象方法或继承类中的成员方法是抽象方法时,会将这个类设置为此种抽象类类型。
4. 接口,其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 结构体中。这些具体的类别在语法分析过程中进行区分。识别出类的类别,类的类名等,并将识别出来的结果存放到类的结构中。