【C++】手搓一個STL風格的string容器

C++ string類的解析式高效實現

GitHub地址

有夢想的電信狗

1. 引言:字符串處理的復雜性

? 在C++標準庫中,string類作為最常用的容器之一,其內部實現復雜度遠超表面認知。本文將通過一個簡易仿照STLstring類的完整實現,揭示其設計精髓。我們將從內存管理、操作優化等維度,逐步構建一個簡單支持核心功能的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字符串數據的內容一樣,但空間和地址不同
  • 交換s2tmp中成員變量的值,
    • 交換前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的空間析構了

  • 交換tmpthis對象的成員變量,更改指針指向。

    • 構造的tmpstr相同,且為深拷貝構造
    • 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值傳遞,傳遞給形參tmpstring tmp = s3。局部對象tmp中有和s3一樣的數據
  • this->swap(tmp);:直接和局部對象交換資源。
  • 函數結束后,形參對象tmp銷毀,調用析構,釋放s1的舊空間。

革命性改進

  1. 參數值傳遞,自動調用拷貝構造
  2. swap操作保證強異常安全
  3. 代碼量減少60%
  4. 自動清理舊資源

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_backpush_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_backappend即可
  • 為了滿足連續+=的功能,需要返回當前對象,也就是*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 = _sizesize\0的下標,挪動時從末尾的 \0 開始挪動
    • end + len是需要挪動的字符調整后的下標位置

    • end類型為size_t,跳出循環時,end 的值 為 pos - 1

      • pos0時,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

      • pos0時,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
      • 修改_sizepos
  • 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;
}
  • 我們在輸入數據時,是用空格和換行符分隔多個值的
  • cinscanf在讀取數據時,也是以空格和換行符對多個值進行分隔的,因此,注定cinscanf默認讀取不到空格和換行
  • 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 未到達127buff內還存在有數據

    • 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;
}
  • strnullptrpos > _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兼容,最終在功能完備性與運行效率之間達到精妙平衡。

以上就是本文的所有內容了,如果覺得文章寫的不錯,還請留下免費的贊和收藏,也歡迎各位大佬在評論區交流

分享到此結束啦
一鍵三連,好運連連!

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/78961.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/78961.shtml
英文地址,請注明出處:http://en.pswp.cn/web/78961.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

辰鰻科技朱越洋:緊扣時代契機,全力投身能源轉型戰略賽道

國家能源局于4月28日出臺的《關于促進能源領域民營經濟發展若干舉措的通知》&#xff08;以下簡稱《通知》&#xff09;&#xff0c;是繼2月民營企業座談會后深化能源領域市場化改革的關鍵政策&#xff0c;標志著民營經濟在“雙碳”目標引領下正式進入能源轉型的核心賽道。 自…

Vue實現不同網站之間的Cookie共享功能

前言 最近有小伙伴在聊天室中提到這么一個需求&#xff0c;就是說希望用戶在博客首頁中登錄了之后&#xff0c;可以跳轉到管理系統去發布文章。這個需求的話就涉及到了不同網站之間cookie共享的功能&#xff0c;那么咱們就來試著解決一下這個功能。 實現方式 1. 后端做中轉 …

在一臺服務器上通過 Nginx 配置實現不同子域名訪問靜態文件和后端服務

一、域名解析配置 要實現通過不同子域名訪問靜態文件和后端服務&#xff0c;首先需要進行域名解析。在域名注冊商或 DNS 服務商處&#xff0c;為你的兩個子域名 blog.xxx.com 和 api.xxx.com 配置 A 記錄或 CNAME 記錄。將它們的 A 記錄都指向你服務器的 IP 地址。例如&#x…

Opencv進階操作:圖像拼接

文章目錄 前言一、圖像拼接的原理1. 特征提取與匹配2. 圖像配準3. 圖像變換與投影4. 圖像融合5. 優化與后處理 二、圖像拼接的簡單實現&#xff08;案例實現&#xff09;1.引入庫2.定義cv_show()函數3.創建特征檢測函數detectAndDescribe()4.讀取拼接圖片5.計算圖片特征點及描述…

LLM 論文精讀(三)Demystifying Long Chain-of-Thought Reasoning in LLMs

這是一篇2025年發表在arxiv中的LLM領域論文&#xff0c;主要描述了長思維鏈 Long Chain-of-Thought 對LLM的影響&#xff0c;以及其可能的生成機制。通過大量的消融實驗證明了以下幾點&#xff1a; 與shot CoT 相比&#xff0c;long CoT 的 SFT 可以擴展到更高的性能上限&…

計算機網絡常識:緩存、長短連接 網絡初探、URL、客戶端與服務端、域名操作 tcp 三次握手 四次揮手

緩存&#xff1a; 緩存是對cpu&#xff0c;內存的一個節約&#xff1a;節約的是網絡帶寬資源 節約服務器的性能 資源的每次下載和請求都會造成服務器的一個壓力 減少網絡對資源拉取的延遲 這個就是瀏覽器緩存的一個好處 表示這個html頁面的返回是不要緩存的 忽略緩存 需要每次…

《構建社交應用用戶激勵引擎:React Native與Flutter實戰解析》

React Native憑借其與JavaScript和React的緊密聯系&#xff0c;為開發者提供了一個熟悉且靈活的開發環境。在構建用戶等級體系時&#xff0c;它能夠充分利用現有的前端開發知識和工具。通過將用戶在社交應用中的各種行為進行量化&#xff0c;比如發布動態的數量、點贊評論的次數…

接口自動化測試框架詳解(pytest+allure+aiohttp+ 用例自動生成)

&#x1f345; 點擊文末小卡片&#xff0c;免費獲取軟件測試全套資料&#xff0c;資料在手&#xff0c;漲薪更快 近期準備優先做接口測試的覆蓋&#xff0c;為此需要開發一個測試框架&#xff0c;經過思考&#xff0c;這次依然想做點兒不一樣的東西。 接口測試是比較講究效…

Linux-----文件系統

文件大家都知道&#xff0c;前面的我的博客課程也為大家解釋了關于文件的打開等&#xff0c;今天我們要談論的是 文件在沒被打開的時候在磁盤中的位置和找到它的方式。 畫圖為大家展示&#xff1a; 方便理解 我們從下面幾個方面入手&#xff1a; 1. 看看物理磁盤 2. 了解一…

C++ set替換vector進行優化

文章目錄 demo代碼解釋&#xff1a; 底層原理1. 二叉搜索樹基礎2. 紅黑樹的特性3. std::set 基于紅黑樹的實現優勢4. 插入操作5. 刪除操作6. 查找操作 demo #include <iostream> #include <set>int main() {// 創建一個存儲整數的std::setstd::set<int> myS…

如何巧妙解決 Too many connections 報錯?

1. 背景 在日常的 MySQL 運維中&#xff0c;難免會出現參數設置不合理&#xff0c;導致 MySQL 在使用過程中出現各種各樣的問題。 今天&#xff0c;我們就來講解一下 MySQL 運維中一種常見的問題&#xff1a;最大連接數設置不合理&#xff0c;一旦到了業務高峰期就會出現連接…

QT的布局和彈簧及其代碼解讀

this指的是真正的當前正在顯示的窗口 main函數&#xff1a; Widget w是生成了一個主窗口&#xff0c;QT Designer是在這個主窗口里塞組件 w.show()用來展示這個主窗口 頭文件&#xff1a; namespace Ui{class Widget;}中的class Widget和下面的class Widget不是一個東西 Ui…

《AI大模型應知應會100篇》第52篇:OpenAI API 使用指南與最佳實踐

第52篇&#xff1a;OpenAI API 使用指南與最佳實踐 &#x1f4cc; 摘要 本文將帶你從零開始掌握 OpenAI API 的核心使用方法&#xff0c;涵蓋從基礎調用到高級功能的完整實戰路徑。通過詳細的代碼示例、圖文解析和可運行的 Python 腳本&#xff0c;幫助你快速上手 GPT-3.5、GP…

C#學習7_面向對象:類、方法、修飾符

一、類 1class 1)定義類 訪問修飾符class 類名{ 字段 構造函數&#xff1a;特殊的方法&#xff08;用于初始化對象&#xff09; 屬性 方法... } eg: public class Person { // 字段 private string name; private int a…

湖北理元理律師事務所:債務優化中的“生活保障”方法論

債務危機往往伴隨生活質量驟降&#xff0c;如何在還款與生存間找到平衡點&#xff0c;成為債務優化的核心挑戰。湖北理元理律師事務所基于多年實務經驗&#xff0c;提出“雙軌并行”策略&#xff1a;法律減負與生活保障同步推進。 債務優化的“溫度法則” 1.生存資金預留機制…

Jetpack Compose與Kotlin UI開發革命

Jetpack Compose + Kotlin:Android UI 開發的革命 簡介 Jetpack Compose 是 Google 推出的現代 Android UI 工具包,結合 Kotlin 語言,徹底改變了傳統 Android 開發的模式。過去,開發者依賴 XML 布局和命令式編程(如 findViewById 和手動更新視圖),導致代碼冗長且易出錯…

基于pyqt的上位機開發

目錄 安裝依賴 功能包含 運行結果 安裝依賴 pip install pyqt5 pyqtgraph pyserial 功能包含 自動檢測串口設備&#xff0c;波特率選擇/連接斷開控制&#xff0c;數據發送/接收基礎框架&#xff0c;實時繪圖區域&#xff08;需配合數據解析&#xff09; ""&q…

QT人工智能篇-opencv

第一章 認識opencv 1. 簡單概述 OpenCV是一個跨平臺的開源的計算機視覺庫&#xff0c;主要用于實時圖像處理和計算機視覺應用?。它提供了豐富的函數和算法&#xff0c;用于圖像和視頻的采集、處理、分析和顯示。OpenCV支持多種編程語言&#xff0c;包括C、Python、Java等&…

Python自學第5天:字符串相關操作

1.字符串運算符 作符描述字符串連接*重復輸出字符串[]通過索引獲取字符串中字符[ : ]截取字符串中的一部分&#xff0c;遵循左閉右開原則&#xff0c;str[0:2] 是不包含第 3 個字符的。in成員運算符 - 如果字符串中包含給定的字符返回 Truenot in成員運算符 - 如果字符串中不包…

RabbitMq(尚硅谷)

RabbitMq 1.RabbitMq異步調用 2.work模型 3.Fanout交換機&#xff08;廣播模式&#xff09; 4.Diret交換機&#xff08;直連&#xff09; 5.Topic交換機&#xff08;主題交換機&#xff0c;通過路由匹配&#xff09; 6.Headers交換機&#xff08;頭交換機&#xff09; 6…