這篇文章運(yùn)用簡單易懂的例子給大家介紹php的字符串如何管理 zend_string,代碼非常詳細(xì),感興趣的小伙伴們可以參考借鑒,希望對(duì)大家能有所幫助。
成都創(chuàng)新互聯(lián)公司專注于泊頭網(wǎng)站建設(shè)服務(wù)及定制,我們擁有豐富的企業(yè)做網(wǎng)站經(jīng)驗(yàn)。 熱誠為您提供泊頭營銷型網(wǎng)站建設(shè),泊頭網(wǎng)站制作、泊頭網(wǎng)頁設(shè)計(jì)、泊頭網(wǎng)站官網(wǎng)定制、微信小程序開發(fā)服務(wù),打造泊頭網(wǎng)絡(luò)公司原創(chuàng)品牌,更為您提供泊頭網(wǎng)站排名全網(wǎng)營銷落地服務(wù)。
任何程序都需要管理字符串。在這里,我們將詳細(xì)介紹適合 PHP 需求的自定義解決方案:zend_string
。每次 PHP 需要使用字符串時(shí),都會(huì)使用 zend_string
結(jié)構(gòu)。該結(jié)構(gòu)僅僅是 C 語言的 char *
字符串類型的簡單精簡包裝。
它添加了內(nèi)存管理的功能,所以同一字符串可以在多個(gè)地方共享,而無需重復(fù)。另外,一些字符串是“內(nèi)部的”,即“持久的”分配,并通過內(nèi)存管理特殊管理,以便它們不會(huì)在多個(gè)請(qǐng)求中被銷毀。之后,那些從Zend 內(nèi)存管理獲得永久分配。
這里是簡單的zend_string
結(jié)構(gòu):
struct _zend_string { zend_refcounted_h gc; zend_ulong h; size_t len; char val[1]; };
如你所見,該結(jié)構(gòu)嵌入了一個(gè) zend_refcounted_h
標(biāo)頭。這個(gè)是內(nèi)存管理和引用需要用到的。 由于該字符串很有可能作為哈希表檢查的關(guān)鍵字,因此它在 h
字段中嵌入了其哈希值。這是無符號(hào)長整型 zend_ulong
。僅在需要對(duì) zend_string
進(jìn)行哈希處理時(shí)會(huì)用到,特別是和哈希表:zend_array一起用時(shí)。這很有可能。
如你所知,字符串知道其長度為 len
字段,以支持“二進(jìn)制字符串。二進(jìn)制字符串是嵌入一個(gè)或多個(gè) NUL
字符(\0)的字符串。當(dāng)傳遞給庫函數(shù),那些字符串會(huì)被截?cái)啵駝t無法正確計(jì)算其長度。所以在 zend_string
中,字符串的長度總是已知的。請(qǐng)注意,該長度計(jì)算的 ASCII 字符(字節(jié)),不計(jì)算最后的NUL
,而是計(jì)算最終的中間的 NUL。例如,字符串 “foo” 在 zend_string
中存儲(chǔ)為 “foo\0”,且它的長度為3。另外,字符串 “foo\0bar” 將存儲(chǔ)為 “foo\0bar\0”,且其長度為7。
最終,該字符存儲(chǔ)在 char[1]
。這不是 char *
,而是 char[1]
。為什么?這是一種稱為 “C struct hack” 的內(nèi)存優(yōu)化(你可以使用帶有這些術(shù)語的搜索引擎)?;旧?,它允許引擎為 zend_string
結(jié)構(gòu)和要存儲(chǔ)的字符分配空間,作為一個(gè)單獨(dú)的 C 指針。這優(yōu)化了內(nèi)存,因?yàn)閮?nèi)存訪問將是一個(gè)連續(xù)分配的塊,而不是兩個(gè)分散的塊(一個(gè)用于存儲(chǔ) zend_string *
,另一個(gè)用于存儲(chǔ) char *
)。
必須記住這種 struct hack,由于內(nèi)存布局看起來像 C 字符位于 C zend_string
結(jié)構(gòu)的末尾,因此當(dāng)使用 C 調(diào)試器(或調(diào)試字符串)時(shí)可能會(huì)感覺到/看到過。該 hack 是完全由 API 管理,當(dāng)你操作 zend_string
結(jié)構(gòu)時(shí)會(huì)用到。
像 Zvals,你不需要手動(dòng)操作 zend_string
內(nèi)部字段,而總是為此使用宏。還存在觸發(fā)字符串操作的宏。這并不是函數(shù),而是宏,都存儲(chǔ)在必需的 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);
上面簡單的例子為你展示了基本的字符串管理。應(yīng)該為 zend_string_init()
函數(shù)(實(shí)際上是宏,但先讓我們忽略它)給出完整的 char *
C 字符串和它的長度。類型為 int 的最后一個(gè)參數(shù)應(yīng)該為 0 或 1。如果傳遞0,則要求引擎通過 Zend 內(nèi)存管理使用請(qǐng)求綁定的堆分配。這種分配在當(dāng)前請(qǐng)求結(jié)束后時(shí)銷毀。如果你不這么做,則在調(diào)試版本中,引擎會(huì)提醒你內(nèi)存泄漏。如果傳遞1,則要求了所謂的“持久”分配,引擎將使用傳統(tǒng)的 C malloc()
調(diào)用,并且不會(huì)以任何方式追蹤內(nèi)存分配。
注意
如果你需要更多有關(guān)內(nèi)存管理的信息,你可以閱讀專用章節(jié)。
然后,我們來顯示字符串。我們使用 ZSTR_VAL()
宏訪問字符數(shù)組。ZSTR_LEN()
允許訪問長度信息。zend_string
相關(guān)宏都以 ZSTR_**()
開始,注意和 Z_STR**()
宏不一樣。
注意
長度使用
size_t
類型存儲(chǔ),為了顯示它,printf()
必須使用 “%zd”。你應(yīng)該總是使用正確的printf()
格式。否則可能會(huì)導(dǎo)致應(yīng)用程序崩潰或創(chuàng)建安全問題否則可能會(huì)導(dǎo)致內(nèi)存泄漏和。有關(guān)printf()
格式的詳細(xì)信息,請(qǐng)?jiān)L問此鏈接
最后,我們使用 zend_string_release()
釋放字符串。該釋放是強(qiáng)制的。這與內(nèi)存管理有關(guān)?!搬尫拧笔且粋€(gè)簡單的操作:字符串的引用計(jì)數(shù)遞減,如果減到0,API會(huì)為你釋放字符串。如果忘記釋放字符串,則很可能造成內(nèi)存泄漏。
注意
在 C 語言中,你必須總是考慮內(nèi)存管理。如果你分配——不管是直接使用
malloc()
,或者使用能為你這樣做的 API,在某些時(shí)候你必須使用free()
。否則可能會(huì)導(dǎo)致內(nèi)存泄漏,并轉(zhuǎn)換為任何人都不能安全使用的糟糕設(shè)計(jì)程序。
如果你需要訪問哈希值,可使用 ZSTR_H()
。但創(chuàng)建 zend_string
時(shí),不會(huì)自動(dòng)計(jì)算其哈希值。而當(dāng)將該字符串與 HashTable API 一起使用時(shí),它將為你完成。如果你強(qiáng)制立即計(jì)算哈希值,可使用 ZSTR_HASH()
或 zend_string_hash_val()
。當(dāng)哈希值被計(jì)算出來,它會(huì)被保存起來并且不再被計(jì)算。無論如何,你必須使用 zend_string_forget_hash_val()
重新計(jì)算——因?yàn)槟愀淖兞俗址闹担?/p>
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 的一個(gè)非常棒的特性是:允許某部分通過簡單的聲明“擁有”字符串。引擎不會(huì)在內(nèi)存復(fù)制字符串,而是遞增其引用計(jì)數(shù)(作為字符串zend_refcounted_h
的一部分)。這允許在代碼的多個(gè)地方共享一個(gè)內(nèi)存。
由此,當(dāng)我們討論“復(fù)制”一個(gè) zend_string
時(shí),實(shí)際上并沒有復(fù)制內(nèi)存中的任何東西。如果需要(這仍是可能的操作),之后我們來討論“復(fù)制”字符串。開始吧:
zend_string *foo, *bar, *bar2, *baz; foo = zend_string_init("foo", strlen("foo"), 0); /* 創(chuàng)建變量foo,值為“foo” */ bar = zend_string_init("bar", strlen("bar"), 0); /* 創(chuàng)建變量bar,值為"bar" */ /* 創(chuàng)建變量bar2,共享變量bar的值。 另外遞增"bar"字符串的引用計(jì)數(shù)到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)); /* 在內(nèi)存中復(fù)制"bar"字符串,創(chuàng)建變量 baz, 使 baz 單獨(dú)擁有新創(chuàng)建的"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"); /* 更改第二個(gè)"bar"字符串的最后一個(gè)字符, 變?yōu)?baz" */ ZSTR_VAL(baz)[ZSTR_LEN(baz) - 1] = 'z'; /* 當(dāng)字符串改變時(shí),忘記舊哈希值(如果已計(jì)算), 因此其哈希值必須更改并重新計(jì)數(shù) */ 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"字符串的引用計(jì)數(shù)到1 */ zend_string_release(bar2); /* 銷毀(釋放)bar和bar2變量中的"bar"字符串 */ zend_string_release(baz); /* 銷毀(釋放)"baz"字符串 */
我們一開始僅分配 “foo” 和 “bar”。然后,我們創(chuàng)建 bar
的副本到bar2
字符串。這里,必須記?。涸趦?nèi)存中,bar
和 bar2
指向同一 C 字符串,更改一個(gè)將更改第二個(gè)。這是 zend_string_copy()
行為:它僅遞增 C 字符串的引用計(jì)數(shù)。
如果想要分離字符串,即想在內(nèi)存中擁有該字符串的兩個(gè)不同副本,我們必須使用 zend_string_dup()
復(fù)制。然后我們將 bar2
變量字符串復(fù)制到 baz
變量。現(xiàn)在,baz
變量嵌入它的字符串副本,并且可以改變它而不影響 bar2
。這就是我們要做的:我們用‘z’改變了‘bar’最后的‘r’,之后,我們顯示它,并釋放所有字符串。
注意,我們忘記哈希值(如果它在之前已經(jīng)計(jì)算,則不需要考慮其細(xì)節(jié))。這是一個(gè)值得記住的好習(xí)慣。就像我們?cè)f過,如果 zend_string
作為 HashTables 的一部分,則使用哈希值。這在開發(fā)中是很常見的,并且改變字符串的值必須重新計(jì)算哈希值。忘記這一步驟將導(dǎo)致可能需要花一些時(shí)間去追蹤錯(cuò)誤。
zend_string
API 允許其他操作,例如擴(kuò)展或縮小字符串,更改大小寫或比較字符串。目前尚未有連接字符串操作,但是很容易執(zhí)行:
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 字符串文字進(jìn)行比較 */ if (!zend_string_equals_literal(FOO, "foobar")) { foobar = zend_string_copy(FOO); /* realloc() 將 C 字符串分配到更大的緩沖區(qū) */ 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)); /* 比較兩個(gè) zend_string */ if (!zend_string_equals(FOO, foobar)) { /*復(fù)制字符串并改為小寫*/ foo_lc = zend_string_tolower(foo); } php_printf("This is FOO in lower-case: %s\n", ZSTR_VAL(foo_lc)); /* 釋放內(nèi)存 */ zend_string_release(FOO); zend_string_release(bar); zend_string_release(foobar); zend_string_release(foo_lc);
現(xiàn)在你知道如何管理和操作 zend_string
,讓我們看看它們與 zval
容器的互動(dòng)。
注意
你必須熟悉 zval,如果不熟悉,閱讀Zvals專用章節(jié)。
宏將允許你將 zend_string
存儲(chǔ)到 zval
,或從 zval
讀取 zend_string
:
zval myval; zend_string *hello, *world; zend_string_init(hello, "hello", strlen("hello"), 0); /* 存儲(chǔ)字符串到 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:將其替換為另一個(gè) */ Z_STR(myval) = world; /* ... */
你必須記住的是,以ZSTR_***(s)
開頭的每個(gè)宏都會(huì)作用到 zend_string
。
ZSTR_VAL()
ZSTR_LEN()
ZSTR_HASH()
每個(gè)以 Z_STR**(z)
開頭的宏都會(huì)作用于嵌入到 zval
中的 zend_string
。
Z_STRVAL()
Z_STRLEN()
Z_STRHASH()
還有一些你可能不需要的東西也存在。
簡單介紹一下。在 C 語言中,字符串是字符數(shù)組(char foo[]
)或者指向字符的指針(char *
)。它們并不知道其長度,這就是它們?yōu)槭裁茨┪彩?NUL(知道字符串的開始和結(jié)尾,就可以知道它的長度)。
在 PHP 7 之前,zend_string
結(jié)構(gòu)還未出現(xiàn)。在那時(shí),還是使用傳統(tǒng)的 char * / int
。你可能仍會(huì)在 PHP 源代碼中找到使用了罕見的 char * / int
,而不是 zend_string
。你也可能發(fā)現(xiàn) API 功能,可以一邊使用 zend_string
,另一邊使用 char * / int
來交互。
在任何可能的地方:使用 zend_string
。那些罕見的沒有使用 zend_string
的地方,是因?yàn)樵谀抢锸褂盟鼈儾]有什么意義,但是你仍會(huì)發(fā)現(xiàn)在 PHP 源代碼中有很多對(duì) zend_string
的引用。
在這里簡單的介紹一下 interned 字符串。你在擴(kuò)展開發(fā)中應(yīng)該需要這樣的概念。Interned 字符串也和 OPCache 擴(kuò)展交互。
Interned 字符串是去重復(fù)的字符串。當(dāng)與 OPCache 一起使用時(shí),它還可以在請(qǐng)求之間循環(huán)使用。
假設(shè)你想要?jiǎng)?chuàng)建字符串“foo”。你更想做的是簡單地創(chuàng)建一個(gè)新字符串“foo”:
zend_string *foo; foo = zend_string_init("foo", strlen("foo"), 0); /* ... */
但是有一個(gè)問題:字符串是不是在你需要之前已經(jīng)創(chuàng)建了?當(dāng)你需要一個(gè)字符串時(shí),你的代碼會(huì)在PHP生命中的某個(gè)時(shí)刻執(zhí)行,這意味著在你需要完全相同的字符串(在我們的示例中為“ foo”)之前發(fā)生了一些代碼。
Interned 字符串是關(guān)于要求引擎去探查 interned 字符串存儲(chǔ),并且如果它能找到你的字符串,會(huì)重用已經(jīng)分配的指針。如果沒有找到:創(chuàng)建一個(gè)新的字符串并“intern” 它,這使得它可用于 PHP 源代碼的其他部分(其他擴(kuò)展,引擎本身等)。
這里有個(gè)例子:
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);
上面的代碼創(chuàng)建了一個(gè)非常經(jīng)典的新 zend_string
。然后,我們將創(chuàng)建的 zend_string
傳遞給 zend_new_interned_string()
。該函數(shù)在引擎 interned 字符串緩沖區(qū)查找相同的字符串(這里是“foo”)。如果找到它(意味著有人已經(jīng)創(chuàng)建了這樣的字符串),那么它將釋放你的字符串(可能釋放它),并且用 interned 字符串緩沖區(qū)中的字符串替代它。如果找不到:它將被添加到 interned 字符串緩沖區(qū),使它在將來可使用或可用于 PHP 的其他部分。
你必須注意內(nèi)存分配。Interned 字符串總是將 refcount 設(shè)為1,因?yàn)樗鼈儾槐乇灰?,由于它們?huì)和 interned 字符串緩沖區(qū)共享,因此不可被銷毀。
例:
zend_string *foo, *foo2; foo = zend_string_init("foo", strlen("foo"), 0); foo2 = zend_string_copy(foo); /* 遞增 foo 的引用計(jì)數(shù) */ /* 引用計(jì)數(shù)退回 1,即使現(xiàn)在字符串在三個(gè)不同的地方被使用 */ foo = zend_new_interned_string(foo); /* 這沒有任何作用,因?yàn)?foo 是 interned */ zend_string_release(foo); /* 這沒有任何作用,因?yàn)?foo2 是 interned*/ zend_string_release(foo2); /* 在流程結(jié)束時(shí),PHP 將清除它的 interned 字符串緩沖區(qū), 因此 free() 我們 "foo" 字符串本身 */
這都是關(guān)于垃圾收集的。
當(dāng)字符串是 interned,更改其 GC 標(biāo)志以添加 IS_STR_INTERNED
標(biāo)志,不管使用的是什么內(nèi)存分配類(基于永久或基于請(qǐng)求)。當(dāng)你想要復(fù)制或釋放字符串,都會(huì)檢查該標(biāo)志。如果是 interned 字符串,當(dāng)你復(fù)制該字符串時(shí),引擎不會(huì)遞增它的引用計(jì)數(shù)。但是如果你釋放字符串,它也不會(huì)遞減或釋放它。它不做任何事情。在進(jìn)程生命周期的最后,它會(huì)銷毀它的 interned 字符串緩沖區(qū),并且釋放你的 interned 字符串。
事實(shí)上,此過程比這更為復(fù)雜。如果你使用的是請(qǐng)求處理中的 interned 字符串,那么該字符串肯定被 interned。但是,如果你是在 PHP 處理一個(gè)請(qǐng)求時(shí)使用 interned 字符串,那么該字符串只會(huì)在當(dāng)前請(qǐng)求被 interned,并在之后會(huì)清理掉。如果你不使用 OPCache 擴(kuò)展,那這一切都是有效的,有時(shí)你不應(yīng)該使用它。
當(dāng)使用 OPCache 擴(kuò)展,如果你使用請(qǐng)求處理中的 interned 字符串,那么該字符串肯定被 interned ,并且和并行產(chǎn)生的每個(gè) PHP 的進(jìn)程或線程共享。另外,如果當(dāng)你處理一個(gè)請(qǐng)求時(shí)使用 interned 字符串,該字符串也將由 OPCache 本身進(jìn)行 interned,并且共享給并行產(chǎn)生的每個(gè) PHP 進(jìn)程或線程。
然后,在觸發(fā) OPCache 擴(kuò)展時(shí),會(huì)更改 Interned 字符串機(jī)制。OPCache 不僅允許從請(qǐng)求來的 interned 字符串,而且允許將它們共享給同一池的每個(gè) PHP 進(jìn)程。這樣做是使用了共享內(nèi)存。當(dāng)保存一個(gè) interned 字符串時(shí),OPCache 也會(huì)添加 IS_STR_PERMANENT
標(biāo)志到它的 GC 信息。該標(biāo)志表示用于結(jié)構(gòu)(這里是zend_string
)的內(nèi)存分配是永久的,它可以是共享的只讀內(nèi)存段。
Interned 字符串可節(jié)省內(nèi)存,因?yàn)樵趦?nèi)存中,同樣的字符串不會(huì)再被保存。但是當(dāng)它經(jīng)常需要查找 interned 字符串存儲(chǔ)時(shí),可能會(huì)浪費(fèi)一些 CPU 時(shí)間,即使該進(jìn)程如今已經(jīng)優(yōu)化了。作為一名擴(kuò)展設(shè)計(jì)師,這是全局規(guī)則:
警告
不要試圖修改(寫入)一個(gè) interned 字符串,否則很可能崩潰。
Interned 字符串詳情請(qǐng)看 Zend/zend_string.c。
關(guān)于php的字符串如何管理 zend_string就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,可以學(xué)到更多知識(shí)。如果覺得文章不錯(cuò),可以把它分享出去讓更多的人看到。