前言:上篇文章我們對string類及其常用的接口方法的使用進行了分享,這篇文章將著重進行對這些常用的接口方法的內(nèi)部細節(jié)進行分享和模擬實現(xiàn)。
目錄
一.基礎(chǔ)框架
二.遍歷字符串
1.[]運算符重載
2.迭代器
3.范圍for
三.常用方法
1.增加
2.刪除
3.調(diào)整
4.交換
5.查找
6.截取
7.比較
四.流操作
總結(jié)
一.基礎(chǔ)框架
首先我們要清楚,string類定義的是字符串對象,所以就類似于線性表,有長度,容量等成員變量:
class string
{
public:
//構(gòu)造函數(shù)
string(const char* str = "")
:_size(strlen(str))
{
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
//析構(gòu)函數(shù)
~string()
{
delete[] _str;
_str = nullptr;
_size = 0;
_capacity = 0;
}
//轉(zhuǎn)換C語言格式
const char* c_str() const
{
return _str;
}
//清除
void clear()
{
_size = 0;
_str[_size] = '\0';
}
//深拷貝s2(s1)
string(const string& s)
{
_str = new char[_capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
//s1 = s2
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;
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
其中不能缺少的就是構(gòu)造函數(shù)、析構(gòu)函數(shù)和拷貝構(gòu)造函數(shù),這里我們直接用缺省函數(shù)將無參構(gòu)造和帶參構(gòu)造結(jié)合為一體。
但是由于如果我們不自己寫一個深拷貝函數(shù),就會默認執(zhí)行淺拷貝成員函數(shù),這樣會導(dǎo)致兩個字符串同源,所以需要給出深拷貝函數(shù)。
值得注意的是真正的strlen不會統(tǒng)計字符串中的‘\0’,所以我們給_str開空間時應(yīng)+1。
二.遍歷字符串
1.[]運算符重載
上篇文章中我們知道遍歷字符串有三種方式:[]運算符重載,迭代器,以及范圍for。下面我們就來一一實現(xiàn)。
首先我們需要將字符串的長度方法size和容量方法capacity定義出來:
//長度
size_t size() const
{
return _size;
}
//容量
size_t capacity() const
{
return _capacity;
}
一般情況下當方法里調(diào)用的成員無需發(fā)生改變時,都會將這些方法用const修飾。?
而[]運算符重載自然是通過運算符重載函數(shù)來實現(xiàn):
//遍歷
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
這里我們添加assert函數(shù)來斷言,防止越界訪問。
返回值使用引用格式,能夠?qū)崿F(xiàn)可讀可寫:
?但此時會產(chǎn)生一個問題,如果我想讓一個const修飾的對象來調(diào)用該方法,就會導(dǎo)致權(quán)限放大而出錯。
如果給這個方法加上const,那我們就無法修改其內(nèi)容了。
所以我們使用函數(shù)重載,為其單獨創(chuàng)造一個只讀的const修飾的函數(shù)方法:
const char& operator[](size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
2.迭代器
我們已經(jīng)了解迭代器的本質(zhì)和指針類似,我們這里我們就先用指針的實現(xiàn)迭代器的功能:
//迭代器
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
用typedef將char*指針重命名為iterator,再定義出begin和end兩個方法,便能實現(xiàn)迭代器功能:
值得注意的是,const修飾的對象想要調(diào)用迭代器,也必須調(diào)用對應(yīng)const修飾的迭代器,所以迭代器我們也需要進行重載:
typedef const char* const_iterator;
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
迭代器這樣的模擬實現(xiàn)方法也能幫助我們更深入的了解其內(nèi)部構(gòu)造。
3.范圍for
范圍for實際上并沒有那么復(fù)雜,其本質(zhì)也是迭代器。
所以只要有迭代器的存在,保證iterator、begin。end這些單詞不變,不管是我們自己實現(xiàn)的還是C++庫內(nèi)的,都可以使用范圍for:
但是如果iterator、begin。end這些單詞發(fā)生變化,就無法在使用范圍for。?
三.常用方法
1.增加
增加無非有四種方式:尾插單個字符push_back、尾插字符串a(chǎn)ppend和+=運算符重載,以及任意位置的插入insert,增加字符就意味著要考慮擴容問題,這就要實現(xiàn)reserve方法來配合使用。
尾插單個字符可以通過每次擴容兩倍容量,但是如果尾插一個長度為len的字符串,每次擴容兩倍或是更多倍都并不一定就能滿足容量, 所以這里直接擴容size+len個空間:
//擴容
void reserve(size_t len)
{
if (len > _capacity)
{
char* tmp = new char[len + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = len;
}
}
//尾插單字符
void push_back(char ch)
{
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
_str[_size] = ch;
_size++;
_str[_size] = '\0';
}
//尾插字符串
void append(const char* str)
{
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
strcpy(_str + _size, str);
_size += len;
}
因為字符串的末尾都要有'\0'的存在,所以擴容時,要始終保持實際容量比字符串容量多1。
當尾插單個字符時,因為該字符是直接覆蓋了'\0',所以尾插之后要再字符串末尾再補上'\0'。
而尾插字符串時,因為strcpy在進行拷貝時也會直接將'\0'拷貝,所以無需再補。?
而+=運算符的重載,就是以上述兩個方法為基層的擴展:
//+=運算符重載
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
string& operator+=(const char* str)
{
append(str);
return *this;
}
測試如下:
任意位置的插入, 則需要進行字符串的挪動:
//任意位置插入
//單字符
void insert(char ch, size_t pos)
{
assert(pos <= _size);
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
size_t end = _size + 1;
while (end > pos)
{
_str[end] = _str[end - 1];
end--;
}
_str[pos] = ch;
_size++;
}
//字符串
void insert(const char* str, size_t pos)
{
assert(pos <= _size);
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
size_t end = _size + len;
while (end >= pos + len)
{
_str[end] = _str[end - len];
end--;
}
for (size_t i = 0; i < len; i++)
{
_str[pos++] = str[i];
}
_size += len;
}
在進行字符串插入時,要注意的是我們要從pos位置將后邊的字符向后空出len個位置。
同時我們不能使用strcpy進行插入,因為它會將要插入的字符串的'\0'一并插入,導(dǎo)致一個位置的字符被覆蓋,所以這里我們采用循環(huán)插入。
?測試如下:
2.刪除
string類中只有一個常用的刪除方法:erase,它的功能是在指定位置后刪除若干個字符,如果沒有指定要刪除的字符數(shù)量,則默認將后邊的字符全刪除:
//刪除
void erase(size_t pos, size_t len = npos)
{
assert(pos < _size);
if (len == npos || len >= _size - pos)
{
_str[pos] = '\0';
_size = pos;
}
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
采用缺省函數(shù)的方式,如果我們沒有傳入len,他就會等于npos,npos在C++中表示一個常數(shù),表示不存在的位置。
如果len無傳入或是len的長度不小于要刪除的字符串長度,這都可以認為是要將pos位置后的字符串全部刪除,此時便可直接在pos位置用'\0'。
使用npos需要在類中定義public成員變量:
?? ?public:
?? ??? ?static const int npos;
以及在類外賦值:
?? ?const int string::npos = -1;
?測試如下:
3.調(diào)整
在string中有一個方法可以兼?zhèn)湓黾雍蛣h除兩種簡單功能,名為resize,它的作用是調(diào)整字符串:
傳入一個參數(shù)n,和一個字符ch,如果當前字符串長度小于n,則擴容字符串長度至n個,并將多出的位置用字符ch填充,如果不傳字符ch,則默認填充'\0'。
如果當前字符串長度大于n,則將字符串長度縮減到n:
//調(diào)整
void resize(size_t n, char ch = '\0')
{
if (n <= _size)
{
_str[n] = '\0';
_size = n;
}
else
{
reserve(n);
for (size_t i = _size; i < n; i++)
{
_str[i] = ch;
}
_str[n] = '\0';
_size = n;
}
}
測試如下:
4.交換
自己實現(xiàn)string類中的swap交換函數(shù),有一個很好用的方法,那就是借用std庫中的swap函數(shù):
//交換
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
?因為std庫中的swap函數(shù)是模版函數(shù),可以進行任意類型的交換,所以我們直接投機取巧,將兩者的成員變量依次進行交換,測試如下:
?但是這樣的寫法并不是我們所熟悉的swap(s1,s2),所以我們可以通過函數(shù)重載擴展一下:
void swap(string& x, string& y)
{
x.swap(y);
}
值得注意的是這個函數(shù)要寫在string類的外邊,按照就近原則去調(diào)用它,否則會默認先調(diào)用庫里的模版swap函數(shù)。?
測試如下:
5.查找
string類中的查找也分為查找單個字符、查找字符串以及在指定的pos位置向后去查找,找到返回下標,找不到返回npos,所以依然要使用缺省函數(shù):
//查找
//單字符
size_t find(char ch, size_t pos = 0) const
{
assert(pos < _size);
for (size_t i = pos; i < _size; i++)
{
if (_str[i] == ch)
return i;
}
return npos;
}
//字符串
size_t find(const char* str, size_t pos = 0) const
{
assert(pos < _size);
const char* p = strstr(_str + pos, str);
if (p)
return p - _str;
else
return npos;
}
在查找字符串時我們使用到了strstr函數(shù),其返回值為所找到的字符串的首字符指針。
測試如下:
6.截取
截取字符串方法substr,其作用是從字符串的pos位置開始向后截取len長度的字符,當然無論是pos位置還是長度len都可以沒有,依然是缺省函數(shù):
//截取
string substr(size_t pos = 0, size_t len = npos)
{
string sub;
if (len >= _size - pos)
{
for (size_t i = pos; i < _size; i++)
{
sub += _str[i];
}
}
else
{
for (size_t i = pos; i < pos + len; i++)
{
sub += _str[i];
}
}
return sub;
}
測試如下:
7.比較
字符串直接的比較需要我們實現(xiàn)運算符重載:
bool operator==(const string& s1, const string& s2)
{
int ret = strcmp(s1.c_str(), s2.c_str());
return ret == 0;
}
bool operator<(const string& s1, const string& s2)
{
int ret = strcmp(s1.c_str(), s2.c_str());
return ret < 0;
}
bool operator<=(const string& s1, const string& s2)
{
return s1 < s2 || s1 == s2;
}
bool operator>(const string& s1, const string& s2)
{
return !(s1 <= s2);
}
bool operator>=(const string& s1, const string& s2)
{
return !(s1 < s2);
}
bool operator!=(const string& s1, const string& s2)
{
return !(s1 == s2);
}
這一塊的方法,我們建議實現(xiàn)在類外,定義兩個參數(shù),這樣能夠允許一個字符串和一個string對象進行比較。
因為在類內(nèi)定義因為默認類內(nèi)的成員函數(shù)的第一個參數(shù)都是隱藏的非靜態(tài)string對象,所以靜態(tài)的普通字符串傳入就會使權(quán)限放大而出錯。
測試如下:
?
四.流操作
直接上代碼:
//流輸出
ostream& operator<<(ostream& out, const string& s)
{
for (auto ch : s)
{
out << ch;
}
return out;
}
//流提取
istream& operator>>(istream& in, string& s)
{
s.clear();
char ch;
ch = in.get();
while (ch != ' ' && ch != '\n')
{
s += ch;
ch = in.get();
}
return in;
}
輸出較為簡單,直接使用范圍for循環(huán)輸出。
而對于提取到的字符會直接覆蓋s中原有的字符串,所以要先進行清除;此外,因為in默認會跳過空格和回車而不提取它們,這會導(dǎo)致死循環(huán),所以我們使用in.get()函數(shù)來提取。
測試如下:
總結(jié)
關(guān)于string類及其內(nèi)部常用方法的模擬實現(xiàn)就分享到這里啦。文章來源:http://www.zghlxwxcb.cn/news/detail-838908.html
最后希望能得到您的一鍵三連支持,我們下期再見!文章來源地址http://www.zghlxwxcb.cn/news/detail-838908.html
到了這里,關(guān)于C++——string模擬實現(xiàn)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!