扫二维码与项目经理沟通
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流
主要讲述string的模拟实现。
成都创新互联是一家专业提供海丰企业网站建设,专注与网站建设、成都做网站、H5建站、小程序制作等业务。10年已为海丰众多企业、政府机构等服务。创新互联专业网站制作公司优惠进行中。文章目录
1)、总言
对string类,由于库函数中本身有一个,为了避免冲突,这里我们的处理方式有两种:其一是更改类名称,其二是为我们自己模拟实现的类添加命名空间。
本文章中选择了第二种处理方法:
namespace mystring//命名空间
{class string//我们模拟实现的类
{public:
private:
char* _str;
size_t _size;
size_t _capacity;
};
}
1)、模拟实现一:
namespace mystring
{class string
{public:
//string类的构造:
string(const char* str)//传入参数为字符串时
:_str(new char[strlen(str) + 1])//此处+1是多开了一个空间
,_size(strlen(str))
,_capacity(strlen(str))
{ strcpy(_str, str);
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
}
注意事项:
①、此处初始化列表中不能直接_str(str)
,由于后续涉及string类的增删查改,我们最好采用动态开辟的方式。
②、我们之前学习string类的构造函数时有观察到,string类会为开辟出的对象保留一个位置以便放置’\0’,因此此处我们在动态开辟空间时也要将其考虑进去。
2)、模拟实现二:无参构造
思考上述模拟实现一,有没有发现什么缺陷?
当我们使用是一个无参构造呢?因此此处对于它的实现,我们要么提供一个全缺省的构造,要么在此基础上提供一个无参构造。
先来看一看无参构造的情况:
namespace mystring
{class string
{public:
//string类的构造:
string(const char* str)//传入参数为字符串时
:_str(new char[strlen(str) + 1])//此处+1是多开了一个空间
,_size(strlen(str))
,_capacity(strlen(str))
{ strcpy(_str, str);
}
//上述显示写法,我们要么提供一个全缺省的构造,要么提供一个无参构造
string()//假如是无参构造
:_str(new char[1])//此处虽然只动态开辟一个空间,但为了方便后续释放,我们一并带上方括号
, _size(0)
,_capacity(0)
{ _str[0] = '\0';//我们初始化定义空对象,其内部也有一个'\0'
}
//string类析构:
~string()
{ delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
}
注意事项:
①_str(new char[1])
,此处我们使用new开辟空间时,尽管只申请一个空间,但我们仍然使用了方括号的形式,这是为了后续析构函数方便一次性释放(对string(const char* str)
有效、对string()
也有效)。
②关于无参构造函数,初始化列表处_str(new char[1])
,是否能让_str(nullptr)
?
string()//假如是无参构造
:_str(nullptr)
, _size(0)
, _capacity(0)
{}
回答:不能。此处原因不在于后续delete释放空指针。(因为delete、free会检查空指针,如果所释放的对象是空指针,那么它们不会做任何处理)。
此处真正错误的原因在于做一些调用返回时,会出错。比如下述情况:
我们模拟实现c_str,根据之前所学,其会指向类中字符存储的指针。
//string函数实现:c_str
const char* c_str()const
{return _str;
}
void test_string1()
{string s1;
cout<< s1.c_str()<< endl;
}
3)、模拟实现三:全缺省构造
在之前学习模拟实现日期类中,我们也表明,写一个带参+无参,不如写一个全缺省方便。那么如果是含有全缺省的string类,该如何实现呢?怎么给值?
示例一:此处能直接给nullptr吗?为什么?
string(const char* str = nullptr)//缺省参数为空指针
:_str(new char[strlen(str)+1])
,_size(strlen(str))
,_capacity(strlen(str))
{ strcpy(_str, str);
}
回答:不能。因为strlen直接解引用空指针会崩溃。
示例二:
全缺省下的构造:
string(const char* str = "\0")//一种写法
:_str(new char[strlen(str)+1])
,_size(strlen(str))
,_capacity(strlen(str))
{ strcpy(_str, str);
}
但这种写法实际上不严谨,str里实际存储为\0\0。
示例三:
全缺省下的构造:
string(const char* str = "")//另一种写法"" ,常量字符串,以\0结尾。
:_str(new char[strlen(str)+1])
,_size(strlen(str))
,_capacity(strlen(str))
{ strcpy(_str, str);
}
4)、模拟实现四:优化说明
针对上述的构造函数,我们发现初始化列表中我们使用了好多次strlen()
,要知道strlen使用的效率相对较低,那么有没有什么优化的方式呢?
以下举例了一种写法,问题:这样写能不能做到对strlen的优化?
class string
{public:
string(const char* str = "")
: _size(strlen(str))//先用一次strlen
, _capacity(_size)//对_capacity我们使用_size初始化
, _str(new char[_capacity+1])//此处同理
{ strcpy(_str, str);
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
回答:这种写法是错误的。在类和对象·初始化列表中我们有提到,初始化列表的顺序与声明顺序一致,与初始化列表中定义的顺序无关。
若按照上述方法进行初始化,_capacity为随机值,那么_str在动态开辟空间时会崩溃。
此处需要引申一个话题:抛异常(详细介绍将在后续学习到,此处我们需要先学着使用它)。
int main()
{try {mystring::test_string1();
}
catch (const exception& e)
{cout<< e.what()<< endl;
}
return 0;
}
针对上述问题,一个提出的解决方案是将类中成员变量的顺序调换。但这样子将后续的维护问题与成员变量的顺序关联上了,相对来说不是优解。
5)、模拟实现五:优化说明
针对4)中存在的问题,我们的一个解决方案是,混合使用或者干脆不用初始化列表,直接在函数体内初始化。(string类的三个成员都是内置类型,没有必须在初始化列表完成初始化的要求)
string(const char* str)
{ _size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
1)、模拟实现一:
//string函数实现:size()
size_t size()const
{ return _size;
}
//string函数实现:capacity()
size_t capacity()const
{ return _capacity;
}
//string函数实现:operator[]
char& operator[](size_t pos)
{ assert(pos< _size);
return _str[pos];
}
注意事项:
①operator[]
我们之前学习时就讲过,下标访问符号其最终要达到读写目的,因此,我们在实现operator[]
时不能加const,且必须有&。
演示结果如下:
库中的函数示意图:
根据库函数我们这里最好为operator[]
提供两种模式:可读可写&只读
char& operator[](size_t pos)
{ assert(pos< _size);
return _str[pos];
}
const char& operator[](size_t pos)const
{ assert(pos< _size);
return _str[pos];
}
1)、模拟实现一:
需要注意的是,迭代器可能是指针,但不一定完全是指针。
//string函数实现:迭代器
typedef char* iterator;
iterator begin()
{ return _str;
}
iterator end()
{ return _str + _size;
}
演示结果如下:我们支持了迭代器,就支持了范围for。
void test_string2()
{//验证迭代器的实现:
string s1("hello string!");
cout<< s1.c_str()<< endl;
string::iterator it=s1.begin();
while (it< s1.end())
{ cout<< *it<< " ";
++it;
}
cout<< endl;
for (auto ch : s1)//持了迭代器,就支持了范围for
{ cout<< ch<< " ";
}
cout<< endl;
}
总言:如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情 况都是按照深拷贝方式提供。
1)、拷贝构造之浅拷贝说明:
2)、如何进行深拷贝的说明
深拷贝即需要给对象分配自己单独的资源空间。
演示代码如下:
//深拷贝:
string(const string& s)
:_str(new char[s._capacity + 1])
, _capacity(s._capacity)
, _size(s._size)
{ strcpy(_str, s._str);
}
演示结果如下:
3)、如何进行赋值的说明
接上述演示,假设现在我们有一个s3,要将其赋值给s1,该如何实现?
void test_string3()
{//验证拷贝构造
string s1("hello world.");
string s2(s1);
cout<< s1.c_str()<< endl;
cout<< s2.c_str()<< endl;
//验证赋值
string s3("1111111111 1111111111");
s1 = s3;//如何实现?
}
直接用默认生成的赋值运算符重载可以吗?
回答:不行。
原因:默认生成的赋值运算符重载属于浅拷贝,会让s1指向与s3相同的空间。这样一来存在两大问题:①s1更改指向,原先动态开辟的空间没有被释放,则存在内存泄露的现象。②出了作用域调用析构函数时,同一块空间将释放两次。
那么,如何解决这个问题呢?我们可以仿照拷贝构造一样,单独开辟一份资源空间。
赋值运算符重载·传统
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;
}
写法分析:
①s1 = s3;虽然我们可以在s1空间足够充裕时不释放而是直接交换内部数据,但我们会面临s1空间不足,或s1空间远大于s3的情况,因此不如统一路径先释放s1后开辟新空间。
②如果直接使用_delete[ ] _str
,new失败了会将原数据释放,所以先使用一个tmp临时变量完成拷贝,确定new成功再说。(当然此处不这样也行,new失败了本来就要抛异常)。但一种相对比较提倡的写法如下:先开辟空间,再交换数据:
string& operator=(const string& s)
{ if (this != &s)//4
{ char*tmp = new char[s._capacity + 1];//3
strcpy(tmp, s._str);
delete[] _str;
_str = tmp;
_size = s._size;
_capacity = s._capacity;
}
return *this;//5
}
③new char[s._capacity + 1]
多开辟一个空间是留给\0,因为_size和_capacity不计预留的\0。
④if (this != &s)
if语句:此处考虑到的是自己赋值给自己的情况。如果不加if判断语句,那相当于先把s1释放了,再用s1赋值给s1。
⑤return *this;
this指针指向对象的地址,此处需要返回的是对象本身,故需要对this指针解引用。
1)、一个错误的写法说明
赋值运算符重载·自写·错误
string& operator=(const string& s)
{ if (this != &s)
{ string tmps(s);
swap(tmps._str, _str);
_size = s._size;
_capacity = s._capacity;
delete[] tmps._str;//对随机空间进行了析构
}
return *this;
}
错误说明:
①string tmps(s);
此处相当于在拷贝构造函数中使用了拷贝构造。正确写法应该是去调用构造函数:string tmps(s._str);
②delete[] tmps._str;
此处存在两个问题,其一是tmp是临时对象,出了函数作用域会自动调用其析构函数。那这里相当于析构了两次。
③接②,就算我们把上面的delete[] tmps._str;
删除,仍旧会存在报错。因为我们在做的是拷贝构造,假如我先定义string s1;
再赋值s1=s3;
由于s1未初始化,与tmp交换后,tmp得到该处的随机值。那么同样的,临时变量tmp出作用域调用对应的析构函数,会导致delete因释放随机空间而崩溃。(解决方法是为s1设置初始化列表)。
2)、拷贝构造和赋值运算符现代写法说明
string(const string& s)
:_str(nullptr)
,_capacity(0)
,_size(0)
{ string tmps(s._str);
swap(tmps._str, _str);
swap(tmps._size, _size);
swap(tmps._capacity, _capacity);
解释说明:1、此处为s2初始化,是为了后续与tmp做成员交换。在没有初始化时,s2为随机值,与tmp交换后,tmp得到该随机值,但由于tmp是临时变量,出了作用域要销毁,会调用对应的析构函数,会导致delete在释放随机空间时崩溃。(delete释放nullptr不会崩溃)
:_str(nullptr)
,_capacity(0)
,_size(0)
继续优化:
1、string类中也有一个成员函数swap。
2、此处使用了域作用限定符,前面为空白表示全局域。若不加该限定符,编译器会先在局部域中找,结果局部域中有swap,编译器会认为自己调用自己,而且此处局部域中参数不匹配。
string函数实现:swap
void swap(string& tmps)
{ ::swap(tmps._str, _str);
::swap(tmps._size, _size);
::swap(tmps._capacity, _capacity);
}
拷贝构造:
string(const string& s)
:_str(nullptr)
,_capacity(0)
,_size(0)
{ string tmps(s._str);
swap(tmps);//解释:this->swap(tmps);
}
3、swap(tmps);
拷贝构造中,此处先调用string类里的swap,string类中swap调用全局的swap。
同理可得赋值运算符重载:
string& operator=(const string& s)
{ if (this!=&s)
{ //string tmps(s._str);//调用构造
string tmps(s);//调用拷贝构造
swap(tmps);
}
return *this;
}
1、此处两种写法都可以,前者使用的是构造函数,后者使用的是拷贝构造(使用后者的前提是我们自己实现了拷贝构造)
2、此处做得很绝的一点是,出了作用域tmp销毁调用析构,就顺带把s1原先空间给释放掉了。不需要我们手动写delete.
3、关于是否可以写swap(*this,tmps)
,即使用库里的全局swap?回答:不可以,库里的全局swap实际是个模板,假如我们在赋值运算符重载中使用它,库里的swap又在其内部使用了赋值运算符重载,那么此处会造成死循环。
此外:调用string类里swap,根据我们的实现,此处只是简单的内置类型的交换,若使用全局库中的swap,则连同地址也会一并被换掉。
//string函数实现:swap
void swap(string& tmps)
{ ::swap(tmps._str, _str);
::swap(tmps._size, _size);
::swap(tmps._capacity, _capacity);
}
void test_string4()
{string s1("hello world");
string s2("XXXXXXX");
s1.swap(s2);//调用string类里swap:直接交换内部成员变量
swap(s1, s2);//调用全局库中的swap:会去掉拷贝构造。
}
折回上述问题,我们再来简化一下此处的赋值运算符重载:
直接使用传值传参,省掉了tmp。相对更为精髓。
string& operator=(string s)
{ swap(s);
return *this;
}
1)、库函数中声明回顾:
2)、模拟实现
void reserve(size_t n)
{ if (n>_capacity)
{ //开空间:
char* tmp = new char[n + 1];//n个有效空间,一个\0空间
//拷贝值:
strcpy(tmp, _str);
//释放旧空间:
delete[]_str;
//重新给定指向:
_str = tmp;
_capacity = n;//注意别忘记,另reserve里只是对_capacity做了修改,不会变动_size。
}
}
void push_back(char ch)
{ //检查扩容:
if (_size == _capacity)
{ reserve(_capacity == 0 ? 4 : _capacity * 2);
//此处三目运算符是为了预防构造时参数为空的情况。
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';//注意此处别忘了放置\0
}
string& operator+=(char ch)
{ push_back(ch);
return *this;
}
演示结果如下:
1)、库函数中声明回顾:
2)、模拟实现append 1.0
void append(const char* str)
{ //append仍旧需要扩容检查,只是其是在尾部追加字符串而非字符
size_t len = strlen(str);
if (_size + len >_capacity)
{ //此处我们不能直接仿照push_back中的写法进行二倍扩容等。
//所以我们干脆使用reserve重定义存储空间:
reserve(_size + len);
}
//尾插字符串:
strcpy(_str+_size, str);
//修改属性:
_size += len;
//此处_capacity我们在使用reserve时做了处理。
}
此处strcpy(_str + _size, str);
若换为strcat(_str, str)
可以吗?
回答是可以,但是strcat追加需要找\0,相率相对较低。
演示结果如下:
既然实现了上述append功能,我们自然而然可以利用它们实现以下接口:
string& operator+=(const char* str)
{ append(str);
return *this;
}
void append(const string& s)
{ append(s._str);
}
void append(size_t n, char ch)
{ reserve(_size + n);//提前开辟充裕空间
for (int i = 0; i< n; i++)
{ push_back(ch);
}
}
5.3、string:: insert
1)、库函数中声明回顾:
我们主要模拟实现下列这两个。
2)、模拟实现insert 1.0
string& insert(size_t pos, char ch)
{ //首先要断言一下插入的位置:pos需要合法
//此处涉及的一个问题是边界问题:_size位置处能否插入?事实上只要我们处理好最后的收尾工作即可。
assert(pos<= _size);
//扩容检查:
if (_size == _capacity)
{ reserve(_capacity == 0 ? 4 : _capacity * 2);
}
//数据插入:向后挪动数据,从后往前遍历
size_t end = _size;
while (end >= pos)
{ _str[end + 1] = _str[end];
--end;
}
_str[pos] = ch;
//收尾处理:
++_size;
_str[_size] = '\0';
return *this;
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
演示结果如下:
2)、模拟实现insert 2.0
针对1.0版本,是否存在什么问题?
//数据插入:向后挪动数据,从后往前遍历
size_t end = _size;
while (end >= pos)
{ _str[end + 1] = _str[end];
--end;
}
_str[pos] = ch;
看看上述代码,如果pos位置在0处,那么头插会存在问题。若end是int类型数据,那么end=-1时退出循环。这不是正确的吗?但问题是上述代码中end值是size_t无符号整型。-1对应的无符号数是一个很大的值,因此才说此处会陷入死循环中。
所以提出了如下解决方案:如图。
接上图,又有提出那么我们把pos的类型修改了吧。
这种做法行得通,但是事实上我们观察库函数里的pos,其提供的都是size_t类型。而且这里还会有一个问题:pos本意指代下标,使用int类型的pos,万一传入的是负数呢?
PS:这里也解释了为什么assert(pos<= _size);
不检查<0的情况,因为pos为无符号整型。
综上所述,这里我们建议将end初始值置成_size+1,如下述演示:
/insert插入:版本2.0
string& insert(size_t pos, char ch)
{ //首先要断言一下插入的位置:pos需要合法
//此处涉及的一个问题是边界问题:_size位置处能否插入?事实上只要我们处理好最后的收尾工作即可。
assert(pos<= _size);
//扩容检查:
if (_size == _capacity)
{ reserve(_capacity == 0 ? 4 : _capacity * 2);
}
//数据插入:
size_t end = _size+1;
while (end >pos) //理解:end-1 >= pos → end>=pos+1 → end >pos
{ _str[end] = _str[end-1];//步骤一:向后挪动数据,从后往前遍历
--end;
}
_str[pos] = ch;//步骤二:插入数据ch
//收尾处理:
++_size;
_str[_size] = '\0';
return *this;
}
演示结果如下:
3)、模拟实现insert 3.0
假如是插入字符串呢?代码如下:
string& insert(size_t pos, const char* str)
{ assert(pos<= _size);
size_t len = strlen(str);
if (len + _size >_capacity)
{ reserve(len + _size);
}
size_t end = _size + len;
while (end >= pos + len)//等价于 end >pos + len -1;
{ _str[end] = _str[end - len];
--end;
}
strncpy(_str + pos, str,len);
_size += len;
_str[_size ] = '\0';
return *this;
}
演示结果如下:
同理可修改其它:
写法2.0
void push_back(char ch)
{ insert(_size, ch);
}
写法2.0
void append(const char* str)
{ insert(_size, str);
}
1)、库函数中声明回顾:
2)、模拟实现:
演示代码如下:
void erase(size_t pos, size_t len = npos)
{ assert(pos<= _size);
//不需要挪动数据的情况:
if (len == npos || len + pos >_size)
{ _str[pos] = '\0';
_size = pos;
}
else
{ //需要挪动数据的情况:
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
}
演示结果如下:
不是所有的流插入、流提取都需要设置为友元。我们在日期类中使用了友元是因为涉及访问私有成员。而string类中我们完全可以使用下标来实现这一功能。
1)、模拟实现1.0:
演示代码如下:
ostream& operator<<(ostream& cou, const string& s)
{for (size_t i = 0; i< s.size(); ++i)
{ cou<< s[i];
}
return cou;
}
istream& operator>>(istream& ci, string& s)
{char ch;
ch = ci.get();
while (ch != ' ' && ch != '\n')
{ s += ch;
ch = ci.get();
}
return ci;
}
2)、模拟实现2.0:
上述流提取中,若输入字符串很长,不断+=会频繁扩容,效率很低,因此我们可以优化一下 :
istream& operator>>(istream& ci, string& s)
{char ch;
ch = ci.get();
//用于优化流插入:
const int N = 32;//常量数组
char buff[N];
size_t i = 0;
while (ch != ' ' && ch != '\n')
{ buff[i++] = ch;//先将插入字符放入数组中
if (i == N - 1)//若字符放满,则一次性挪动到string类中
{ buff[N] = '\0';//留一个位置给'\0'
s += buff;
i = 0;//注意需要将i置回0以便下一轮使用
}
ch = ci.get();
}
//这是为最后一轮作处理:若ch读到\n或'',则跳出while循环,那么残余部分没有被放入string类中
buff[i] = '\0';
s += buff;
return ci;
}
同理,对于流插入,仍旧有一些细节:
如上图所示:假如类原先就有字符,那么cin后在标准库中会被覆盖,针对这一问题,我们可在类中实现一个clear函数:
void clear()
{ _str[0] = '\0';
_size = 0;
}
istream& operator>>(istream& ci, string& s)
{s.clear();
char ch;
ch = ci.get();
//用于优化流插入:
const int N = 32;//常量数组
char buff[N];
size_t i = 0;
while (ch != ' ' && ch != '\n')
{ buff[i++] = ch;//先将插入字符放入数组中
if (i == N - 1)//若字符放满,则一次性挪动到string类中
{ buff[N] = '\0';//留一个位置给'\0'
s += buff;
i = 0;//注意需要将i置回0以便下一轮使用
}
ch = ci.get();
}
//这是为最后一轮作处理:若ch读到\n或'',则跳出while循环,那么残余部分没有被放入string类中
buff[i] = '\0';
s += buff;
return ci;
}
string substr(size_t pos, size_t len = npos)const
{ assert(pos< _size);
size_t reallen = npos;//是为了记录真实取得的字符长度
if (len == npos || len + pos >_size)
{ reallen = _size - pos;
}
string tmp;
for (size_t i = 0; i< reallen; i++)
{ tmp += _str;
}
}
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;//若找不到,则返回npos
}
size_t find(const char* sub, size_t pos = 0)const
{ const char*p =strstr(_str + pos, sub);//此处为暴力匹配,其它匹配方法:kmp/bm
if (p != nullptr)
{ return npos;
}
else { return p - _str;
}
}
void resize(size_t n, char ch = '\0')
{ if (n >_size)
{ //开辟空间,插入数据
reserve(n);
for (size_t i = _size; i< n; ++i)
{_str[i] = ch;
}
_str[n] = '\0';
_size += n;
}
else
{ //删除数据
_str[n] = '\0';
_size = n;
}
}
bool operator >(const string& s)const
{ return strcmp(_str, s._str) >0;
}
bool operator == (const string & s)const
{ return strcmp(_str, s._str) == 0;
}
bool operator >=(const string& s)const
{ return (*this == s) || (*this >s);
}
bool operator<(const string& s)const
{ return !(*this >= s);
}
bool operator<=(const string& s)const
{ return !(*this >s);
}
bool operator !=(const string& s)const
{ return !(*this == s);
}
1、vs下string类内存大小设置
2、其它string类实现方案
3、引用计数和写时拷贝。
你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流