php4의 객체
옛날 옛적에 PHP는 객체 지향 프로그래밍 구문을 지원하지 않았습니다. Zend 엔진(ZE1)이 php4에 도입되었으며 몇 가지 새로운 기능이 나타났습니다. 객체 데이터 유형을 포함합니다.
PHP 객체 유형의 진화
최초의 객체 지향 프로그래밍(OOP) 지원은 PHP 커널 개발자와 함께 객체 연관의 의미만 구현했습니다. php4 객체는 배열과 일부 메소드를 함께 바인딩합니다." 지금 공부하고 싶은 PHP 객체입니다.
Zend 엔진(ZE2)의 두 번째 주요 버전 릴리스는 PHP5에서 몇 가지 새로운 기능이 도입되었습니다. 예를 들어, 속성과 메서드는 액세스 수정자를 사용하여 클래스 정의 외부에 가시성을 표시할 수 있으며, 함수 오버로드를 사용하여 내부 언어 구성을 정의할 수 있으며, 사용자 정의 동작의 경우 인터페이스를 사용하여 API 표준화를 구현할 수 있습니다. 여러 클래스의 호출 체인 11장 "php5 객체"를 공부할 때 php5의 클래스 정의에서 이러한 기능을 구현하게 됩니다.
구현 클래스
OOP의 세계로 들어가기 전에 가볍게 여행을 떠나야 합니다. 그러므로 5장 "첫 번째 확장" "방금 내장된 스켈레톤 형태"로 확장을 반환하십시오.
원래의 운동에서 독립하기 위해서는 , 이 버전의 이름을 Sample2로 지정할 수 있습니다. PHP 소스 코드 디렉토리의 ext/sample2에 다음 세 파일을 넣으세요:
config.m4
PHP_ARG_ENABLE(sample2, [Whether to enable the "sample2" extension], [ enable-sample2 Enable "sample2" extension support]) if test $PHP_SAMPLE2 != "no"; then PHP_SUBST(SAMPLE2_SHARED_LIBADD) PHP_NEW_EXTENSION(sample2, sample2.c, $ext_shared) fi
php_saple2.h
#ifndef PHP_SAMPLE2_H /* Prevent double inclusion */ #define PHP_SAMPLE2_H /* Define Extension Properties */ #define PHP_SAMPLE2_EXTNAME "sample2" #define PHP_SAMPLE2_EXTVER "1.0" /* Import configure options when building outside of the PHP source tree */ #ifdef HAVE_CONFIG_H #include "config.h" #endif /* Include PHP Standard Header */ #include "php.h" /* Define the entry point symbol * Zend will use when loading this module */ extern zend_module_entry sample2_module_entry; #define phpext_sample2_ptr &sample2_module_entry #endif /* PHP_SAMPLE2_H */
Sample2.c
#include "php_sample2.h" static function_entry php_sample2_functions[] = { { NULL, NULL, NULL } }; PHP_MINIT_FUNCTION(sample2) { return SUCCESS; } zend_module_entry sample2_module_entry = { #if ZEND_MODULE_API_NO >= 20010901 STANDARD_MODULE_HEADER, #endif PHP_SAMPLE2_EXTNAME, php_sample2_functions, PHP_MINIT(sample2), NULL, /* MSHUTDOWN */ NULL, /* RINIT */ NULL, /* RSHUTDOWN */ NULL, /* MINFO */ #if ZEND_MODULE_API_NO >= 20010901 PHP_SAMPLE2_EXTVER, #endif STANDARD_MODULE_PROPERTIES }; #ifdef COMPILE_DL_SAMPLE2 ZEND_GET_MODULE(sample2) #endif
이제 5장과 마찬가지로 phpize, ./configure, make를 실행하여 Sample2.so 확장 모듈을 빌드할 수 있습니다.
이전 config.w32도 작동할 수 있습니다. 일반적으로 여기에 제공된 config.m4와 동일한 수정을 하면
클래스 항목 정의
사용자 공간에서 다음과 같이 클래스를 정의합니다.
<?php class Sample2_FirstClass { } ?>
가능한 한 의심할 바 없이 이것을 확장에서 구현하는 것은 다소 어렵습니다. 먼저, 이전 장에서 int le_sample_descriptor를 정의한 것처럼 소스 코드 파일에 zend_class_entry 포인터를 정의해야 합니다.
zend_class_entry *php_sample2_firstclass_entry;
이제 다음을 수행할 수 있습니다. MINIT 함수에서 클래스를 초기화하고 등록합니다.
PHP_MINIT_FUNCTION(sample2) { zend_class_entry ce; /* 临时变量 */ /* 注册类 */ INIT_CLASS_ENTRY(ce, "Sample2_FirstClass", NULL); php_sample2_firstclass_entry = zend_register_internal_class(&ce TSRMLS_CC); return SUCCESS; }
이 확장을 빌드하고 get_declared_classes()를 테스트하면 이제 Sample2_FirstClass를 사용자 공간에서 사용할 수 있는 것을 볼 수 있습니다.
메서드 구현 정의
이 시점에서 구현하는 것은 stdClass일 뿐이며 물론 사용 가능합니다. 그러나 실제로는 클래스가 무언가를 수행하기를 원합니다.
이를 달성하려면 목표를 달성하려면 5장에서 배운 내용으로 돌아가야 합니다. INIT_CLASS_ENTRY()에 전달된 NULL 매개 변수를 php_sample2_firstclass_functions로 바꾸고 다음과 같이 MINIT 함수 바로 위에 이 구조를 정의해야 합니다. 물론, 이것은 원래 정의 프로세스와 동일합니다. 심지어 이 구조를 설정하는 방법도 매우 유사합니다.
static function_entry php_sample2_firstclass_functions[] = { { NULL, NULL, NULL } };
물론 그럴 수도 있습니다. 또한 PHP_FE(method1, NULL)를 선택합니다. 그러나 5장을 검토하면 함수 구현 이름이 zif_method1이며 잠재적으로 다른 method1() 구현과 충돌할 수 있습니다. 함수의 네임스페이스 안전을 위해 클래스 이름 앞에 붙습니다.
PHP_FALIAS(method1, Sample2_FirstClass_method1, NULL) 형식은 괜찮지만 약간 직관적이지 않으며 나중에 코드를 다시 보면 "왜 PHP_FE()가 사용되지 않았는지 궁금할 것입니다. ?"
이제 함수 목록을 추가했습니다. 이제 클래스가 정의되었으므로 몇 가지 메서드를 정의할 차례입니다. php_sample2_firstclass_functions 구조에 다음 함수를 생성합니다:
PHP_NAMED_FE(method1, PHP_FN(Sample2_FirstClass_method1), NULL)
이에 따라, 함수 목록에 PHP_NAMED_FE() 항목을 추가하세요:
PHP_FUNCTION(Sample2_FirstClass_countProps) { RETURN_LONG(zend_hash_num_elements(Z_OBJPROP_P(getThis()))); }
메서드와 함수 이름이 대소문자인지 확인하기 위해 여기에서 사용자 공간에 표시되는 함수 이름은 모두 소문자입니다. -구분하지 않음, 이름을 모두 소문자로 지정하려면 내부 함수가 필요합니다.
여기서 유일한 새로운 요소는 getThis()입니다. 모든 PHP 버전에서는 매크로로 구문 분석되며 확장자는 This_ptr입니다. 예를 들어, 사용 가능한 객체 인스턴스가 없으면 getThis()는 NULL을 반환합니다.
객체의 데이터 반환 의미입니다. 메소드는 절차적 함수의 메소드와 일치하며 매개변수 승인과 arg_info는 동일한 세트입니다.
static function_entry php_sample2_firstclass_functions[] = { PHP_NAMED_FE(countprops, PHP_FN(Sample2_FirstClass_countProps), NULL) { NULL, NULL, NULL } };
생성자
클래스 생성자는 다른 일반 클래스 메소드와 이름 지정처럼 구현될 수 있습니다. 동일한 규칙을 따릅니다. 생성자의 이름을 클래스 이름으로 지정해야 한다는 점은 다른 두 가지 ZE1 매직 메서드인 __sleep() 및 __wakeup()도 이 방식으로 구현할 수 있다는 것입니다. 🎜>
php4에서 내부 개체 간의 상속은 불완전하므로 피하는 것이 가장 좋습니다. 실제로 다른 개체를 상속해야 하는 경우 다음 ZE1 코드를 복사해야 합니다.PHP_FUNCTION(Sample2_FirstClass_sayHello) { char *name; int name_len; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &name, &name_len) == FAILURE) { RETURN_NULL(); } php_printf("Hello"); PHPWRITE(name, name_len); php_printf("!\nYou called an object method!\n"); RETURN_TRUE; }이러한 함수를 정의하세요. MINIT의 zend_register_internal_class에서 이를 호출할 수 있습니다:
void php_sample2_inherit_from_class(zend_class_entry *ce, zend_class_entry *parent_ce) { zend_hash_merge(&ce->function_table, &parent_ce->function_table, (void (*)(void *))function_add_ref, NULL, sizeof(zval*), 0); ce->parent = parent_ce; if (!ce->handle_property_get) { ce->handle_property_get = parent_ce->handle_property_get; } if (!ce->handle_property_set) { ce->handle_property_set = parent_ce->handle_property_set; } if (!ce->handle_function_call) { ce->handle_function_call = parent_ce->handle_function_call; } if (!zend_hash_exists(&ce->function_table, ce->name, ce->name_length + 1)) { zend_function *fe; if (zend_hash_find(&parent_ce->function_table, parent_ce->name, parent_ce->name_length + 1, (void**)fe) == SUCCESS) { zend_hash_update(&ce->function_table, ce->name, ce->name_length + 1, fe, sizeof(zend_function), NULL); function_add_ref(fe); } } }이러한 방식으로 상속이 가능하더라도 ZE1의 상속은 PHP에서 대부분의 OOP 관행에 대해 내부 개체의 상속을 처리하도록 설계되지 않았기 때문에 피해야 합니다. ZE2 및 수정된 개체 모델은 강력하며 모든 OOP 관련 작업은 이를 사용하여 직접 처리하는 것이 좋습니다.예제 작업
和其它用户空间变量一样, 对象存储在zval *容器中. 在ZE1中, zval *包含了一个HashTable *用于保存属性, 以及一个zend_class_entry *指针, 指向类的定义. 在ZE2中, 这些值被一个句柄表替代, 增加了一个数值的对象ID, 它和资源ID的用法类似.
很幸运, ZE1和ZE2的这些差异被第2章"变量的里里外外"中介绍的Z_*()族宏隐藏了, 因此在你的扩展中不需要关心这些. 下表10.1列出了两个ZE1的宏, 与非OOP的相关宏一致, 它们也有对应的_P和_PP版本, 用来处理一级或两级间访.
创建实例
大部分时间, 你的扩展都不需要自己创建实例. 而是用户空间调用new关键字创建实例并调用你的类构造器.
但你还是有可能需要创建实例, 比如在工厂方法中, ZEND_API中的object_init_ex(zval *val, zend_class_entry *ce)函数可以用于将对象实例初始化到变量中.
要注意, object_init_ex()函数并不会调用构造器. 当在内部函数中实例化对象时, 构造器必须手动调用. 下面的过程函数重演了new关键字的功能逻辑:
PHP_FUNCTION(sample2_new) { int argc = ZEND_NUM_ARGS(); zval ***argv = safe_emalloc(sizeof(zval**), argc, 0); zend_class_entry *ce; if (argc == 0 || zend_get_parameters_array_ex(argc, argv) == FAILURE) { efree(argv); WRONG_PARAM_COUNT; } /* 第一个参数是类名 */ SEPARATE_ZVAL(argv[0]); convert_to_string(*argv[0]); /* 类名存储为小写 */ php_strtolower(Z_STRVAL_PP(argv[0]), Z_STRLEN_PP(argv[0])); if (zend_hash_find(EG(class_table), Z_STRVAL_PP(argv[0]), Z_STRLEN_PP(argv[0]) + 1, (void**)&ce) == FAILURE) { php_error_docref(NULL TSRMLS_CC, E_WARNING, "Class %s does not exist.", Z_STRVAL_PP(argv[0])); zval_ptr_dtor(argv[0]); efree(argv); RETURN_FALSE; } object_init_ex(return_value, ce); /* 如果有构造器则调用, 额外的参数将传递给构造器 */ if (zend_hash_exists(&ce->function_table, Z_STRVAL_PP(argv[0]),Z_STRLEN_PP(argv[0]) + 1)) { /* 对象有构造器 */ zval *ctor, *dummy = NULL; /* 构造器名字是类名 */ MAKE_STD_ZVAL(ctor); array_init(ctor); zval_add_ref(argv[0]); add_next_index_zval(ctor, *argv[0]); zval_add_ref(argv[0]); add_next_index_zval(ctor, *argv[0]); if (call_user_function_ex(&ce->function_table, NULL, ctor, &dummy, /* 不关心返回值 */ argc - 1, argv + 1, /* 参数 */ 0, NULL TSRMLS_CC) == FAILURE) { php_error_docref(NULL TSRMLS_CC, E_WARNING, "Unable to call constructor"); } if (dummy) { zval_ptr_dtor(&dummy); } zval_ptr_dtor(&ctor); } zval_ptr_dtor(argv[0]); efree(argv); }
不要忘了在php_sample2_functions中增加一个引用. 它是你的扩展的过程函数列表, 而不是类方法的列表. 为了使用php_strtolower()函数, 还需要增加#include "ext/standard/php_string.h".
这个函数是目前你实现的最复杂的一个, 其中有几个全新的特性. 首先就是SEPARATE_ZVAL(), 实际上它的功能你已经实现过很多次, 利用zval_copy_ctor()赋值值到一个临时的结构体, 避免修改原始的内容. 不过它是一个宏版本的封装.
php_strtolower()用于将类名转换为小写, 这样做是为了达到php类名和函数名不区分大小写的目的. 这只是附录B中列出的众多PHPAPI工具函数的其中一个.
EG(class_table)是一个全局变量, 所有的zend_class_entry定义都注册到它里面. 要注意的是在ZE1(php4)中这个HashTable存储了一级间访的zend_class_entry *结构体. 而在ZE2(php5)中, 它被存储为两级间访. 这应该不会是一个问题, 因为对这个HashTable的直接访问并不常见, 但知道这一点总归是有好处的.
call_user_function_ex()是你将在第20章"高级嵌入式"中看到的ZENDAPI调用的一部分. 这里你将从zend_get_parameters_ex()接收到的zval **参数栈第一个元素拿走, 这样做就是为了原封不动的将剩余的参数传递给构造器.
译注: 原著中的代码在译者的环境(php-5.4.9)中不能运行, 需要将zend_class_entry *ce修改为二级间访. 下面给出译者测试通过的代码.
PHP_FUNCTION(sample_new) { int argc = ZEND_NUM_ARGS(); zval ***argv = safe_emalloc(sizeof(zval **), argc, 0); zend_class_entry **ce; /* 译注: 这里在译者的环境(php-5.4.9)是二级间访 */ /* 数组方式读取所有传入参数 */ if ( argc == 0 || zend_get_parameters_array_ex(argc, argv) == FAILURE ) { efree(argv); WRONG_PARAM_COUNT; } /* 隔离第一个参数(隔离为了使下面的类型转换不影响原始数据) */ SEPARATE_ZVAL(argv[0]); /* 将第一个参数转换为字符串类型, 并转为小写(因为php的类名是不区分大小写的) */ convert_to_string(*argv[0]); php_strtolower(Z_STRVAL_PP(argv[0]), Z_STRLEN_PP(argv[0])); /* 在类的HashTable中查找提供的类是否存在, 如果存在, ce中就得到了对应的zend_class_entry * */ if ( zend_hash_find(EG(class_table), Z_STRVAL_PP(argv[0]), Z_STRLEN_PP(argv[0]) + 1, (void **)&ce) == FAILURE ) { php_error_docref(NULL TSRMLS_CC, E_WARNING, "Class %s does not exist.", Z_STRVAL_PP(argv[0])); zval_ptr_dtor(argv[0]); efree(argv); RETURN_FALSE; } /* 将返回值初始化为查找到的类的对象 */ object_init_ex(return_value, *ce); /* 检查类是否有构造器 */ if ( zend_hash_exists(&(*ce)->function_table, Z_STRVAL_PP(argv[0]), Z_STRLEN_PP(argv[0]) + 1) ) { zval *ctor, *dummy = NULL; /* 将ctor构造为一个数组, 对应的用户空间形式为: array(argv[0], argv[0]), * 实际上对应于用户空间调用类的静态方法时$funcname的参数形式: * array(类名, 方法名) */ MAKE_STD_ZVAL(ctor); array_init(ctor); zval_add_ref(argv[0]); add_next_index_zval(ctor, *argv[0]); zval_add_ref(argv[0]); add_next_index_zval(ctor, *argv[0]); /* 调用函数 */ if ( call_user_function_ex(&(*ce)->function_table, NULL, ctor, &dummy, argc - 1, argv + 1, 0, NULL TSRMLS_CC) == FAILURE ) { php_error_docref(NULL TSRMLS_CC, E_WARNING, "Unable to call constructor"); } /* 如果有返回值直接析构丢弃 */ if ( dummy ) { zval_ptr_dtor(&dummy); } /* 析构掉临时使用(用来描述所调用方法名)的数组 */ zval_ptr_dtor(&ctor); } /* 析构临时隔离出来的第一个参数(类名) */ zval_ptr_dtor(argv[0]); /* 释放实参列表空间 */ efree(argv); }
接受实例
有时你的函数或方法需要接受用户空间的对象参数. 对于这种目的, zend_parse_parameters()提供了两种格式的修饰符. 第一种是o(小写字母o), 它将验证传递的参数是否是对象, 并将它设置到传递的zval **中. 下面是这种方式的一个简单的用户空间函数示例, 它返回传入对象的类名.
PHP_FUNCTION(sample2_class_getname) { zval *objvar; zend_class_entry *objce; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "o", &objvar) == FAILURE) { RETURN_NULL(); } objce = Z_OBJCE_P(objvar); RETURN_STRINGL(objce->name, objce->name_length, 1); }
第二种修饰符是O(大写字母O), 它不仅允许zend_parse_parameters()验证zval *的类型, 还可以验证所传递对象的类. 要做到这一点, 就需要传递一个zval **容易以及一个zend_class_entry *用来验证, 比如下面的实现就期望传入的是Sample2_FirstClass类的实例:
PHP_FUNCTION(sample2_reload) { zval *objvar; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "O", &objvar, php_sample2_firstclass_entry) == FAILURE) { RETURN_NULL(); } /* 调用假想的"reload"函数 */ RETURN_BOOL(php_sample2_fc_reload(objvar TSRMLS_CC)); }
访问属性
你已经看到了, 类方法可以通过getThis()获取到当前对象实例. 将这个宏的结果或其它包含对象实例的zval *与Z_OBJPROP_P()宏组合, 得到的HashTable *就包含了该对象的所有属性.
对象的属性列表是一个包含zval *的HashTable *, 它只是另外一种放在特殊位置的用户空间变量列表. 和使用zend_hash_find(EG(active_symbol_table), ...)从当前作用域获取变量一样, 你也可以使用第8章"在数组和HashTable上工作"中学习的zend_hash-API去获取或设置对象的属性.
例如, 假设在变量rcvdclass这个zval *中包含的是Sample2_FirstClass的实例, 下面的代码块就可以从它的标准属性HashTable中取到属性foo.
zval **fooval; if (zend_hash_find(Z_OBJPROP_P(rcvdclass), "foo", sizeof("foo"), (void**)&fooval) == FAILURE) { /* $rcvdclass->foo doesn't exist */ return; }
要向属性表中增加元素, 则是这个过程的逆向过程, 调用zend_hash_add()去增加元素, 或者也可以将第8章介绍数组时介绍的add_assoc_*()族函数的assoc替换为property来处理对象.
下面的构造器函数为Sample2_FirstClass的实例提供了一些预先设置的默认属性:
PHP_NAMED_FUNCTION(php_sample2_fc_ctor) { /* 为了简洁, 同时演示函数名可以是任意的, 这里实现的函数名并不是类名 */ zval *objvar = getThis(); if (!objvar) { php_error_docref(NULL TSRMLS_CC, E_WARNING, "Constructor called statically!"); RETURN_FALSE; } add_property_long(objvar, "life", 42); add_property_double(objvar, "pi", 3.1415926535); /* 构造器的返回值会被忽略(请回顾前面构造器的例子) */ }
现在可以通过php_sample2_firstclass_functions列表将它连接到对象的构造器:
PHP_NAMED_FE(sample2_firstclass, php_sample2_fc_ctor, NULL)
译注: 由于前面的sample_new()工厂函数在call_user_function_ex()调用构造器时使用的是静态方法的调用格式, 因此, 如果是使用这个工厂函数触发的构造器调用, getThis()就不会有期望的结果. 因此译者对例子进行了相应的修改, 读者如果在这块遇到问题可以参考译者的代码.
PHP_FUNCTION(sample_new) { int argc = ZEND_NUM_ARGS(); zval ***argv = safe_emalloc(sizeof(zval **), argc, 0); zend_class_entry **ce; /* 译注: 这里在译者的环境(php-5.4.9)是二级间访 */ /* 数组方式读取所有传入参数 */ if ( argc == 0 || zend_get_parameters_array_ex(argc, argv) == FAILURE ) { efree(argv); WRONG_PARAM_COUNT; } /* 隔离第一个参数(隔离为了使下面的类型转换不影响原始数据) */ SEPARATE_ZVAL(argv[0]); /* 将第一个参数转换为字符串类型, 并转为小写(因为php的类名是不区分大小写的) */ convert_to_string(*argv[0]); php_strtolower(Z_STRVAL_PP(argv[0]), Z_STRLEN_PP(argv[0])); /* 在类的HashTable中查找提供的类是否存在, 如果存在, ce中就得到了对应的zend_class_entry * */ if ( zend_hash_find(EG(class_table), Z_STRVAL_PP(argv[0]), Z_STRLEN_PP(argv[0]) + 1, (void **)&ce) == FAILURE ) { php_error_docref(NULL TSRMLS_CC, E_WARNING, "Class %s does not exist.", Z_STRVAL_PP(argv[0])); zval_ptr_dtor(argv[0]); efree(argv); RETURN_FALSE; } /* 将返回值初始化为查找到的类的对象 */ object_init_ex(return_value, *ce); /* 检查类是否有构造器 */ if ( zend_hash_exists(&(*ce)->function_table, Z_STRVAL_PP(argv[0]), Z_STRLEN_PP(argv[0]) + 1) ) { #define DYNAMIC_CONSTRUCTOR #ifndef DYNAMIC_CONSTRUCTOR zval *ctor; #endif zval *dummy = NULL; #ifndef DYNAMIC_CONSTRUCTOR /* 将ctor构造为一个数组, 对应的用户空间形式为: array(argv[0], argv[0]), * 实际上对应于用户空间调用类的静态方法时$funcname的参数形式: * array(类名, 方法名) */ MAKE_STD_ZVAL(ctor); array_init(ctor); zval_add_ref(argv[0]); add_next_index_zval(ctor, *argv[0]); zval_add_ref(argv[0]); add_next_index_zval(ctor, *argv[0]); #endif /* 调用函数 */ if ( call_user_function_ex(&(*ce)->function_table, #ifndef DYNAMIC_CONSTRUCTOR NULL, ctor, #else &return_value, *argv[0], #endif &dummy, argc - 1, argv + 1, 0, NULL TSRMLS_CC) == FAILURE ) { php_error_docref(NULL TSRMLS_CC, E_WARNING, "Unable to call constructor"); } /* 如果有返回值直接析构丢弃 */ if ( dummy ) { zval_ptr_dtor(&dummy); } #ifndef DYNAMIC_CONSTRUCTOR /* 析构掉临时使用(用来描述所调用方法名)的数组 */ zval_ptr_dtor(&ctor); #endif } /* 析构临时隔离出来的第一个参数(类名) */ zval_ptr_dtor(argv[0]); /* 释放实参列表空间 */ efree(argv); }
译注: 现在, 就可以用函数中是否定义DYNAMIC_CONSTRUCTOR这个宏来切换构造器的调用方式, 以方便读者理解.
小结
尽管ZE1/php4提供的类功能最好少用, 但是由于当前php4在产品环境下还是广泛使用的, 因此做这个兼容还是有好处的. 本章涉及的技术可以让你灵活的编写各种功能的代码, 它们现在可以编译运行, 并且未来也将继续可以工作.
下一章, 你将看到php5中真正的面向对象, 如果你想要OOP, 从中你就可以得到升级的理由, 并且, 升级后你肯定再也不愿回头.
以上就是 [翻译][php扩展开发和嵌入式]第10章-php4的对象的内容,更多相关内容请关注PHP中文网(www.php.cn)!