前文提要
string这个容器比较简单,他就是字符的容器,不像别的容器 vector ,可以存内置类型,还可以存自定类型,甚至他还可以自己存自己(本质就是一个二维的数组),但是string只可以存字符,所以 string 是这个容器还可以看出泛型编程的好处
辅助函数
函数一:开空间
void Construction(const size_t length=1)
{
char *ptr=new char[(length+_size)*2];
for(int i=0;i<=_size;i++)
{
ptr[i]=_str[i];
}
swap(ptr,_str);//系统中的库,交换数据
_capacity=(length+_size)*2;
delete []ptr;
}
这一块在老的基础上进行了优化,因为是字符串,那么开俩倍可能不够的情况,虽然会浪费,也可以在写一个判断如果length>2*capacity则开length个,否则就是开俩倍,看个人喜好吧
1: 构造函数
默认拷贝
一般来说默认拷贝都是写一个全缺省的毕竟他一个人就完成了俩个人的工作
public:
string(const char * ch=" ")//默认构造函数
:_size(0)
,_capacity(10)
,str(new char[10])
{
}
拷贝构造
这里有俩种写法
- 现代写法
string(const string &ch)//拷贝构造
:_size(0)
,_capacity(1)
,_str(new char[1])
{
string tmp;//零时变量
tmp+=ch;
//交换数据
swap(_str,tmp._str);
_size=tmp._size;
_capacity=tmp._capacity;
}
本质从无产变成资本了,无情剥削打工人tmp,享受胜利成果
2. 传统写法
string(const string &ch)//拷贝构造
:_size(0)
,_capacity(1)
,_str(new char[1])//传统写法
{
Construction(ch._size);
strcpy(_str,ch._str);
}
自己看空间自己拷贝,看着也差不多呀,确实在这里传统写法和现代写法似乎都差不多而且还是现代写法看上去更简单,下面的赋值拷贝才会体现出来
赋值拷贝
赋值拷贝这一块也有俩种写法,但他的现代写法比传统法的效率高了许多
现代写法:
void operator=(string ch)//赋值拷贝
{
swap(_str,ch._str);
_size=ch._size,_capacity=ch._capacity;//这一块就不需要交换了,因为他析构的时候这些数据都用不到,交换反而效率变低了
}
赋值拷贝与拷贝构造本质上有一点区别就是,拷贝构其实是没有数据的,他是在用另一个容器的数据进行数据初始化,但是赋值拷贝,你已经有数据了和空间了,在用另一个容器初始化,那么你就要面临一个问题就是原来的数据你要手动的去释放,但是现代写法弥补了这一操作,因为出了函数作用域他就会自动的销毁
在传参的时候这个是不是没有写引用而是去拷贝构造,那么又可以实现了上吗的骚操作,那么为啥拷贝构造不用,你瓜不瓜,我都没写拷贝构造,他咋那样玩
2:modify
push_back
void push_back(const char ch)//只可以追加一个
{
if(_size>=_capacity)
{
Construction();//开空间
}
_str[_size++]=ch;
_str[_size]='\0';
}
他只可以用于插入一个字符,底层的实现也比较简,但是记得插入之后把 _size 这个位置,置为\0,你插入的时候把原来的\0给覆盖了
append
这一块的使用可以用到C中的库函数,strcat,直接追加如果不熟悉可以看博主写的这篇博客
void append(const char *ch)//追加一个字符串
{
int space=abs(ch._size-(_capacity-_size));//所需要的空间
reserve(space);
strcat(_str,ch);//入字符
}
+=
在文档中他有三个实现,这里我们就实现一个,在末尾追加一个类,另外俩个接口去附用 push_back 与 append 即可
string & operator+=(const string& ch)
{
if(_capacity-_size<=ch._size)
{
Construction(ch._size);//开空间
}
strcat(_str,ch._str);//入字符
return *this;
}
所以为啥我推荐直接用+= 本质上他已经把 push_back 与append 整合在了一起还新增了一个功能,你说爱不爱
[]
为什么我们可以直接用类名就可以直接访问容器中的数据呢,本质就是我们重载了操作赋,让方括号具有这项功能,且更重要的是他的无理地址空间是连续的,后续在list中这个将会发挥大吉之
char &operator[](size_t index)//普通版本
{
return _str[index];//引用,可以修改
}
const char & operator[](size_t index)const//consy
{
return _str[index];//不可修改
}
erase
指定位置删,同理文档中也重载了好多的版本吗,也是只实现一个,指定位置删除
string& erase(size_t pos,size_t length=-1)
{
assert(!empty());
if(_size-pos<length)//剩余字符少于你要删的字符
{
_str[pos]='\0';
}
else
{
for(int i=0;i<length;i++)
{
while(pos<_size)
{
_str[pos]=_str[++pos];
}
}
}
return *this;
}
也就是传说中的看到一个坑又挖一个坑,用挖坑的泥土填那个坑
insert
指定位置插入,文档中重载了好多的版本,我就实现一个在指定位置插入一个类
string & insert(size_t pos,const string &ch )
{
int space=abs(ch._size-(_capacity-_size));//所需要的空间
reserve(space);
for(int i=0;i<ch._size;i++)
{
int tail=_size+1;
while(tail>pos)
{
_str[tail]=_str[--tail];//挪动数据
}
_str[pos]=ch._str[i];
_size++;
}
return *this;
}
上述中的insert与erase博主不建议大家频繁调用,因为sring的底层其实就是一个连续的空间也就是数组,那么随机位置插入就要挪动数据,那么效率就不像push_back一样是O(1),而是O(size-pos),不说了你自己体会吧
上述中入数据如果不用C语言的函数那么就调用调用push_back,或者自己写个循环可以预先先开辟好空间,这样效率会比较高
3: Capacity
这一块就实现俩接口,resize 与reserve
resize
void resize(size_t n,char c='\0')//开空间初始化
{
if(n<_capacity)
{
Construction(n);//开辟空间
while(_size<_capacity)//初始化空间
{
_str[_size++]=c;//给空间中放字符
}
}
else if(n<_size)
{
_size=n;
_str[_size]='\0';
}
}
如果n< _capacity ,也啥都做,但是如果 n<_size,那么就会缩短size的长度,也就是字符串长度,且他开辟空间可以对空间进行初始化,那么为啥缩短需要加\0,增常不需要,emmm……,那里不需要好兄弟,开辟空间的时候已经把\0填进去了
n<_sizes时运行流程如图所示:
reserve
void reserve(size_t n)//单纯的开空间
{
if(n>_capacity)
{
Construction(n);//开辟n个空间
}
}
reserve n>_capacity 那么就开空间 ,如果小于则啥也不做
4:Iterator(迭代器)
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str+_size;//最后一个数据的下一个位置也就是\0
}
啥这就是迭代器,你你你是不是骗我,者不是炒冷饭吗,哈哈哈,本质上其实现在看来就是如此
本篇中我吧认为比较难,且需要细节把控的借口实现了,但是如size ,capacity这些接口就不需要花篇幅去描述他,上述的接口博主自己全部测试成功,且是更具自己的理解所写,但是效率可能还需要改进,如果你看出我那一块写的不好可以优化欢迎评论区,或者私信探讨。