字符串是以ASCII字符并且以NUL(即'\0')結尾 表示的字符序列
C中的字符串機制
以字符串字面量定義字符串時會將其分配到字面量池中,這個內存區(qū)域通常保存組成字符串的字符序列,該內存區(qū)域通常被認為是全局/靜態(tài)的。字符字面量在池中通常只有一份副本并且是只讀的,這樣可以減少程序的內存占用率。
首先,理解C的字符串運行機制,下面一段簡單的代碼可以得出關于字符串的不同結論
#include <stdio.h>
char *g="Hello";
int main(int argc, char const *argv[])
{
char s[]="Hello";
char *c="Hello";
printf("字符指針c的內存地址:%p\n",&c);
printf("數組s的內存地址:%p\n",&s);
printf("字符指針g的內存地址:%p\n",&g);
printf("字符指針c指向Hello的的內存地址: %p\n",&c[0]);
printf("字符指針g指向Hello的的內存地址: %p\n",&g[0]);
printf("變量s數組的Hello副本的內存地址:%p\n",&s[0]);
printf("Hello字面量的尺寸 %lu\n",sizeof("Hello"));
return 0;
}
從示例代碼中,全局區(qū)聲明并以字符串字面量初始化了字符指針g,g的內存地址是0x108993018,這個地址位于全局數據區(qū)內,而指針g它指向的“Hello”字面量地址是0x108992e9e.這個地址位于字面量池內. 同時,我們也從main函數內部定義了兩個局部變量字符指針變量c,c指針指向的字面量的內存地址和全局字符指針指向的"Hello"字面量是一樣的,那么這里可以得出以下關于聲明char指針并以字面量初始化的時候,可以得出以下特性。
- 將同樣一份字符串字面量的地址直接賦給不同的字符指針,不會產生額外字符串的副本,這些字符指針指向同一份字符串字面量。
另外,在main函數內部的數組s,它內部持有字符串數組的地址和字面量池的字符串的地址是不一樣的。換句話說,
2.以字符串數組初始化字符串字面量會在對應的函數棧內生成另外一份的字面量副本。
char s[]="HELLO"等價于strcpY(s,"HELLO")
3.再次,如果你在全局區(qū)以字符串數組初始化會,會在全局數據區(qū)產生同樣的字符串副本。

以下是運行的隨機結果:

C++中的字符串的機制
string是一個包含多個數據成員的字符串對象,這里只是補充《C++ Primer》這本書關于string對象內部機制沒詳細闡述,做個筆錄,這里不會羅列所有的string對象的api,不熟悉的同學可以看《C++ Primer》相關內容。
c_str()是一個指針指向動態(tài)分配空間中的字符數組中第一個字符的地址。
size()包含字符串的長度。
capacity()包含當前可能存儲在數組中的有效字符數(額外的NUL字符不計算在內)。
-
malloc內存分配
一個涉及到malloc內存管理程序的實現(xiàn)需要3個字段,每個字段都是三個不同指針:- 指向已分配內存的指針;
- 字符串的邏輯大小(該字符串末尾是NUL字符);
-
分配的內存大?。ū仨毚笥诨虻扔谶壿嫶笮?;
C++中的string對象內部基本原理
#include <iostream>
#include <cstdlib>
#include <string>
using std::string;
using std::cout;
using std::endl;
//重寫string類的new操作符,添加一個可以識別malloc操作的輸出
void* operator new(std::size_t n){
cout<<"分配"<<n<<"字節(jié)"<<endl;
return malloc(n);
}
void operator delete(void *p) throw(){
free(p);
}
int main(int argc, char const *argv[])
{
string s("HELLO"); //直接初始化
cout<<"初始化時的狀態(tài):"<<endl;
cout<<"sizeof:"<<sizeof(s)<<endl;
cout<<"size:"<<s.size()<<endl;
cout<<"分配的內存尺寸(capacity):"<<s.capacity()<<endl;
for(size_t i=6;i<24;++i){
s.push_back('+');
cout<<i<<":"<<s<<endl;
}
cout<<"push_back('+')之后的內存尺寸是"<<endl;
cout<<"sizeof:"<<sizeof(s)<<endl;
cout<<"size:"<<s.size()<<endl;
cout<<"分配的內存尺寸(capacity):"<<s.capacity()<<endl;
運行結果

在for循環(huán)前:我們通過調用string三個提到三個基本方法,起初分配的內存是24字節(jié),但允許容納有效的字符是22個,為什么呢?
因為HELLO后的第6個位置(索引5)包含一個NUL字符(即'\0'),而malloc初始化分配的24個字節(jié)里的最后一個字節(jié)位置也包含一個界定符,我認為也是NUL字符。有效字符的長度是不將NUL字符計算在內的,所有capacity方法才顯示22.初始化的狀態(tài)如下圖所示(重申:另外不同的計算機硬件,不同的OS和編譯器環(huán)境,malloc初始化時,申請的內存空間是不一樣的):
c++ string對象初始化的狀態(tài)
在for循環(huán)過程中,我們向malloc剩余的備用空間塞入'+'字符(覆蓋了索引5的NUL字符算起),直到索引22的位置也就是最后一個NUL字符的前一個字節(jié)位置),string對象內部就觸發(fā)malloc申請擴容的操作,而申請的內存總數是之前的2倍。
屏幕快照 2019-08-16 上午6.23.08.png
上面的代碼,for的循環(huán)的長度加大,總之大于24的任意一個正整數,多實驗幾次。會得到如下基本特征。
- 當size()方法得出字符數達到capacity()得出的有效容納的字符數,string對象內部就會觸發(fā)malloc的內存重新擴容。
- 每次malloc擴容后的申請的內存空間尺寸是之前的內存空間尺寸的2倍。
基于這兩點,C++的string對象內部封裝了涉及malloc操作的指針操作,這大大減輕了程序猿對指針操作不當,帶來程序不可預測的可能性。同時使用雙倍擴容的方法也最大限度減少了因字符串長度后續(xù)增加的頻繁malloc操作帶來的系統(tǒng)消耗。但是它是以內存空間為代價的,堆里和字面量池總有著相同一份相同的字符串副本。
后記:
理解C++的string對象底層其實就是malloc動態(tài)分配堆內存的機制之后,后面關于字符串的拼接,復制,查找等基本原理,你心里就有底了.要徹底理解字符串的話,推薦閱讀《深入理解c指針》這本書,里面關于字符串的描述比《征服C指針》講得更加深入。
額外問題:
調試C++程序的時候,有時你需要查看string對象內部的指針,雖然c_str()可以輸出字符串首個字符的內存地址,但標準庫cout操作會自動對c_str()的內部指針做解引操作,因此cout不能直接得出字符串的地址,而是打印對應的字符串。不需要折騰cout,直接簡單的粗暴方法是用C的printf函數
#include <stdio.h>
#include <string>
int main(int argc, char const *argv[])
{
string s("HELLO"); //直接初始化
printf("變量的s內存地址:%p\n",&s);
printf("變量的s2內存地址:%p\n",&s2);
printf("字符串對象s的地址:%p\n",&s[0]);
printf("字符串對象s2的地址:%p\n",s2.c_str());
printf("字符串字面量:%p\n",&"HELLO");
return 0;
}
輸出
變量的s內存地址:0x7ffee7221568
變量的s2內存地址:0x7ffee7221550
字符串對象s的地址:0x7ffee7221569
字符串對象s2的地址:0x7ffee7221551
字符串字面量:0x1089dfea8


