PHP內核探索 —— 變量的類型:PHP弱類型變量特性是如何實現?
所有的編程語言都要提供一種數據的存儲與檢索機制,PHP也不例外。其它語言大都需要在使用變量之前先定義,并且它的類型也是無法再次改變的,而PHP卻允許程序猿自由的使用變量而無須提前定義,甚至可以隨時隨意的對已存在的變量轉換成其它任何PHP支持的數據類型。在程序在運行的時候,PHP還會自動的根據需求轉換變量的類型。
如果你用過PHP,肯定體驗過PHP的弱類型的變量體系。眾所周知,PHP引擎是用C寫的,而C確實一種強類型的編程語言,PHP內核中是如何用C來實現自己的這種弱類型特性的?下面談談變量的類型。
PHP在內核中是通過zval這個結構體來存儲變量的,它的定義在Zend/zend.h文件里,簡短精煉,只有四個成員組成:
struct _zval_struct {zvalue_value value;/* 變量的值 */zend_uint refcount__gc;zend_uchar type;/* 變量當前的數據類型 */zend_uchar is_ref__gc;};typedef struct _zval_struct zval;//在Zend/zend_types.h里定義的:typedef unsigned int zend_uint;typedef unsigned char zend_uchar;
zval里的refcout__gc是zend_uint類型,也就是unsinged int型,is_ref__gc和type則是unsigned char型的。保存變量值的value則是zvalue_value類型(PHP5),它是一個Union,同樣定義在了Zend/zend.h文件里:
typedef union _zvalue_value {long lval;/* long value */double dval;/* double value */struct {char *val;int len;} str;HashTable *ht;/* hash table value */zend_object_value obj;} zvalue_value;
在以上實現的基礎上,PHP語言得以實現了8種數據類型,這些數據類型在內核中的分別對應于特定的常量,它們分別是:
常量名稱:IS_NULL第一次使用的變量如果沒有初始化過,則會自動的賦予這個變量,當然我們也可以在PHP語言中通過null這個常量來給予變量null類型的值。 這個類型的值只有一個 ,就是NULL,它和0與false是不同的。IS_BOOL布爾類型的變量有兩個值,true或者false。在PHP語言中,while、if等語句會自動的把表達式的值轉成這個類型的。IS_LONGPHP語言中的整型,在內核中是通過所在操作系統的singed long數據類型來表示的。在最常見的32位操作系統中,它可以存儲從-2147483648 到 +2147483647范圍內的任一整數。有一點需要注意的是,如果PHP語言中的整型變量超出最大值或者最小值,它并不會直接溢出,而是會被內核轉換成IS_DOUBLE類型的值然后再參與計算。再者,因為使用了singed long來作為載體,所以這也就解釋了為什么PHP語言中的整型數據都是帶符號的了。$a=2147483647;$a++;echo $a;//會正確的輸出2147483648;IS_DOUBLEPHP中的浮點數據是通過C語言中的singed double型變量來存儲的,這最終取決與所在操作系統的浮點型實現。 我們做為程序猿,應該知道計算機是無法精準的表示浮點數的,而是采用了科學計數法來保存某個精度的浮點數。用科學計數法,計算機只用8位便可以保存2.225x10^(-308)1.798x10^308之間的浮點數。用計算機來處理浮點數簡直就是一場噩夢,十進制的0.5專成二進制是0.1,0.8轉換后是0.1100110011....。但是當我們從二進制轉換回來的時候,往往會發現并不能得到0.8。我們用1除以3這個例子來解釋這個現象:1/3=0.3333333333.....,它是一個無限循環小數,但是計算機可能只能精確存儲到0.333333,當我們再乘以三時,其實計算機計算的數是0.333333*3=0.999999,而不是我們平時數學中所期盼的1.0.IS_STRINGPHP中最常用的數據類型——字符串,在內存中的存儲和C差不多,就是一塊能夠放下這個變量所有字符的內存,并且在這個變量的zval實現里會保存著指向這塊內存的指針。與C不同的是,PHP內核還同時在zval結構里保存著這個字符串的實際長度,這個設計使PHP可以在字符串中嵌入‘0’字符,也使PHP的字符串是二進制安全的,可以安全的存儲二進制數據!本著艱苦樸素的作風,內核只會為字符串申請它長度+1的內存,最后一個字節存儲的是‘0’字符,所以在不需要二進制安全操作的時候,我們可以像通常C語言的概念那樣來使用它。IS_ARRAY數組是一個非常特殊的數據類型,它唯一的功能就是包含別的變量。在C語言中,一個數組只能承載一種類型的數據,而PHP語言中的數組則靈活的多,它可以承載任意類型的數據,這一切都是HashTable的功勞,每個HashTable中的元素都有兩部分組成:索引與值,每個元素的值都是一個獨立的zval(確切的說應該是指向某個zval的指針)。IS_OBJECT和數組一樣,對象也是用來存儲復合數據的,但是與數組不同的是,對象還需要保存以下信息:方法、訪問權限、類常量以及其它的處理邏輯。相對與zend engine V1,V2中的對象實現已經被徹底修改,所以我們PHP擴展開發者如果需要自己的擴展支持面向對象的工作方式,則應該對PHP5和PHP4分別對待!IS_RESOURCE有一些數據的內容可能無法直接呈現給PHP用戶的,比如與某臺mysql服務器的鏈接,或者直接呈現出來也沒有什么意義。但用戶還需要這類數據,因此PHP中提供了一種名為Resource(資源)的數據類型。有關這個數據類型的事宜將在第九章中介紹,現在我們只要知道有這么一種數據類型就行了。
zval結構體里的type成員的值便是以上某個IS_*常量之一。內核通過檢測變量的這個成員值來知道他是什么類型的數據并做相應的后續處理。
如果要我們檢測一個變量的類型,最直接的辦法便是去讀取它的type成員的值:
void describe_zval(zval *foo){if (foo->type == IS_NULL){php_printf('這個變量的數據類型是: NULL'); } else {php_printf('這個變量的數據類型不是NULL,這種數據類型對應的數字是: %d', foo->type); }}
上述做法看起來沒有錯誤,但它是一種被強烈禁止一種做法!
PHP內核以后可能會修改變量的實現方式,所以檢測type的方法可能在以后就不能用了。為了解決這個兼容問題,zend頭文件中定義了大量的宏,供我們檢測、操作變量使用,使用這些宏不但讓我們的程序更易讀,還具有更好的兼容性。這里我們用Z_TYPE_P()宏來改寫上面那個程序。
void describe_zval(zval *foo){ if ( Z_TYPE_P(foo) == IS_NULL ) {php_printf('這個變量的數據類型是: NULL'); } else {php_printf('這個變量的數據類型不是NULL,這種數據類型對應的數字是: %d', foo->type); }}
php_printf()函數是內核對printf()函數的一層封裝,我們可以像使用printf()函數那樣使用它。
以_P一個p結尾的宏的參數大多是*zval型變量,此外獲取變量類型的宏還有兩個,分別是Z_TYPE和Z_TYPE_PP,前者的參數是zval型,而后者的參數則是**zval。這樣我們便可以猜測一下php內核是如何實現gettype這個函數了,代碼如下:
//開始定義php語言中的函數gettypePHP_FUNCTION(gettype){//這個arg間接指向就是我們傳給gettype函數的參數。是一個zval**結構//所以我們要對他使用__PP后綴的宏。zval **arg;//這個if的操作主要是讓arg指向參數~if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, 'Z', &arg) == FAILURE) {return;}//調用Z_TYPE_PP宏來獲取arg指向zval的類型。//然后是一個switch結構,RETVAL_STRING宏代表這gettype函數返回的字符串類型的值switch (Z_TYPE_PP(arg)) {case IS_NULL:RETVAL_STRING('NULL', 1);break;case IS_BOOL:RETVAL_STRING('boolean', 1);break;case IS_LONG:RETVAL_STRING('integer', 1);break;case IS_DOUBLE:RETVAL_STRING('double', 1);break;case IS_STRING:RETVAL_STRING('string', 1);break;case IS_ARRAY:RETVAL_STRING('array', 1);break;case IS_OBJECT:RETVAL_STRING('object', 1);break;case IS_RESOURCE:{char *type_name;type_name = zend_rsrc_list_get_rsrc_type(Z_LVAL_PP(arg) TSRMLS_CC);if (type_name) {RETVAL_STRING('resource', 1);break;}}default:RETVAL_STRING('unknown type', 1);}}
以上三個宏的定義在Zend/zend_operators.h里,定義分別是:
#define Z_TYPE(zval) (zval).type #define Z_TYPE_P(zval_p) Z_TYPE(*zval_p) #define Z_TYPE_PP(zval_pp) Z_TYPE(**zval_pp)
相關文章: