常州市城鄉(xiāng)建設(shè)學(xué)院網(wǎng)站如何在百度推廣自己的產(chǎn)品
目錄
1、基本成員變量
2、默認(rèn)成員函數(shù)
構(gòu)造函數(shù)
析構(gòu)函數(shù)
拷貝構(gòu)造函數(shù)(深拷貝)
賦值運算符重載?
3、容量與大小相關(guān)的函數(shù)
size
capacity?
4、字符串訪問相關(guān)函數(shù)
operator [ ]重載
迭代器?
5、增加的相關(guān)函數(shù)?
reserve擴容
resize?
push_back追加字符
append追加字符串
operator+=?
insert?
6、刪除的相關(guān)函數(shù)
erase
clear清除數(shù)據(jù)
7、查找的相關(guān)函數(shù)
find
8、c_str獲取字符串
9、swap交換函數(shù)
10、非成員函數(shù)
關(guān)系運算符函數(shù)重載
<<流插入運算符重載
>>流提取運算符重載
getline函數(shù)
整型和字符串之間的轉(zhuǎn)換函數(shù)
1、基本成員變量
namespace Fan {//使用命名空間防止我們實現(xiàn)的string與庫里的string沖突class string{public://......private:char* _str; //存儲字符串size_t _size; //有效字符的個數(shù)size_t _capacity; //實際存儲的有效字符的個數(shù),不包含'\0'const static size_t npos;};const size_t string::npos = -1; }
2、默認(rèn)成員函數(shù)
構(gòu)造函數(shù)
這里的構(gòu)造函數(shù)我們最好寫成全缺省函數(shù),與庫里面的構(gòu)造函數(shù)相一致。
//全缺省的默認(rèn)構(gòu)造函數(shù) string(const char* str = "") //標(biāo)準(zhǔn)庫里string定義對象的默認(rèn)值為空串""//按聲明的順序進(jìn)行初始化:_size(strlen(str)), _capacity(_size) {_str = new char[_capacity + 1]; //在堆上為_str開辟空間,+1是給'\0'預(yù)留的strcpy(_str, str); //把常量字符串的內(nèi)容拷貝過去 }
析構(gòu)函數(shù)
string類里面的_str成員指向的空間是在堆區(qū),堆區(qū)的空間不能自動銷毀,因此需要我們手動去進(jìn)行銷毀。
//析構(gòu)函數(shù) ~string() {if (_str){delete[] _str;_str = nullptr;_size = _capacity = 0;} }
拷貝構(gòu)造函數(shù)(深拷貝)
首先我們不寫時編譯器會默認(rèn)生成一個拷貝構(gòu)造函數(shù),不過是值拷貝或者淺拷貝,是按照字節(jié)拷貝的。
編譯器默認(rèn)生成的拷貝構(gòu)造函數(shù)是淺拷貝,淺拷貝針對日期類是非常合適的,但是像string類這樣_str是動態(tài)開辟在堆上的。如果我們使用值拷貝會導(dǎo)致:1、同一塊空間會析構(gòu)兩次。2、一個對象修改會影響另外一個。因此我們需要進(jìn)行深拷貝。深拷貝的核心要點在于我和你有一樣的值,但是使用的不是同一塊空間。
深拷貝有兩種寫法:傳統(tǒng)寫法和現(xiàn)代寫法。
- 1、傳統(tǒng)寫法:
?傳統(tǒng)寫法就是先開辟一塊能夠容納原字符串大小的空間,最后把拷貝的對象的字符串?dāng)?shù)據(jù)拷貝到新開的空間里面。
//拷貝構(gòu)造函數(shù) //不能用淺拷貝,原因如下:1、析構(gòu)兩次 2、一個對象修改會影響另外一個 // 傳統(tǒng)寫法 string(const string& s):_size(strlen(s._str)), _capacity(_size) {_str = new char[_capacity + 1];strcpy(_str, s._str); }
- 2、現(xiàn)代寫法:
傳統(tǒng)寫法是自己開空間然后拷貝數(shù)據(jù),而現(xiàn)代寫法就是創(chuàng)建中間變量。比如我們拿s1作為參數(shù)去拷貝構(gòu)造s2,我們創(chuàng)建一個對象tmp,然后拿s1._str的字符串作為參數(shù)給tmp對象完成構(gòu)造。然后再利用swap函數(shù)把tmp對象的_str、_size、_capacity全部與s2的交換即可完成現(xiàn)代方法的深拷貝。不過需要注意的是我們需要把s2的數(shù)據(jù)置空,避免交換后tmp調(diào)用析構(gòu)函數(shù)會出現(xiàn)析構(gòu)隨機值的錯誤現(xiàn)象。
//現(xiàn)代方法 string(const string& s):_str(nullptr), _size(0), _capacity(0) {string tmp(s._str); swap(tmp); }
賦值運算符重載?
- 思路:
如果我們把s3賦值給s1,這里并不能直接進(jìn)行賦值,我們需要考慮兩個問題。
- 如果我s1的空間小于s3,那么直接拷貝過去會導(dǎo)致越界。
- 如果我s1的空間遠(yuǎn)大于s3的空間,直接拷貝過去就會導(dǎo)致空間的過度浪費。只有在我s1和s3的空間差不多大的時候,才可以直接進(jìn)行拷貝。
綜上:我們先把s1原先指向的內(nèi)容delete掉,然后再重新開辟一個和s3一樣大的空間,需要注意的是記得多開一個字節(jié),因為還有'\0'字符。我們再利用strcpy把s3的內(nèi)容拷貝給s1即可。
?優(yōu)化點:
- 要避免自己給自己賦值,如若自己給自己賦值,我們直接返回。所以要加上if條件判斷
- 如果我new失敗那么就會導(dǎo)致拋異常,而先前我依舊釋放了s1,此時就把s1給破壞了。為了避免這一點,我們可以先開空間再拷貝數(shù)據(jù)最后釋放從而進(jìn)行優(yōu)化。
- 1、傳統(tǒng)寫法:
//賦值運算符重載--> 深拷貝 //s1=s3 s1.operator=(&s1,s3); string& operator=(const string& s) {//防止自己給自己賦值if (this != &s){/*//法一//先刪除原先s1的所有空間,防止賦值后s1過大導(dǎo)致空間浪費,s1過小導(dǎo)致空間不夠delete[] _str;//給s1開辟與s3等價空間大小,要多開一字節(jié)給'\0'_str = new char[strlen(s._str) + 1];strcpy(_str, s._str);*///法二優(yōu)化//先開辟空間char* tmp = new char[s._capacity + 1];strcpy(tmp, s._str);delete[] _str;_str = tmp;_size = s._size;_capacity = s._capacity;}return *this; }
- 2、現(xiàn)代寫法:
這里的現(xiàn)代寫法與上文拷貝構(gòu)造的現(xiàn)代方法沒有什么區(qū)別。
//現(xiàn)代寫法 string& operator=(const string& s) {if (this != &s){string tmp(s._str);swap(tmp);}return *this; }
還有一種更加簡潔的現(xiàn)代方法,上述寫法是引用傳參,這里我們可以直接傳值傳參,讓編譯器自動調(diào)用拷貝構(gòu)造函數(shù),再把拷貝出來的對象作為右值與左值進(jìn)行交換。
//法二:簡潔版string& operator=(string s) //傳值傳參調(diào)用拷貝構(gòu)造,s就是s3的深拷貝{swap(s); //交換這兩個對象return *this;}
簡潔版本的弊端就在于無法避免自己給自己賦值,但很少會出現(xiàn)自己給自己賦值的行為。
3、容量與大小相關(guān)的函數(shù)
size
直接返回隱含this指針指向的_size即為字符串長度
//返回字符串的長度 size_t size() const //不改變內(nèi)部成員,最好加上const {return _size; }
capacity?
?直接返回隱含this指針指向的_capacity即可
//返回字符串容量 size_t capacity() const //不改變內(nèi)部成員,最好加上const {return _capacity; }
4、字符串訪問相關(guān)函數(shù)
operator [ ]重載
有了operator[ ]運算符重載,我們就可以直接用下標(biāo)+[ ]進(jìn)行元素訪問,不過這里我們還需要提供一個const版本的operator[ ]運算符重載,以便于普通對象和const對象均可以調(diào)用而不會出現(xiàn)權(quán)限方法的問題。
//版本1 char& operator[](size_t pos) //引用返回,便于后續(xù)修改返回的字符 {assert(pos < _size); //確保pos位置的合法性,不能超過字符串return _str[pos]; //返回pos位置字符的引用 } //版本2 const char& operator[](size_t pos)const {assert(pos < _size);return _str[pos]; }
迭代器?
string類的迭代器我們可以將其理解為字符指針。
- begin函數(shù)的作用就是返回字符串中的第一個字符的地址
- end函數(shù)的作用就是返回字符串最后一個字符的后一個位置地址,即'\0'的地址
//版本1 typedef char* iterator; iterator begin() {return _str; //返回第一個有效字符的指針 } iterator end() {return _str + _size; //返回最后一個字符后一個位置的地址,即'\0'的地址 }
和上文的operator[ ]重載一樣,我們也需要寫一個const版本的迭代器,以便于后續(xù)的const對象也能夠調(diào)用。
//版本2:只讀,const對象可調(diào)用 typedef const char* const_iterator; const_iterator begin()const {return _str; //返回第一個有效字符的指針 } const_iterator end()const {return _str + _size; //返回最后一個字符后一個位置的地址,即'\0'的地址 }
還有一種基于迭代器的遍歷方式:范圍for
范圍for的底層實現(xiàn)原理和迭代器沒兩樣,只不過看著比較高級。
void test_string() {Fan::string s1("hello world");//迭代器Fan::string::iterator it = s1.begin();while (it != s1.end()){cout << *it << " "; //h e l l o w o r l dit++;}cout << endl;//范圍forfor (auto& ch : s1) //加上引用,相當(dāng)于是每個字符的別名,便于修改{ch -= 1;}for (auto& ch : s1){cout << ch << " "; } }
為了驗證范圍for是基于迭代器的,我們把模擬迭代器的代碼注釋掉我們再看下結(jié)果:
我們可以看到當(dāng)我們把模擬迭代器的代碼注釋掉以后范圍for也不起作用了。?
5、增加的相關(guān)函數(shù)?
reserve擴容
reserve擴容只影響_capacity空間,不影響_size,其有以下兩點規(guī)則
- 當(dāng)n大于對象當(dāng)前的capacity時,將capacity擴大到n或者大于n。
- 當(dāng)n小于對象當(dāng)前的capacity時,無需操作。
//reserve擴容 void reserve(size_t n) {if (n > _capacity){char* tmp = new char[n + 1]; //每次開空間一定要多給一個字節(jié)給'\0'strcpy(tmp, _str);//釋放舊空間delete[] _str;//把新空間賦給_str_str = tmp;//更新容量_capacity_capacity = n;} }
resize?
resize是將字符串調(diào)整為n個字符的長度,不僅會改變_size大小,還會改變_capacity的大小。規(guī)則如下:
- 如果n小于當(dāng)前的_size長度,將_size縮小到n
- 如果n大于當(dāng)前的_size長度,將_size擴大到n,擴大的字符默認(rèn)為'\0'
//resize調(diào)整大小 void resize(size_t n, char ch = '\0') {//如果n<_size,就保留前n個字符即可,把下標(biāo)n置為'\0'if (n < _size){_size = n;_str[_size] = '\0';}else{//如果n>_capacity,就要擴容if (n > _capacity){reserve(n);}for (size_t i = _size; i < n; i++){//把剩余的字符置為ch_str[i] = ch;}_size = n;_str[_size] = '\0';} }
push_back追加字符
?首先我們要考慮需不需要擴容,如果需要,我們直接復(fù)用reserve函數(shù)進(jìn)行增容,追加字符以后我們要把最后一個下標(biāo)_size對應(yīng)的值置為'\0'。
//push_back void push_back(char ch) {//法一//先檢查是否需要擴容if (_size == _capacity){//復(fù)用reserve進(jìn)行擴容,如果一開始容量為0,記得處理,因為容量*2依舊為0reserve(_capacity == 0 ? 4 : _capacity * 2);}_str[_size] = ch;_size++;_str[_size] = '\0'; //注意最后一個值恒為'\0'以確保字符串的完整性 }
這里我們還可以使用后文寫好的insert尾插字符,因為當(dāng)insert函數(shù)中的pos為_size時即為尾插:
//push_back尾插字符 void push_back(char ch) {//法二:復(fù)用insert尾插入字符insert(_size, ch); }
append追加字符串
使用append追加字符串首先要做的就是判斷是否需要擴容,擴容后利用strcpy函數(shù)把追加的字符串拷貝到原字符串末尾即可,這里不需要額外處理'\0',因為strcpy默認(rèn)把'\0'拷貝過去。
//append void append(const char* str) {//統(tǒng)計追加字符串后的長度size_t len = _size + strlen(str);//判斷是否需要擴容if (len > _capacity){reserve(len);}//把字符串追加到末尾strcpy(_str + _size, str);_size = len; }
我們這里也可以使用后文的insert追加字符串來完成,因為當(dāng)pos為_size時,就是在尾部追加字符串。
void append(const char* str) {//法二:復(fù)用insert函數(shù)insert(_size, str); }
operator+=?
operator+=可以追加字符、字符串、對象等。
- 追加字符:直接復(fù)用push_back
//operator+=字符 string& operator+=(char ch) {//復(fù)用push_backpush_back(ch);return *this; }
- 追加字符串:直接復(fù)用append
//operator+=字符串 string& operator+=(const char* str) {//復(fù)用appendappend(str);return *this; }
insert?
insert的作用是在指定pos位置往后插入字符或者字符串。
- insert在pos位置插入字符
這里我們首先要判斷pos的合法性,然后再進(jìn)行挪動數(shù)據(jù)。我們從'\0'位置的下一個位置(_size+1)開始往前挪動。因此定義end指向'\0'后一個位置,當(dāng)end挪到與pos位置重合時停止,最后把插入的字符ch挪到下標(biāo)pos處。最后更新_size++。
//insert插入字符 string& insert(size_t pos, char ch) {assert(pos <= _size);if (_size == _capacity){//復(fù)用reserve進(jìn)行擴容,如果一開始容量為0,記得處理。否則容量*2依舊為0reserve(_capacity == 0 ? 4 : _capacity * 2);}//依次挪動size_t end = _size + 1;while (end > pos){_str[end] = _str[end - 1];end--;}//當(dāng)end挪動到pos的位置時停止挪動,并把ch賦值到pos的下標(biāo)處_str[pos] = ch;_size += 1;return *this; }
?測試如下:
void test_string() {Fan::string s("hello world");s.insert(6, '#');s += '#';cout << s.c_str() << endl; //hello #world#for (auto& ch : s){cout << ch << " ";}cout << "$" << endl; //h e l l o # w o r l d # $s += '\0';for (auto& ch : s){cout << ch << " ";}cout << "$" << endl; //h e l l o # w o r l d # $s.insert(0, '#');cout << s.c_str() << endl;//#hello #world# }
- insert在pos位置插入字符串
我們首先判斷是否需要擴容,接下來挪動數(shù)據(jù)。定義變量end為_size+len的位置,把pos處往后的字符串整體往后挪動直至空出插入字符串的空間。然后再利用strncpy函數(shù)把插入的字符串拷貝過去。
//insert插入字符串 string& insert(size_t pos, const char* str) {assert(pos <= _size);size_t len = strlen(str);if (len == 0){//如果傳進(jìn)來的字符串為空,直接返回即可return *this;}if (_size + len > _capacity){//判斷是否擴容reserve(_size + len);}size_t end = _size + len;//當(dāng)end>=pos+len時都不結(jié)束循環(huán)while (end >= pos + len){_str[end] = _str[end - len];end--;}//不能使用strcpy,因為會把\0也拷進(jìn)去,就會出錯strncpy(_str + pos, str, len);_size += len;return *this; }
?測試如下:
void test_string() {Fan::string s("hello world");s.insert(0, "xxx");cout << s.c_str() << endl; //xxxhello world }
6、刪除的相關(guān)函數(shù)
erase
如果給定刪除的長度len為npos無符號值,或者說len+pos的長度>=_size,那么我們可以直接把pos位置的值設(shè)定為'\0'即可,因為此時就是把pos后的數(shù)據(jù)全部刪除。除了此種特殊情況,其余的就是從pos+len處開始往前挪動直到_size+1為止。pos后的數(shù)據(jù)往前覆蓋即可。
//erase刪除 void erase(size_t pos, size_t len = npos) {assert(pos < _size);if (len == npos || pos + len >= _size){//此種情況是直接刪除pos后的所有數(shù)據(jù),直接把pos處設(shè)定為'\0'即可_str[pos] = '\0';_size = pos;}else{size_t begin = pos + len;while (begin <= _size){_str[begin - len] = _str[begin];begin++;}_size -= len;} }
測試如下:
void test_string() {Fan::string s("hello world");s.insert(0, "xxx");cout << s.c_str() << endl; //xxxhello worlds.erase(0, 3);cout << s.c_str() << endl; //hello world }
clear清除數(shù)據(jù)
clear函數(shù)是用來清除原字符串的所有數(shù)據(jù),但其空間仍保留。所以我們只需要把下標(biāo)0的位置置為'\0',并把有效字符個數(shù)_size置為0即可。
//clear清除數(shù)據(jù) void clear() {_str[0] = '\0';_size = 0; }
7、查找的相關(guān)函數(shù)
find
find函數(shù)分為查找字符和字符串
- find查找字符:
我們直接進(jìn)行遍歷即可:
//find查找字符: size_t find(char ch, size_t pos = 0) {for (; pos < _size; pos++){if (_str[pos] == ch)return pos;}//沒有找到就返回npos,-1return npos; //-1 }
- find查找字符串:
這里我們可以直接復(fù)用C語言的strstr函數(shù)進(jìn)行查找,不過該函數(shù)返回的是地址,我們想要獲得下標(biāo)直接利用地址相減即可。
//find查找字符串: size_t find(const char* str, size_t pos = 0) {//直接復(fù)用C語言函數(shù)strstr即可,strstr函數(shù)返回的是地址const char* p = strstr(_str + pos, str); if (p == nullptr){return npos;}else{//返回下標(biāo)直接用p-str即可return p - _str;} }
8、c_str獲取字符串
?c_str用于獲取C類型的字符串,直接返回字符串首地址。
//c_str 獲取C形式的字符串 const char* c_str()const //最好加上const,便于普通對象和const對象均可調(diào)用 {return _str; }
9、swap交換函數(shù)
swap函數(shù)用于交換兩個對象的數(shù)據(jù),我們可以通過復(fù)用庫里面的swap函數(shù)來完成,但是要在前面加上作用域限定符"::"。讓編譯器在全局域的庫里調(diào)用swap函數(shù)。
//swap交換函數(shù) void swap(string& s) {std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity); }
10、非成員函數(shù)
關(guān)系運算符函數(shù)重載
關(guān)系運算符有==、!=、<、<=、>、>=這6類,我們在先前的日期類中已經(jīng)講過類似的。這里的關(guān)系運算符重載我們不把它放到成員函數(shù)里面。
我們直接復(fù)用庫里面的strcmp函數(shù)進(jìn)行字符串大小比較即可。此外,和日期類一樣,在寫好<和==的重載后,剩下的4個關(guān)系運算符我們直接進(jìn)行復(fù)用即可。
//1.operator< bool operator<(const string& s1, const string& s2) {return strcmp(s1.c_str(), s2.c_str()) < 0; } //2.operator== bool operator==(const string& s1, const string& s2) {return strcmp(s1.c_str(), s2.c_str()) == 0; }
剩下4個關(guān)系運算符我們直接復(fù)用上面兩個:
//3、operator<= bool operator<=(const string& s1, const string& s2) {return s1 < s2 || s1 == s2; } //4、operator> bool operator>(const string& s1, const string& s2) {return !(s1 <= s2); } //5、operator>= bool operator>=(const string& s1, const string& s2) {return !(s1 < s2); } //6、operator!= bool operator!=(const string& s1, const string& s2) {return !(s1 == s2); }
<<流插入運算符重載
?這里我們可以通過范圍for來完成<<運算符的重載
//流插入運算符重載 ostream& operator<<(ostream& out, const string& s) {for (auto ch : s){out << ch;}return out; }
>>流提取運算符重載
這里實現(xiàn)的過程中我們要注意的是當(dāng)遇到空格或者換行符就要停止讀取。此外,在一開始要記得調(diào)用clear函數(shù)把原字符串的所有數(shù)據(jù)進(jìn)行清空。然后再正常往后輸入數(shù)據(jù),否則新數(shù)據(jù)累加到原數(shù)據(jù)后面,就達(dá)不到預(yù)期效果。
//流提取運算符重載 istream& operator>>(istream& in, string& s) {//法一//先把原字符串里的數(shù)據(jù)清空才可以輸入新的數(shù)據(jù)s.clear();char ch;ch = in.get(); //使用get()函數(shù)才能獲取空格或者換行字符while (ch != ' ' && ch != '\n'){s += ch;ch = in.get();}return in; }
這里有一個缺陷,如果我們頻繁輸入大量字符就會導(dǎo)致多次擴容,擴容會在效率上有所損耗,因此我們可以提前開辟一個128字節(jié)大小的數(shù)組,我們把每次輸入的字符放到數(shù)組里面,最后遇到停止的符號時我們就把字符+=到字符串s上。如若下標(biāo)加到127,我們就把數(shù)組的字符+=到字符串s上,然后重置數(shù)組為'\0',更新下標(biāo)為0即可。
//流提取運算符重載 istream& operator>>(istream& in, string& s) {//法二//先把原字符串里的數(shù)據(jù)清空才可以輸入新的數(shù)據(jù)s.clear();char ch;ch = in.get(); //使用get()函數(shù)才能獲取空格或者換行字符char buff[128] = { '\0' };size_t i = 0;while (ch != ' ' && ch != '\n'){buff[i++] = ch;if (i == 127){s += buff;memset(buff, '\0', 128);i = 0;}ch = in.get();}s += buff;return in; }
getline函數(shù)
getline函數(shù)與上述寫的<<流提取運算符重載非常的相似,唯一不同的地方在于getline只有在遇到換行符才停止讀取,而<<在遇到換行符停止外,遇到空格也會停止讀取。
//getline函數(shù) istream& getline(istream& in, string& s) {s.clear();char ch;ch = in.get();//getline函數(shù)只有在遇到換行符才會停止while (ch != '\n'){s += ch;ch = in.get();}return in; }
整型和字符串之間的轉(zhuǎn)換函數(shù)
stoi和to string
?
代碼演示:
void test_string() {int i;cin >> i;string s = to_string(i);int val = stoi(s); }