您好,登錄后才能下訂單哦!
這篇文章運用簡單易懂的例子給大家介紹php的字符串如何管理 zend_string,代碼非常詳細,感興趣的小伙伴們可以參考借鑒,希望對大家能有所幫助。
任何程序都需要管理字符串。在這里,我們將詳細介紹適合 PHP 需求的自定義解決方案:zend_string
。每次 PHP 需要使用字符串時,都會使用 zend_string
結構。該結構僅僅是 C 語言的 char *
字符串類型的簡單精簡包裝。
它添加了內存管理的功能,所以同一字符串可以在多個地方共享,而無需重復。另外,一些字符串是“內部的”,即“持久的”分配,并通過內存管理特殊管理,以便它們不會在多個請求中被銷毀。之后,那些從Zend 內存管理獲得永久分配。
這里是簡單的zend_string
結構:
struct _zend_string { zend_refcounted_h gc; zend_ulong h; size_t len; char val[1]; };
如你所見,該結構嵌入了一個 zend_refcounted_h
標頭。這個是內存管理和引用需要用到的。 由于該字符串很有可能作為哈希表檢查的關鍵字,因此它在 h
字段中嵌入了其哈希值。這是無符號長整型 zend_ulong
。僅在需要對 zend_string
進行哈希處理時會用到,特別是和哈希表:zend_array一起用時。這很有可能。
如你所知,字符串知道其長度為 len
字段,以支持“二進制字符串。二進制字符串是嵌入一個或多個 NUL
字符(\0)的字符串。當傳遞給庫函數,那些字符串會被截斷,否則無法正確計算其長度。所以在 zend_string
中,字符串的長度總是已知的。請注意,該長度計算的 ASCII 字符(字節),不計算最后的NUL
,而是計算最終的中間的 NUL。例如,字符串 “foo” 在 zend_string
中存儲為 “foo\0”,且它的長度為3。另外,字符串 “foo\0bar” 將存儲為 “foo\0bar\0”,且其長度為7。
最終,該字符存儲在 char[1]
。這不是 char *
,而是 char[1]
。為什么?這是一種稱為 “C struct hack” 的內存優化(你可以使用帶有這些術語的搜索引擎)。基本上,它允許引擎為 zend_string
結構和要存儲的字符分配空間,作為一個單獨的 C 指針。這優化了內存,因為內存訪問將是一個連續分配的塊,而不是兩個分散的塊(一個用于存儲 zend_string *
,另一個用于存儲 char *
)。
必須記住這種 struct hack,由于內存布局看起來像 C 字符位于 C zend_string
結構的末尾,因此當使用 C 調試器(或調試字符串)時可能會感覺到/看到過。該 hack 是完全由 API 管理,當你操作 zend_string
結構時會用到。
像 Zvals,你不需要手動操作 zend_string
內部字段,而總是為此使用宏。還存在觸發字符串操作的宏。這并不是函數,而是宏,都存儲在必需的 Zend/zend_string.h 頭文件:
zend_string *str; str = zend_string_init("foo", strlen("foo"), 0); php_printf("This is my string: %s\n", ZSTR_VAL(str)); php_printf("It is %zd char long\n", ZSTR_LEN(str)); zend_string_release(str);
上面簡單的例子為你展示了基本的字符串管理。應該為 zend_string_init()
函數(實際上是宏,但先讓我們忽略它)給出完整的 char *
C 字符串和它的長度。類型為 int 的最后一個參數應該為 0 或 1。如果傳遞0,則要求引擎通過 Zend 內存管理使用請求綁定的堆分配。這種分配在當前請求結束后時銷毀。如果你不這么做,則在調試版本中,引擎會提醒你內存泄漏。如果傳遞1,則要求了所謂的“持久”分配,引擎將使用傳統的 C malloc()
調用,并且不會以任何方式追蹤內存分配。
注意
如果你需要更多有關內存管理的信息,你可以閱讀專用章節。
然后,我們來顯示字符串。我們使用 ZSTR_VAL()
宏訪問字符數組。ZSTR_LEN()
允許訪問長度信息。zend_string
相關宏都以 ZSTR_**()
開始,注意和 Z_STR**()
宏不一樣。
注意
長度使用
size_t
類型存儲,為了顯示它,printf()
必須使用 “%zd”。你應該總是使用正確的printf()
格式。否則可能會導致應用程序崩潰或創建安全問題否則可能會導致內存泄漏和。有關printf()
格式的詳細信息,請訪問此鏈接
最后,我們使用 zend_string_release()
釋放字符串。該釋放是強制的。這與內存管理有關。“釋放”是一個簡單的操作:字符串的引用計數遞減,如果減到0,API會為你釋放字符串。如果忘記釋放字符串,則很可能造成內存泄漏。
注意
在 C 語言中,你必須總是考慮內存管理。如果你分配——不管是直接使用
malloc()
,或者使用能為你這樣做的 API,在某些時候你必須使用free()
。否則可能會導致內存泄漏,并轉換為任何人都不能安全使用的糟糕設計程序。
如果你需要訪問哈希值,可使用 ZSTR_H()
。但創建 zend_string
時,不會自動計算其哈希值。而當將該字符串與 HashTable API 一起使用時,它將為你完成。如果你強制立即計算哈希值,可使用 ZSTR_HASH()
或 zend_string_hash_val()
。當哈希值被計算出來,它會被保存起來并且不再被計算。無論如何,你必須使用 zend_string_forget_hash_val()
重新計算——因為你改變了字符串的值:
zend_string *str; str = zend_string_init("foo", strlen("foo"), 0); php_printf("This is my string: %s\n", ZSTR_VAL(str)); php_printf("It is %zd char long\n", ZSTR_LEN(str)); zend_string_hash_val(str); php_printf("The string hash is %lu\n", ZSTR_H(str)); zend_string_forget_hash_val(str); php_printf("The string hash is now cleared back to 0!"); zend_string_release(str);
zend_string
API 的一個非常棒的特性是:允許某部分通過簡單的聲明“擁有”字符串。引擎不會在內存復制字符串,而是遞增其引用計數(作為字符串zend_refcounted_h
的一部分)。這允許在代碼的多個地方共享一個內存。
由此,當我們討論“復制”一個 zend_string
時,實際上并沒有復制內存中的任何東西。如果需要(這仍是可能的操作),之后我們來討論“復制”字符串。開始吧:
zend_string *foo, *bar, *bar2, *baz; foo = zend_string_init("foo", strlen("foo"), 0); /* 創建變量foo,值為“foo” */ bar = zend_string_init("bar", strlen("bar"), 0); /* 創建變量bar,值為"bar" */ /* 創建變量bar2,共享變量bar的值。 另外遞增"bar"字符串的引用計數到2 */ bar2 = zend_string_copy(bar); php_printf("We just copied two strings\n"); php_printf("See : bar content : %s, bar2 content : %s\n", ZSTR_VAL(bar), ZSTR_VAL(bar2)); /* 在內存中復制"bar"字符串,創建變量 baz, 使 baz 單獨擁有新創建的"bar"字符串 */ baz = zend_string_dup(bar, 0); php_printf("We just duplicated 'bar' in 'baz'\n"); php_printf("Now we are free to change 'baz' without fearing to change 'bar'\n"); /* 更改第二個"bar"字符串的最后一個字符, 變為"baz" */ ZSTR_VAL(baz)[ZSTR_LEN(baz) - 1] = 'z'; /* 當字符串改變時,忘記舊哈希值(如果已計算), 因此其哈希值必須更改并重新計數 */ zend_string_forget_hash_val(baz); php_printf("'baz' content is now %s\n", ZSTR_VAL(baz)); zend_string_release(foo); /* 銷毀(釋放)"foo"字符串 */ zend_string_release(bar); /* 遞減"bar"字符串的引用計數到1 */ zend_string_release(bar2); /* 銷毀(釋放)bar和bar2變量中的"bar"字符串 */ zend_string_release(baz); /* 銷毀(釋放)"baz"字符串 */
我們一開始僅分配 “foo” 和 “bar”。然后,我們創建 bar
的副本到bar2
字符串。這里,必須記住:在內存中,bar
和 bar2
指向同一 C 字符串,更改一個將更改第二個。這是 zend_string_copy()
行為:它僅遞增 C 字符串的引用計數。
如果想要分離字符串,即想在內存中擁有該字符串的兩個不同副本,我們必須使用 zend_string_dup()
復制。然后我們將 bar2
變量字符串復制到 baz
變量。現在,baz
變量嵌入它的字符串副本,并且可以改變它而不影響 bar2
。這就是我們要做的:我們用‘z’改變了‘bar’最后的‘r’,之后,我們顯示它,并釋放所有字符串。
注意,我們忘記哈希值(如果它在之前已經計算,則不需要考慮其細節)。這是一個值得記住的好習慣。就像我們曾說過,如果 zend_string
作為 HashTables 的一部分,則使用哈希值。這在開發中是很常見的,并且改變字符串的值必須重新計算哈希值。忘記這一步驟將導致可能需要花一些時間去追蹤錯誤。
zend_string
API 允許其他操作,例如擴展或縮小字符串,更改大小寫或比較字符串。目前尚未有連接字符串操作,但是很容易執行:
zend_string *FOO, *bar, *foobar, *foo_lc; FOO = zend_string_init("FOO", strlen("FOO"), 0); bar = zend_string_init("bar", strlen("bar"), 0); /* 將 zend_string 與 C 字符串文字進行比較 */ if (!zend_string_equals_literal(FOO, "foobar")) { foobar = zend_string_copy(FOO); /* realloc() 將 C 字符串分配到更大的緩沖區 */ foobar = zend_string_extend(foobar, strlen("foobar"), 0); /* 在重新分配的足夠大的“FOO”之后,連接"bar" */ memcpy(ZSTR_VAL(foobar) + ZSTR_LEN(FOO), ZSTR_VAL(bar), ZSTR_LEN(bar)); } php_printf("This is my new string: %s\n", ZSTR_VAL(foobar)); /* 比較兩個 zend_string */ if (!zend_string_equals(FOO, foobar)) { /*復制字符串并改為小寫*/ foo_lc = zend_string_tolower(foo); } php_printf("This is FOO in lower-case: %s\n", ZSTR_VAL(foo_lc)); /* 釋放內存 */ zend_string_release(FOO); zend_string_release(bar); zend_string_release(foobar); zend_string_release(foo_lc);
現在你知道如何管理和操作 zend_string
,讓我們看看它們與 zval
容器的互動。
注意
你必須熟悉 zval,如果不熟悉,閱讀Zvals專用章節。
宏將允許你將 zend_string
存儲到 zval
,或從 zval
讀取 zend_string
:
zval myval; zend_string *hello, *world; zend_string_init(hello, "hello", strlen("hello"), 0); /* 存儲字符串到 zval */ ZVAL_STR(&myval, hello); /* 從 zval 的 zend_string 中讀取 C 字符串 */ php_printf("The string is %s", Z_STRVAL(myval)); zend_string_init(world, "world", strlen("world"), 0); /* 將 zend_string 更改為 myval:將其替換為另一個 */ Z_STR(myval) = world; /* ... */
你必須記住的是,以ZSTR_***(s)
開頭的每個宏都會作用到 zend_string
。
ZSTR_VAL()
ZSTR_LEN()
ZSTR_HASH()
每個以 Z_STR**(z)
開頭的宏都會作用于嵌入到 zval
中的 zend_string
。
Z_STRVAL()
Z_STRLEN()
Z_STRHASH()
還有一些你可能不需要的東西也存在。
簡單介紹一下。在 C 語言中,字符串是字符數組(char foo[]
)或者指向字符的指針(char *
)。它們并不知道其長度,這就是它們為什么末尾是 NUL(知道字符串的開始和結尾,就可以知道它的長度)。
在 PHP 7 之前,zend_string
結構還未出現。在那時,還是使用傳統的 char * / int
。你可能仍會在 PHP 源代碼中找到使用了罕見的 char * / int
,而不是 zend_string
。你也可能發現 API 功能,可以一邊使用 zend_string
,另一邊使用 char * / int
來交互。
在任何可能的地方:使用 zend_string
。那些罕見的沒有使用 zend_string
的地方,是因為在那里使用它們并沒有什么意義,但是你仍會發現在 PHP 源代碼中有很多對 zend_string
的引用。
在這里簡單的介紹一下 interned 字符串。你在擴展開發中應該需要這樣的概念。Interned 字符串也和 OPCache 擴展交互。
Interned 字符串是去重復的字符串。當與 OPCache 一起使用時,它還可以在請求之間循環使用。
假設你想要創建字符串“foo”。你更想做的是簡單地創建一個新字符串“foo”:
zend_string *foo; foo = zend_string_init("foo", strlen("foo"), 0); /* ... */
但是有一個問題:字符串是不是在你需要之前已經創建了?當你需要一個字符串時,你的代碼會在PHP生命中的某個時刻執行,這意味著在你需要完全相同的字符串(在我們的示例中為“ foo”)之前發生了一些代碼。
Interned 字符串是關于要求引擎去探查 interned 字符串存儲,并且如果它能找到你的字符串,會重用已經分配的指針。如果沒有找到:創建一個新的字符串并“intern” 它,這使得它可用于 PHP 源代碼的其他部分(其他擴展,引擎本身等)。
這里有個例子:
zend_string *foo; foo = zend_string_init("foo", strlen("foo"), 0); foo = zend_new_interned_string(foo); php_printf("This string is interned : %s", ZSTR_VAL(foo)); zend_string_release(foo);
上面的代碼創建了一個非常經典的新 zend_string
。然后,我們將創建的 zend_string
傳遞給 zend_new_interned_string()
。該函數在引擎 interned 字符串緩沖區查找相同的字符串(這里是“foo”)。如果找到它(意味著有人已經創建了這樣的字符串),那么它將釋放你的字符串(可能釋放它),并且用 interned 字符串緩沖區中的字符串替代它。如果找不到:它將被添加到 interned 字符串緩沖區,使它在將來可使用或可用于 PHP 的其他部分。
你必須注意內存分配。Interned 字符串總是將 refcount 設為1,因為它們不必被引用,由于它們會和 interned 字符串緩沖區共享,因此不可被銷毀。
例:
zend_string *foo, *foo2; foo = zend_string_init("foo", strlen("foo"), 0); foo2 = zend_string_copy(foo); /* 遞增 foo 的引用計數 */ /* 引用計數退回 1,即使現在字符串在三個不同的地方被使用 */ foo = zend_new_interned_string(foo); /* 這沒有任何作用,因為 foo 是 interned */ zend_string_release(foo); /* 這沒有任何作用,因為 foo2 是 interned*/ zend_string_release(foo2); /* 在流程結束時,PHP 將清除它的 interned 字符串緩沖區, 因此 free() 我們 "foo" 字符串本身 */
這都是關于垃圾收集的。
當字符串是 interned,更改其 GC 標志以添加 IS_STR_INTERNED
標志,不管使用的是什么內存分配類(基于永久或基于請求)。當你想要復制或釋放字符串,都會檢查該標志。如果是 interned 字符串,當你復制該字符串時,引擎不會遞增它的引用計數。但是如果你釋放字符串,它也不會遞減或釋放它。它不做任何事情。在進程生命周期的最后,它會銷毀它的 interned 字符串緩沖區,并且釋放你的 interned 字符串。
事實上,此過程比這更為復雜。如果你使用的是請求處理中的 interned 字符串,那么該字符串肯定被 interned。但是,如果你是在 PHP 處理一個請求時使用 interned 字符串,那么該字符串只會在當前請求被 interned,并在之后會清理掉。如果你不使用 OPCache 擴展,那這一切都是有效的,有時你不應該使用它。
當使用 OPCache 擴展,如果你使用請求處理中的 interned 字符串,那么該字符串肯定被 interned ,并且和并行產生的每個 PHP 的進程或線程共享。另外,如果當你處理一個請求時使用 interned 字符串,該字符串也將由 OPCache 本身進行 interned,并且共享給并行產生的每個 PHP 進程或線程。
然后,在觸發 OPCache 擴展時,會更改 Interned 字符串機制。OPCache 不僅允許從請求來的 interned 字符串,而且允許將它們共享給同一池的每個 PHP 進程。這樣做是使用了共享內存。當保存一個 interned 字符串時,OPCache 也會添加 IS_STR_PERMANENT
標志到它的 GC 信息。該標志表示用于結構(這里是zend_string
)的內存分配是永久的,它可以是共享的只讀內存段。
Interned 字符串可節省內存,因為在內存中,同樣的字符串不會再被保存。但是當它經常需要查找 interned 字符串存儲時,可能會浪費一些 CPU 時間,即使該進程如今已經優化了。作為一名擴展設計師,這是全局規則:
警告
不要試圖修改(寫入)一個 interned 字符串,否則很可能崩潰。
Interned 字符串詳情請看 Zend/zend_string.c。
關于php的字符串如何管理 zend_string就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。