1. 前言
引用是PHP中最令人迷惑、最容易被誤解的概念之一。因此,它有幸成為了初/中級PHP崗位經久不衰的面試題,很多基礎不牢者也常常栽在這裡。
我們知道,在C/C++中一個引用型別的變數,它實際儲存的值其實是被引用的變數的地址,因此當被引用的值改變時,引用者所表現出來的值也會隨之改變。這當然非常容易理解,PHP的引用也似乎與之沒什麼不同。
我們上一章講過,每個PHP變數都是一個zval
,那麼一個引用型變數,它自然儲存了指向另一個zval
的指標了?我們看一下這一段PHP指令碼:
$foo = 10;
$bar = &$foo;
unset($foo);
var_dump($foo); // NULL
var_dump($bar); // int(10)
如果這個假設是正確的,$bar
儲存了指向$foo
的指標,那麼輸出的結果不會是10,而是NULL
。那引用究竟是何方神聖呢?且容我慢慢道來。
2. 基本概念
2.1 引用與引用計數
引用與引用計數的概念常被混淆。我們在上一章講過“引用計數”的概念。大家知道,陣列、字串、物件、資源屬於GC型別,一個GC型別的例項中儲存了它的引用數量。當引用數量降為0,即不再有zval
指向它時,它就會在特定的時機被銷燬。
我們這次講的“引用”,指的不是zval
對GC型別的引用,而是一種名為“引用”的GC型別,與上面說的4種型別屬於並列關係。
2.2 zend_reference
引用型別的資料儲存在結構體zend_reference
中,與其他GC型別同理:
struct _zend_reference {
zend_refcounted_h gc;
zval val;
};
可見,zend_reference
是對zval
的封裝。這裡要注意,封裝的是zval
本身而不是它的指標,這一點非常關鍵。
2.3 建立引用
對於任意GC型別我們都可以直接使用emalloc()
為其分配記憶體,實際開發中,可以藉助zend_type.h中提供了一些常用的巨集進行操作。
#define ZVAL_NEW_REF(z, r) do { \
zend_reference *_ref = \
(zend_reference *) emalloc(sizeof(zend_reference)); \
GC_REFCOUNT(_ref) = 1; \
GC_TYPE_INFO(_ref) = IS_REFERENCE; \
ZVAL_COPY_VALUE(&_ref->val, r); \
Z_REF_P(z) = _ref; \
Z_TYPE_INFO_P(z) = IS_REFERENCE_EX; \
} while (0)
巨集ZVAL_NEW_REF()
接受兩個zval
的指標作為引數,它的行為有三:
- 使用
emalloc()
在堆上為一個zend_reference
分配記憶體並將其初始化。 - 將第二個
zval
的值拷貝給引用的val
成員。 - 使第一個
zval
指向這個引用。
同理,還有一些功能相似的巨集:
ZVAL_NEW_EMPTY_REF()
只接受一個引數,它不會給zend_reference
的val
成員賦值。ZVAL_NEW_PERSISTENT_REF()
使用malloc()
而非emalloc()
分配記憶體,這意味著該引用不會被ZendMM回收。
#define ZVAL_MAKE_REF(zv) do { \
zval *__zv = (zv); \
if (!Z_ISREF_P(__zv)) { \
ZVAL_NEW_REF(__zv, __zv); \
} \
} while (0)
巨集ZVAL_MAKE_REF()
接受一個zval
作為引數,如果型別不是引用,則用其值初始化一個新引用,並指向它。
2.4 解引用
當我們使用一個引用時,我們往往只關心它所指向的zval
。將一個引用型別的zval
轉化為它所引用的zval
的操作,被稱為解引用。
#define ZVAL_DEREF(z) do { \
if (UNEXPECTED(Z_ISREF_P(z))) { \
(z) = Z_REFVAL_P(z); \
} \
} while (0)
巨集ZVAL_DEREF()
接受一個zval
的指標作為引數。先判斷其的型別是否為引用,如果是,使這個指標指向其所引用的zval
(即zend_reference
的val
成員)的地址。
一個容易與其混淆的巨集ZVAL_UNREF()
需要謹慎使用。
#define ZVAL_UNREF(z) do { \
zval *_z = (z); \
zend_reference *ref; \
ZEND_ASSERT(Z_ISREF_P(_z)); \
ref = Z_REF_P(_z); \
ZVAL_COPY_VALUE(_z, &ref->val); \
efree_size(ref, sizeof(zend_reference)); \
} while (0)
它將一個zval
所引用的zval
的值拷貝給它,然後直接銷燬對應的zend_reference
。當且僅當這個zend_reference
的引用數量為1的時候你才可以這樣做,否則其他指向這個引用的zval
將指向無效的記憶體。
3. 引用的使用
3.1 引用賦值
瞭解了引用的基本概念後,我們可以開始分析,在PHP中引用是如何被使用的。下面我們分析前言中給出的一段簡單指令碼:
$bar = &$foo;
我們剛才知道,引用的值是儲存在一個zend_reference
的val
成員中的,那麼這一過程在底層的簡化表示,應該是這樣的:
ZVAL_MAKE_REF(foo);
ZVAL_REF(bar, Z_REF_P(foo));
GC_ADDREF(Z_REFVAL_P(foo)); // 注意:refcount應+1
也就是說,現在$foo
已經不是一個整型的zval
了,而是和$bar
一樣,同時指向一個zend_reference
,其val
成員為曾經的$foo
,即值為10的整型。
unset($foo)
則是類似如下所示的過程:
zval_ptr_dtor(foo);
ZVAL_NULL(foo);
我們在上一章講過zval_ptr_dtor()
這個巨集,它會判斷一個zval
是否為GC型別,如果是,對其引用數量減1。此時若引用數量為0,則執行銷燬操作。
unset($foo)
以後,仍有$bar
指向儲存了整型變數10的zend_reference
,因此在var_dump($bar)
時我們得到了“int(10)”的輸出。
3.2 引用傳參和引用返回
一般情況下,函式傳參的過程中,作為引數的變數會被複制,若它指向除了引用之外的GC型別,則其引用數量也會遞增。若作為引數的變數本身是引用型別,則先會對其進行解引用。返回值也是同理。
然而,當我們指定引用傳參時,情況變得有些不同。如下所示:
function func(&$foo) {
++$foo;
}
$bar = 10;
$baz = &bar;
func($bar);
func($baz);
上例中,當$bar
被當作引數傳遞給func()
時,為了能夠使函式內對該引數變數的修改對外部有效,一個zend_reference
會被建立,其val
成員的值為$bar
的值,隨後,函式外的$bar
和函式內的$foo
都會同時指向這個引用。類似地,$baz
被作為引數傳遞時本身已經是引用型別,則解引用不會發生。
引用返回的原理與引用傳參類似,這裡不再贅述。
當我們在進行PHP擴充套件開發時,如果希望實現的函式支援引用傳參和引用返回,我們需要在其對應的zend_internal_arg_info
中顯式地指定。相關的巨集中包含了這樣的引數,例如:
// pass_by_ref為1則為引用傳參
#define ZEND_ARG_INFO(pass_by_ref, name) { #name, 0, pass_by_ref, 0},
// return_reference為1則為引用返回
#define ZEND_BEGIN_ARG_INFO_EX(name, _unused, return_reference, required_num_args) \
static const zend_internal_arg_info name[] = { \
{ (const char*)(zend_uintptr_t)(required_num_args), 0, return_reference, 0 },
假設我們實現的函式foo()
接受一個引數,按引用傳參,且返回引用,則它的引數型別表如下所示:
ZEND_BEGIN_ARG_INFO_EX(foo_arginfo, 0, 1, 1)
ZEND_ARG_INFO(1, bar)
ZEND_END_ARG_INFO()
4. 間接變數
講到這裡,引用的神祕面紗已經被揭開。一些讀者不免會好奇,那麼究竟有沒有一個zval
直接指向另一個zval
的情況呢?答案是有的。
回顧上一章對zval
的講解,聯合體zend_value
的一個成員zv
的型別就是一個指向zval
的指標。一個儲存了另一個zval
的指標的zval
被稱為間接變數,型別為IS_INDIRECT
,通過巨集Z_INDERECT()
和Z_INDIRECT_P()
可用來方便地訪問它所指向的zval
。
雖然間接變數無法用來實現PHP中的引用,但它本身就是一種更加原始、直接的引用,沒有額外分配記憶體所產生的時間和空間開銷。Zend引擎內部有多處使用了間接變數。
4.1 符號表
符號表是間接變數的最常見的使用場合。符號表是一個zend_array
,它包含了一系列鍵值對,對於一個函式的符號表來說,鍵為區域性變數名,值為指向對應區域性變數的間接變數。
PHP的區域性變數連續地儲存在當前呼叫的函式的棧幀內,有兩種方式可以對其進行訪問,偏移量或者變數名。
function foo() {
$foo = 'bar';
$bar = 'baz';
echo $foo; // 通過偏移量訪問foo
echo $$foo; // 通過變數名訪問bar
}
當我們直接使用一個變數時,即是通過偏移量訪問,因為在編譯期,每個區域性變數在棧幀中的偏移量已經確定,且表示式使用的是哪一個變數也是確定的。因此,無需使用變數名就可以進行訪問,編譯生成的位元組碼也不包含其使用的變數名。
然而,當我們使用可變變數時,就無法直接通過偏移量快速訪問,因為函式名儲存在一個可變的字串中,在編譯期無法確定。這時就要對符號表進行一次雜湊查詢,進而得到需要的區域性變數。
4.2 類成員的預設值
間接變數也被用於類成員中。我們可以直接在類成員的定義中為其預設值,例如:
class foo {
private $bar = 'hello';
// ...
}
$bar
成員的預設值在指令碼即將被執行的初始化時期被建立,且只會被建立一次,直到指令碼執行結束後才會被銷燬。每當foo
的例項被建立,它的$bar
成員都會被初始化為指向該預設值的間接變數。顯然,這裡不需要引用計數,使用間接變數而不是引用是一種高效的做法。
5. 下期預告
下次我會為大家帶來PHP字串相關的講解,敬請期待。
本作品採用《CC 協議》,轉載必須註明作者和本文連結