本文假定你對C/C++的string文法已經有基本的了解。
-
如果你對C++的string的内部實作原理不了解的話,請先閱讀這一篇
《對[C/C++]指針與字元串的總結》,本篇是對前一篇的内容的延伸。
- 如果對字元串字面量不了解的話,請先閱讀《C/C++中的字元字面量》。
C ++的string對象實質上就是一個容器,其内部有一個c_str方法能夠傳回一個指向的實質存儲字元串副本的資料成員。即通過string::c_str()配合printf函數可以擷取的字元串副本的記憶體位址。
棧中的string的記憶體配置設定
首先,我們來看看如下代碼的
關于string對象内部的棧中記憶體配置設定,不少C++讀物強力建議在C++開發中使用标準庫的string對象,而非C版本的char*指針和char[]數組。但沒有詳細告訴讀者為什麼?string對象底層都做了些什麼,是以了解string内部實作原理,對于你後續使用string類實作各種字元串操作的算法非常有必要,以下代碼,是前一篇文章代碼的深入的
示範版本首先我們在全局作用域重載了operator new和operator delete的函數原型,内部分别用C版本的malloc和free函數,目的在于:顯式展示給讀者,你在使用string過程中,它已經在底層自動完成了所有的記憶體配置設定和記憶體釋放。實際開發過程不建議這樣重載
operator new和
operator delete。
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLicmbw5CMwQTM3MGO2YWZwYTMjBDNlRmMhVGN3kDOlNmNkNzYl9CX0JXZ252bj91Ztl2Lc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
show_str()函數是用于列印傳入參數string對象
str内部的字元串的位址和函數内部的局部變量的string對象tmp的内部字元串的位址。
下面是
調用函數:
首先繼續進行下文之前,需要說明的是Linux下的x86_64版本的GCC/G++編譯器預設情況下(編譯時沒有附帶 -O 優化選項),仍然按照x86平台的過程調用約定組織程式棧,下文編譯時使用的是預設設定。
從上面程式輸出看來,在每次調用show_str()函數輸出的記憶體位址看來,string對象内部持有字元串副本的記憶體配置設定都發生在程式棧幀中,有一些有趣的分析。
- main函數我們知道string對象内部持有字元串副本的位址是"0x7ffc5b140990",輸出的參數位址跟main函數中的變量you是一緻的,因為我們show_str()的參數類型是const string&即使用了引用傳參,我們這裡避免了字元串的拷貝.
- 每次string類型的局部變量指派操作,string對象内部自動執行字元串拷貝,從每次列印的tmp程式位址可以得知。
匿名字元串字面量
我們第二次調用show_str()函數時,你們是否思考過如下兩個問題。
- 0x7ffc5b1409b0從那裡冒出來的,為何跟main函數的you不是一緻的?
- 我們又沒有定義新的string類型的局部變量,0x7ffc5b1409b0這個位址為什麼後面會出現了兩次?
首先,解答第一個疑問,從記憶體尋址的角度分析,一個變量必定對應于一個記憶體位址,也就是0x7ffc5b1409b0這個位址必定存在一個變量與之對應,但第二次調用show_str()函數,我們沒有向其傳入任何定義的string類型的局部變量,隻是直接傳入一個字元串字面量。關鍵就是在這裡,當我們直接向show_str傳入一個字元串字面量之前,C++編譯器會隐式建立一個臨時變量,我們假設變量的名稱是任意的x。隐式的臨時變量它的内部字元串副本的位址自然就指向0x7ffc5b1409b0這個位址,我們第二次調用show_str的代碼,即如下代碼所示
int
接下來回答第二個問題就非常簡單,由于C++已經隐式地定義了
std
那麼後續調用任意的被調用函數的傳參類型隻要是const string&,那麼傳入同一個匿名的字元串字面量。自然列印的都是同一個隐式局部變量的内部字元串副本的位址。
另外比較蹊跷的是tmp每次調用show_str輸出的位址是相同的,因為我們這裡陸續調用的了相同show_str函數,那麼show_str棧幀結構基本上一樣的,如果你調用不同尺寸的函數,輸出結果就會不一樣。
堆中的string的記憶體配置設定
這次,我稍微做一下改動,現在我們在main中傳入一個比之前
更長的尺寸為33位元組的字元串字面量,如下圖
這次string對象的記憶體配置設定已經發生變化,show_str()函數中的他們的内部資料成員分别指向各自堆中配置設定的記憶體塊,的字元副本分别存儲這些堆中的記憶體塊。如上圖輸出都分别調用了void* operator new(size_t)的重載版本。
到這裡你就應該要思考兩個問題
- 為什麼在處理“Hello,Word!!”隻在棧中進行記憶體配置設定?
- 為什麼在處理“Hello,My name is peter!!”這樣的字元串,就會在堆中進行記憶體配置設定?
沒錯,答案就是字元串字面量的長度決定的。這個我在前一編《對[C/C++]指針與字元串的總結》已經提到過,但當時我沒有指出,觸發string對象内部的new操作的準确閥值是多少。請看如下表
string對象内部約定:
- 隻要傳入的字元串字面量小于上表的閥值,string内部實作在棧中配置設定記憶體,有個很騷的名字 小型字元串優化 (Small String Optimisation)。
- 隻要大于上述C++編譯器指定閥值,string對象内部會隐式執行new操作在堆中根據指定的字元串尺寸配置設定 初次記憶體 。
- 如果後續任何字元串的push_back操作,string會根據“double方案”的記憶體配置設定方式對堆記憶體執行擴容操作,見前文《對[C/C++]指針與字元串的總結》。
- 還有根據RAII的約定,C++編譯器會對string對象在其調用函數的生命周期結束之時自動執行垃圾回收。(見上圖的輸出)。
:到這裡,如果還沒搞懂如下代碼背後的記憶體含義的話,建議還是去補補棧和堆記憶體管理的知識,再去深入了解string對象。這樣會讓你少走很多彎路。
string s=new string(....)
和
void
我們從記憶體位址的角度,分析了string對象在棧中和堆中的記憶體配置設定細節。從這篇文章你應該知道,在C++中掌握記憶體分析方法是多麼地重要,本篇用到了以前我所寫随筆的程式棧和堆記憶體管理的知識。
擴充閱讀,如果關注我的讀者應該了解我寫軟文的套路是一環扣一環的,可能在說string的話題,然後有跳到程式棧,這就是所謂的知識碎片整理。
- 《第2篇:C/C++ 記憶體布局與程式棧》
- 《第3篇:戲說程式棧-call指令和ret指令》
- 《第4篇:戲說程式棧-棧幀》
- 《第5篇-戲說程式棧-寄存器和函數狀态》
- 《第6篇-戲說程式棧 x86_64過程調用》
後記
了解string對象的行為之後,接下來我們如何考慮使用什麼方法來避免字元串頻繁的拷貝,有些經驗的“老油條”應該都領略過了const string&這類參數類型聲明并不能從根本上解決問題(上例子的程式輸出已經隐藏地說明了這一點)。于是C++17就有了string_view這個标準庫的擴充,這個擴充極大地解決了string拷貝的空間成本和時間成本問題。我們後續文章會繼續新的話題。