深入探索C++ string的底層實(shí)現(xiàn)
一、成員變量
private: char* _str;//用來存儲(chǔ)字符串 size_t _size;//用來表示有效字符數(shù) size_t _capacity;//用來表示可以存儲(chǔ)有效字符的容量 public: static size_t npos;//要在類外面定義
string
本質(zhì)上是一個(gè)動(dòng)態(tài)順序表,它可以根據(jù)需要?jiǎng)討B(tài)的擴(kuò)容,所以字符串一定是通過在堆上動(dòng)態(tài)申請空間進(jìn)行存儲(chǔ)的,因此_str
指向存儲(chǔ)字符串的空間,_size
用來表示有效字符數(shù),_capacity
用來表示可以存儲(chǔ)有效字符的容量數(shù)。
二、成員函數(shù)
2.1 默認(rèn)構(gòu)造函數(shù)
string(const char* str = "") :_str(new char[strlen(str) + 1])//strlen計(jì)算的是有效字符的個(gè)數(shù),而我們存儲(chǔ)的時(shí)候要在字符串的最后存一個(gè)'\0' ,_size(strlen(str)) ,_capacity(_size) { //memcpy(_str, str, _size); //strcpy(_str, str);//常量字符串就是遇到'\0'終止,所以直接用strcpy也可以 memcpy(_str, str, strlen(str) + 1); }
注意:默認(rèn)構(gòu)造函數(shù)需要注意的地方是:首先形參必須加上 const
修飾,這樣才能用 C 語言中的常量字符串來初始化 string
類對象,形參的的缺省值直接給一個(gè)空字符串即可,注意空字符串是用""
表示,該字符串只有結(jié)尾默認(rèn)的一個(gè) '\0'
,"\0"
并不表示空字符串,它表示該字符串有一個(gè)字符 '\0'
,它的結(jié)尾還有一個(gè)默認(rèn)的 '\0'
,因此有兩個(gè) '\0'
,nullptr
也不能表示空字符串,他表示的是空指針。其次需要注意初始化列表的順序,應(yīng)該嚴(yán)格按照成員變量的出現(xiàn)順序。strlen
計(jì)算的是字符串中有效字符的個(gè)數(shù),不算 '\0'
,而常量字符串的結(jié)尾默認(rèn)有一個(gè) '\0'
,因此在用 new
開辟空間的時(shí)候需要多開一個(gè)用來存儲(chǔ)結(jié)尾的 \0
。_capacity
表示的是可以存儲(chǔ)有效字符的容量,而字符串結(jié)尾默認(rèn)的 '\0'
并不算作有效字符,因此最初的 _capacity
就是形參 str
的長度。最后記得在構(gòu)造函數(shù)體內(nèi)將形參 str
的字符拷貝到動(dòng)態(tài)申請的空間中。
小Tips:涉及到字符串拷貝的地方,建議使用 memcpy
,strcpy
默認(rèn)遇到 \0
就終止,但是不排除 \0
就是 string
對象中的有效字符。但是 strcpy
會(huì)默認(rèn)在結(jié)尾加 \0
,而 memcpy
不會(huì),因此使用 memcpy
的時(shí)候需要注意拷貝得到的字符串結(jié)尾是否有 \0
。
2.2 拷貝構(gòu)造函數(shù)
//傳統(tǒng)寫法 string(const string& str) :_str(new char[str._size + 1]) ,_size(str._size) ,_capacity(_size) { memcpy(_str, str._str, str._size + 1); } //現(xiàn)代寫法 string(const string& str) :_str(nullptr) , _size(0) ,_capacity(0) { string tmp(str._str); swap(tmp); }
注意:現(xiàn)代寫法不需要我們親自去申請空間初始化,而是調(diào)用構(gòu)造函數(shù)去幫我們完成。最后再將初始化好的 tmp
交換過來,這里一定要通過初始化列表對 *this
進(jìn)行初始化,不然交換給 tmp
后,里面都是隨機(jī)值,最終出了作用域 tmp
去銷毀的時(shí)候就會(huì)出問題?,F(xiàn)代寫法的坑點(diǎn)在于,如果 string
對象中有 '\0'
,只會(huì)把 '\0'
前面的字符拷貝過去。
2.3 operator=
//傳統(tǒng)寫法 string& operator=(const string& s) { if (this != &s) { char* tmp = new char[s._capacity + 1]; memcpy(tmp, s._str, s._size + 1); delete[] _str; _str = tmp; _size = s._size; _capacity = s._capacity; } return *this; }
注意:這種寫法需要我們自己去開辟空間新空間 tmp
,自己去釋放舊空間 _str
,下面將對這種寫法加以改進(jìn),通過已有的接口來幫我們完成這些工作。
//現(xiàn)代寫法 string& operator=(const string& s) { if (this != &s) { string tmp(s);//通過調(diào)用拷貝構(gòu)造來創(chuàng)建空間 //tmp是局部變量,出了作用于會(huì)自動(dòng)銷毀,把待銷毀的資源通過交換,給tmp std::swap(_str, tmp._str); std::swap(_size, tmp._size); std::swap(_capacity, tmp._capacity); //std::swap(*this, tmp);//錯(cuò)誤的寫法 } return *this; } //現(xiàn)代寫法優(yōu)化 void swap(string& s) { std::swap(_str, s._str); std::swap(_size, s._size); std::swap(_capacity, s._capacity); } string& operator=(string s) { swap(s); return *this; } //優(yōu)化版本,連拷貝構(gòu)造函數(shù)也不需要我們自己去調(diào)用啦,直接通過形參去調(diào)用
注意:這種寫法通過調(diào)用拷貝構(gòu)造來幫我們申請空間,在利用局部對象出了作用就會(huì)被銷毀的特點(diǎn),將需要釋放的資源通過 swap
交換給這個(gè)局部變量,讓這個(gè)局部變量幫我們銷毀。這里不能直接用 swap
交換兩個(gè) string
類對象,會(huì)導(dǎo)致棧溢出,因?yàn)?nbsp;swap
函數(shù)中會(huì)調(diào)用賦值運(yùn)算符重載,而賦值運(yùn)算符重載又要調(diào)用 swap
成了互相套娃。我們可以不用庫里面的 swap
,自己實(shí)現(xiàn)一個(gè) Swap
用來交換兩個(gè) string
對象。
2.4 c_str()
char* c_str() const { return _str; }
注意:記得加上 const
,這樣普通的 string
類對象可以調(diào)用,const
類型的 string
類對象也可以調(diào)用,普通對象來調(diào)用就是權(quán)限的縮小。
2.5 size()
size_t size() const { return _size; }
2.6 operator[ ]
//讀寫版本 char& operator[](size_t pos) { assert(pos < _size); return _str[pos]; }
//只讀版本 const char& operator[](size_t pos) const { assert(pos < _size); return _str[pos]; }
注意:這兩個(gè)運(yùn)算符重載函數(shù)構(gòu)成函數(shù)重載,對象在調(diào)用的時(shí)候會(huì)走最匹配的,普通對象會(huì)調(diào)用讀寫版本,const
對象會(huì)調(diào)用只讀版本。
2.7 iterator
iterator
是 string
類的內(nèi)嵌類型,也可以說是在 string
類里面定義的類型,在一個(gè)類里面定義類型有兩種方法,typedef
和 內(nèi)部類。string
類的 iterator
是通過前者來實(shí)現(xiàn)的,即對字符指針 char*
通過 typedef
得到的。
typedef char* iterator; typedef const char* const_iterator; //可讀可寫版本 iterator begin() { return _str; } iterator end() { return _str + _size; } //只讀版本 const_iterator begin() const { return _str; } const_iterator end() const { return _str + _size; }
2.8 reserve
void reserve(size_t n = 0) { if (n > _capacity) { char* tmp = new char[n + 1]; //strcpy(tmp, _str); memcpy(tmp, _str, _size + 1); _capacity = n; delete[] _str; _str = tmp; } }
2.9 resize
void resize(size_t n, char ch = '\0') { if (n < _size) { erase(n); } else { reserve(n); for (size_t i = _size; i < n; i++) { _str[i] = ch; } _size = n; _str[_size] = '\0'; } }
注意:reserve
函數(shù)不會(huì)進(jìn)行縮容,因此在擴(kuò)容前要先進(jìn)程判斷,只有當(dāng)形參 n
大于當(dāng)前容量的時(shí)候才擴(kuò)容。
2.10 push_back
void push_back(char ch) { //先檢查容量,進(jìn)行擴(kuò)容 if (_size == _capacity) { reserve(_capacity == 0 ? 4 : _capacity * 2); } _str[_size++] = ch; _str[_size] = '\0'; }
注意:需要注意對空串的追加,空串的 _capacity = 0
,因此在調(diào)用 reserve
函數(shù)進(jìn)行擴(kuò)容的時(shí)候,不能簡單傳遞 _capacity*2
,要先進(jìn)行判斷,當(dāng) capacity == 0
的時(shí)候,給它一個(gè)初始大小。
2.11 append
void append(const char* str) { if (_size + strlen(str) > _capacity) { reserve(_size + strlen(str)); } //strcpy(_str + _size, str);//常量字符串就是遇到'\0'終止,所以直接用strcpy也可以 memcpy(_str + _size, str, strlen(str) + 1); _size += strlen(str); }
2.12 operator+=
//追加一個(gè)字符串 string& operator+=(const char* str) { append(str); return *this; } //追加一個(gè)字符 string& operator+=(char ch) { push_back(ch); return *this; }
注意:+= 需要有返回值。
2.13 insert
//插入n個(gè)字符 void insert(size_t pos, size_t n, char ch) { assert(pos <= _size); //檢查容量,擴(kuò)容 if (_size + n > _capacity) { reserve(_size + n); } //挪動(dòng)數(shù)據(jù) size_t end = _size; while (end != npos && end >= pos) { _str[end + n] = _str[end--]; } //插入數(shù)據(jù) size_t i = pos; while (i < pos + n) { _str[i++] = ch; } _size += n; }
注意:這里需要注意挪動(dòng)數(shù)據(jù)時(shí)的判斷條件,因?yàn)?nbsp;end
和 pos
都是 sizt_t
類型,所以當(dāng) pos = 0
的時(shí)候 end >= pos
永遠(yuǎn)成立,此時(shí)就會(huì)有問題,只把 end
改成 int
也解決不了問題,在比較的時(shí)候會(huì)發(fā)生整形提升,最終還是永遠(yuǎn)成立。一種解決方法就是想上面一樣,加一個(gè) size_t
類型的成員變量 npos
,把它初始化成 -1,即整形最大值,判斷 end
是否等于 npos
,等于說明 end
已經(jīng)減到 -1 了,就應(yīng)該停止挪動(dòng)。解決上面的問題還有一種方法,上面的問題出現(xiàn)在 pos = 0
時(shí),end
會(huì)減到 -1,最終變成正的無窮大,導(dǎo)致判斷條件永遠(yuǎn)成立,那我們可以將 end
初始化成 _size + n
,把 end - n
上的字符挪到 end
位置上,此時(shí)計(jì)算 pos = 0
,也不會(huì)出現(xiàn) end
減到 -1 的情況,代碼如下:
//插入n個(gè)字符 void insert(size_t pos, size_t n, char ch) { assert(pos <= _size); //檢查容量,擴(kuò)容 if (_size + n > _capacity) { reserve(_size + n); } //挪動(dòng)數(shù)據(jù) size_t end = _size + n; while (end >= pos + n) { _str[end] = _str[end - n]; --end; } //插入數(shù)據(jù) size_t i = pos; while (i < pos + n) { _str[i++] = ch; } _size += n; }
小Tips:npos作為一個(gè)靜態(tài)成員變量,必須在類外面進(jìn)行初始化(定義),并且不能在聲明時(shí)給默認(rèn)值,默認(rèn)值是給初始化列表用的,而靜態(tài)成員變量屬于該類所有對象共有,并不會(huì)走初始化列表。但是!但是!!,整形的靜態(tài)成員變量變量在加上 const 修飾后就可以在聲明的地方給默認(rèn)值,注意!僅限整形。其他類型的靜態(tài)成員變量在加 const 修飾后仍需要在類外面定義。
const static size_t npos = -1;//可以 //const static double db = 1.1//不可以
//插入一個(gè)字符串 void insert(size_t pos, const char* str) { assert(pos <= _size); size_t len = strlen(str); if (_size + len > _capacity) { reserve(_size + len); } //挪動(dòng) size_t end = _size + len; while (end >= pos + len) { _str[end] = _str[end - len]; --end; } //插入 for (size_t i = 0; i < len; i++) { _str[pos + i] = str[i]; } _size += len; }
2.14 erase
void erase(size_t pos, size_t len = npos) { assert(pos < _size); if (len == npos || pos + len >= _size)// { _str[pos] = '\0'; _size = pos; } else { //挪動(dòng)覆蓋 size_t end = pos + len; while (end <= _size) { _str[end - len] = _str[end++]; } _size -= len; } }
注意:pos
將整個(gè)數(shù)組劃分成兩部分,[0,pos-1]是一定不需要?jiǎng)h除的區(qū)域,[pos,_size-1]是待刪除區(qū)域,一定不需要?jiǎng)h除的區(qū)域有 pos
個(gè)元素,我們希望刪除 len
個(gè)字符,當(dāng)一定不會(huì)刪除的字符數(shù)加我們希望刪除的字符數(shù)如果大于或等于全部的有效字符數(shù),那就說明待刪除區(qū)域的所有字符都要?jiǎng)h除,即當(dāng) pos + len >= _size
的時(shí)候就是要從 pos
位置開始刪除后面的所有字符,刪完后加的把 pos
位置的字符置為 \0
。
2.15 find
//查找一個(gè)字符 size_t find(char ch, size_t pos = 0) { assert(pos < _size); for (size_t i = 0; i < _size; i++) { if (_str[i] == ch) { return i; } } return npos; } //查找一個(gè)字符串 size_t find(const char* str, size_t pos = 0) { assert(pos < _size); const char* ptr = strstr(_str, str); if (ptr == NULL) { return npos; } else { return ptr - _str; } }
2.16 substr
string substr(size_t pos = 0, size_t len = npos) { assert(pos < _size); size_t n = len; if (len == npos || pos + len >= _size) { n = _size - pos; } string tmp; tmp.reserve(n); for (size_t i = 0; i < n; i++) { tmp += _str[i + pos]; } return tmp; }
2.17 operator<<
ostream& operator<<(ostream& out, const wcy::string& str) { for (auto e : str) { out << e; } return out; }
注意:因?yàn)樯婕暗礁偁幾蟛僮鲾?shù)的原因,流插入和流提取運(yùn)算符重載要寫在類外面。其次,不能直接打印 str._str
或者通過 str.c_str()
來打印,因?yàn)?nbsp;string
對象中可能會(huì)有 \0
作為有效字符存在,前面兩種打印方法,遇到 \0
就停止了,無法完整將一個(gè) string
對象打印出來,正確的做法是逐個(gè)打印。
小Tips:無論是形參還是返回值,只要涉及到 ostream
或 istream
都必須要用引用,因?yàn)檫@倆類不允許拷貝或者賦值的。
2.18 operator>>
istream& operator>>(istream& in, wcy::string& str) { if (str._size != 0) { str.erase(0); } //in >> str._str;//這樣寫是錯(cuò)的,空間都沒有 char ch; ch = in.get(); while (ch == ' ' || ch == '\n')//清除緩沖區(qū) { ch = in.get(); } while (ch != ' ' && ch != '\n') { str += ch; ch = in.get(); } return in; }
注意:空格符 ' ' 和換行符 \n 作為輸入時(shí)分割多個(gè) string 對象的標(biāo)志,是不能直接用 istream 對象來讀取的,即 cin >> ch 是讀不到空格符和換行符。需要借助 get() 成員函數(shù)才能讀取到空格符和換行符。其次庫中對 string 進(jìn)行二次流提取的時(shí)候會(huì)進(jìn)行覆蓋,所以我們在插入前也要先進(jìn)行判斷。上面這種寫法,在輸入的字符串很長的情況下會(huì)多次調(diào)用 reserve 進(jìn)行擴(kuò)容,為了解決這個(gè)問題,我們可以對其進(jìn)行優(yōu)化。
//優(yōu)化版本 istream& operator>>(istream& in, wcy::string& str) { /*if (str._size != 0) { str.erase(0); }*/ //in >> str._str;//這樣寫是錯(cuò)的,空間都沒有 str.clear(); char buff[128] = { '\0' }; char ch; ch = in.get(); while (ch == ' ' || ch == '\n') { ch = in.get(); } size_t i = 0; while (ch != ' ' && ch != '\n') { buff[i++] = ch; if (i == 127) { str += buff; i = 0; } ch = in.get(); } if (i != 0) { buff[i] = '\0'; str += buff; } return in; }
注意:這里的做法是,先開辟一個(gè)數(shù)組,將輸入的字符存儲(chǔ)到數(shù)組中,然后從數(shù)組中拷貝到 string
對象當(dāng)中。
2.19 operator<
bool operator<(const string& s) const { size_t i1 = 0; size_t i2 = 0; while (i1 < _size && i2 < s._size) { if (_str[i1] < s[i2]) { return true; } else if (_str[i1] > s[i2]) { return false; } else { i1++; i2++; } } if (i1 == _size && i2 == s._size) { return false; } else if (i1 < _size) { return false; } else { return true; } }
注意:string
類對象是按照 ASCII 進(jìn)行比較的。其次,這里不能直接復(fù)用 strcmp
或者 memcmp
,前者遇到 '\0'
就會(huì)終止,后者只能比較長度相等的部分。所以我們可以自己來寫比較邏輯,也可以復(fù)用 memcmp
然后進(jìn)行補(bǔ)充。
//復(fù)用memcpy bool operator<(const string& s) const { int ret = memcmp(_str, s._str, _size < s._size ? _size : s._size); return ret == 0 ? _size < s._size : ret < 0; }
2.20 operator==
bool operator==(const string& s) const { return _size == s._size && memcmp(_str, s._str, _size < s._size ? _size : s._size) == 0; }
有了 < 和 ==,剩下的直接復(fù)用即可。
2.21 <=、>、>=、!=
bool operator<=(const string& s) const { return *this < s || *this == s; } bool operator>(const string& s) const { return !(*this <= s); } bool operator>=(const string& s) const { return !(*this < s); } bool operator!=(const string& s) const { return !(*this == s); }
三、結(jié)語
今天的分享到這里就結(jié)束啦!
以上就是深入探索C++ string的底層實(shí)現(xiàn)的詳細(xì)內(nèi)容,更多關(guān)于C++ string底層實(shí)現(xiàn)的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
C++實(shí)現(xiàn)矩陣原地轉(zhuǎn)置算法
這篇文章主要介紹了C++實(shí)現(xiàn)矩陣原地轉(zhuǎn)置算法,非常經(jīng)典的算法,需要的朋友可以參考下2014-08-08C語言實(shí)現(xiàn)學(xué)生學(xué)籍管理系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了C語言實(shí)現(xiàn)學(xué)生學(xué)籍管理系統(tǒng),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-01-01C/C++判斷傳入的UTC時(shí)間是否當(dāng)天的實(shí)現(xiàn)方法
在項(xiàng)目中經(jīng)常會(huì)顯示一個(gè)時(shí)間,如果這個(gè)時(shí)間在今日內(nèi)就顯示為時(shí)分秒,否則顯示為年月日,有需要的朋友可以參考一下2014-01-01