C++ string類的解析式高效實現
GitHub地址
有夢想的電信狗
1. 引言:字符串處理的復雜性
? 在C++
標準庫中,string
類作為最常用的容器之一,其內部實現復雜度遠超表面認知。本文將通過一個簡易仿照STL
的string
類的完整實現,揭示其設計精髓。我們將從內存管理、操作優化等維度,逐步構建一個簡單支持核心功能的string
類。
2. 基礎架構設計
2.1 成員變量聲明
- 為了和標準庫中的
string
區分,我們把自己實現的string
封裝在m_string
這個命名空間中 string
的底層是存放字符的順序表,因此我們采用順序表的結構來實現- 基本結構如下:
namespace m_string {class string {public://成員函數...etc...我們逐一實現//迭代器,運算符重載等...private:size_t _size; // 當前有效字符數size_t _capacity; // 存儲容量(不含結束符'\0'),會進行擴容char* _str; // 動態數組的指針public://靜態成員變量 類內聲明、類外(定義)初始化const static size_t npos; };//類內聲明、類外初始化 特殊標記值 const size_t string::npos = -1; //建議靜態成員變量,聲明和定義分離
}
設計要點:
- _size的大小代表了當前空間的內容,已存放的字符的個數:包括字符和\0
- 三成員架構是順序表結構的經典設計:
_size
:記錄有效數據個數_capacity
:管理當前的最大容量,擴容后容量更新,注意_capacity
不包含末尾的\0
_str
:指針指向堆內存
- 靜態常量
npos
的類外定義需特別注意,要在類外進行初始化。令size_t
類型的npos
值為-1
,用來表示整型的最大值 - 成員變量設為訪問權限設為
private
, 對外提供public
的成員函數,符合面向對象中封裝的思想
2.2 迭代器實現
STL中的迭代器,可能是指針,也可能不是指針
string的迭代器本質上是char,因為string本質上就是一個字符數組,天生適合用指針來訪問*
public:// 將char* 封裝成 iterator 迭代器typedef char* iterator;typedef const char* const_iterator;// 普通對象的迭代器 // 普通對象的迭代器如果加了const, 會導致非const對象只能返回const_iterator,失去修改元素的能力(違反直覺)。iterator begin() { return _str; //數組名是第一個元素的地址}iterator end() {//指針相加 _str + _size 得到最后一個元素的下一個位置的指針return _str + _size; }//const對象的迭代器const_iterator begin() const {return _str; }const_iterator end() const {return _str + _size;}
begin()
方法要返回數組的第一個位置的指針end()
方法要返回數組最后一個元素的下一個位置- 普通版本和
const
版本需分別實現
實現分析:
- 迭代器本質是原生指針的封裝
- 訪問權限是
public
, 調用begin/end
方法返回相應的迭代器。 - 通過
const
重載實現常量迭代器 - 與標準庫的迭代器體系完全兼容
3. 構造函數與析構函數
3.1 基礎構造函數
v1
// 初始實現(存在問題)
// 初始化列表是按照成員變量在類中聲明的次序進行初始化的
string(const char* str = "\0") : _size(strlen(str)), _capacity(_size) //利用_size來初始化_capacity, _str(new char[_capacity+1])
{strcpy(_str, str); // 中間含\0時會被截斷
}
問題發現:
- 使用
strcpy()
會因"hello world\0hello Linux"
這樣中間含有'\0'
的字符串導致數據丟失。 - 初始化列表是按照成員變量在類中聲明的次序完成初始化的,與初始化列表中實現的順序無關。
- 如果有人調整了初始化列表的初始化順序或成員變量的聲明順序,那么利用
_size
來初始化_capacity
會發生未定義行為。
- 如果有人調整了初始化列表的初始化順序或成員變量的聲明順序,那么利用
優化版本:
v2
// 參數列表是 用c風格的字符串 const char* 進行構造。c風格的字符串默認以\0為結束符
// 因此傳入 "hello world\0hello Linux" 這樣中間含有'\0'的字符串,導致數據丟失是調用者的問題
string(const char* str = "") { //默認構造會構造一個""空字符串_size = strlen(str);_capacity = _size; //capacity表示可以存放的下的字符個數_str = new char[_capacity + 1]; //開空間+1 要存放'\0'//strcpy(_str, str); //拷貝數據,遇到中間含有\0的字符串會有Bug//strcpy默認會 拷貝\0, 因此使用memcpy,需要將拷貝的字節數+1,考慮\0的位置memcpy(_str, str, _size + 1);
}
思路:
strlen(str)
計算傳入字符串的長度,并賦值給_size
_capacity = _size,
,初始化時,默認使容量和有效字符和數相等,構造時不額外開空間_str = new char[_capacity + 1]
,為字符串分配空間,_capacity + 1
為字符串結尾的\0
預留位置memcpy(_str, str, _size + 1)
,最后將源字符串中的數據拷貝至新開辟的空間。
優化點:
-
使用
memcpy()
替代strcpy()
,memcpy()
是拷貝內存中的數據,遇到\0
不會結束 -
參數缺省值改為空字符串
""
-
均為內置類型,不使用初始化列表進行初始化,而是在構造函數內部完成初始化,消除初始化列表順序依賴
3.2 析構函數
~string() {delete[] _str;_str = nullptr;_size = _capacity = 0;
}
析構函數的思路和實現較為簡單
- 釋放管理字符數組的空間
_str
,注意釋放數組要使用delete []
- 管理字符數組的空間的指針
_str
置空 - 將
_size
和_capacity
置零
3.3 拷貝構造函數
往期文章對深拷貝的簡單總結介紹:
https://blog.csdn.net/2301_80064645/article/details/145593384?fromshare=blogdetail&sharetype=blogdetail&sharerId=145593384&sharerefer=PC&sharesource=2301_80064645&sharefrom=from_link
傳統實現:
手動開辟釋放內存。
// 拷貝構造 要實現深拷貝,開一塊新的空間,拷貝數據,并初始化。
// 新開一塊空間,并進行深拷貝防止兩個指針指向同一塊空間
string(const string& str) {_str = new char[str._capacity + 1]; // _capacity不包含\0, +1 考慮 \0//strcpy(_str, str._str);memcpy(_str, str._str, str._size + 1);_size = str._size;_capacity = str._capacity;
}
拷貝構造函數需要實現深拷貝。如果是淺拷貝的話,字符串指針會存下同一塊空間的地址,析構時會對同一塊空間析構兩次,會引發錯誤
- 開辟新空間,
new char[str._capacity + 1]
,大小為_capacity + 1
memcpy
拷貝原數據到新空間,拷貝大小為_size + 1
,確保字符串末尾的\0
也被拷貝。- 更新
_size
和_capacity
現代寫法:
// 交換兩個string對象成員變量的內容
void swap(string& str) { std::swap(_str, str._str); //不能直接交換兩個對象std::swap(_size, str._size);std::swap(_capacity, str._capacity);
}
string(const string& s) :_str(nullptr),_size(0),_capacity(0)
{string tmp(s.c_str()); //用s的數據 調用構造函數 新構造一個 局部tmp對象, 具有不同的字符數組 地址this->swap(tmp); //交換tmp和s2內的三個成員變量,//交換后tmp內的_str為nullptr,局部對象出了函數作用域銷毀后,析構tmp對象//構造tmp時使用s.c_str()初始化,而c_str返回以\0結尾的C風格字符串//若s._str中間含有\0,tmp的構造會截斷數據,導致拷貝不完整
}
s2(s1)
- 將s2初始化為
_str(nullptr)
,_size(0)
,_capacity(0)
- 用
s1.c_str()
構造一個和s1
一樣的局部對象tmp
,字符串數據的內容一樣,但空間和地址不同。 - 交換
s2
和tmp
中成員變量的值,- 交換前:
tmp._str
指向和s1
內容一樣的一塊新空間。拷貝構造時,s2
待初始化,沒有空間 - 交換后:
s2._str
指向之前tmp._str
管理的那塊空間,tmp._str
指向待初始化的那塊空間
- 交換前:
- 之后局部對象出了函數作用域銷毀后,析構待初始化
tmp
對象,析構前tmp
內的數據為nullptr 0 0
優勢對比:
方法 | 異常安全 | 代碼復用 | 可維護性 |
---|---|---|---|
傳統實現 | 不安全 | 低 | 差 |
現代寫法 | 強保證 | 高 | 優秀 |
4. 賦值運算符的進化
4.1 傳統實現
// s1 = s3
string& operator=(const string& str) {if (this != &str) {char* newSpace = new char[str._capacity + 1]; //開空間memcpy(newSpace, str._str, str._size + 1); //拷數據delete[] _str; //被賦值前可能為非空string,因此要釋放原空間_str = newSpace;_size = str._size;_capacity = str._capacity;}return *this;
}
-
賦值運算符要實現深拷貝,賦值后,兩個
string
對象要擁有兩塊獨立的空間,并對賦值前的那塊空間進行機構 -
if (this != &str)
:自己給自己賦值時直接跳過。 -
傳統實現,手動開空間,拷貝數據
-
更改指針前,析構原空間
delete[] _str
被賦值前可能為非空string,因此要釋放原空間_str = newSpace
;
-
更新
_size
和_capacity
-
返回
*this
,滿足連續賦值的需求
潛在風險:
new
可能拋出異常導致原對象損壞
4.2 現代實現
// s1 = s3
string& operator=(const string& str) {if (this != &str) {string tmp(str); //反正tmp為局部對象,出了作用域也要銷毀,不如讓他銷毀時,順便把s1的空間析構了// s1 想要tmp管理的那塊空間std::swap(_str, tmp._str); //不能直接交換兩個對象,否則會引發無窮賦值std::swap(_size, tmp._size);std::swap(_capacity, tmp._capacity);//可以直接//this->swap(tmp);}return *this;
}
-
s1 = s3
-
深拷貝創建局部對象
tmp
,反正tmp
為局部對象,出了作用域也要銷毀,不如讓他銷毀時,順便把s1
的空間析構了 -
交換
tmp
和this
對象的成員變量,更改指針指向。- 構造的
tmp
和str
相同,且為深拷貝構造 this->swap(tmp)
之后,s1
接管了tmp
的數據,tmp
接管了s1
的數據。
- 構造的
-
返回
*this
,滿足連續賦值的需求 -
函數結束后,局部對象
tmp
銷毀,調用析構,釋放賦值前的舊空間(s1)
。
4.3 終版實現
// s1 = s3
string& operator=(string tmp) { //直接利用函數參數,深拷貝s3,函數結束后,形參自動析構this->swap(tmp); // 將s3的拷貝和s1 也就是 *this 交換return *this; // 返回*this, 也就是 s1
}
- 既然現代實現要構造局部對象,那不妨直接在形參中使用值傳遞的方式構造局部對象,形參
tmp
是右值實參的拷貝- 自定義類型對象在值傳參時,會默認被要求去調用拷貝構造函數。
- 實參
s3
值傳遞,傳遞給形參tmp
。string tmp = s3
。局部對象tmp
中有和s3
一樣的數據
this->swap(tmp);
:直接和局部對象交換資源。- 函數結束后,形參對象
tmp
銷毀,調用析構,釋放s1
的舊空間。
革命性改進:
- 參數值傳遞,自動調用拷貝構造
swap
操作保證強異常安全- 代碼量減少60%
- 自動清理舊資源
5. 容量管理策略
5.1 reserve
///可以用reserve預留空間,來實現擴容
// reserve實現的均是異地擴容
//考慮特殊情況的話,memcpy會更好
void reserve(size_t request_capacity) { //request_size指的是要新存放的字符的個數if (request_capacity > _capacity) { //請求的空間大于_capacity時,才擴容char* newSpace = new char[request_capacity + 1]; //多開一個空間存放\0//strcpy(newSpace, _str); //new是異地擴容memcpy(newSpace, _str, _size + 1);delete[] _str;_str = newSpace;_capacity = request_capacity;}
}
擴容思路:
- 期望容量
request_capacity
大于當前容量_capacity
時,才進行擴容 - 采取異地擴容策略,
newSpace
存放新空間的地址 - 使用
memcpy
而非strcpy
。strcpy
拷貝數據時,遇到\0
就終止了,遇到中間含有\0
的字符串會帶來意外的結果。- 使用
memcpy
來拷貝空間中的所有數據,包括中間的\0
- 將原來的字符串空間釋放:
delete[] _str;
- 將新空間的地址賦值給管理字符數組的指針:
_str = newSpace;
- 更新容積:
_capacity = request_capacity
擴容策略:
- 異地擴容保證數據完整性
- 精確計算拷貝字節數
(_size+1)
,此字節數是數組中有效字符的個數 - 典型應用場景:
push_back
時的容量檢查
5.2 resize
void resize(size_t newSize, char ch = '\0') {if (newSize < _size) {_size = newSize;_str[_size] = '\0';}else {//reserve會判斷newSize和_capacity的大小,超過擴容,等于時不做處理reserve(newSize);//擴容后進行初始化for (size_t i = _size; i < newSize; ++i)_str[i] = ch;_size = newSize;_str[_size] = '\0';}
}
雙模式操作:
- 收縮(
if邏輯
):直接截斷if (newSize < _size)
:判斷指定newSize
是否小于當前size
- 小于的話,直接截斷,更新
_size = newSize;
- 再將字符串最后一個字符的下一個位置設為
\0
,_str[_size] = '\0';
- 擴展(
else邏輯
):填充指定字符- 期望大小大于當前
_size
,通過reserve(newSize)
進行擴容- 進入
else
邏輯后,newSize
是>= _size
,等于時reserve(newSize)
不做處理。 - 大于時會進行擴容
- 進入
- 擴容后,對下標在
_size
及之后的位置進行初始化,用字符ch
進行填充 - 初始化后,更新
_size
,將末尾位置設置\0
- 期望大小大于當前
6. 字符串操作優化
6.1 push_back
+=
的本質是調用了push_back
,因此先實現push_back
void push_back(char ch) {//先考慮擴容if (_size == _capacity) {//二倍擴容,并防止空構造的字符串reserve(_capacity == 0 ? 4 : _capacity * 2);}_str[_size++] = ch; //放字符_str[_size] = '\0'; //加上\0
}
+=
操作可以直接復用push_back
,push_back
是在字符串末尾插入一個字符- 插入前考慮數組是否還有容量,
_size == _capacity
時表示容量已滿 reserve
擴容,_capacity == 0 ? 4 : _capacity * 2
- 若
push_back
前為空串,則分配空間容量為4
。 - 若
push_back
前為已有數據且容量已滿,則二倍擴容
- 若
- 更新狀態
- 插入字符
- 在末尾放上
\0
;
6.2 append
void append(const char* str) {size_t len = strlen(str);if(_size + len > _capacity){//至少擴容到 _size + lenreserve(_size + len);}memcpy(_size, str, len + 1); //拷貝大小為 len + 1, 要拷貝\0_size += len;
}
- 計算待插入的字符串的長度
- 檢查容量,
if(_size + len > _capacity)
為真,代表需要擴容 - 擴容至少擴容到
_size + len
- 拷貝數據到
_size
位置開頭的空間 - 拷貝大小為
len+1
,為\0
預留空間 - 最后更新
_size
的大小
6.3 operator+=
利用+=
可以方便的在字符串后面追加字符或字符串
+=字符
string& operator+=(const char ch){push_back(ch);return *this;
}
+=字符串
string& operator+=(const char* str){append(str);return *this;
}
+=字符/字符串
直接復用push_back
和append
即可- 為了滿足連續
+=
的功能,需要返回當前對象,也就是*this
6.3 insert實現
6.3.1 insert字符串
void insert(size_t pos, const char* str) {assert(pos <= _size); //_size位置是字符串的末尾,可以在字符串的末尾插入assert(str != nullptr); // 確保str非空size_t len = strlen(str);//擴容if (_size + len > _capacity)reserve(_size + len);size_t end = _size; //從末尾的 \0 開始挪動//如果在 0 位置插入 end最后會變成 size_t -1 也就是npos 整形的最大值while (end >= pos && end != npos) { _str[end + len] = _str[end];--end;}for (size_t i = 0; i < len; ++i)_str[pos + i] = str[i];_size += len;_str[_size] = '\0'; //添加字符串結束標志
}
- 越界檢查,確保待插入字符串
str
為非空 - 計算待插入串的長度,根據長度判斷是否需要擴容并擴容,至少要擴容到
_size + len
長度 size_t end = _size
,size
是\0
的下標,挪動時從末尾的\0
開始挪動-
end + len
是需要挪動的字符調整后的下標位置 -
end
類型為size_t
,跳出循環時,end
的值 為pos - 1
- 當
pos
為0
時,size_t end = -1
的值為整型的最大值,是大于pos
的。為了避免進入死循環,需增加循環條件為end >= pos && end != npos
- 當
-
_str[pos + i] = str[i]
:挪動數據過后,再將待插入字符串逐個字符插入
-
- 更新狀態
_size += len
:更新大小_str[_size] = '\0'
:添加字符串結束標志
6.3.2 insert n個字符
- 從
pos
位置開始,插入n
個相同的字符
//讓插入的那個字符的下標變成pos
void insert(size_t pos, size_t n, char ch) {assert(pos <= _size); //_size位置是字符串的末尾,可以在字符串的末尾插入//擴容if (_size + n > _capacity)reserve(_size + n);//挪動數據//當傳入的pos為0時,end會變成-1,end是size_t,-1會變成整形的最大值,會一直進入循環//size_t end = _size; //int end = _size; //while (end >= (int)pos) { //運算符兩端 兩個操作數類型不一致時,會進行 提升// // 一般是范圍小的向范圍大的進行提升// _str[end + n] = _str[end];// --end;//}size_t end = _size;while (end >= pos && end != npos) {_str[end + n] = _str[end];--end;}for (size_t i = 0; i < n; ++i) {_str[pos + i] = ch;}_size += n;_str[_size] = '\0'; //添加字符串結束標志
}
- 越界檢查,確保待插入位置有效
- 根據待插入字符的個數,根據個數判斷是否需要擴容并擴容,至少要擴容到
_size + len
長度 size_t end = _size
,_size
是\0
的下標,挪動時從末尾的\0
開始挪動-
end + n
是需要挪動的字符調整后的下標位置 -
end
類型為size_t
,跳出循環時,end
的值 為pos - 1
- 當
pos
為0
時,size_t end = -1
的值為整形的最大值,是大于pos
的。為了避免進入死循環,需增加循環條件為end >= pos && end != npos
- 當
-
_str[pos + i] = ch
:挪動數據過后,再將待插入字符循環依次插入
-
- 更新狀態
_size += n
:更新大小_str[_size] = '\0'
:添加字符串結束標志
6.3.3 insert總結
在實現的過程中,我們不難發現,實現在pos
位置插入n
個字符和插入一個字符串
的思路高度相似
插入字符時
:每個字符已經通過參數傳入,挪動數據后直接賦值即可插入字符串時
:每個字符需要從長度為len
的字符串中取
insert多個字符和insert字符串,除了細節問題,邏輯上沒有任何區別!
6.4 erase
//從pos位置開始刪,向后刪除len個字符,不傳參默認全部刪完
void erase(size_t pos, size_t len = npos) {assert(pos <= _size);if (len == 0)return;// 刪完的情況 : pos + len == _size 時,pos + len位置放的是\0,大于時才全部刪完if (len == npos || pos + len > _size) { //這兩種都是刪完的情況_str[pos] = '\0';_size = pos;}// 刪除一部分的情況else {size_t end = pos + len; // 跳過要刪除的三個字符,end從需要挪動的第一個字符開始//_str[end] == '\0'時(end == _size),可以把'\0'也挪過來,也可以不挪,最后統一設置\0while (end < _size) { _str[pos++] = _str[end++];}_size -= len;}_str[_size] = '\0'; //統一設置結尾符
}
刪除策略:
- 尾部刪除:直接截斷
- 中間刪除:前向覆蓋
思路分析:
- 對
pos
進行越界檢查 - 先判斷要全部刪完的情況
len == npos || pos + len > _size
時,代表要刪完pos
及之后的字符,直接截斷- 直接將
pos
位置設為\0
- 修改
_size
為pos
- 直接將
else
是刪除完從pos
開始的len
個字符的情況:- 跳過要刪除的三個字符,
pos+len
位置是需要挪動的第一個字符,開始依次向前挪動數據,到_size
(\0)結束 - 更新狀態:
_size -= len
_
- 跳過要刪除的三個字符,
- 最后統一設置結束符
_str[_size] = '\0'
7. 運算符重載的藝術
7.1 比較類運算符
<和==
bool operator<(const string& s) const {int ret = memcmp(_str, s._str, std::min(_size, s._size)); //前面小于后面時,返回值小于0return ret == 0 ? _size < str._size : ret < 0;
}bool operator==(const string& str) const {return _size == str._size //兩個字符串相等,其長度一定相等,優先比較長度&& memcmp(_str, str._str, _size) == 0; //再比較其內容是否相等
}
實現技巧:
- 使用
memcmp()
提升比較效率 - 長度優先比較策略
<邏輯:
-
只比較兩字符串同等長度的內存(內容),用更小的那個長度進行比較
-
將比較結果存入
ret
中 -
ret == 0
時,代表兩字符串共有長度的部分相同,此時_size < str._size
為真時才是小于(共有長度相同,誰短誰就小)。 -
ret != 0
時,比較的結果memcmp
已經幫我們比較好了ret < 0
代表前面小于后面ret > 0
代表前面大于后面
==邏輯
- 只有長度相等的字符串才有可能相等
- 長度相等后,再用
memcmp(_str, str._str, _size) == 0
判斷是否相等return _size == str._size && memcmp(_str, str._str, _size) == 0
- 兩個字符串相等,其長度一定相等。
- 優先比較長度,如果長度不相等,會觸發
&&的截斷特性
- 再用
memcmp
比較其內容是否相等
- 優先比較長度,如果長度不相等,會觸發
其他邏輯復用<和==
// <= 小于或等于
bool operator<=(const string& str) const {return *this < str || *this == str;
}
// > 小于等于取反
bool operator>(const string& str) const {return !(*this <= str);
}
// >= 大于或等于
bool operator>=(const string& str) const {return (*this < str);
}
// != ==取反
bool operator!=(const string& str) const {return !(*this == str);
}
7.2 []重載
// []重載
// const對象調const版本[] 普通對象調普通版本[]
char& operator[](size_t pos) { //返回引用,讀寫版本assert(pos < _size);return _str[pos];
}
const char& operator[](size_t pos) const { //只讀版本assert(pos < _size);return _str[pos];
}
char&
做返回值實現了普通對象可讀可寫的效果- 用
assert(pos < _size)
檢查[]
訪問是否越界 []重載
返回傳入下標位置的字符的引用即可- 為
const對象
實現const版本[]
,普通對象實現普通版本[]
7.3 流運算符重載
流插入<<
// c的字符數組,以\0為終止算長度
// string不看\0,以size為終止算長度
ostream& operator<<(ostream& out, const m_string:: string& s) {//ostream這個類做了一件事,做了防拷貝,因此 ostream 類對象 做參數或返回值時要用引用//實現方式 1//out << s.c_str(); // 調用c_str() 可以直接打印//out << s.c_str; // 訪問s._str 實現字符串打印// 以上兩種方式,打印 c_str 遇到 "hello world\0hello Linux" 中間含有\0的字符串時,是錯誤的// 流插入的要求是,有多少內容,打印多少內容 因此不能遇到\0終止// 使用一個一個遍歷字符的方式 實現打印/*for (int i = 0; i < s.size(); ++i)out << s[i];*/for (auto& e : s)out << e;return out;
}
-
成員函數的第一個形參是
this
,為了符合<<
的習慣,期望第一個形參應當是流對象。根據實現方式的不同,<<
要重載成全局函數或友元函數1. out << s.c_str()
:調用c_str()
可以直接用C風格的字符串打印2. out << s.c_str
:訪問s._str
實現字符串打印,此時<<
需要重載為友元函數,訪問私有變量s.c_str
- 以上兩種方式,打印
c_str
遇到"hello world\0hello Linux"
中間含有\0
的字符串時,是錯誤的。因為流插入的要求是,有多少內容,打印多少內容 因此不能遇到\0終止
-
最終我們
使用for循環一個一個遍歷字符的方式,實現打印
。傳統的for
循環和范圍for
都可以實現 -
const m_string:: string& s
:對只讀對象加const
限定 -
ostream
做了特殊處理,做了防止值拷貝,因此ostream
類對象做參數或返回值時要傳引用 -
函數返回
ostream& out
對象,實現連續<<
輸出流
流提取>>
v1實現功能版
istream& operator>>(istream& in, m_string::string& str) {//一個一個字符讀/*char ch;in >> ch;*///用in.get() 讀str.clear(); // 對同一個string進行多次輸入時,每次輸入前要進行初始化 // 如果不初始化,會出現字符串堆疊char ch = in.get();//以空格或換行分割字符串while (ch != ' ' && ch != '\n') {str += ch;//in >> ch;ch = in.get();}return in;
}
- 我們在輸入數據時,是用空格和換行符分隔多個值的。
- 而
cin
和scanf
在讀取數據時,也是以空格和換行符對多個值進行分隔的,因此,注定cin
和scanf
默認讀取不到空格和換行
istream
類對象的get()
方法來解決這個問題,get()
可以讀入任意的字符,包括空格和換行。- 我們可以通過對循環讀取的條件的控制,來實現
空格分隔
或\n分隔
多個值
- 我們可以通過對循環讀取的條件的控制,來實現
str.clear()
:可能會向同一個string
對象中多次輸入內容,為了防止數據重復,每次輸入前要進行初始化(用clear清空
)。while (ch != ' ' && ch != '\n')
- 此循環條件,遇到空格
' '
或\n
時,不進入循環,因此是以空格和換行符分隔多個值的 str += ch
:將讀到的字符,依次追加到str
末尾實現字符串的讀取ch = in.get()
:追加后接著讀取下一個字符
- 此循環條件,遇到空格
- 最后返回
in
對象,實現連續讀取
我們V1
實現的方式存在一些問題:
- ==我們輸入的第一個字符不能是空格和換行!==V2版本解決這樣的問題
- 多次
+=
會調用push_back
,存在面對長字符串時多次擴容的性能損耗 V2
版本將解決以上問題
v2最終優化版
istream& operator>>(istream& in, m_string::string& s) {s.clear(); //每次讀取要先清空緩沖區,防止 多次輸入數據時,數據重疊char ch = in.get();char buff[127] = { '\0' };//清空 第一個有效數據 來臨前的空格和換行while (ch == ' ' || ch == '\n')ch = in.get(); // 讀了之后,直接去讀下一個字符,就可以表示清除了int i = 0;//while (ch != '\n') { // 這種寫法,類似于getline的實現,以換行符作為多個字符串的分隔,可以讀到空格while (ch != ' ' && ch != '\n') { //這種寫法讀不到字符串中的空格或換行//可以選擇 使用 \n 還是 ' '作為字符串的分隔符//s += ch; //+=,當輸入的字符串非常大時,會不斷擴容,利用buff減少擴容的次數buff[i++] = ch;if (i == 127) {buff[i] = '\0';s += buff;i = 0;}ch = in.get();}// i != 0 說明里面還有數據if (i != 0) {buff[i] = '\0';s += buff;}return in;
}
-
每次讀取前清空存放數據的字符串
-
char ch = in.get()
char buff[127] = { '\0' }
,用get
一個一個讀取所有的字符。char buff[127]
利用一個緩沖區來減緩+=
的頻繁擴容問題 -
while (ch == ' ' || ch == '\n') ch = in.get();
清空第一個有效字符來臨前的空格和換行- 第一個有效字符來臨前時,遇到空格或換行都不讀入
ch = in.get()
:遇到非有效的空格和換行,讀了之后,直接去讀下一個字符,就可以表示清除了
-
用
i
來記錄當前字符的個數,通過i
映射到緩沖區的下標 -
兩種不同條件的
while
循環,可以控制 使用\n
還是空格或\n
作為字符串的分隔符while (ch != '\n')
這種寫法,類似于getline
的實現,以換行符作為多個字符串的分隔,可以讀到空格while (ch != ' ' && ch != '\n')
:一般實現,這種寫法,空格和換行都是字符串的分隔符,因此讀不到字符串中的空格或換行
-
buff[i++] = ch
:將讀到的每個字符填充到提前開辟的緩沖區buff
中i == 127
時,代表緩沖區已滿,將buff[127]
位置元素設為字符串結尾\0
s += buff
:再將緩沖區內的字符串追加到s
中i = 0
:最后將i
置零,重新向緩沖區中填充數據
-
in.get
接著讀取剩余字符 -
循環結束后,
i != 0
時,說明 i 未到達127,buff
內還存在有數據buff[i] = '\0'
:設置字符串結尾s += buff
:將緩沖區中的數據追加到string
中
通過添加
buff
緩沖區減少了擴容次數
關鍵設計:
- 輸出流直接遍歷輸出
- 輸入流采用緩沖區減少擴容
- 跳過前導空白符空格和\n
8. 查找字符和子串
find字符
//查找單個字符
size_t find(char ch, size_t pos = 0) const {assert(pos < _size);for (size_t i = pos; i < _size; ++i) {if (_str[i] == ch)return i;}return npos;
}
- 檢查斷言
pos < _size
- 循環遍歷,找到了的話返回下標
- 全部遍歷結束時,沒找到,返回
npos
find子串
//查找子串,返回子串的下標
size_t find(const char* str, size_t pos = 0) const {if (!str || pos > _size) // 處理空指針和越界posreturn npos;const char* ptr = strstr(_str + pos, str);return ptr ? ptr - _str : npos;
}
str
為nullptr
或pos > _size
返回npos
- 調用C語言的庫函數
strstr
,從_str + pos
位置開始找,ptr
接收函數的返回值 ptr
不為空代表找到了,return ptr - _str
,數組中,指針-指針,得到下標ptr
為空時代表未找到,返回npos
9. 其他重要接口
c_str()與clear()
//返回c_str const 修飾this指針指向的對象,可以讓普通對象和const對象都可以調用
//函數內不修改對象,建議加上const
const char* c_str() const { //返回數組名return _str;
}//清空數據
void clear() {_str[0] = '\0';_size = 0;
}
- 返回C語言風格的字符串
const char*
,與C風格的字符串形成良好的兼容。 - 清空數據,代表數組中沒有字符,則第一個元素直接設為字符串的結束標記
\0
- 再將
_size
設為0
size()與capacity()
//const 修飾 this指針指向的對象,可以讓普通對象和const對象都可以調用
size_t size() const { //直接返回字符串當前的大小return _size;
}
size_t capacity() const { //返回當前string對象的容積return _capacity;
}
- const 修飾 this指針指向的對象,可以讓
普通對象
和const
對象都可以調用 size()
直接返回當前已有的字符的個數,返回_size
capacity()
直接返回當前字符數組的最大容量,用_capacity
表示- 函數內不修改當前對象,加上
const
10. 結語
? 通過從零構建高性能字符串類的實踐,我們深刻理解了STL容器的設計哲學。該實現以三成員架構(size/capacity/str
)為核心,采用深拷貝確保數據獨立性,通過swap
技巧實現異常安全的賦值操作。現代寫法通過參數值傳遞自動完成資源轉移,將代碼復雜度降低60%。容量管理采用二倍擴容策略與精準內存預分配,平衡了空間效率與時間性能。運算符重載通過內存級比較和流緩沖優化,實現了接近原生指針的訪問效率。查找算法巧妙復用C庫函數,在保證正確性的前提下提升執行效能。整個設計嚴格遵循RAII原則,通過迭代器封裝實現STL兼容,最終在功能完備性與運行效率之間達到精妙平衡。
以上就是本文的所有內容了,如果覺得文章寫的不錯,還請留下免費的贊和收藏,也歡迎各位大佬在評論區交流
分享到此結束啦
一鍵三連,好運連連!