一、引言
? ? ? ? 在之前的文章中,我們一同學習了有關類和對象、模板、動態內存管理的相關知識,那么接下來一段時間我們將要趁熱打鐵,一起來手撕C++庫中最重要的一個庫----STL中的一些容器,在手撕它們之前,我將先介紹一下對應的容器,在這個過程中,我們將加深之前學習過的相關知識。
二、string的前置內容
? ? ? ? 1、string的介紹
? ? ? ? 事實上string并不是STL中的一個容器,這是由于C++的發展歷史的原因,但是隨著語言的發展,string與STL中的容器的風格以及使用方法已經有著很高的耦合度了,所以我將它放在STL這里一起學習。
? ? ? ? string是一個字符串類,可以借助其提供的接口進行字符串的各種增刪查改操作,它的底層其實就是一個存儲字符數組的順序表。
·? ? ? ? 2、string中的常用接口
? ? ? ? 事實上,string所提供的接口是有很多冗余的,大概有100多個接口,在實現部分都只涉及到一些常用的接口,通過這些常用的接口已經可以處理平常所能遇到的幾乎所有情況了,如果需要了解所有接口或者對于以下接口的使用還不太熟悉的話,可以調轉到以下網站了解:cplusplus.com - The C++ Resources Networkhttps://legacy.cplusplus.com/三、手撕一個string類
? ? ? ? 1、string類的成員變量及整體的框架
? ? ? ? 為了與庫中的string區分,我們將在自己的命名空間中進行定義相關變量,這個命名空間的名字是無所謂的,為了方便起見,我將直接展開std命名空間,但是在真實場景下,不直接展開會更好一些?
? ? ? ? 同時在這里說明一下,我們將在寫一個函數之前先將庫中的函數頭放在前面,以其為導引,同時在這里簡單的說明一下該函數的作用
? ? ? ? string類是一個字符類型的順序表,所以需要一個內置的字符類型的指針來管理字符串中的內容,對于一個順序表,需要記錄它的長度,同時我們將動態開辟空間,所以記錄此時string中的空間是多少也是非常重要的:
????????
? ? ? ? 涉及到類似順序表的結構,_size、_capacity都是必要的,這個在以后也經常用到,同時在命名時,在成員變量之前加一個‘_’可以區分成員變量與外部變量
? ? ? ? 2、類的構造函數與析構函數
? ? ? ? 庫中的函數頭:
????????
? ? ? ? 以上是庫中所提供的所有的構造函數,我們只實現圖中圈出的兩個,還有幾個是拷貝構造函數,在后面會實現,同時由于析構函數名一定并且沒有返回值,不傳參,這里就不展示了
? ? ? ? 一般來說構造函數我們更傾向于在初始化列表進行初始化,但是由于要開空間,同時對于一個內置類型來說,在函數體內賦值和在初始化列表進行初始化的效率差別不是很大,所以我都采用函數體內賦值:
????????
? ? ? ?可以看到,在上面我們只實現了一個構造函數但是由于它是帶缺省值的,如果不傳參默認會構造一個空串,所以與庫中的函數是配合的,需要注意的是,在開空間時一定要多開一個空間,這個位置是留給‘\0’的
? ? ? ? 3、類中留給類外讀取信息的幾個簡單函數
? ? ? ? 庫中的函數頭:
? ? ? ? ????????(1).size函數:返回此時字符串中的元素個數
????????????????
? ? ? ????????? (2).c_str函數:返回一個c風格的字符串
????????????????
? ? ? ? ? ? ? ? 這兩個函數實現起來是很簡單的,就不做過多的介紹了:
????????????????
? ? ? ? 4、[]操作符重載
? ? ? ? 庫中的函數頭:
????????
? ? ? ? []操作符重載的作用是讓我們實現的類類型可以像正常的數組一樣使用[]訪問下標的方式來訪問字符串中指定位置的內容,使用起來很方便,同時代碼的可讀性變高
? ? ? ? 可以看到,該操作重載有兩個函數重載,這兩個函數重載都是很有必要的,當this指針不被const修飾時,函數提供給非const類型使用,可讀可寫;當this指針被const修飾時,函數提供給const類型使用,只讀不寫:
????????
? ? ? ? 可以看到,該函數的實現也是非常的方便的,并且兩個重載的內容也一模一樣,這時候我們可以使用模板來簡化代碼,但由于這是很簡單的兩個重載,所以沒有必要,在以后STL中其它部分,我們會大量的使用模板的相關知識
? ? ? ? 5、完成迭代器的封裝
? ? ? ? 庫中的函數頭:
? ? ? ? ? ? ? ? (1).begin
????????????????
? ? ? ? ? ? ? ? (2).end
????????????????
? ? ? ? 迭代器是STL的相關容器中的一個重要工具,常被用來遍歷容器,它是類中定義的一個類型,使用起來和指針非常相似,在類中定義類型有兩種方式,封裝內部類和typedef,很明顯使用指針就可以直接遍歷一個字符類型的數組,所以string類的迭代器我們可以直接使用typedef封裝字符指針的方式
? ? ? ? 了解迭代器之后,我們還需要了解begin和end是留給用戶使用迭代器的兩個接口,begin返回指向首元素的迭代器,end返回指向末尾元素下一個位置的迭代器,它們也都進行了重載,同樣分為只讀不寫和可讀可寫,在之前已經詳細介紹過,這里就不多介紹了:
????????
? ? ? ? 6、一個特殊的常量
? ? ? ? 庫中的介紹:
????????
? ? ? ? 在庫中定義了一個靜態成員變量npos,它的值是-1,但由于它是size_t類型的,所以實質上是一個非常大的正整數,我們默認一個自負床不會有這么長,所以在庫中的函數將npos這一靜態成員變量當作無窮大來使用:
????????
? ? ? ? 在這里需要注意:類中的靜態成員變量一定要記得在類外進行定義,雖然const修飾的靜態成員變量是可以i直接在類內聲明時直接定義的,但為了統一性,這里我還是在類外進行了定義
? ? ? ?7、拷貝構造函數
? ? ? ? 庫中的函數頭:
????????
? ? ? ? 拷貝構造函數的作用在這里就不多介紹了,它在之后的函數返回一個string類型、傳參時傳一個string類型、賦值運算符重載時都會用到:
????????
? ? ? ? 8、開空間函數
? ? ? ? 庫中的函數頭:
? ? ? ? reserve:開指定大小的空間,將原數據進行拷貝,剩余部分不做處理
? ? ? ??
????????
? ? ? ? 9、插入相關函數-----append
? ? ? ? 庫中的函數頭:
????????
? ? ? ? append函數是用于在字符串末尾插入的函數,可以看到庫中提供了很多的函數重載,接下來我們只會實現以上圈出的幾個,同時我們將借助缺省值簡化代碼:
????????? ? ? ?
? ? ? ? 糾正一下:不小心忘記進行:_size += n\_size += sublen啦,這是在寫完之后測試的時候發現的,所以一定要邊寫邊測試啊!!!
? ? ? ? 10、交換函數
? ? ? ? 庫中的函數頭:
????????
? ? ? ? swap函數顧名思義就是交換兩個string類對象,需要注意的是我們將完成深拷貝,也就是說要重新開空間,而不能簡單的交換兩個類對象的成員變量,這時候我們可以借助算法庫中提供的swap函數,它可以幫助我們完成深拷貝,事實上我們可以直接進行深拷貝,但是借助算法庫實現會更簡單:
????????
? ? ? ? 需要注意的是:在這里我們需要指定標準命名空間
? ? ? ? 11、賦值運算符重載
? ? ? ? 庫中的函數頭:
????????
? ? ? ? 庫中海還有該函數的其它重載,但其實都可以通過該函數完成剩余操作,所以只實現這一個,同時,由于我們可以借助一種更簡單的方式來完成該函數的實現,所以接下來我對該函數的實現時,函數頭與庫中的略有差異,但是對于用戶的使用是相同的:
????????
? ? ? ? 對以上代碼做一下解釋:我們的函數實參傳給形參會進行臨時拷貝,這是會調用我們之前實現過的拷貝構造函數進行深拷貝,再交換*this與str,*this就變成了str,str是形參,出作用域銷毀,調用析構函數,不會造成內存泄漏,*this出函數還存在,進行傳引用返回
? ? ? ? 12、其它運算符重載
? ? ? ? 由于接下來都是正常的運算符重載,功能也是顯而易見的,所以在這里就不提供庫中的函數頭了,直接進行相關的實現:
????????
//其它運算符重載//+=運算符重載
string& operator+=(const string& str)
{return append(str);
}
string& operator+=(const char* s)
{return append(s);
}
string& operator+=(char c)
{return append(1, c);
}
//+運算符重載
string operator+(const string& str)
{string strtmp(*this);return (strtmp += str);
}
string operator+(const char* s)
{string strtmp(*this);return (strtmp += s);
}
string operator+(char c)
{string strtmp(*this);return (strtmp += c);
}
//<運算符重載
bool operator < (const string& s) const
{int minlen = _size;if (s._size < _size){minlen = s._size;}for (int i = 0; i < minlen; i++){if (_str[i] != s._str[i]){return _str[i] < s._str[i];}}return _size < s._size;
}
//==運算符重載
bool operator==(const string& s) const
{if (_size != s._size) return false;for (int i = 0; i < _size; i++){if (_str[i] != s._str[i]){return false;}}return true;
}
//<=運算符重載
bool operator<=(const string& s) const
{return ((*this < s) || (*this == s));
}
//>運算符重載
bool operator>(const string& s) const
{return (!(*this < s) && !(*this == s));
}
//>=運算符重載
bool operator>=(const string& s) const
{return ((*this > s) || (*this == s));
}
//!=運算符重載
bool operator!=(const string& s) const
{return !(*this == s);
}
? ? ? ? 以上就是所有可能用到的運算符重載了,由于很長,所以我將代碼直接放在上面,可以看到,由于這些邏輯運算符有天然的互斥性,所以我們可以很好的進行代碼復用,大大提高了代碼的質量
? ? ? ? 13、插入相關函數-----push_back
? ? ? ? 庫中的函數頭:
????????
? ? ? ? 該函數的作用是在字符串末尾插入一個字符,無返回值:
????????
? ? ? ? 在這個位置我們可以直接進行代碼復用
? ? ? ? 14、插入相關函數-----insert
? ? ? ? 庫中的函數頭:
????????
? ? ? ? 庫中對于插入函數也是提供了非常多的重載,我們只實現圖中圈出的幾個,同時我們會使用缺省值的方式簡化代碼:
?????????
//插入函數
string& insert(size_t pos, const string& str, size_t subpos = 0, size_t sublen = npos)
{assert(pos < _size);assert(subpos < str._size);if (subpos + sublen >= str._size) sublen = str._size - subpos;if (_capacity < _size + sublen + 1){reserve(_size + sublen + 1 + 0.5 * sublen);}int end = _size , next = end + sublen;while (next > pos){_str[next--] = _str[end--];}for (int i = 0; i < sublen; i++){_str[pos + i] = str._str[i];}_size += sublen;return *this;
}
string& insert(size_t pos, const char* s, size_t n = npos)
{string strtmp(s);return insert(pos, s, 0, n);
}
string& insert(size_t pos, size_t n, char c)
{assert(pos < _size);if (_capacity < _size + n + 1){reserve(_size + n + 1 + 0.5 * n);}int end = _size, next = end + n;while (next > pos){_str[next] = _str[end];}for (int i = 0; i < n; i++){_str[pos + n] = c;}_size += n;return *this;
}
? ? ? ? 可以看到,在上面我們也進行了一定程度的代碼復用,所以代碼復用是非常重要的,可以大大提高代碼質量
? ? ? ? 15、刪除函數
? ? ? ? 庫中的函數頭:
????????
? ? ? ? erase函數的作用是從pos位置開始,刪除len長度的字符:
????????
? ? ? ? 16、查找相關函數
? ? ? ? 庫中的函數頭:
????????
? ? ? ? 庫中對于查找函數也是實現了多個函數重載,在這里我們只實現上面圈出的兩個,其他的通過借助這兩個函數也是都可以完成的:
????????
? ? ? ? 在上面的實現中我們借助了C語言常用的幾個函數,簡化了代碼
? ? ? ? 17、返回子串的函數
? ? ? ? 庫中的函數頭:
????????
? ? ? ? 該函數的作用是返回從pos位置開始len長度的子串:
????????
? ? ? ? 18、清理函數
? ? ? ? 庫中的函數頭:
????????
? ? ? ? 該函數可以清空字符串中的數據:
????????
四、最終的string類
? ? ? ? 終于,我們一起寫完了一個簡陋版的string類,需要注意的是在這個過程中我們更關注的是了解string的使用、加深對于類和對象等知識的理解和提升我們的代碼能力,并不是寫出一個更好的string類,最終完成的string類如下:
????????
namespace bea
{class string{public://相關函數//構造函數string(const char* s = ""){int n = strlen(s);_str = new char[n + 1];memcpy(_str, s, n + 1);_size = n;_capacity = n;}//size函數size_t size() const{return _size;}//c_str函數const char* c_str() const{return _str;}//[]操作符重載// // 普通類型char& operator[](size_t pos){assert(pos < _size);return _str[pos];}//const類型const char& operator[](size_t pos) const{assert(pos < _size);return _str[pos];}//迭代器相關接口//先進行類型的封裝typedef char* iterator;typedef const char* const_iterator;//begin函數iterator begin(){return _str;}const_iterator begin() const{return _str;}//end函數iterator end(){return _str + _size;}const_iterator end() const{return _str + _size;}string(const string& str){_str = new char[str._capacity + 1];memcpy(_str, str._str, _size + 1);_size = str._size;_capacity = str._capacity;}//開空間函數void reserve(size_t n = 0){if (n > _capacity){char* strtmp = new char[n + 1];memcpy(strtmp, _str, _size + 1);delete[] _str;_str = strtmp;_capacity = n;}}//尾接函數//尾接C風格字符串//接入s字符串的前n個字符string& append(const char* s, size_t n = npos){size_t len = strlen(s);if (n > len) n = len;if (_capacity < _size + n + 1){reserve(_size + n + 1 + 0.5 * n);}for (int i = 0; i < n; i++){_str[_size + i] = s[i];}_str[_size + n + 1] = '\0';_size += n;return *this;}//尾接一個string//接入str的subpos開始sublen長度的子串string& append(const string& str, size_t subpos = 0, size_t sublen = npos){assert(subpos < str._size);if (subpos + sublen >= str._size) sublen = _size - subpos;if (_capacity < _size + sublen + 1){reserve(_size + sublen + 1 + 0.5 * sublen);}for (int i = 0; i < sublen; i++){_str[_size + i] = str._str[i];}_str[_size + sublen + 1] = '\0';_size += sublen;return *this;}//尾接n個chstring& append(size_t n, char ch){if (_capacity < _size + n + 1){reserve(_size + n + n * 0.5);}for (int i = 0; i < n; i++){_str[_size + i] = ch;}_str[_size + n + 1] = '\0';_size += n;return *this;}//交換函數void swap(string& str){std::swap(_str, str._str);std::swap(_size, str._size);std::swap(_capacity, str._capacity);}//賦值運算符重載string& operator=(string str){swap(str);return *this;}//其它運算符重載//+=運算符重載string& operator+=(const string& str){return append(str);}string& operator+=(const char* s){return append(s);}string& operator+=(char c){return append(1, c);}//+運算符重載string operator+(const string& str){string strtmp(*this);return (strtmp += str);}string operator+(const char* s){string strtmp(*this);return (strtmp += s);}string operator+(char c){string strtmp(*this);return (strtmp += c);}//<運算符重載bool operator < (const string& s) const{int minlen = _size;if (s._size < _size){minlen = s._size;}for (int i = 0; i < minlen; i++){if (_str[i] != s._str[i]){return _str[i] < s._str[i];}}return _size < s._size;}//==運算符重載bool operator==(const string& s) const{if (_size != s._size) return false;for (int i = 0; i < _size; i++){if (_str[i] != s._str[i]){return false;}}return true;}//<=運算符重載bool operator<=(const string& s) const{return ((*this < s) || (*this == s));}//>運算符重載bool operator>(const string& s) const{return (!(*this < s) && !(*this == s));}//>=運算符重載bool operator>=(const string& s) const{return ((*this > s) || (*this == s));}//!=運算符重載bool operator!=(const string& s) const{return !(*this == s);}//push_back函數void push_back(char c){*this += c;}//插入函數string& insert(size_t pos, const string& str, size_t subpos = 0, size_t sublen = npos){assert(pos < _size);assert(subpos < str._size);if (subpos + sublen >= str._size) sublen = str._size - subpos;if (_capacity < _size + sublen + 1){reserve(_size + sublen + 1 + 0.5 * sublen);}int end = _size , next = end + sublen;while (next > pos){_str[next--] = _str[end--];}for (int i = 0; i < sublen; i++){_str[pos + i] = str._str[i];}_str[pos + sublen + 1] = '\0';_size += sublen;return *this;}string& insert(size_t pos, const char* s, size_t n = npos){string strtmp(s);return insert(pos, s, 0, n);}string& insert(size_t pos, size_t n, char c){assert(pos < _size);if (_capacity < _size + n + 1){reserve(_size + n + 1 + 0.5 * n);}int end = _size, next = end + n;while (next > pos){_str[next] = _str[end];}for (int i = 0; i < n; i++){_str[pos + n] = c;}_str[pos + n + 1] = '\0';_size += n;return *this;}//刪除函數string& erase(size_t pos = 0, size_t len = npos){assert(pos < _size);if (pos + len >= _size){_str[pos] = '\0';_size -= len;return *this;}int end = pos + len, front = pos;while (front < _size){_str[front++] = _str[end++];}_size -= len;_str[pos + len + 1] = '\0';return *this;}//找字符函數size_t find(char ch, size_t pos = 0){assert(pos < _size);for (int i = pos; i < _size; i++){if (_str[i] == ch) return i;}return npos;}//找字符串size_t find(const char* str, size_t pos = 0){assert(pos < _size);const char* ptr = strstr(_str + pos, str);if (ptr){return ptr - _str;}else{return npos;}}//子串函數string substr(size_t pos = 0, size_t len = npos){if (pos == 0 && len >= _size) return *this;if (pos + len >= _size) len = _size - pos;string tmp;for (int i = pos; i < pos + len; i++) tmp.push_back(_str[i]);return tmp;}//清除函數void clear(){_str[0] = '\0';_size = 0;}//析構函數~string(){delete[] _str;_str = nullptr;_size = _capacity = 0;}private:char* _str;size_t _size;size_t _capacity;static const size_t npos;};const size_t string::npos = -1;
}
五、結語
? ? ? ? 以上就是本期關于手撕string類的所有內容了,感謝大家的閱讀,歡迎各位于晏、亦菲和我一起交流、學習、進步!!!? ? ? ??
????????
????????
????????
????????
? ? ??
????????????????
? ? ? ? ? ? ?
????????????????? ? ? ??
????????
????????
????????????????????????????????
????????????????