🐱作者:一只大喵咪1201
做網(wǎng)站、網(wǎng)站設(shè)計(jì),成都做網(wǎng)站公司-創(chuàng)新互聯(lián)建站已向千余家企業(yè)提供了,網(wǎng)站設(shè)計(jì),網(wǎng)站制作,網(wǎng)絡(luò)營銷等服務(wù)!設(shè)計(jì)與技術(shù)結(jié)合,多年網(wǎng)站推廣經(jīng)驗(yàn),合理的價格為您打造企業(yè)品質(zhì)網(wǎng)站。
🐱專欄:《C++學(xué)習(xí)》
🔥格言:你只管努力,剩下的交給時間!
上篇文章中本喵介紹了C++標(biāo)準(zhǔn)庫中string類的使用,下面本喵來模擬實(shí)現(xiàn)一下string類。庫中的string類是將模板實(shí)例化并且typedef后得到的類,所以我們直接實(shí)現(xiàn)類,而忽略模板。
string類的模擬實(shí)現(xiàn)首先就是來實(shí)現(xiàn)類和對象中的四大默認(rèn)成員函數(shù),構(gòu)造函數(shù),拷貝構(gòu)造函數(shù),析構(gòu)函數(shù),賦值運(yùn)算符重載函數(shù)。
🍛構(gòu)造函數(shù)//使用C字符串構(gòu)造
string(const char* str = "")
{ _size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
這是使用C字符串進(jìn)行的構(gòu)造。
- 使用C字符串進(jìn)行構(gòu)造的時候,需要加一個缺省值,當(dāng)創(chuàng)建一個空的string的時候就會使用到這個缺省值。
- _size表示的是有效字符的個數(shù),不包括’\0’,是通過C語言中的strlen函數(shù)求出來的。
- _capacity表示的是有效空間的大小,同樣不包括’\0’所占的空間,在最初構(gòu)造一個string對象的時候,讓_size和_capacity的大小一樣。
- 使用new開辟動態(tài)空間來存放字符串的時候,開辟的空間需要比_capacity大一個,這多出來的一個是專門用來存放’\0’的,一個字符串中只有一個’\0’。
為了查看效果,需要實(shí)現(xiàn)一個打印字符串的成員函數(shù):
//字符串打印
const char* c_str()
{ return _str;
}
可以看到,使用C字符串,空的string,以及使用string類都可以實(shí)現(xiàn)一個新的string類對象的創(chuàng)建。
編譯器自動生成的默認(rèn)拷貝構(gòu)造函數(shù)進(jìn)行的淺拷貝,如下圖:
當(dāng)使用s1來拷貝構(gòu)造s2的時候,就會導(dǎo)致如上圖所示的問題,新的s2指向的動態(tài)空間和s1指向的動態(tài)空間是同一塊空間,這樣不僅在釋放的時候會導(dǎo)致錯誤,而且在使用的時候也會在改變s2中的內(nèi)容的同時將s1中的內(nèi)容改變,所以對于拷貝構(gòu)造函數(shù)需要進(jìn)行深拷貝。
所謂深拷貝就是將內(nèi)容全部復(fù)制過來,但是s1和s2使用的是兩塊空間:
而深拷貝的實(shí)現(xiàn)方式又有倆種,分別是傳統(tǒng)方式和現(xiàn)代方式。
傳統(tǒng)方式實(shí)現(xiàn):
//傳統(tǒng)拷貝構(gòu)造函數(shù)
string(const string& s)
{ _size = s._size;
_capacity = s._capacity;
_str = new char[_capacity + 1];
strcpy(_str, s._str);
}
這種方式在新的string對象創(chuàng)建的時候新開辟了一個和原對象一樣大小的空間,并且將原對象中的字符串拷貝過來。
現(xiàn)代方式實(shí)現(xiàn):
//現(xiàn)代拷貝構(gòu)造函數(shù)
string(const string& s)
:_str(nullptr)
,_size(0)
,_capacity(0)
{ string tmp(s._str);
std::swap(_str, tmp._str);
std::swap(_size, tmp._size);
std::swap(_capacity, tmp._capacity);
}
上面的便是現(xiàn)代版本的拷貝構(gòu)造函數(shù)實(shí)現(xiàn)的思想,下面本喵用圖來給大家解釋:
tmp通過s1中的字符串構(gòu)造了一個新的string類對象,而此時s2的_str指向的空間中是隨機(jī)值。
為了讓s2中的內(nèi)容變成和s1中的一樣,只要將臨時的類對象tmp中內(nèi)容拿過來即可。
此時就將tmp和s2中的內(nèi)容全部進(jìn)行了交換,而s2也沒有開辟新的空間就實(shí)現(xiàn)了拷貝構(gòu)造的目的。
注意:
紅色框中的初始化列表中,必須將s2的_str置為空,否則會編譯報(bào)錯。
- s2拿走了tmp中的_str,而將自己的_str給了tmp,當(dāng)tmp生命周期結(jié)束的時候,會釋放空間,如果不是nullptr就會釋放原本不屬于tmp而屬于s2的空間,所以就會報(bào)錯.。
swap函數(shù)的實(shí)現(xiàn):
上面使用的swap是標(biāo)準(zhǔn)算法庫中的swap函數(shù),但是在string標(biāo)準(zhǔn)庫中還有一個swap函數(shù):
這倆個函數(shù)是有區(qū)別的。
- 標(biāo)準(zhǔn)算法庫中的swap是一個函數(shù)模板,它會自動推演數(shù)據(jù)類型,如果使用這個函數(shù)進(jìn)行倆個string類對象的交換,會發(fā)生三次拷貝構(gòu)造,系統(tǒng)開銷比較大。
- 所以在交換string類對象的時候使用string庫中的swap函數(shù)比較合適。
既然是string的模擬實(shí)現(xiàn),所以同樣也需要我們自己實(shí)現(xiàn)一個。
//交換函數(shù)
void swap(string& s)
{ std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
在string中的swap同樣是通過標(biāo)準(zhǔn)算法庫中的swap實(shí)現(xiàn)的,細(xì)心的小伙伴肯定發(fā)現(xiàn)了一個問題,在上面代碼中,使用標(biāo)準(zhǔn)算法庫中的swap交換的數(shù)據(jù)都是內(nèi)置類型的,而函數(shù)模板中有一個語句c(a),也就是用a來拷貝構(gòu)造c。
如果a和c都是自定義類型我們不難理解,但是此時的a和c都是內(nèi)置類型。
說明:
內(nèi)置類型也有拷貝構(gòu)造函數(shù)以及析構(gòu)函數(shù),一般情況下是不用考慮這個的,而且在C語言中是沒有的,但是為了迎合模板,就不得不有。
可以看到,內(nèi)置類型也可以使用拷貝構(gòu)造以及賦值運(yùn)算符重載等方式來創(chuàng)建。
此時現(xiàn)代方式的拷貝構(gòu)造函數(shù)就有了更加簡潔的寫法:
string(string& s)
:_str(nullptr)
,_size(0)
,_capacity(0)
{ string tmp(s._str);
swap(tmp);
}
此時就非常簡潔的實(shí)現(xiàn)了深度拷貝。
🍛賦值運(yùn)算符重載和拷貝構(gòu)造函數(shù)一樣,如果使用編譯器自動生成的默認(rèn)賦值運(yùn)算符重載函數(shù),也是會有淺拷貝的問題,所以賦值也是需要深度拷貝的。同樣的,也是有傳統(tǒng)和現(xiàn)代倆種實(shí)現(xiàn)方式。
傳統(tǒng)方式:
//傳統(tǒng)賦值運(yùn)算符重載函數(shù)
string& operator=(const string& s)
{ if (this != &s)
{ _size = s._size;
_capacity = s._capacity;
delete[] _str;
_str = new char[_capacity + 1];
strcpy(_str, s._str);
}
return *this;
}
和拷貝構(gòu)造函數(shù)一樣,也開辟一個和原本類對象一樣大小的空間,然后將內(nèi)容賦值過來。
- 需要判斷一下是否是自己給自己賦值,如果是的話就直接返回this指向的內(nèi)容。
- 給類對象賦值以后,這個類對象原本指向的空間不再使用了,因?yàn)殚_辟了新的空間,所以要將原本指向的空間使用delete釋放掉。
現(xiàn)代方式:
和拷貝構(gòu)造函數(shù)現(xiàn)代方式實(shí)現(xiàn)的思路一樣,也是拿其他對象的構(gòu)造成果,堅(jiān)決不自己開辟空間。
//現(xiàn)代賦值運(yùn)算符重載函數(shù)
string& operator=(string& s)
{ if (this != &s)
{ //string tmp(s._str);
string tmp(s);
swap(tmp);
}
return *this;
}
使用s拷貝構(gòu)造一個臨時對象tmp,然后將tmp中的內(nèi)容拿走,最后返回this指針的內(nèi)容。
既然都是找一個中間的打工仔,而不自己實(shí)現(xiàn),可以直接使用形參這個打工仔:
string& operator=(string s)
{ swap(s);
return *this;
}
此時就更加的簡潔,而且形參的地址必定和this中的值不同,所以都不用排除自己給自己賦值的情況。
🍛析構(gòu)函數(shù)
- 現(xiàn)代版本的深度拷貝僅是為了代碼更加簡潔而不考慮效率。
析構(gòu)函數(shù)沒有什么要點(diǎn),非常的簡單:
//析構(gòu)函數(shù)
~string()
{ delete[] _str;
_size = 0;
_capacity = 0;
}
直接使用delete釋放空間即可。
🍚常用的string接口 🍛[]操作符重載在使用string的時候,可以想方法C字符串?dāng)?shù)組一樣使用[]來訪問string類對象中的字符,下面本喵來給大家看看它是如何實(shí)現(xiàn)的:
//[]重載
//可讀可寫
char& operator[](size_t pos)
{ assert(pos< _size);
return _str[pos];
}
//可讀不可寫
const char& operator[](size_t pos) const
{ assert(pos< _size);
return _str[pos];
}
操作符[]的重載有倆個重載函數(shù),一個是可讀可寫的,一個是只讀不可寫的。
可以看到,成果的將s1中字符串的前5個字符的ASCII碼都加了1。
我們指定,迭代器是行為上像指針一樣的東西,在string類中,迭代器就是一個指針:
typedef char* iterator;
此時一個迭代器類型便實(shí)現(xiàn)好了,有了迭代器后就需要有對應(yīng)的接口來配合迭代器使用。
//begin()
iterator begin()
{ return _str;
}
//end()
iterator end()
{ return _str + _size;
}
此時倆個和迭代器最常用的接口便實(shí)現(xiàn)好了。
通過迭代器,將s1中的內(nèi)容全部打印了出來。
范圍for:
范圍for一直給我們的映像都很神奇,現(xiàn)在可以揭下它的神秘面紗了。
此時范圍for是正常的。
僅僅將begin函數(shù)中的b換成大寫的B。
在執(zhí)行的時候報(bào)了一大堆的錯誤,都是和begin有關(guān)的。
- 在編譯的時候,編譯器會將范圍for的代碼轉(zhuǎn)換成使用迭代器的代碼,然后再進(jìn)行編譯。
- 此時begin函數(shù)沒有了,所以使用迭代器的方式就無法實(shí)現(xiàn)了,所以就會報(bào)錯。
編譯器是不認(rèn)識范圍for這個語法的,所以它必須進(jìn)行轉(zhuǎn)換以后編譯器才能夠認(rèn)識,而且這個轉(zhuǎn)換是寫死了的,必須使用迭代器iterator,begin,end函數(shù),少一個都不行。
🍛size()和capacity()成員函數(shù)size()是用來獲取效字符的個數(shù)的,capacity()是用來獲取有效空間的大小的。
//size
size_t size() const
{ return _size;
}
//capacity()
size_t capacity() const
{ return _capacity;
}
🍚增
🍛改變?nèi)萘?ol>//reserve()
void reserve(size_t n)
{ if (n >_capacity)
{ char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
該函數(shù)的作用就是在擴(kuò)容,和C語言中的realloc的作用相似,但是C++中并沒有realloc這樣的函數(shù),所以擴(kuò)容就需要我們手動擴(kuò)容,就在新開辟一塊空間,并且將舊空間中的內(nèi)容拷貝進(jìn)去,再將舊空間釋放掉。
//resize()
void reszie(size_t n, char ch = '\0')
{ if (n<= _size)
{ _size = n;
_str[_size] = '\0';
}
else
{ reserve(n);
for (int i = _size; i< n; ++i)
{_str[i] = ch;
}
_size = n;
_str[_size] = '\0';
}
}
resize分三種情況:
- n小于等于原本的_size,此時是在縮容,只需要將_size的值改為n,并且將下標(biāo)為n的位置處放一個’\0’。
- n大于原本的_size,并小于原本的_capacity,此時不需要進(jìn)行擴(kuò)容只需要將原本的_size設(shè)置成n即可,并且將相比于原本字符多出來的位置用形參ch來填充,如果沒有傳ch,就使用缺省值’\0’。
- n大于原本的_capacity,此時需要進(jìn)行擴(kuò)容,之后的操作和上面的一樣。
運(yùn)行結(jié)果符號預(yù)期。
//push_back()
void push_back(char ch)
{ if (_size == _capacity)
{ size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
_str[_size++] = ch;
_str[_size] = '\0';
}
尾插插入的只有一個字符串,而且是在字符串的末尾插入,在插入之前需要判斷容量是否夠用,如果不夠則需要擴(kuò)容。
- 當(dāng)原本的容量就是0的時候,就不能簡單二倍擴(kuò)容,因?yàn)?乘2以后還是0,就需要給定一個初始容量,這里本喵給的初始容量是4。
//append()
void append(const char* str)
{ size_t len = strlen(str);
if (_size + len >_capacity)
{ reserve(_size + len);
}
strcpy(_str + _size, str);
_size += len;
}
尾部插入字符串。同樣需要判斷是否需要擴(kuò)容,之后在原本的’\0’位置處開始,放入插入的字符串,因?yàn)椴迦氲淖址詭А痋0’,所以插入以后不需要自己再寫’\0’。
//+=一個字符
string& operator+=(char ch)
{ push_back(ch);
return *this;
}
//+=字符串
string& operator+=(const char* str)
{ append(str);
return *this;
}
最常使用的就是+=,因?yàn)樗目勺x性高,而它實(shí)現(xiàn)的原理就是在運(yùn)算符重載的時候復(fù)用了push_back和append函數(shù)。
+=一個字符和一個字符串都實(shí)現(xiàn)了。
- 第一個紅色框中的方法,如果不將pos強(qiáng)轉(zhuǎn)為int類型的話,當(dāng)在0位置插入字符的時候,就會在while的條件判斷時發(fā)生隱式類型轉(zhuǎn)換,int類型的end提升成size_t,此時就不會出現(xiàn)負(fù)數(shù),就會造成死循環(huán)。
在挪動數(shù)據(jù)的時候同樣有倆種方式,其中第一種和上面的插入一個字符時的注意點(diǎn)一樣。
下面本喵來畫圖分析一下,插入的過程:
- 移動完數(shù)據(jù)以后,將要插入的字符串,除’\0’以外的所有字符復(fù)制到pos位置開始的空間中,如上圖中黃色線。
和我們分析的結(jié)果一致。
- 靜態(tài)成員變量的聲明在類中,定義必須在類外,但是有一個特殊類型,整型家族的靜態(tài)成員就可以在聲明的時候給一個缺省值。
- 由于是size_t類型的-1,所以它的實(shí)際大小就是32個1的二進(jìn)制,轉(zhuǎn)換成十進(jìn)制大致是42億9千萬。
npos也代表值string類對象字符串的末尾。
//erase
string& erase(size_t pos, size_t len = npos)
{ assert(pos< _size);
if (len == npos || pos + len >= _size)
{ _str[pos] = '\0';
_size = pos;
}
else
{ strcpy(_str + pos, _str + pos + len);
_size -= len;
}
return *this;
}
如果要刪除的字符串長度已經(jīng)超出了現(xiàn)有字符串的長度,那么就從指定位置開始全部刪除完,否則就需要將對應(yīng)位置以后相應(yīng)長度的字符串刪除后,再將剩下的字符移動到前面。
//clear()
void clear()
{ _size = 0;
_str[_size] = '\0';
}
clear是情況所有字符串,但是不改變?nèi)萘俊?/p>
對應(yīng)的結(jié)果是符號我們的預(yù)期的。
//find()
//查找一個字符
size_t find(char ch, size_t pos = 0) const
{ assert(pos< _size);
while (pos< _size)
{ if (_str[pos] == ch)
{return pos;
}
pos++;
}
return npos;
}
//查找一個字符串
size_t find(const char* str, size_t pos = 0) const
{ assert(pos< _size);
const char* ptr = strstr(_str + pos, str);
if (ptr == nullptr)
{ return npos;
}
else
{ return ptr - _str;
}
}
這是查找一個字符和一個字符串的代碼,找到了就返回下標(biāo),找不到就返回npos的值,在查找字符串的時候,使用了C語言中的strstr查找字串的函數(shù)。
結(jié)果也符合我們的預(yù)期。
在前面本喵已經(jīng)實(shí)現(xiàn)過一次了,這里再提及一下。
ostream& operator<<(ostream& out, const string& s)
{for (size_t i = 0; i< s.size(); ++i)
{ out<< s[i];
}
return out;
}
istream& operator>>(istream& in, string& s)
{s.clear();
char ch = in.get();
while (ch != ' ' && ch != '\n')
{ s += ch;
ch = in.get();
}
return in;
}
- cin和scanf一樣,當(dāng)遇到空格或者換行符時,自動將字符串分割
- 如果輸入的內(nèi)容很多的話,就會不停地+=,也會導(dǎo)致不停擴(kuò)容,導(dǎo)致系統(tǒng)開銷較大。
改進(jìn):
istream& operator>>(istream& in, string& s)
{s.clear();
char buff[128] = {'\0' };
size_t i = 0;
char ch = in.get();
while (ch != ' ' && ch != '\n')
{if (i == 127)
{s += buff;
i = 0;
}
buff[i++] = ch;
ch = in.get();
}
if (i >= 0)
{buff[i] = '\0';
s += buff;
}
return in;
}
先開辟一個數(shù)組,將輸入的字符放在數(shù)組中,當(dāng)數(shù)組滿了后進(jìn)行一次擴(kuò)容,此時即使輸入很長的字符串,擴(kuò)容的頻率也不會很高。
🍚總結(jié)我們自己模擬實(shí)現(xiàn)的string并不是要和標(biāo)準(zhǔn)庫中的完全一樣,只是為了了解string的底層原理,讓我們在使用起來更加得心應(yīng)手。
你是否還在尋找穩(wěn)定的海外服務(wù)器提供商?創(chuàng)新互聯(lián)www.cdcxhl.cn海外機(jī)房具備T級流量清洗系統(tǒng)配攻擊溯源,準(zhǔn)確流量調(diào)度確保服務(wù)器高可用性,企業(yè)級服務(wù)器適合批量采購,新人活動首月15元起,快前往官網(wǎng)查看詳情吧
分享標(biāo)題:【C++學(xué)習(xí)】string的模擬實(shí)現(xiàn)-創(chuàng)新互聯(lián)
文章分享:http://jinyejixie.com/article12/djssdc.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供App設(shè)計(jì)、虛擬主機(jī)、網(wǎng)站改版、搜索引擎優(yōu)化、品牌網(wǎng)站建設(shè)、網(wǎng)站維護(hù)
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會在第一時間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時需注明來源: 創(chuàng)新互聯(lián)
猜你還喜歡下面的內(nèi)容