往期精选(欢迎转发~~)
- 如何看待程序员35岁职业危机?
- Java全套学习资料(14W字),耗时半年整理
- 我肝了三个月,为你写出了GO核心手册
- 消息队列:从选型到原理,一文带你全部掌握
- 肝了一个月的ETCD,从Raft原理到实践
- 更多…
在PHP中,常量的名字是一个简单值的标识符,在脚本执行期间该值不能改变。 和变量一样,常量
默认为大小写敏感,但是按照我们的习惯常量标识符总是大写的。首先看下常量与变量的区别,常量是在变量的zval结构的基础上添加了额外的元素,结构如下(php/Zend/zend_constants.h):
typedef struct _zend_constant {
zval value; /* PHP内部变量的存储结构,参考之前的文章 */
int flags; /* 常量的标记 */
char *name; /* 常量名称 */
uint name_len; /* 常量名称长度 */
int module_number; /* 模块号 */
} zend_constant;
在常量的结构中,除了与变量一样的zval结构,它还包括属于常量的标记,常量名以及常量所在的模块号。在了解常量的存储结构后,我们来看PHP常量的定义过程,示例如下:
define('ANDY', 'My Name Is Andy!');
在常量结构中,zend_constant.value记录字符串“My Name Is Andy!”的值,zend_constant.name的值为“ANDY”,并在zend_constant.name_len中记录该字符串的长度,zend_constant.flags记录该常量的类型,包括CONST_CS(区分大小写)、CONST_PERSISTENT(持久化常量)和CONST_CT_SUBST(编译时可以替换的常量)。从这个示例出发,我们看看define定义常量的实现过程,代码如下(php/Zend/zend_builtin_functions.c):
/* {{{ proto bool define(string constant_name, mixed value[, boolean case_insensitive])
Define a new constant */
ZEND_FUNCTION(define)
{
zend_string *name;
zval *val, val_free;
zend_bool non_cs = 0;
int case_sensitive = CONST_CS;
zend_constant c;
#ifndef FAST_ZPP
if (zend_parse_parameters(ZEND_NUM_ARGS(), "Sz|b", &name, &val, &non_cs) == FAILURE) {
return;
}
#else
ZEND_PARSE_PARAMETERS_START(2, 3)
Z_PARAM_STR(name)
Z_PARAM_ZVAL(val)
Z_PARAM_OPTIONAL
Z_PARAM_BOOL(non_cs)
ZEND_PARSE_PARAMETERS_END();
#endif
... // 值类型判断和处理,此次省略
ZVAL_DUP(&c.value, val);
zval_ptr_dtor(&val_free);
register_constant:
c.flags = case_sensitive; /* non persistent */
c.name = zend_string_copy(name);
c.module_number = PHP_USER_CONSTANT;
if (zend_register_constant(&c) == SUCCESS) {
RETURN_TRUE;
} else {
RETURN_FALSE;
}
}
该方法中,先对变量进行初始化,包括name、val等,然后将变量的值赋值给zend_constant结构,也就是变量c,最后将该常量通过函数zend_register_constant()注册到常量列表中。
上面讲述的是自定义常量的初始化,下面让我们看看标准常量的初始化,可能你会问,什么是标准常量呢?标准常量是PHP内置的一些常量,如错误报警级别常量E_ALL,E_WARNING等。在Zend引擎启动后,会执行如下的标准常量注册操作:php_module_startup() -> zend_startup() -> zend_register_standard_constants(),下面是该函数源码(php/Zend/zend_constants.c):
void zend_register_standard_constants(void)
{
REGISTER_MAIN_LONG_CONSTANT("E_ERROR", E_ERROR, CONST_PERSISTENT | CONST_CS);
REGISTER_MAIN_LONG_CONSTANT("E_RECOVERABLE_ERROR", E_RECOVERABLE_ERROR, CONST_PERSISTENT | CONST_CS);
REGISTER_MAIN_LONG_CONSTANT("E_WARNING", E_WARNING, CONST_PERSISTENT | CONST_CS);
REGISTER_MAIN_LONG_CONSTANT("E_PARSE", E_PARSE, CONST_PERSISTENT | CONST_CS);
REGISTER_MAIN_LONG_CONSTANT("E_NOTICE", E_NOTICE, CONST_PERSISTENT | CONST_CS);
REGISTER_MAIN_LONG_CONSTANT("E_STRICT", E_STRICT, CONST_PERSISTENT | CONST_CS);
REGISTER_MAIN_LONG_CONSTANT("E_DEPRECATED", E_DEPRECATED, CONST_PERSISTENT | CONST_CS);
REGISTER_MAIN_LONG_CONSTANT("E_CORE_ERROR", E_CORE_ERROR, CONST_PERSISTENT | CONST_CS);
REGISTER_MAIN_LONG_CONSTANT("E_CORE_WARNING", E_CORE_WARNING, CONST_PERSISTENT | CONST_CS);
REGISTER_MAIN_LONG_CONSTANT("E_COMPILE_ERROR", E_COMPILE_ERROR, CONST_PERSISTENT | CONST_CS);
REGISTER_MAIN_LONG_CONSTANT("E_COMPILE_WARNING", E_COMPILE_WARNING, CONST_PERSISTENT | CONST_CS);
REGISTER_MAIN_LONG_CONSTANT("E_USER_ERROR", E_USER_ERROR, CONST_PERSISTENT | CONST_CS);
REGISTER_MAIN_LONG_CONSTANT("E_USER_WARNING", E_USER_WARNING, CONST_PERSISTENT | CONST_CS);
REGISTER_MAIN_LONG_CONSTANT("E_USER_NOTICE", E_USER_NOTICE, CONST_PERSISTENT | CONST_CS);
REGISTER_MAIN_LONG_CONSTANT("E_USER_DEPRECATED", E_USER_DEPRECATED, CONST_PERSISTENT | CONST_CS);
REGISTER_MAIN_LONG_CONSTANT("E_ALL", E_ALL, CONST_PERSISTENT | CONST_CS);
REGISTER_MAIN_LONG_CONSTANT("DEBUG_BACKTRACE_PROVIDE_OBJECT", DEBUG_BACKTRACE_PROVIDE_OBJECT, CONST_PERSISTENT | CONST_CS);
REGISTER_MAIN_LONG_CONSTANT("DEBUG_BACKTRACE_IGNORE_ARGS", DEBUG_BACKTRACE_IGNORE_ARGS, CONST_PERSISTENT | CONST_CS);
/* true/false constants */
{
REGISTER_MAIN_BOOL_CONSTANT("TRUE", 1, CONST_PERSISTENT | CONST_CT_SUBST);
REGISTER_MAIN_BOOL_CONSTANT("FALSE", 0, CONST_PERSISTENT | CONST_CT_SUBST);
REGISTER_MAIN_BOOL_CONSTANT("ZEND_THREAD_SAFE", ZTS_V, CONST_PERSISTENT | CONST_CS);
REGISTER_MAIN_BOOL_CONSTANT("ZEND_DEBUG_BUILD", ZEND_DEBUG, CONST_PERSISTENT | CONST_CS);
}
REGISTER_MAIN_NULL_CONSTANT("NULL", CONST_PERSISTENT | CONST_CT_SUBST);
}
上述代码,以REGISTER_MAIN_LONG_CONSTANT()为例,它是一个宏,用于注册一个长整形数字的常量,因为C是强类型语言,需要对不同类型的数据进行分别处理,该宏展开后为下面这个函数:
ZEND_API void zend_register_long_constant(const char *name, uint name_len,long lval, int flags, int module_number TSRMLS_DC)
{
zend_constant c;
c.value.type = IS_LONG;
c.value.value.lval = lval;
c.flags = flags;
c.name = zend_strndup(name, name_len-1);
c.name_len = name_len;
c.module_number = module_number;
zend_register_constant(&c TSRMLS_CC);
}
这里就很好理解了,先给zend_constant赋值,后将常量注册,是不是很简单哈:)
到这里我们就可以很容易总结出“用户定义常量”和“标准常量”初始化的区别了,前者是用户自己定义,或者是直接在PHP系统源码中写死,然后直接依次读取,共同点是都需要先初始化常量数组zend_constant,然后注册。常量除了“用户定义常量”和“标准常量”,还有个“魔术常量”,我也简单讲讲。
PHP中有八个魔术常量,它们的值随着它们在代码中的位置改变而改变, 所以称他们为魔术常量。例如__LINE(我靠,这个编辑器,LINE后面不能再加两个下划线,大家脑补哈)的值就依赖于它在脚本中所处的位置来决定, 这些特殊的常量不区分大小写,在php帮助手册中说明如下:
__LINE__ 文件中的当前行号。
__FILE__ 文件的完整路径和文件名。如果用在被包含文件中,则返回被包含的文件名。自 PHP 4.0.2 起, __FILE__ 总是包含一个绝对路径(如果是符号连接,则是解析后的绝对路径),而在此之前的版本有时会包含一个相对路径。
__DIR__ 文件所在的目录。如果用在被包括文件中,则返回被包括的文件所在的目录。它等价于 dirname(__FILE__)。除非是根目录,否则目录中名不包括末尾的斜杠。(PHP 5.3.0中新增) =
__FUNCTION__ 函数名称(PHP 4.3.0 新加)。自 PHP 5 起本常量返回该函数被定义时的名字(区分大小写)。在 PHP 4 中该值总是小写字母的。
__CLASS__ 类的名称(PHP 4.3.0 新加)。自 PHP 5 起本常量返回该类被定义时的名字(区分大小写)。在 PHP 4 中该值总是小写字母的。类名包括其被声明的作用区域(例如 Foo\Bar)。注意自 PHP 5.4 起 __CLASS__ 对 trait 也起作用。当用在 trait 方法中时,__CLASS__ 是调用 trait 方法的类的名字。
__TRAIT__ Trait 的名字(PHP 5.4.0 新加)。自 PHP 5.4 起此常量返回 trait 被定义时的名字(区分大小写)。Trait 名包括其被声明的作用区域(例如 Foo\Bar)。
__METHOD__ 类的方法名(PHP 5.0.0 新加)。返回该方法被定义时的名字(区分大小写)。
__NAMESPACE__ 当前命名空间的名称(区分大小写)。此常量是在编译时定义的(PHP 5.3.0 新增)。
PHP内核会在词法解析时将这些常量的内容赋值进行替换,而不是在运行时进行分析, 如下PHP代
码:
<?PHP
echo __LINE__;
function demo() {
echo __FUNCTION__;
} demo();
PHP已经在词法解析时将这些常量换成了对应的值,以上的代码可以看成如下的PHP代码:
<?PHP
echo 2;
function demo() {
echo "demo";
} demo();
那么替换的原理是什么呢?《深入理解PHP内核》一书给出以下源码,不过在php7.0中已经不存在该部分源码,估计这部分源码版本已经比较老,只是用来参考下:
<ST_IN_SCRIPTING>"__FUNCTION__" {
char *func_name = NULL;
if (CG(active_op_array)) {
func_name = CG(active_op_array)->function_name;
}
if (!func_name) {
func_name = "";
}
zendlval->value.str.len = strlen(func_name);
zendlval->value.str.val = estrndup(func_name, zendlval->value.str.len);
zendlval->type = IS_STRING;
return T_FUNC_C;
}
然后该书中给出的解释也不太对,表述如下:
“就是这里,当当前中间代码处于一个函数中时,则将当前函数名赋值给zendlval(也就是token
T_FUNC_C的值内容), 如果没有,则将空字符串赋值给zendlval, 这个值在语法解析时会直接赋值给返回值,这样我们就在生成的中间代码中看到了这些常量的位置都已经赋值好了。”
说实话,感觉这段话理解起来很费劲,特别是“将当前函数名赋值给zendlval(也就是token
T_FUNC_C的值内容)”,将函数名赋值给zendlval,这里我没有异议,但后面又附加了个解释“也就是token
T_FUNC_C的值内容”,我查了下源码,T_FUNC_C的token在源码中定义如下:
%token T_FUNC_C "__FUNCTION__ (T_FUNC_C)"
然后T_FUNC_C在源码中的宏定义如下:
#define T_FUNC_C 376
所以确定不明白原作者的意思,可能他想表达的是该魔术变量在词法分析过程中,已经将该魔术变量替换成固定的值的,只是在讲述源码过程中,实在是很难明白,这部分后续我再仔细研究下源码,然后将结果更新到该篇博文中。