本文假定你對(duì)C/C++的string語(yǔ)法已經(jīng)有基本的了解拟逮。
- 如果你對(duì)C++的string的內(nèi)部實(shí)現(xiàn)原理不了解的話,請(qǐng)先閱讀這一篇
《對(duì)[C/C++]指針與字符串的總結(jié)》撬统,本篇是對(duì)前一篇的內(nèi)容的延伸。 - 如果對(duì)字符串字面量不了解的話敦迄,請(qǐng)先閱讀《C/C++中的字符字面量》。
C ++的string對(duì)象實(shí)質(zhì)上就是一個(gè)容器,其內(nèi)部有一個(gè)c_str方法能夠返回一個(gè)指向的實(shí)質(zhì)存儲(chǔ)字符串副本的數(shù)據(jù)成員凭迹。即通過(guò)string::c_str()配合printf函數(shù)可以獲取的字符串副本的內(nèi)存地址罚屋。
棧中的string的內(nèi)存分配
首先,我們來(lái)看看如下代碼的關(guān)于string對(duì)象內(nèi)部的棧中內(nèi)存分配,不少C++讀物強(qiáng)力建議在C++開(kāi)發(fā)中使用標(biāo)準(zhǔn)庫(kù)的string對(duì)象,而非C版本的char*指針和char[]數(shù)組。但沒(méi)有詳細(xì)告訴讀者為什么嗅绸?string對(duì)象底層都做了些什么,因此理解string內(nèi)部實(shí)現(xiàn)原理,對(duì)于你后續(xù)使用string類(lèi)實(shí)現(xiàn)各種字符串操作的算法非常有必要脾猛,以下代碼,是前一篇文章代碼的深入的演示版本
首先我們?cè)谌肿饔糜蛑剌d了operator new和operator delete的函數(shù)原型,內(nèi)部分別用C版本的malloc和free函數(shù),目的在于:顯式展示給讀者,你在使用string過(guò)程中,它已經(jīng)在底層自動(dòng)完成了所有的內(nèi)存分配和內(nèi)存釋放鱼鸠。實(shí)際開(kāi)發(fā)過(guò)程不建議這樣重載operator new和operator delete猛拴。
show_str()函數(shù)是用于打印傳入?yún)?shù)string對(duì)象str內(nèi)部的字符串的地址和函數(shù)內(nèi)部的局部變量的string對(duì)象tmp的內(nèi)部字符串的地址。
下面是調(diào)用函數(shù)
輸出結(jié)果:
首先繼續(xù)進(jìn)行下文之前蚀狰,需要說(shuō)明的是Linux下的x86_64版本的GCC/G++編譯器默認(rèn)情況下(編譯時(shí)沒(méi)有附帶 -O 優(yōu)化選項(xiàng)),仍然按照x86平臺(tái)的過(guò)程調(diào)用約定組織程序棧愉昆,下文編譯時(shí)使用的是默認(rèn)設(shè)置。
從上面程序輸出看來(lái),在每次調(diào)用show_str()函數(shù)輸出的內(nèi)存地址看來(lái),string對(duì)象內(nèi)部持有字符串副本的內(nèi)存分配都發(fā)生在程序棧幀中,有一些有趣的分析麻蹋。
- main函數(shù)我們知道string對(duì)象內(nèi)部持有字符串副本的地址是"0x7ffc5b140990",輸出的參數(shù)地址跟main函數(shù)中的變量you是一致的,因?yàn)槲覀僺how_str()的參數(shù)類(lèi)型是const string&即使用了引用傳參,我們這里避免了字符串的拷貝.
- 每次string類(lèi)型的局部變量賦值操作,string對(duì)象內(nèi)部自動(dòng)執(zhí)行字符串拷貝跛溉,從每次打印的tmp程序地址可以得知缓呛。
匿名字符串字面量
我們第二次調(diào)用show_str()函數(shù)時(shí),你們是否思考過(guò)如下兩個(gè)問(wèn)題影晓。
- 0x7ffc5b1409b0從那里冒出來(lái)的,為何跟main函數(shù)的you不是一致的?
- 我們又沒(méi)有定義新的string類(lèi)型的局部變量,0x7ffc5b1409b0這個(gè)地址為什么后面會(huì)出現(xiàn)了兩次呈枉?
首先,解答第一個(gè)疑問(wèn),從內(nèi)存尋址的角度分析,一個(gè)變量必定對(duì)應(yīng)于一個(gè)內(nèi)存地址,也就是0x7ffc5b1409b0這個(gè)地址必定存在一個(gè)變量與之對(duì)應(yīng),但第二次調(diào)用show_str()函數(shù),我們沒(méi)有向其傳入任何定義的string類(lèi)型的局部變量,只是直接傳入一個(gè)字符串字面量刹勃。關(guān)鍵就是在這里堪侯,當(dāng)我們直接向show_str傳入一個(gè)字符串字面量之前,C++編譯器會(huì)隱式創(chuàng)建一個(gè)臨時(shí)變量,我們假設(shè)變量的名稱(chēng)是任意的x荔仁。隱式的臨時(shí)變量它的內(nèi)部字符串副本的地址自然就指向0x7ffc5b1409b0這個(gè)地址,我們第二次調(diào)用show_str的代碼,即如下代碼所示
int main(void){
std::string you="Hello,World!!";
show_str(you);
.....
//show_str("Hello,World!!")會(huì)等價(jià)于如下代碼
std::string& x="Hello,World!!";//隱式創(chuàng)建
show_str(x);
....
}
接下來(lái)回答第二個(gè)問(wèn)題就非常簡(jiǎn)單,由于C++已經(jīng)隱式地定義了
std::string& x="Hello,World!!";
那么后續(xù)調(diào)用任意的被調(diào)用函數(shù)的傳參類(lèi)型只要是const string&伍宦,那么傳入同一個(gè)匿名的字符串字面量。自然打印的都是同一個(gè)隱式局部變量的內(nèi)部字符串副本的地址咕晋。
另外比較蹊蹺的是tmp每次調(diào)用show_str輸出的地址是相同的,因?yàn)槲覀冞@里陸續(xù)調(diào)用的了相同show_str函數(shù)雹拄,那么show_str棧幀結(jié)構(gòu)基本上一樣的,如果你調(diào)用不同尺寸的函數(shù)掌呜,輸出結(jié)果就會(huì)不一樣滓玖。
堆中的string的內(nèi)存分配
這次,我稍微做一下改動(dòng),現(xiàn)在我們?cè)趍ain中傳入一個(gè)比之前更長(zhǎng)的尺寸為33字節(jié)的字符串字面量,如下圖
對(duì)應(yīng)的輸出
這次string對(duì)象的內(nèi)存分配已經(jīng)發(fā)生變化,show_str()函數(shù)中的他們的內(nèi)部數(shù)據(jù)成員分別指向各自堆中分配的內(nèi)存塊质蕉,的字符副本分別存儲(chǔ)這些堆中的內(nèi)存塊势篡。如上圖輸出都分別調(diào)用了void* operator new(size_t)的重載版本翩肌。
到這里你就應(yīng)該要思考兩個(gè)問(wèn)題
- 為什么在處理“Hello,Word!!”只在棧中進(jìn)行內(nèi)存分配?
- 為什么在處理“Hello,My name is peter!!”這樣的字符串,就會(huì)在堆中進(jìn)行內(nèi)存分配禁悠?
沒(méi)錯(cuò)念祭,答案就是字符串字面量的長(zhǎng)度決定的。這個(gè)我在前一編《對(duì)[C/C++]指針與字符串的總結(jié)》已經(jīng)提到過(guò)碍侦,但當(dāng)時(shí)我沒(méi)有指出,觸發(fā)string對(duì)象內(nèi)部的new操作的準(zhǔn)確閥值是多少粱坤。請(qǐng)看如下表
string對(duì)象內(nèi)部約定:
- 只要傳入的字符串字面量小于上表的閥值,string內(nèi)部實(shí)現(xiàn)在棧中分配內(nèi)存,有個(gè)很騷的名字小型字符串優(yōu)化(Small String Optimisation)。
- 只要大于上述C++編譯器指定閥值,string對(duì)象內(nèi)部會(huì)隱式執(zhí)行new操作在堆中根據(jù)指定的字符串尺寸分配初次內(nèi)存瓷产。
- 如果后續(xù)任何字符串的push_back操作,string會(huì)根據(jù)“double方案”的內(nèi)存分配方式對(duì)堆內(nèi)存執(zhí)行擴(kuò)容操作,見(jiàn)前文《對(duì)[C/C++]指針與字符串的總結(jié)》站玄。
- 還有根據(jù)RAII的約定,C++編譯器會(huì)對(duì)string對(duì)象在其調(diào)用函數(shù)的生命周期結(jié)束之時(shí)自動(dòng)執(zhí)行垃圾回收。(見(jiàn)上圖的輸出)濒旦。
建議:到這里,如果還沒(méi)搞懂如下代碼背后的內(nèi)存含義的話株旷,建議還是去補(bǔ)補(bǔ)棧和堆內(nèi)存管理的知識(shí),再去深入了解string對(duì)象尔邓。這樣會(huì)讓你少走很多彎路晾剖。
string s=new string(....)
和
void my_app(const string &s){
string tmp=s;
}
我們從內(nèi)存地址的角度,分析了string對(duì)象在棧中和堆中的內(nèi)存分配細(xì)節(jié)梯嗽。從這篇文章你應(yīng)該知道齿尽,在C++中掌握內(nèi)存分析方法是多么地重要,本篇用到了以前我所寫(xiě)隨筆的程序棧和堆內(nèi)存管理的知識(shí)慷荔。
擴(kuò)展閱讀雕什,如果關(guān)注我的讀者應(yīng)該了解我寫(xiě)軟文的套路是一環(huán)扣一環(huán)的,可能在說(shuō)string的話題显晶,然后有跳到程序棧贷岸,這就是所謂的知識(shí)碎片整理。
- 《第2篇:C/C++ 內(nèi)存布局與程序椓坠停》
- 《第3篇:戲說(shuō)程序棧-call指令和ret指令》
- 《第4篇:戲說(shuō)程序棧-棧幀》
- 《第5篇-戲說(shuō)程序棧-寄存器和函數(shù)狀態(tài)》
- 《第6篇-戲說(shuō)程序棧 x86_64過(guò)程調(diào)用》
后記
了解string對(duì)象的行為之后,接下來(lái)我們?nèi)绾慰紤]使用什么方法來(lái)避免字符串頻繁的拷貝,有些經(jīng)驗(yàn)的“老油條”應(yīng)該都領(lǐng)略過(guò)了const string&這類(lèi)參數(shù)類(lèi)型聲明并不能從根本上解決問(wèn)題(上例子的程序輸出已經(jīng)隱藏地說(shuō)明了這一點(diǎn))偿警。于是C++17就有了string_view這個(gè)標(biāo)準(zhǔn)庫(kù)的擴(kuò)展,這個(gè)擴(kuò)展極大地解決了string拷貝的空間成本和時(shí)間成本問(wèn)題唯笙。我們后續(xù)文章會(huì)繼續(xù)新的話題螟蒸。