?
🌟?各位看官好,我是egoist2023!
🌍?種一棵樹最好是十年前,其次是現在!
💬?注意:本章節只詳講string中常用接口及實現,有其他需求查閱文檔介紹。
🚀?今天通過了解string接口,從而實現封裝自己的string類達到類似功能。
👍?如果覺得這篇文章有幫助,歡迎您一鍵三連,分享給更多人哦!
引入
string類的文檔介紹?--> 如有需要自行查閱文檔中接口實現。
auto和范圍for
auto關鍵字(自動推導類型):
- 在早期C/C++中auto的含義是:使用auto修飾的變量,是具有自動存儲器的局部變量,后來這個不重要了。C++11中,標準委員會變廢為寶賦予了auto全新的含義即:auto不再是一個存儲類型指示符,而是作為一個新的類型指示符來指示編譯器,auto聲明的變量必須由編譯器在編譯時期推導而得。
- 用auto聲明指針類型時,用auto和auto*沒有任何區別,但用auto聲明引用類型時則必須加&。
- 當在同一行聲明多個變量時,這些變量必須是相同的類型,否則編譯器將會報錯,因為編譯器實際只對第一個類型進行推導,然后用推導出來的類型定義其他變量。
- auto不能作為函數的參數,可以做返回值,但謹慎使用。
- auto不能直接用來聲明數組。
范圍for(底層就是迭代器):
- 對于一個有范圍的集合而言,由程序員來說明循環的范圍是多余的,有時候還會容易犯錯誤。因此C++11中引入了基于范圍的for循環。for循環后的括號由冒號“ :”分為兩部分:第一部分是范圍內用于迭代的變量,第二部分則表示被迭代的范圍,自動迭代,自動取數據,自動判斷結束。
- 范圍for可以作用到數組和容器對象上進行遍歷
- 范圍for的底層很簡單,容器遍歷實際就是替換為迭代器,這個從匯編層也可以看到。
了解string常用接口
?1.常見構造
(constructor) 函數名稱 | 功能說明 |
string() (重點) | 構造空的 string 類對象,即空字符串 |
string(const char* s) (重點) | 用 C-string 來構造 string 類對象 |
string(size_t n, char c) | string 類對象中包含 n 個字符c |
string(const string&s) (重點) | 拷貝構造函數 |
2.容量操作
函數名稱 | 功能說明 |
size(重點) | 返回字符串有效字符長度 |
length | 返回字符串有效字符長度 |
capacity | 返回空間總大小 |
empty | 檢測字符串釋放為空串,是返回 true ,否則返回 false |
clear | 清空有效字符(不改變底層空間大小) |
reserve | 為字符串預留空間 |
resize | 將有效字符的個數該成 n 個,多出的空間用字符 c 填充 |
注意:1. size() 與 length() 方法底層實現原理完全相同,引入 size() 的原因是保持與其他接口容器一致,而length函數是由于歷史原因遺留的。2. resize(size_t n) 與 resize(size_t n, char c) 都是將字符串中有效字符個數改變到 n 個,不同的是當字符個數增多時: resize(n) 用 0 來填充多出的元素空間, resize(size_t n, charc) 用字符 c 來填充多出的元素空間。注意: resize 在改變元素個數時,如果是將元素個數增多,可能會改變底層容量的大小,如果是將元素個數減少,底層空間總大小不變。3. reserve(size_t res_arg=0) :為 string 預留空間,不改變有效元素個數,當 reserve 的參數小于 string 的底層空間總大小時, reserver 不會改變容量大小。
?3.迭代器訪問
函數名稱 | 功能說明 |
operator[] (重點) | 返回 pos 位置的字符, const string 類對象調用 |
begin + end | begin 獲取一個字符的迭代器 + end 獲取最后一個字符下一個位 置的迭代器 |
rbegin + rend | begin 獲取一個字符的迭代器 + end 獲取最后一個字符下一個位 置的迭代器 |
范圍 for | \ |
4.修改操作
函數名稱 | 功能說明 |
push_back | 在字符串后尾插字符 c |
append | 在字符串后追加一個字符串 |
operator+= ( 重點 ) | 在字符串后追加字符串 str |
c_str ( 重點 ) | 返回 C 格式字符串 |
find + npos ( 重 點 ) | 從字符串 pos 位置開始往后找字符 c ,返回該字符在字符串中的 位置 |
rfind | 從字符串 pos 位置開始往后找字符c,返回該字符在字符串中的位置 |
substr | 在 str 中從 pos 位置開始,截取 n 個字符,然后將其返回 |
5.非成員函數
函數 | 功能說明 |
operator+ | 盡量少用,因為傳值返回,導致深拷貝效率低 |
operator>> (重點) | 輸入運算符重載 |
operator<< (重點) | 輸出運算符重載 |
getline (重點) | 獲取一行字符串 |
relational operators (重點) | 大小比較 |
string類模擬實現
底層結構
class string
{
public://...private:char* _str = nullptr;int _size = 0;int _capacity = 0;const static size_t npos;
};
?在上面定義的結構當中,其常量npos表示字符串末尾之前的所有字符,在substr接口中有使用。
const size_t string::npos = -1; //-1的無符號整數即表示最大值
1.常見構造?
我們知道無論如何字符串當中末尾總會存' \0 ' ,作為標記。因此在構造字符串string時,一定要多開一個空間存 ' \0 ' 。那如果new空間失敗呢?采用拋異常的方式,在外進行捕獲異常(之后會講)。
在如下一段程序中,將字符串str拷貝到string當中,但是這樣會導致多次析構一塊空間導致程序崩潰的問題。
string::string(const char* str):_str(new char[strlen(str)+1]){strcpy(_str, str);}
淺/深拷貝
淺拷貝:也稱位拷貝,編譯器只是將對象中的值拷貝過來 。如果 對象中管理資源 ,最后就會 導致多個對象共享同一份資源,當一個對象銷毀時就會將該資源釋放掉,而此時另一些對象不知道該資源已經被釋放,以為還有效,所以當繼續對資源進項操作時,就會發生發生了訪問違規 。如下圖當中, s1 、 s2 共用同一塊內 存空間,在釋放時同一塊空間被釋放多次而引起程序崩潰 ,這種拷貝方式,稱為淺拷貝。
?深拷貝:不單單是把數據拷貝過去,還需要開一塊內存空間,防止指向同一塊空間。
string::string(const char* str):_size(strlen(str)){_str = new char[_size + 1];//如果失敗需要捕獲異常_capacity = _size;strcpy(_str, str);}string::string(size_t n, char ch):_str(new char[n + 1]), _size(n), _capacity(n){for (size_t i = 0;i < n;i++){_str[i] = ch;}_str[_size] = '\0';}//析構?string::~string(){delete[] _str;_str = nullptr;_size = _capacity = 0;}?
拷貝構造、賦值運算法重載(重點)
拷貝構造:
?目標是將s中的數據拷貝到_str中,那我們直接調用strcpy函數將s數據拷過來即可??
string::string(const string& s){strcpy(_str, s._str);}
?但是這樣會導致析構時多次析構一塊空間,從而報錯(依然是淺拷貝的問題)。
string::string(const string& s){_str = new char[s._capacity + 1];strcpy(_str, s._str);_size = s._size;_capacity = s._capacity;}
賦值運算符重載:
特殊情況下可能自己給自己賦值,為了不再拷貝一次做判斷。
string& string::operator=(const string& s){if (this != &s){delete _str;_str = new char[s._capacity + 1];strcpy(_str, s._str);_size = s._size;_capacity = s._capacity;}return *this;}
現代寫法
實際上,上面的兩段代碼顯得過于笨拙且冗雜,都是老老實實自己手寫申請空間。而在如下一段程序當中,借用構造函數來完成拷貝及其賦值。而這種方法,也是實踐當中最常用到的現代寫法。
void string::swap(string& s){std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);}//拷貝構造簡潔化 --> 現代寫法string::string(const string& s){string tmp(s._str);swap(tmp);}
在如上一段程序當中,通過構造函數構造tmp。s這里是引用傳參,即出了作用域不會銷毀?;而tmp是屬于這個棧內的空間,出了作用域就會銷毀。此時我們借助swap的特性,將_str指向的指針進行交換,此時就是*this指向了新申請的空間,再將個數和空間交換即可。
這樣看,和平日寫的拷貝構造是差不多的。別著急,我們再來看看賦值運算符重載的簡化實現。
- 方法一:仍然采用上面思想寫賦值重載;
- 方法二:實際上,當我們寫完了拷貝構造后,我們甚至還能再借助拷貝構造的特性來完成賦值重載。此時,我們不再使用引用傳參,而是借助拷貝構造出s,而s出了作用域就會銷毀,此時我們再借助swap來進行交換。這樣來看,這種現代寫法是不是既簡潔又充滿著妙處。
string& string::operator=(string s){//s即是拷貝構造過來的swap(s); //出了作用域就會析構return *this;}
2.容量操作
//增容
void string::reserve(size_t n)
{char* tmp = new char[n + 1];strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = n;
}
3. 迭代器訪問
什么是迭代器?
迭代器的作用是用來訪問容器(用來保存元素的數據結構)中的元素,所以使用迭代器,我們就可以訪問容器中里面的元素。那迭代器不就相當于指針一個一個訪問容器中的元素嗎?并不是,迭代器是像指針一樣的行為,但是并不等同于指針,且功能更加豐富,這點需在之后慢慢體會。(本章節體現并不是很明顯)
typedef char* iterator;
typedef const char* const_iterator;iterator begin()
{return _str;
}iterator end()
{return _str + _size;
}const_iterator begin() const
{return _str;
}const_iterator end() const
{return _str + _size;
}
4. 修改操作
push_back插入邏輯:
- 當插入元素大于容器容量時,需進行擴容操作;
- _size的位置是' \0 ',但直接將插入元素覆蓋即可,_size++,重新加上' \0 '?。
void string::push_back(char x)
{if (_size + 1 > _capacity){reserve(_capacity == 0 ? 4 : 2 * _capacity);}_str[_size++] = x;_str[_size] = '\0';
}
?append插入邏輯:
- 計算需要插入字符串的長度len,若string的個數+len大于容量則需擴容;
- 若個數+len長度大于2倍擴容時,則應擴容到個數+len容量;
- 往string末尾插入字符串。
void string::append(const char* str)
{size_t len = strlen(str);if (len + _size > _capacity){int NewCapacity = 2 * _capacity;if (len + _size > 2 * _capacity){NewCapacity = len + _size;}reserve(NewCapacity);}strcpy(_str + _size, str);_size += len;
}
?+=運算符重載邏輯:
- 如果插入的是字符串,則采用append函數的邏輯;
- 如果插入的是字符,則采用push_back函數的邏輯;
- 無論哪種情況,實現方式都和以上兩種代碼實現方式是相同的,因此我們可以以復用的方式,更容易維護我們的代碼。
string& string::operator+=(const char* str)
{append(str);return *this;
}
string& string::operator+=(char x)
{push_back(x);return *this;
}
?insert函數實現邏輯:
- 擴容邏輯與其上是類似的,區別在于插入元素后的數據是從后往前還是從前往后挪動;
- 如果是從前往后挪動,那么會發生覆蓋數據的現象,而從后往前就不會,這點在之前也有強調過;
void string::insert(size_t pos, size_t n, char ch){assert(pos <= _size);//擴容if (_size + n > _capacity){// size_t newCapacity = 2 * _capacity;if (_size + n > 2 * _capacity){newCapacity = _size + n;}reserve(newCapacity);}//int end = _size;//while (end >= (int)pos)//這里不強轉會有err//{// _str[end + n] = _str[end];// --end;//}size_t end = _size + n;while (end > pos + n - 1){_str[end] = _str[end - n];--end;}for (size_t i = 0;i < n;i++){_str[pos + i] = ch;}_size += n;}
- ?擴容邏輯與其上對應重載函數是一樣的;
- 一樣是需要將pos后的位置進行挪動后,思路是類似的,那能否復用上面的實現函數呢?
如果復用上面的函數,那么該往這位置插入的字符串都是相同的一個字符,這樣想似乎不能復用。
但是沒關系,這些位置剛好是為要插入字符串預留的,那么我們只要將這些位置覆蓋一遍即可。
void string::insert(size_t pos, const char* str){size_t n = strlen(str);insert(pos, n, 'x');for (size_t i = 0;i < n;i++){_str[i + pos] = str[i];}}
復用 :通過犧牲空間方法。
string tmp(n, ch);insert(pos, tmp.c_str());
5. 非成員函數
- 當字符串長度小于16時,使用內部固定的字符數組來存放
- 當字符串長度大于等于16時,從堆上開辟空間
union _Bxty{ ??????? // storage for small buffer or pointer to larger one????????value_type _Buf [ _BUF_SIZE ];????????pointer _Ptr ;????????char _Alias [ _BUF_SIZE ]; // to permit aliasing} _Bx ;
流提取
vs下額外定義了個buff數組以減少擴容,提高效率。我們同樣采用這種思想造類似的輪子。
//cin>>s
istream& operator>>(istream& in, string& s)
{s.clear();//char ch = in.get();//while (ch != ' ' && ch != '\n')//{// s += ch;// ch = in.get();//}//為了減少頻繁的擴容,定義一個數組char buff[1024];char ch = in.get();size_t i = 0;while (ch != ' ' && ch != '\n'){buff[i++] = ch;if (i == 1023){buff[i] = '\0';s += buff;i = 0;}ch = in.get();}if (i > 0){buff[i] = '\0';s += buff;}return in;
}
?流插入
//cout<<s
ostream& operator<<(ostream& out, const string& s)
{for (auto ch : s){out << ch;}return out;
}
getline函數(難點)
實現邏輯:
- 每次輸入都往buff數組中填入數據;
- 當數據超過buff數組容量時,將數組里的數據加到string當中,buff數組從0開始繼續填入數據;
- 如果ch==delim時,不再填入數據,將buff數組里剩下的數據加到string當中。
istream& getline(istream& is, string& s, char delim)
{char buff[1024];char ch = is.get();size_t i = 0;while (ch != delim){buff[i++] = ch;if (i == 1023){buff[i] = '\0';s += buff;i = 0;}ch = is.get();}if (i > 0){buff[i] = '\0';s += buff;}return is;
}
代碼實現
string.h
#pragma once
#include<iostream>
#include<string.h>
#include<assert.h>
using namespace std;namespace egoist
{class string{public://迭代器typedef char* iterator;typedef const char* const_iterator;iterator begin(){return _str;}iterator end(){return _str + _size;}const_iterator begin() const{return _str;}const_iterator end() const{return _str + _size;}//計算串的size和capacitysize_t size() const{return _size;}size_t capacity() const{return _capacity;}//構造函數string(const char* str = "");string(size_t n, char ch);//交換void swap(string& s);//拷貝構造string(const string& s);const char* c_str() const{return _str;}void reserve(size_t n);void push_back(char x);void append(const char* str);=重載運算符//string& operator=(const string& s);//現代簡潔化string& operator=(string s);string& operator+=(const char* str);string& operator+=(char x);//比較大小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;bool operator>=(const string& s) const;//[]運算符重載char& operator[](size_t pos){assert(pos < _size);assert(pos >= 0);return _str[pos];}const char& operator[](size_t pos) const{assert(pos < _size);assert(pos >= 0);return _str[pos];}void insert(size_t pos, size_t n, char ch);void insert(size_t pos, const char* str);void erase(size_t pos = 0, size_t len = npos);size_t find(char ch, size_t pos = 0);size_t find(const char* str, size_t pos = 0);void clear(){_str[0] = '\0';_size = 0;}string substr(size_t pos, size_t len = npos);//析構~string();private:char* _str = nullptr;int _size = 0;int _capacity = 0;const static size_t npos;};//cout<<sostream& operator<<(ostream& out, const string& s);//cin>>sistream& operator>>(istream& in, string& s);istream& getline(istream& is, string& s, char delim = '\n');}
string.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"string.h"namespace egoist
{const size_t string::npos = -1;string::string(const char* str):_size(strlen(str)){_str = new char[_size + 1];//如果失敗需要捕獲異常_capacity = _size;strcpy(_str, str);}string::string(size_t n, char ch):_str(new char[n + 1]), _size(n), _capacity(n){for (size_t i = 0;i < n;i++){_str[i] = ch;}_str[_size] = '\0';}拷貝構造//string::string(const string& s)//{// _str = new char[s._capacity + 1];// strcpy(_str, s._str);// _size = s._size;// _capacity = s._capacity;//}void string::swap(string& s){std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);}//拷貝構造簡潔化 --> 現代寫法string::string(const string& s){string tmp(s._str);swap(tmp);}void string::reserve(size_t n){//需要增容 --> 為了和new配套使用,不用reallocchar* tmp = new char[n + 1];strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = n;}void string::push_back(char x){if (_size + 1 > _capacity){reserve(_capacity == 0 ? 4 : 2 * _capacity);}_str[_size++] = x;_str[_size] = '\0';}void string::append(const char* str){size_t len = strlen(str);if (len + _size > _capacity){int NewCapacity = 2 * _capacity;if (len + _size > 2 * _capacity){NewCapacity = len + _size;}reserve(NewCapacity);}strcpy(_str + _size, str);_size += len;}//=運算符重載//string& string::operator=(const string& s)//{// if (this != &s)// {// delete _str;// _str = new char[s._capacity + 1];// strcpy(_str, s._str);// _size = s._size;// _capacity = s._capacity;// }// return *this;//}//現代簡潔化 --> 通過調用拷貝構造string& string::operator=(string s){//s即是拷貝構造過來的swap(s); //出了作用域就會析構return *this;}string& string::operator+=(const char* str){append(str);return *this;}string& string::operator+=(char x){push_back(x);return *this;}//比較大小bool string::operator==(const string& s) const{return strcmp(_str, s._str) == 0;}bool string::operator!=(const string& s) const{return !(*this == s);}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);}void string::insert(size_t pos, size_t n, char ch){assert(pos <= _size);//擴容if (_size + n > _capacity){// size_t newCapacity = 2 * _capacity;if (_size + n > 2 * _capacity){newCapacity = _size + n;}reserve(newCapacity);}//int end = _size;//while (end >= (int)pos)//這里不強轉會有err//{// _str[end + n] = _str[end];// --end;//}size_t end = _size + n;while (end > pos + n - 1){_str[end] = _str[end - n];--end;}for (size_t i = 0;i < n;i++){_str[pos + i] = ch;}_size += n;}void string::insert(size_t pos, const char* str){由于高度相似,可采用復用//assert(pos <= _size);//size_t n = strlen(str);擴容//if (_size + n > _capacity)//{// // // size_t newCapacity = 2 * _capacity;// if (_size + n > 2 * _capacity)// {// newCapacity = _size + n;// }// reserve(newCapacity);//}//size_t end = _size + n;//while (end > pos + n - 1)//{// _str[end] = _str[end - n];// --end;//}size_t n = strlen(str);insert(pos, n, 'x');for (size_t i = 0;i < n;i++){_str[i + pos] = str[i];}//通過犧牲空間方法復用/*string tmp(n, ch);insert(pos, tmp.c_str());*/}void string::erase(size_t pos, size_t len){assert(pos >= 0);if (len > _size - pos){_str[pos] = '\0';_size = pos;}else {for (size_t i = pos;i <= _size;i++){_str[i] = _str[i + len];}_size -= len;}}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;}size_t string::find(const char* str, size_t pos){const char* p = strstr(_str + pos, str);if (p == nullptr){return npos;}else{return p - _str;}}string string::substr(size_t pos, size_t len){size_t leftlen = _size - pos;if (len > leftlen)len = leftlen;string tmp;tmp.reserve(len);for (size_t i = 0; i < len; i++){tmp += _str[pos + i];}return tmp;}string::~string(){delete[] _str;_str = nullptr;_size = _capacity = 0;}//cout<<sostream& operator<<(ostream& out, const string& s){for (auto ch : s){out << ch;}return out;}//cin>>sistream& operator>>(istream& in, string& s){s.clear();//char ch = in.get();//while (ch != ' ' && ch != '\n')//{// s += ch;// ch = in.get();//}//為了減少頻繁的擴容,定義一個數組char buff[1024];char ch = in.get();size_t i = 0;while (ch != ' ' && ch != '\n'){buff[i++] = ch;if (i == 1023){buff[i] = '\0';s += buff;i = 0;}ch = in.get();}if (i > 0){buff[i] = '\0';s += buff;}return in;}istream& getline(istream& is, string& s, char delim){char buff[1024];char ch = is.get();size_t i = 0;while (ch != delim){buff[i++] = ch;if (i == 1023){buff[i] = '\0';s += buff;i = 0;}ch = is.get();}if (i > 0){buff[i] = '\0';s += buff;}return is;}
}
擴展 --> 引用計數的寫時拷貝
?