常量,顧名思義是一個常態的量值。它與值只綁定一次,它的作用在于有肋于增加程序的可讀性和可靠性。在PHP中,常量的名字是一個簡單值的標識符,在腳本執行期間該值不能改變。和變量一樣,常量默認為大小寫敏感,但是按照我們的習慣常量標識符總是大寫的。常量名和其它任何 PHP 標簽遵循同樣的命名規則。合法的常量名以字母或下劃線開始,后面跟著任何字母,數字或下劃線。在這一小節我們一起看下常量與我們常見的變量有啥區別,它在執行期間的不可改變的特性是如何實現的以及常量的定義過程。
首先看下常量與變量的區別,常量是在變量的zval結構的基礎上添加了一額外的元素。如下所示為PHP中常量的內部結構。
## 常量的內部結構
typedef struct _zend_constant {
zval value; /* zval結構,PHP內部變量的存儲結構,在第一小節有說明 */
int flags; /* 常量的標記如 CONST_PERSISTENT | CONST_CS */
char *name; /* 常量名稱 */
uint name_len;
int module_number; /* 模塊號 */
} zend_constant;
在Zend/zend_constants.h文件的33行可以看到如上所示的結構定義。在常量的結構中,除了與變量一樣的zval結構,它還包括屬于常量的標記,常量名以及常量所在的模塊號。
在了解了常量的存儲結構后,我們來看PHP常量的定義過程。一個例子。
define('TIPI', 'Thinking In PHP Internal');
這是一個很常規的常量定義過程,它使用了PHP的內置函數**define**。常量名為TIPI,值為一個字符串,存放在zval結構中。從這個例子出發,我們看下define定義常量的過程實現。
## define定義常量的過程
define是PHP的內置函數,在Zend/zend_builtin_functions.c文件中定義了此函數的實現。如下所示為部分源碼:
?
/* {{{ proto bool define(string constant_name, mixed value, boolean case_insensitive=false) Define a new constant */
ZEND_FUNCTION(define)
{
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "sz|b", &name,
&name_len, &val, &non_cs) == FAILURE) {
return;
}
?
... // 類常量定義 此處不做介紹
?
... // 值類型判斷和處理
?
c.value = *val;
zval_copy_ctor(&c.value);
if (val_free) {
zval_ptr_dtor(&val_free);
}
c.flags = case_sensitive; /* non persistent */
c.name = zend_strndup(name, name_len);
c.name_len = name_len+1;
c.module_number = PHP_USER_CONSTANT;
if (zend_register_constant(&c TSRMLS_CC) == SUCCESS) {
RETURN_TRUE;
} else {
RETURN_FALSE;
}
}
/* }}} */
上面的代碼已經對對象和類常量做了簡化處理,其實現上是一個將傳遞的參數傳遞給新建的zend_constant結構,并將這個結構體注冊到常量列表中的過程。關于大小寫敏感,函數的第三個參數表示是否**大小不敏感**,默認為false(大小寫敏感)。這個參數最后會賦值給zend_constant結構體的flags字段。其在函數中實現代碼如下:
zend_bool non_cs = 0; // 第三個參數的臨時存儲變量
int case_sensitive = CONST_CS; // 是否大小寫敏感,默認為1
?
if(non_cs) { // 輸入為真,大小寫不敏感
case_sensitive = 0;
}
?
c.flags = case_sensitive; // 賦值給結構體字段
從上面的define函數的實現來看,**PHP對于常量的名稱在定義時其實是沒有所謂的限制**。如下所示代碼:
define('^_^', 'smile');
?
if (defined('^_^')) {
echo 'yes';
}else{
echo 'no';
}
//$var = ^_^; //語法錯誤
$var = constant("^_^");
通過defined函數測試表示,`^_^`這個常量已經定義好,這樣的常量無法直接調用,只能使用constant()方法來獲取到,否則在語法解析時會報錯,因為它不是一個合法的標示符。
除了CONST_CS標記,常量的flags字段通常還可以用CONST_PERSISTENT和CONST_CT_SUBST。
CONST_PERSISTENT表示這個常量需要持久化。這里的持久化內存申請時的持久化是一個概念,非持久常量會在請求結束時釋放該常量,如果讀者還不清楚PHP的生命周期,可以參考,[PHP生命周期](#)這一小節,也就是說,如果是非持久常量,會在RSHUTDOWN階段就將該常量釋放,否則只會在MSHUTDOWN階段將內存釋放,在用戶空間,也就是用戶定義的常量都是非持久化的,通常擴展和內核定義的常量會設置為持久化,因為如果常量被釋放了,而下次請求又需要使用這個常量,該常量就必須在請求時初始化一次,而對于常量這些不變的量來說就是個沒有意義的重復計算。
在PHP,只有標量才能被定義為常量,而在內核C代碼中,一些字符串,數字等作為代碼的一部分,并且他們被定義成PHP內核中的常量。這些常量屬于靜態對象,被給定了一個絕對地址,當釋放這些常量時,我們并不需要將這些靜態的內存釋放掉,從而也就有了我們這里的CONST_PERSISTENT標記。
CONST_CT_SUBST我們看注釋可以知道其表示Allow compile-time substitution(在編譯時可被替換)。在PHP內核中這些常量包括:TRUE、FALSE、NULL、ZEND_THREAD_SAFE和ZEND_DEBUG_BUILD五個。
## 標準常量的初始化
通過define()函數定義的常量的模塊編號都是PHP_USER_CONSTANT,這表示是用戶定義的常量。除此之外我們在平時使用較多的常量:如錯誤報告級別E_ALL, E_WARNING等常量就有點不同了。這些是PHP內置定義的常量,他們屬于標準常量。
在Zend引擎啟動后,會執行如下的標準常量注冊操作。**php_module_startup() -> zend_startup() -> zend_register_standard_constants()]**
?
void zend_register_standard_constants(TSRMLS_D)
{
... // 若干常量以REGISTER_MAIN_LONG_CONSTANT設置,
REGISTER_MAIN_LONG_CONSTANT("E_ALL", E_ALL, CONST_PERSISTENT | CONST_CS);
...
}
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);
}
代碼很容易理解,前面看到注冊內置常量都是用了CONST_PERSISTENT標志位,也就是說,這些常量都是持久化常量。
## 魔術常量
PHP提供了大量的預定義常量,有一些是內置的,也有一些是擴展提供的,只有在加載了這些擴展庫時才會出現。
不過PHP中有七個魔術常量,他們的值其實是變化的,它們的值隨著它們在代碼中的位置改變而改變。所以稱他們為魔術常量。例如 __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 中該值總是小寫字母的 |
| __METHOD__ | 類的方法名(PHP 5.0.0 新加)。返回該方法被定義時的名字(區分大小寫)。 |
| __NAMESPACE__ | 當前命名空間的名稱(大小寫敏感)。這個常量是在編譯時定義的(PHP 5.3.0 新增) |
> PHP中的一些比較_魔術_的變量或者標示都習慣使用下劃線來進行區分, 所以在編寫PHP代碼時也盡量不要定義雙下線開頭的常量。
PHP內核會在詞法解析時將這些常量的內容賦值進行替換,而不是在運行時進行分析。如下PHP代碼:
<?PHP
echo __LINE__;
function demo() {
echo __FUNCTION__;
}
demo();
PHP已經在詞法解析時將這些常量換成了對應的值,以上的代碼可以看成如下的PHP代碼:
<?PHP
echo 2;
function demo() {
echo "demo";
}
demo();
如果我們使用VLD擴展查看以上的兩段代碼生成的中間代碼,你會發現其結果是一樣的。
前面我們有說PHP是在詞法分析時做的賦值替換操作,以__FUNCTION__為例,在Zend/zend_language_scanner.l文件中,__FUNCTION__是一個需要分析的元標記(token):
<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(因此在頂級作用域名中直接打印__FUNCTION__會輸出空格)。這個值在語法解析時會直接賦值給返回值。這樣我們就在生成的中間代碼中看到了這些常量的位置都已經賦值好了。
和__FUNCTION__類似,在其附近的位置,上面表格中的其它常量也進行了類似的操作。
> 前面有個比較特殊的地方,當`func_name`不存在時,__FUNCTION__被替換成空字符串, 你可能會想,怎么會有變量名不存在的方法呢,這里并不是匿名方法,匿名方法的`function_name` 并不是空的,而是:"{closure}", 有興趣的讀者可以去代碼找找在那里給定義了。
這里涉及PHP字節碼的編譯,在PHP中,一個函數或者一個方法會變編譯成一個opcode array opcode array的function name字段標示的就是這個函數或方法的名稱,同時一段普通的代碼 也會被當成一個完整實體被編譯成一段opcode array,只不過沒有函數名稱。
在PHP5.4中增加了對于trait類的常量定義:__TRAIT__。
**這些常量其實相當于一個常量模板,或者說是一個占位符,在詞法解析時這些模板或占位符就被替換成實際的值**
- 第一章 準備工作和背景知識
- 第一節 環境搭建
- 第二節 源碼結構、閱讀代碼方法
- 第三節 常用代碼
- 第四節 小結
- 第二章 用戶代碼的執行
- 第一節 生命周期和Zend引擎
- 第二節 SAPI概述
- Apache模塊
- 嵌入式
- FastCGI
- 第三節 PHP腳本的執行
- 詞法分析和語法分析
- opcode
- opcode處理函數查找
- 第四節 小結
- 第三章 變量及數據類型
- 第一節 變量的結構和類型
- 哈希表(HashTable)
- PHP的哈希表實現
- 鏈表簡介
- 第二節 常量
- 第三節 預定義變量
- 第四節 靜態變量
- 第五節 類型提示的實現
- 第六節 變量的生命周期
- 變量的賦值和銷毀
- 變量的作用域
- global語句
- 第七節 數據類型轉換
- 第八節 小結
- 第四章 函數的實現
- 第一節 函數的內部結構
- 函數的內部結構
- 函數間的轉換
- 第二節 函數的定義,傳參及返回值
- 函數的定義
- 函數的參數
- 函數的返回值
- 第三節 函數的調用和執行
- 第四節 匿名函數及閉包
- 第五節 小結
- 第五章 類和面向對象
- 第一節 類的結構和實現
- 第二節 類的成員變量及方法
- 第三節 訪問控制的實現
- 第四節 類的繼承,多態及抽象類
- 第五節 魔術方法,延遲綁定及靜態成員
- 第六節 PHP保留類及特殊類
- 第七節 對象
- 第八節 命名空間
- 第九節 標準類
- 第十節 小結
- 第六章 內存管理
- 第一節 內存管理概述
- 第二節 PHP中的內存管理
- 第三節 內存使用:申請和銷毀
- 第四節 垃圾回收
- 新的垃圾回收
- 第五節 內存管理中的緩存
- 第六節 寫時復制(Copy On Write)
- 第七節 內存泄漏
- 第八節 小結
- 第七章 Zend虛擬機
- 第一節 Zend虛擬機概述
- 第二節 語法的實現
- 詞法解析
- 語法分析
- 實現自己的語法
- 第三節 中間代碼的執行
- 第四節 PHP代碼的加密解密
- 第五節 小結
- 第八章 線程安全
- 第二節 線程,進程和并發
- 第三節 PHP中的線程安全
- 第九章 錯誤和異常處理
- 第十章 輸出緩沖
- 第十六章 PHP語言特性的實現
- 第一節 循環語句
- foreach的實現
- 第二十章 怎么樣系列(how to)
- 附錄
- 附錄A PHP及Zend API
- 附錄B PHP的歷史
- 附錄C VLD擴展使用指南
- 附錄D 怎樣為PHP貢獻
- 附錄E phpt測試文件說明
- 附錄F PHP5.4新功能升級解析
- 附錄G:re2c中文手冊