文章目錄
- 引言
- 1.類的基本結構
- 2.構造函數和析構函數
- 3.基本成員函數
- 總結

引言
在C++編程中,字符串操作是非常常見且重要的任務。標準庫中的std::string類提供了豐富且強大的功能,使得字符串處理變得相對簡單。然而,對于學習C++的開發者來說,深入理解std::string的內部實現原理是非常有益的。通過親手實現一個類似的String類,不僅可以幫助我們掌握面向對象編程的基本概念,還能增強我們對內存管理和字符串操作的理解。
在這篇博客中,我們將從零開始,逐步實現一個自定義的C++ String類。我們的目標是構建一個功能完整且高效的字符串類,同時盡可能地模仿std::string的行為。我們將討論類的基本結構、構造函數和析構函數的實現、基本成員函數的設計、運算符重載、內存管理,以及如何編寫測試代碼來驗證我們的實現。
通過這篇文章,您將學到如何在C++中進行動態內存分配和管理,如何實現深拷貝和移動語義,如何重載運算符以提升類的易用性,等等。無論您是剛剛入門的C++學習者,還是希望深入理解C++底層實現的開發者,這篇文章都將為您提供寶貴的知識和實踐經驗。
讓我們一起來探索C++ String類的實現之旅吧!
1.類的基本結構
1.1定義類
#include<iostream>
#include<assert.h>
using namespace std;
namespace lyrics
{class string{public:typedef char* iterator;typedef const char* const_iterator;//迭代器iterator begin();iterator end();const_iterator begin()const;const_iterator end()const;//構造函數string(const char* str = "");string(const string& s);//析構函數~string();//const char* c_str() const;//返回大小size_t size() const;//運算符重載char& operator[](size_t pos);const char& operator[](size_t pos)const;//空間擴容void reserve(size_t n);//尾插一個字符void push_back(char ch);//尾插一個字符串void append(const char* str);//運算符重載+=操作string& operator+=(char ch);string& operator+=(const char* str);//插入操作,插入一個字符串和插入一個字符void insert(size_t pos, char ch);void insert(size_t pos, const char* str);//刪除某段字符void erase(size_t = 0, size_t len = npos);//查找某個字符串或者字符size_t find(char ch, size_t pos = 0);size_t find(const char* str, size_t pos = 0);//賦值拷貝string& operator=(const string& s);//交換函數void swap(string& s);//取子串string substr(size_t pos = 0, size_t = npos);//比較函數運算符重載bool operator<(const string& s)const;bool operator<=(const string& s)const;bool operator>(const string& s)const;bool operator>=(const string& s)const;bool operator==(const string& s)const;//清理void clear();private:size_t _size;size_t _capacity;char* _str;const static size_t npos;};//非成員函數,,重載流插入和流提取istream& operator>>(istream& is, string& str);ostream& operator<<(ostream& is, string& str);
}
用命名空間形成類域將其與全局作用域隔開,防止發生命名沖突
1.2私有成員變量
- size_t _size;
_size表示當前string的有效空間
- size_t _capacity;
_capaciity表示當前string的總的空間容量
- char _str;*
_str表示存儲字符串的指針
- const static size_t npos;
npos表示一個靜態變量
1.3公有成員函數
公有成員函數代碼上有標識
2.構造函數和析構函數
2.1構造函數
這里我們直接將構造函數和拷貝構造寫成一個函數
string::string(const char* str)//指定類域//strlen的效率很低//初始化列表+寫在內部函數:_size(strlen(str))
{_str = new char[_size + 1];_capacity = _size;strcpy(_str, str);
}
2.2賦值拷貝函數
注意:這里賦值拷貝函數由于我們不知道兩個串到底有多長,所以我們直接將需要賦值拷貝的串給釋放了,然后重新開一個空間,將s中的串拷貝給新的空間,這樣雖然很暴力,但是少了很多不必要的討論
string& string::operator=(const string& s)
{char* tmp = new char[s._capacity + 1];strcpy(tmp, s._str);delete[] _str;_str = tmp;_size = s._size;_capacity = s._capacity;return *this;
}
2.3c_str函數
const char* string::c_str() const
{return _str;
}
** 2.4析構函數**
由于str的空間是我們手動開辟的所以,需要我們用Delete來釋放,這里釋放之后將其置位空指針即可,然后重置我們的size和capacity
string::~string()
{delete[] _str;_str = nullptr;_size = 0;_capacity = 0;
}
3.基本成員函數
3.1獲取字符串長度
size_t string::size() const
{return _size;
}
3.2operator[]重載
這里直接返回pos位置對應的元素即可
char& string::operator[](size_t pos)
{assert(pos < _size);return _str[pos];//返回pos位置的字符
}
3.3const版本的operator[]重載
//const版本的[]重載
const char& string::operator[](size_t pos)const
{return _str[pos];
}
3.4預開辟空間
注意:這里預開辟的空間要是比實際空間小,則不進行操作,若預開辟的空間比實際空間大,則進行空間的開辟
void string::reserve(size_t n)
{if (n > _capacity){//開新空間char* tmp = new char[n + 1];//拷貝數據strcpy(tmp, _str);//釋放新空間delete[] _str;//指向新空間_str = tmp;//更新容量_capacity = n;}
}
3.5尾插
這里尾插一個字符也很簡單,先檢查一下空間是否允許再插入,如果空間不夠則先開辟兩倍的空間,如果以前的空間是0,則先預開辟4個空間
//尾插一個字符
void string::push_back(char ch)
{if (_capacity == _size){size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;reserve(newcapacity);}_str[_size++] = ch;_str[_size] = '\0';
}
3.6尾插一個字符串
這里尾插一個字符串,只需要先檢查一下空間是否夠用,然后再進行尾插,尾插可以直接調用字符串拷貝函數,將字符串拷貝到指定的位置
//尾插一個字符串
void string::append(const char* str)
{size_t len = strlen(str);if (_capacity == _size){reserve(_size + len);//當前的size+len}//strcat(_str, str);//效率不高//從當前位置開始自己去找\0,所以效率不高strcpy(_str + _size, str);//_str+_size就是\0的位置_size += len;
}
3.7迭代器
注意:下面的迭代器iterator是提前在頭文件中聲明好的,在.cpp文件中直接用,不明白的可以看上面的頭文件中的聲明
- 非const版本的迭代器
//普通版本的迭代器
string::iterator string::begin()
{return _str;
}
string::iterator string::end()
{return _str + _size;
}
- const版本的迭代器
//const版本的迭代器
string::const_iterator string::begin()const
{return _str;
}
string::const_iterator string::end()const
{return _str + _size;
}
** 3.8operator+=重載**
由于在實際使用中push_back和append的使用確實比較少,,也沒有+=方便,所以下面我們直接重載一個operator+=操作,+=操作只需要復用上面的push_back和append即可
//運算符重載
//傳引用返回出了作用域這個對象還在
string& string::operator+=(char ch)
{push_back(ch);return *this;
}
string& string::operator+=(const char* str)
{append(str);return *this;
}
3.9隨機插入一個字符串和一個字符
- 插入一個字符
這里還是需要檢查一下空間是否重充足,還需要檢查一下插入的位置是否合法,insert的效率也不是很高,因為它需要移動插入位置后面的整個子串,當頭插的時候時間復雜度變成了O(N)
void string::insert(size_t pos, char ch)
{assert(pos <= _size);if (_capacity == _size){size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;reserve(newcapacity);}size_t end = _size + 1;while (end > pos)//因為有符號和無符號比較,兩個類型不同會將有符號強制類型轉換成無符號//所以這里直接把pos強制類型轉換成int{_str[end] = _str[end - 1];end--;}_str[pos] = ch;_size++;
}
- 插入一個字符串
插入一個字符串,可以直接服用insert插入單個字符串的版本,這里我寫成了注釋,大家可以試試,如果不想復用還是可以參考上面插入單個字符串的思路,但是需要注意的是,移動的距離不是1了變成len了,還有一個需要注意的點,就是控制邊界條件,當end到達pos+len的時候由于這個位置的元素還是需要被移動,所以這里是大于的是pos+len-1
void string::insert(size_t pos, const char* str)
{assert(pos <= _size);size_t len = strlen(str);if (_capacity == _size){reserve(_size + len);//當前的size+len}//第一種方法//int end = len - 1;//while (end >= 0)//{// insert(pos, str[end]);// end--;//}size_t end = _size + len;//找到插入的后一個位置while (end > pos + len - 1){_str[end] = _str[end - len];end--;}memcpy(_str + pos, str, len);_size += len;
}
4.0刪除某段字符串
注意:在聲明中len的缺省參數給的是npos,當我的長度大于pos對應的后面對應的長度的時候,這時候就有多少刪多少,所以我們需要判斷一下,第一個if判斷的就是判斷我們刪除的長度是否已經超過了后面的長度,如果超過了就直接進入第一個if刪除后面的所有,也就是把pos位置置為\0,然后將_size更新,如果不是的話可以直接將pos+len位置的子字符串拷貝到pos位置之后
//從pos位置刪除len個字符
void string::erase(size_t pos, size_t len)
{assert(pos < _size);//當刪除的長度len大于后面的長度的時候//直接把后面的刪完if (len >= _size - pos){_str[pos] = '\0';_size = pos;}else{strcpy(_str + pos, _str + pos + len);//直接把后面的copy到前面_size -= len;}
}
4.1查找函數
- 查找單個字符
size_t string::find(char ch, size_t pos)
{for (size_t i = pos;i < _size;i++){if (_str[i] == ch){return i;}}return npos;
}
- 查找字符串
查找字符串的話可以直接用C語言的庫函數進行查找
size_t string::find(const char* sub, size_t pos)
{const char* str = strstr(_str + pos, sub);return str - _str;
}
4.2深拷貝
//深拷貝
string::string(const string& s)
{_str = new char[s._capacity + 1];strcpy(_str, s._str);_size = s._size;_capacity = s._capacity;
}
4.3交換函數
這里不用庫里的交換函數因為庫里的交換函數的效率太低了,我們可以簡單看看庫里交換函數的代碼
這里可以看到庫里的swap函數是直接拷貝構造一個零時的對象,然后進行兩次賦值拷貝,這樣做效率是極低的,因為是內置類型,兩次賦值拷貝都會進行創建新空間,然后釋放舊的空間,這樣的成本是很大的,所以可以直接寫一個swap對內置類型進行交換,直接交換兩個指針的指向,還有size和capacity即可
void string::swap(string& s)
{//內置類型交換代價更小std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);
}
4.4取子串
string string::substr(size_t pos, size_t len)
{//檢查pos是否合法assert(pos <= _size);//如果len大于后面的長度那么就后面有多少取多少if (len > _size - pos){//直接取后面的子串string sub(_str + pos);//從pos位置開始進行拷貝構造!!!!//返回子串return sub;}else{//構造子串string sub;//預開辟空間sub.reserve(len);//循環拷貝for (size_t i = 0;i < len;i++){sub += _str[pos + i];}//返回子串return sub;}
}
4.5比較函數operator的一系列重載
這里只需要重載兩個即可,其他的只需要進行復用就夠了,比較函數的重載可以直接調用C語言中的字符串比較函數
bool string::operator<(const string& s)const
{return strcmp(_str, s._str) < 0;
}
bool string::operator<=(const string& s)const
{return *this < s || *this == s;
}
bool string::operator>(const string& s)const
{return !(*this <= s);
}
bool string::operator>=(const string& s)const
{return !(*this < s);
}
bool string::operator==(const string& s)const
{return strcmp(_str, s._str);
}
4.6流插入和流提取
- 流插入
注意:流插入重載的時候需要清除前面的字符串,所以這里我們提供了一個clear函數進行以前字符串的清理,這里由于is不能識別空格或者回車,所以我們直接調用is的成員函數get,get可以識別空格和回車,然后識別到回車之后,直接停止賦值,返回值是istream
void string::clear()
{_str = '\0';_size = 0;
}
istream& operator>>(istream& is, string& str)
{str.clear();char ch = is.get();while (ch != ' '&& ch != '\n'){str += ch;}return is;
}
- 流提取
流提取也不用直接訪問成員變量,流提取可以直接一個字符一個字符的訪問,通過operator[]的重載訪問,一個一個大打印
ostream& operator<<(ostream& os, string& str)
{for (size_t i = 0;i < str.size();i++){os << str[i];}return os;
}
總結
在這篇博客中,我們從零開始,逐步實現了一個自定義的 C++ String 類。通過這個過程,我們不僅深入了解了字符串操作的內部工作原理,還掌握了許多 C++ 編程的重要概念和技巧。讓我們回顧一下我們在這篇文章中所做的工作:
-
類的基本結構
我們定義了 String 類的基本結構,包括私有成員變量和公共成員函數。我們了解了如何封裝數據,保護類的內部實現細節,并提供一個干凈的公共接口。 -
構造函數和析構函數
我們實現了默認構造函數、拷貝構造函數、移動構造函數和析構函數,確保我們的 String 類能夠正確地初始化、復制、移動和銷毀對象。我們討論了深拷貝和移動語義的區別,以及如何有效地管理資源。 -
基本成員函數
我們實現了獲取字符串長度的 length 函數和返回 C 風格字符串的 c_str 函數。這些函數使我們的 String 類更實用,并與 C++ 標準庫中的 std::string 類的行為保持一致。 -
運算符重載
我們重載了拷貝賦值運算符和移動賦值運算符,以確保我們的 String 類支持賦值操作,同時有效地管理內存。我們還可以進一步擴展,重載其他運算符,如加法運算符和比較運算符。 -
內存管理
我們深入探討了動態內存分配和釋放的細節,確保我們的 String 類不會產生內存泄漏。通過使用 RAII(資源獲取即初始化)原則,我們構建了一個健壯且高效的字符串類。 -
示例和測試
通過示例代碼和單元測試,我們驗證了 String 類的正確性和功能。這不僅提高了我們的代碼質量,也幫助我們發現并修復了潛在的問題。 -
優化與改進
雖然我們的 String 類已經具備了基本功能,但還有許多可以進一步優化和擴展的地方。我們可以添加更多的成員函數,如子字符串查找、字符串替換等,來增強類的功能。此外,性能優化也是一個重要方面,可以通過減少不必要的內存分配和拷貝來實現。
通過實現這個自定義的 String 類,我們不僅學會了如何在 C++ 中操作字符串,還增強了我們的面向對象編程技能和內存管理能力。希望這篇文章能夠激發您對 C++ 編程的興趣,并鼓勵您繼續探索和學習更多的編程技巧和設計模式。
感謝您的閱讀!如果您有任何問題或建議,請隨時在評論區留言,我們將一起討論和交流。