歷史
STL 最初是一套獨立的泛型庫(Alexander Stepanov 等人貢獻),后來被吸納進 C++ 標準庫;std::basic_string 則是早期 C++ 標準(Cfront / ARM 時代)就存在的“字符串類”,并非 STL 原生物。
std::string 實際上是 std::basic_string<char> 的 typedef,提供了類似 STL 容器的接口(迭代器、begin()/end()、push_back()、size()?等),因此常被稱作“近似 STL 容器”或“序列式容器”。
模擬實現 string 類
實現 string 類的構造、拷貝構造、賦值運算符重載、析構函數、以及對 string 對象的增刪改查。
成員變量
private:char* _str;size_t _size;size_t _capacity;
構造函數
提供缺省參數的構造函數,使用常量字符串初始化;因為需要兼容 C 字符串的 '\0',所以為 _str 多開 1 字節的空間用以存放 '\0' 。
myString(const char* str = ""):_size(strlen(str)),_capacity(_size){_str = new char[_capacity + 1];strcpy(_str, str);}
拷貝構造?
注意需要對成員 _str 指向的空間深拷貝;另外還有一種寫時拷貝可以進一步節省空間并提高效率。
myString(const myString& str):_str(nullptr),_size(str.size()),_capacity(str.capacity()){reserve(str.capacity()); // reserve 中已經為 '\0' 多開了 1 字節strcpy(_str, str.c_str());}
增 -- push_back
如果要向 _str 尾部增加元素,先要檢查空間是否足夠,如果不夠,封裝擴容邏輯至函數 reserve:
reserve
異地擴容至 n+1 字節,多出來的 1 字節是給 '\0' 的
void reserve(size_t n){if (n > _capacity){char* tmp = new char[n + 1]; // 開新空間strcpy(tmp, _str); // 拷貝數據,memcpy 也行swap(tmp, _str); // 交換指針delete[] tmp; // 釋放舊空間}}
接著在 push_back 中檢查空間是否足夠,如果不夠,使用一個 三目表達式 來決定擴容至多大空間;最后向開辟好的空間內寫入字符,并將下一個位置設置為 '\0',以兼容 C 類型的字符串;
void push_back(const char& ch){if (_size == _capacity){size_t new_capacity = _capacity == 0 ? 2 : _capacity * 2;reserve(new_capacity);}_str[_size] = ch;_str[_size + 1] = '\0';_size++;}
增 -- insert
標準庫中實現了很多重載,我來模擬一部分:
myString& insert(size_t pos, const myString& str)
在 pos 位置,插入同類型 myString str;那么需要向后挪動 pos 位置之后的元素,每個元素挪動 str._size 個空間;這里需要獲取形參 str 的私有變量,實現簡單的函數來獲取:
size_t size(){return _size;}size_t capacity(){return _capacity;}
接下來向后挪動元素,但是要判斷所需空間是否足夠,不夠就擴容:
myString& insert(size_t pos, const myString& str){if (_capacity < _size + str.size()) // 判斷接下來需要的空間是否足夠{reserve(_size + str.size());}}
發現編譯這段報錯,原因是形參 str 對象被 const 修飾,不能調用非 const 成員函數,將成員函數也用 const 修飾其 this 指針即可:
size_t size() const{return _size;}size_t capacity() const{return _capacity;}
擴容問題解決,從 pos 位置開始,向后挪動元素:
myString& insert(size_t pos, const myString& str){if (_capacity < _size + str.size()) // 判斷接下來需要的空間是否足夠{reserve(_size + str.size());}size_t begin = pos, end = _size; // _size 處的'\0'一起挪動while (end >= begin) // 挪動數據{_str[end + str.size()] = _str[end];end--;}}
現在向 pos 處寫入 str._str 的數據,但需要獲取?str._str 私有指針變量以訪問其數據:
char* c_str() const{return _str;}
插入數據之后,返回原 myString 對象引用:
myString& insert(size_t pos, const myString& str){if (_capacity < _size + str.size()) // 判斷接下來需要的空間是否足夠{reserve(_size + str.size());}size_t begin = pos, end = _size; // _size 處的'\0'一起挪動while (end >= begin) // 挪動數據{_str[end + str.size()] = _str[end];end--;}for (size_t i = 0; i < str.size(); i++) // 插入數據{_str[pos + i] = str.c_str()[i];}return *this;}
但是在挪動數據時,有個 bug :如果傳入形參 pos 為 0 ,也就是頭插;
那么 begin 為 0 ,挪動數據 while 的循環終止條件是 end < begin ,end 要等于 -1 循環才會終止;
但關鍵問題在于 end 變量類型是無符號整數 size_t,當 end = 0,再 end-- 之后,end 會變成 42億多,這使 while 變成死循環;
解決辦法:可以強制類型轉換,不會有什么問題
int begin = (int)pos, end = (int)_size; // _size 處的'\0'一起挪動while (end >= begin) // 挪動數據{_str[end + str.size()] = _str[end];end--;}
myString& insert (size_t pos, size_t n, char c)
在 pos 處插入 n 個 c 字符:判斷空間容量是否足夠 -- 挪動數據 -- 插入數據,與上面的實現類似,累了。
增 -- append
理解為什么大佬們會吐槽 string 的設計繁瑣了(lll¬ω¬)
myString& append (const myString& str)
追加同類型 myString :檢查容量,copy 該 myString 數據到 this->_str 末尾。
myString& append(const myString& str){if (_capacity < _size + str.size()){reserve(_size + str.size());}strcpy(_str + _size, str.c_str()); // strcpy 會一并拷貝字符串末尾的 '\0'return *this;}
刪 -- pop_back
刪掉最后一個元素,這個接口是 C++11 增加的,可能是為了向 STL 的容器看齊吧
void pop_back(){_size--;}
刪 -- erase
myString& erase (size_t pos = 0, size_t len = npos)
從 pos 開始,刪掉 len 個字符,npos 是無符號最大整數 0x7fffffff
實際上,是將從 pos+len 到 _size 的數據向前挪動 len 個位置進行覆蓋,最后 _size-len 即可
注意,如果 pos+len > _size ,即 len > _size-pos ,就會越界訪問,所以要處理 len 過大的問題
myString& erase(size_t pos = 0, size_t len = string::npos){assert(pos < _size);if (len > _size - pos) // 處理 len 過大造成越界訪問{len = _size - pos;}for (size_t i = 0; i < _size-(pos+len)+1; i++) // 向前覆蓋數據{_str[pos + i] = _str[pos + len + i];}_size -= len;return *this;}
查 -- find
size_t find (const myString& str, size_t pos = 0) const
從 pos 位置開始,找子串,直接調用 C庫函數 strstr 找,strstr 找到返回 該子串在原字符串中的指針,找不到會返回 NULL
size_t find(const myString& str, size_t pos = 0) const{char* sub = strstr(_str + pos, str.c_str());if (sub){return sub - _str;}else{return string::npos;}}
size_t find(char c, size_t pos)
找字符首次出現位置
size_t find(char c, size_t pos){for (size_t i = pos; i < _size; i++){if (_str[i] == c){return i;}}return npos;}
創建子串 -- substr
myString substr (size_t pos = 0, size_t len = npos) const
從字符串 pos 位置開始,取 len 個字符創建新的子串,并傳值返回
為新對象開空間,拷貝子串內容
myString substr(size_t pos = 0, size_t len = string::npos) const{assert(pos < _size);if (len > _size - pos) // 處理 len 過大造成越界訪問{len = _size - pos;}myString sub;sub.reserve(len); // 存 len 字節memcpy(sub._str, _str + pos, len); // memcpy可以拷貝固定大小sub._size = len;sub._str[_size] = '\0'; // len 不包括 '\0'return sub;}
賦值運算符重載
myString& operator= (const myString& str)
兩個已存在的對象之間的賦值運算符重載,注意深拷貝
myString& operator= (const myString& str){char* tmp = new char[str.capacity() + 1];strcpy(tmp, str._str);delete[] _str; // 釋放舊空間_str = tmp; // 指向新空間_size = str._size;_capacity = str._capacity;}
析構函數
~myString()
注意釋放對象中的資源即可
~myString(){delete[] _str;_size = _capacity = 0;}
小結
基本沒有復用,主要是練個手熟;要想快,還得當 CV programmer!