总言
STL:主要介绍string类的模拟实现,加深对string类各接口的熟悉度。(重点理解:迭代器、深拷贝)
文章目录
- 总言
- 1、对构造函数、析构、string::c_str
- 2、对 string::size、string::capacity、string::operator[ ]
- 3、迭代器(begin、end)、范围for
- 4、拷贝构造和赋值
- 5、增删查改
- 6、其它相关知识
- Fin、共勉。
1)、总言
对string类,由于库函数中本身有一个,为了避免冲突,这里我们的处理方式有两种:其一是更改类名称,其二是为我们自己模拟实现的类添加命名空间。
本文章中选择了第二种处理方法:
namespace mystring//命名空间
{
class string//我们模拟实现的类
{
public:
private:
char* _str;
size_t _size;
size_t _capacity;
};
}
通常情况下,_size
和_capacity
不会出现负数的情况,因此我们采取size_t
的类型。
1、对构造函数、析构、string::c_str
1.1、构造函数模拟实现1.0:string (const char* s);
1)、模拟实现一:
namespace mystring
{
class string
{
public:
//string类的构造:
string(const char* str)//传入参数为字符串时
:_str(new char[strlen(str) + 1])//此处+1是多开了一个空间
,_size(strlen(str))
,_capacity(strlen(str))
{
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
}
注意事项:
①、此处初始化列表中,能否直接将str
赋值给_str
?
回答:不能。_str(str)
,我们传入的str
其类型是常量字符串const char*
,而string类中的_str
成员变量是char*
。
解决方案:由于后续涉及string类的增删查改,我们最好采用动态开辟的方式。
②、new char[strlen(str) + 1]
:我们之前学习string类的构造函数时有观察到,string类会为开辟出的对象保留一个位置以便放置'\0'
,因此此处我们在动态开辟空间时也要将其考虑进去。
③、_capacity(strlen(str))
:但是实际计算大小时,记录的是有效字符的个数,故这个'\0'
并没有记录进去。(这里要注意strlen和sizeof的区别)
问题: 思考上述模拟实现,有没有发现什么缺陷?
回答: 对_str
我们只开辟空间,没有将str
中的数据拷贝过来,不能达成真正以字符串构造的目的。
//string类的构造:
string(const char* str)//传入参数为字符串时
:_str(new char[strlen(str) + 1])//此处+1是多开了一个空间
,_size(strlen(str))
,_capacity(strlen(str))
{
strcpy(_str, str);
}
2)、问题说明及后续模拟实现引入
问题引入: 上述模拟实现1.0不能满足所有条件,因为我们完全可以在初始化时传值:
string s1();
这样一个无参构造,需要如何解决其构造函数?
方案: 此处对它的实现,①我们要么提供一个全缺省的构造,要么②在1.0的基础上提供一个无参构造。
1.2、构造函数模拟实现2.0:无参构造函数(附析构函数、模拟实现c_str)
根据上述解决方案,此处选择无参构造+带参的方式。带参在模拟实习1.0中讲解过,以下为无参构造实现过程的情况:
1.2.1、string( );
问题:对无参的构造函数,是否还需要申请空间?为什么?
回答: 需要。虽然我们说无参,但根据模拟实现一可知,实际需要一个空间放置不被统计'\0'
。
实现如下:
namespace mystring
{
class string
{
public:
string()//假如是无参构造
:_str(new char[1])
, _size(0)
,_capacity(0)
{
_str[0] = '\0';//我们初始化定义空对象,其内部也有一个'\0'
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
}
注意事项:
① _str(new char[1])
,此处我们使用new
开辟空间时,尽管只申请一个空间,但我们仍然使用了方括号的形式,这是为了后续析构函数方便一次性释放。 这种new[]
的统一写法,(对string(const char* str)
有效、对string()
也有效)。
② 关于无参构造函数,初始化列表处_str(new char[1])
,是否能让_str(nullptr)
?
string()//假如是无参构造
:_str(nullptr)
, _size(0)
, _capacity(0)
{}
回答:不能。此处原因不在于后续delete
释放空指针。(因为delete、free
会检查空指针,如果所释放的对象是空指针,那么它们不会做任何处理)。 此处真正错误的原因在于做一些调用返回时,会出错。比如下述情况:
模拟实现c_str,根据之前所学,其会指向类中字符存储的指针。如果被_str
被赋值为nullptr
,那么后续调用s.c_str
,通过指针打印时,会解引用空指针。
//string函数实现:c_str
const char* c_str()const
{
return _str;
}
void test_string1()
{
string s1;
cout << s1.c_str() << endl;
}
1.2.2、c_string()
相关解释见上述。
//string函数实现:c_str
const char* c_str()const
{
return _str;
}
1.2.2、~string( );
要注意,我们申请了空间,则需要在析构函数中将其销毁。
//string类析构:
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
1.3、构造函数模拟实现3.0:全缺省的构造函数
在之前学习模拟实现日期类中,我们也表明,写一个带参+无参,不如写一个全缺省方便。那么如果是含有全缺省的string类,该如何实现呢?怎么给值?
1.3.1、string(const char* str = “”);
模拟实现3.1: 全缺省中,缺省参数是否能直接给nullptr
?为什么?
string(const char* str = nullptr)//缺省参数为空指针
:_str(new char[strlen(str)+1])
,_size(strlen(str))
,_capacity(strlen(str))
{
strcpy(_str, str);
}
回答: 不能。因为当我们无参构造一个string
类时,此时由于str=nullptr
,那么接下来初始化列表中,函数strlen(str)
计算字符串长度时,解引用空指针会导致崩溃。
模拟实现3.2: 根据3.1,此处全缺省中,缺省参数该如何给?
这里提供了"\0"
的写法,问:是否可行?
全缺省下的构造:
string(const char* str = "\0")//一种写法
:_str(new char[strlen(str)+1])
,_size(strlen(str))
,_capacity(strlen(str))
{
strcpy(_str, str);
}
回答: 可以,strlen
统计'\0'
前字符个数,故此处在无参构造情况下,strlen(str)
计算结果为0,那么strlen(str)+1 =1
,则实际只new
出来一个空间。
但这种写法实际上不严谨,str
里实际存储为\0\0
。
模拟实现3.3:
采用空串""
:实际默认隐含存储了一个'\0'
。
全缺省下的构造:
string(const char* str = "")//另一种写法"" ,常量字符串,以\0结尾。
:_str(new char[strlen(str)+1])
,_size(strlen(str))
,_capacity(strlen(str))
{
strcpy(_str, str);
}
1.3.2、优化处理:引入抛异常、对strlen
问题说明: 针对上述小节中的构造函数,我们发现初始化列表中使用了好多次strlen()
,要知道strlen
使用的效率相对较低(找\0
,
O
(
N
)
O(N)
O(N)),那么有没有什么优化的方式?
:_str(new char[strlen(str)+1])
,_size(strlen(str))
,_capacity(strlen(str))
优化version1.0:以下这样写能不能做到对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;
}
优化version2.0: 针对version1.0中存在的问题,一个解决方案是,混合使用或者干脆不用初始化列表,直接在函数体内初始化。这样做既能解决strlen带来的效率问题,又能解决初始化列表依赖关系。
string(const char* str = "")
{
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
PS:在类和对象(四)中,我们学习过有些成员变量必须在初始化列表中初始化,这里string类的三个成员都每次必要,故可用上述方法解决strlen效率问题。
1、引用成员变量
2、const成员变量
3、自定义类型成员(且该类没有默认构造函数时)
2、对 string::size、string::capacity、string::operator[ ]
2.1、string::size、string::capacity
库中的函数示意图:
模拟实现:
//string函数实现:size()
size_t size()const
{
return _size;
}
//string函数实现:capacity()
size_t capacity()const
{
return _capacity;
}
细节说明: 由于不需要修改类中值,对this
指针可使用const
修饰。
2.2、string::operator[ ]
库中的函数示意图:
模拟实现: 根据库函数,这里为operator[]
重载了两种模式:可读可写&只读
//string函数实现: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];
}
演示结果如下:
3、迭代器(begin、end)、范围for
库中的函数示意图:
3.1、普通迭代器、范围for
1)、迭代器模拟实现:
string类中的迭代器就是其原生指针。 需要注意的是,迭代器可能是指针,但不一定完全是指针。
//string函数实现:迭代器
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
2)、范围for
演示结果如下:实现了迭代器,就支持了范围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;
}
反向迭代器相对于迭代器更为复杂一些,相关内容将在后续学习。
3.2、const迭代器
相比之下,const迭代器的特点是只能读,不能写。
typedef const char* const_iterator;
const_iterator begin()const
{
return _str;
}
const_iterator end()const
{
return _str + _size;
}
4、拷贝构造和赋值
总言: 如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情况都是按照深拷贝方式提供。
库中示意图:
4.1、写法1.0
4.1.1、拷贝构造:version1.0
1)、回顾浅拷贝
相关章节:默认生成的拷贝构造函数,对内置类型按内存存储中的字节序完成拷贝(浅拷贝),对自定义类型是调用其拷贝构造函数完成拷贝的。
注意:这里要理解s1、s2
两次析构,由于_str
指向的动态空间相同,当我们析构s1
时,虽然将其置空,但只是针对s1._str
而言,对于s2._str
,其仍旧指向原先空间,但该空间权限已经被收回。
2)、如何进行深拷贝的说明
深拷贝即需要给对象分配自己单独的资源空间。
演示代码如下:
//深拷贝:
string(const string& s)
:_str(new char[s._capacity + 1])
, _capacity(s._capacity)
, _size(s._size)
{
strcpy(_str, s._str);
}
1、new char[s._capacity + 1]
对于我们自己的_str
,使用new
申请新的空间,空间大小同s
中的有效数据_capacity
,同时还要把隐藏的'\0'
加上。
2、除了要把_capacity
和_size
的值匹配上,对于_str
,不要忘记将值拷贝过来。 由于string
类中_str
是字符指针,我们可以直接使用字符串拷贝函数strcpy
。
演示结果如下: 如图所示,s1,s2
指向的地址空间不同,这样delete
时不会对同一块空间释放两次,且修改时不会造成连锁反应。
4.1.2、赋值运算符重载:version1.0
1)、问题引入:如何进行赋值?
接上述演示,假设现在我们有一个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
更改指向,原先动态开辟的空间没有被释放,则 存在内存泄露的问题。②出了作用域调用析构函数时,同一块空间将释放两次。
那么,如何解决这个问题呢?我们可以仿照拷贝构造一样,单独开辟一份资源空间。(深拷贝)
2)、模拟实现
string& operator=(const string& s)
{
if (this != &s)
{
delete[] _str;//1、释放原有空间及资源
_str = new char[s._capacity + 1];//2、开辟新空间
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
细节理解:
①: s1 = s3
;虽然我们可以在s1
空间足够充裕时不释放而是直接交换内部数据,但我们会面临s1
空间不足,或s1
空间远大于s3
的情况,因此不如统一路径先释放s1
后开辟新空间。
②: 如果直接使用_str = new char[s._capacity + 1];
,new失败了会将原数据释放,所以先使用一个tmp临时变量完成拷贝,确定new成功再说。(当然此处不这样也行,new失败了本来就要抛异常)。但一种相对比较提倡的写法如下:先开辟空间,再交换数据(若空间开辟失败就不用后续数据交换流程):
string& operator=(const string& s)
{
if (this != &s)//4、判断是否为自己给自己赋值
{
char* tmp = new char[s._capacity + 1];//3、此处仍旧要+1.
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判断语句,那相当于先把s1释放了,再用s1赋值给s1。
⑤: return *this;
this指针指向对象的地址,此处需要返回的是对象本身,故需要对this指针解引用。
4.2、写法2.0
总体思路: 用形参中的类,临时构造一个tmps
类,与待拷贝的*this
类进行数据交换。
即,借助已经写成的构造函数,使用数据交换swap
来完成拷贝构造。
4.2.1、拷贝构造写法2.0
4.2.2.1、拷贝构造:version2.0
1)、拷贝构造写法说明
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、此处为*this
初始化,是为了后续与tmp
做成员交换。在没有初始化时,*this
其类成员变量为随机值,与tmp
交换后,tmp
中,成员变量得到原先*this
成员变量的随机值。由于tmp
是临时变量,出了作用域要销毁,会调用对应的析构函数,会导致delete
释放随机空间,崩溃。(delete释放nullptr不会崩溃)
故而使用了初始化列表:
:_str(nullptr)
,_capacity(0)
,_size(0)
4.2.2.2、拷贝构造:version2.1(string::swap实现)
1)、string::swap模拟实现
1、string类中也有一个成员函数swap,我们可以顺带实现它。
void swap(string& tmps)
{
::swap(tmps._str, _str);
::swap(tmps._size, _size);
::swap(tmps._capacity, _capacity);
}
2、::swap
此处使用了域作用限定符,前面为空白表示全局域,使用条件为我们将std
在全局域展开。若不加该限定符,编译器会先在局部域中找,结果局部域中有swap,编译器会认为自己调用自己,但会因为局部域中的swap参数不匹配而报错。PS:此处也可以修改为std::swap
。
3、这样一来在拷贝构造2.0的写法中我们可以直接使用类中的swap
。
swap(tmps);//解释:this->swap(tmps);
2)、将上述string::swap用于拷贝构造中
说明: swap(tmps);
拷贝构造中,此处先调用string类里的swap,string类中swap调用全局的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);
}
4.2.2.、赋值运算符重载2.0:
4.2.2.1、赋值运算符重载version2.0(含std::swap与string::swap说)
1)、赋值运算符重载:
根据拷贝构造2.0中的写法,同理可得赋值运算符重载:
string& operator=(const string& s)
{
if (this!=&s)
{
//string tmps(s._str);//可以调用构造
string tmps(s);//也可以调用拷贝构造
swap(tmps);
}
return *this;
}
说明:
1、string tmps(s._str);
、string tmps(s);
:此处两种写法都可以,前者使用的是构造函数,后者使用的是拷贝构造(使用后者的前提是我们自己实现了拷贝构造)
2、此处做得很绝的一点是,出了作用域tmp销毁调用析构,就顺带把s1
原先交换前指向的动态空间给释放掉了,不需要我们在此处手动写delete(析构函数中已经写了)。
2)、swap函数说明:
1、关于是否可以写::swap(*this,tmps)
,即,使用库里的全局swap
?
回答: 不可以,库里的全局swap
实际是个模板,假如我们在赋值运算符重载中使用它,全局库里的swap又在其内部使用了赋值运算符重载a=b
,那么此处会造成死循环。( std::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:会去掉拷贝构造。
}
4.2.2.2、赋值运算符重载:使用传值传参进一步简化
折回上述问题,关于赋值运算符重载:若直接使用传值传参,则可省掉了tmp,相对更为精髓。(但与库中函数形参不匹配)
string& operator=(string s)
{
swap(s);
return *this;
}
5、增删查改
5.1、string::reserve、string::push_back、string::operator+=(char c)
5.1.1、string::reserve
1)、库函数中声明回顾:
1、如果 n 大于当前字符串容量,则该函数会导致容器将其容量增加到 n 个字符(或更大)。
2、此函数对字符串长度没有影响,并且无法更改其内容。
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。
}
}
5.1.2、string::push_back
注意事项:
1、插入数据前要进行扩容检查
2、尾插,是在_size
下标处,故结束后不要忘记放置'\0'
void push_back(char ch)
{
//检查扩容:
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
//此处三目运算符是为了预防构造时参数为空的情况。
}
//尾插
_str[_size] = ch;
++_size;
_str[_size] = '\0';//注意此处别忘了放置\0
}
5.1.3、string::operator+=(char ch)
如上述,operator+=
在类中有三个函数重载,这里实现的是+=一个字符
的情况,可借助push_back
完成。
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
演示结果如下:
5.2、string::append、string::operator+=(const char* str)
库函数中声明回顾:
5.2.1、string::append (const char* s)
思路分析:
①首先要计算出插入的字符串的长度。
②对比插入前后扩容问题 ,若容量空间不够,则需要扩容,但这里不能直接使用二倍扩容,这种方法不一定满足容量空间(比如原先空间为8,二倍扩容为16,如果插入字符串str+_size值大于16,容量空间不够)。
③在容量空间足够的情况下, 将需要的字符串拷贝/追加过 来,④并完成对字符串string长度的更新工作。
void append(const char* str)
{
//append仍旧需要扩容检查,只是其是在尾部追加字符串而非字符
size_t len = strlen(str);
if (_size + len > _capacity)
{
//此处我们不能直接仿照push_back中的写法进行二倍扩容等。
//所以我们干脆使用reserve重定义存储空间:
reserve(_size + len);//至少需要_size+len的空间
}
//尾插字符串:
strcpy(_str+_size, str);
//修改属性:
_size += len;
//此处_capacity我们在使用reserve时做了处理。
}
此处strcpy(_str + _size, str);
若换为strcat(_str, str)
可以吗?回答是可以,但是strcat追加需要找\0
,相率相对较低。
演示结果如下:
既然实现了上述append功能,我们自然而然可以利用它们实现以下接口:
5.2.2、operator+=(const char* str)
由于push_back
只是尾插一个字符,要实现operator+=字符串
,此处借助了上述的append
函数。
string& operator+=(const char* str)
{
append(str);
return *this;
}
5.2.3、append(const string& s)
同理,append
中给定的是类对象本身,那么我们只需要调用append
函数,将传入参数修改其成员变量_str
即可。
void append(const string& s)
{
append(s._str);
}
5.2.4、append(size_t n, char ch)
void append(size_t n, char ch)
{
reserve(_size + n);//提前开辟充裕空间
for (int i = 0; i < n; i++)
{
push_back(ch);
}
}
5.3、string:: insert(实现insert,可实现push_back、append)
库函数中声明回顾: 这里主要模拟实现下列这两个。
5.3.1、string::insert(size_t pos, char ch)
1)、模拟实现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;//这里默认把_size处的'\0'也算入了
while (end >= pos)
{
_str[end + 1] = _str[end];
--end;
}
_str[pos] = ch;
//收尾处理:
++_size;
_str[_size] = '\0';//实际_size挪动时把'\0'也一并挪动了,这里从严谨角度再赋值一次。(当然,若不算入另有写法)
return *this;
}
演示结果如下:
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
对应的无符号数是一个很大的值,因此才说此处会陷入死循环中。
所以有如下几种解决方案:
1、如下图,将end的类型改为int类型,是否可行?(否)
2、既如此,那么把pos的类型也一并修改,总该可以了吧? (否)
回答:这种做法行得通,但是事实上我们观察库函数里的pos,其提供的都是size_t类型。而且这里还会有一个问题:pos本意指代下标,使用int类型的pos,万一传入的是负数呢?
PS:这里也解释了为什么assert(pos <= _size);
不检查<0
的情况,因为pos为无符号整型。
3、综上所述,这里我们建议将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;
}
演示结果如下:
5.3.2、string:: insert(size_t pos, const char* str)
假如是插入字符串呢?代码如下:
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);
}
5.4、string::erase
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;
}
}
演示结果如下:
5.5、流插入和流提取
不是所有的流插入、流提取都需要设置为友元。 我们在日期类中使用了友元是因为涉及访问私有成员。而string类中我们完全可以使用下标来实现这一功能。
5.5.1、基本实现
演示代码如下:
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;
}
5.5.2、优化处理(含string::clear())
上述流提取中,若输入字符串很长,不断+=会频繁扩容,效率很低,因此我们可以优化一下 :
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;
}
5.6、其它接口:比较、substr、resize、find
5.6.1、stirng::substr
string substr(size_t pos, size_t len = npos)const
{
assert(pos < _size);
size_t reallen = len;//是为了记录真实取得的字符长度
if (len == npos || len + pos > _size)
{
reallen = _size - pos;//真实长度为[pos,_size)
}
string tmp;
for (size_t i = 0; i < reallen; i++)
{
tmp += _str[pos+i];
}
return tmp;
}
5.6.2、string::find
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;
}
}
5.6.3、string::resize
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;
}
}
5.6.4、operator比较运算符重载
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);
}
6、其它相关知识
6.1、
1、vs下string类内存大小设置
2、其它string类实现方案
3、针对浅拷贝而设置的方案:引用计数和写时拷贝。
浅拷贝存在的两问题:
①、析构两次/多次
②、一个对象修改影响另一个对象
针对一:引用计数,最后一个析构对象释放空间
针对二:写时拷贝,只要在真正需要深拷贝时才开辟新空间,若实际中没有相关场景需求则不写。
6.2、整体要实现的框架汇总
模拟实现string类(此处列举声明):
namespace mystring
{
class string
{
friend ostream& operator<<(ostream& _cout, const mystring::string& s);
friend istream& operator>>(istream& _cin, mystring::string& s);
public:
typedef char* iterator;
public:
/ 默认成员函数
string(const char* str = "");
string(const string& s);
string& operator=(const string& s);
~string();
/
/// iterator
iterator begin();
iterator end();
/
/// modify
void push_back(char c);
string& operator+=(char c);
void append(const char* str);
string& operator+=(const char* str);
void clear();
void swap(string& s);
const char* c_str()const;
/
/// capacity
size_t size()const;
size_t capacity()const;
bool empty()const;
void resize(size_t n, char c = '\0');
void reserve(size_t n);
/
/// access
char& operator[](size_t index);
const char& operator[](size_t index)const;
/
/// relational operators
bool operator<(const string& s);
bool operator<=(const string& s);
bool operator>(const string& s);
bool operator>=(const string& s);
bool operator==(const string& s);
bool operator!=(const string& s);
/
// 返回c在string中第一次出现的位置
size_t find(char c, size_t pos = 0) const;
// 返回子串s在string中第一次出现的位置
size_t find(const char* s, size_t pos = 0) const;
// 在pos位置上插入字符c/字符串str,并返回该字符的位置
string& insert(size_t pos, char c);
string& insert(size_t pos, const char* str);
// 删除pos位置上的元素,并返回该元素的下一个位置
string& erase(size_t pos, size_t len);
private:
char* _str;
size_t _capacity;
size_t _size;
};
}