目录
10.删除操作的实现 删除erase中的参数有pos(开始删除的位置),len删除字符的长度(个数)
底层模拟实现并不是与源代码一模一样,而是加入了一定简化,使得当前阶段能够理解。
基本成员变量;
这里只是一部分,用来展示基本的成员
private:
char* _str;
size_t _size;
size_t _capacity;
1.基本框架
1.初始化构造函数,析构函数的模拟实现(非常重要)
string在初始化的时候,可以省略不给字符,也可以用字符串去初始化。所以我们自然会想到去写一个函数重载:1.一个不给参数,.2给一个字符串。
1用初始化列表
string()
:_str(new char[1]),
_size(0),
_capacity(0)
{
}
2.字符串初始化
这里有一个细节:尽量不要在初始化列表就写:_capacity=_size,因为如果实现时把成员变量之间的上下位置打乱,写反,会导致初始化执行的顺序改变(初始化列表初始化的顺序时的顺序是按照成员变量的书写顺序来进行的,比如说_capaciy在_size前面,那么会先初始化_capacity,会导致出错:_size化石随机值的时候就赋值给_capacity),所以我们在下面函数体里面再赋值。
string(const char* str)
:_size(strlen(str))
{
_capacity = _size == 0 ? 3 : _size;
_str = new char[_capacity+1];//这里要把反斜杠0的位置加上
strcpy(_str, str);//这里会把反斜杠0拷贝进去
}
但是默认构造函数只有一个,所以我们需要将其和二唯一:利用缺省参数可以做到这一点:这里给一个空串即可,里面会自动产生反斜杠0,至于_capacity为什么不可以为零,后面会再细说
这里解答一个问题:为很么空间要开辟_capacity+1?因为其实_capacity指代的是有效元素的空间,即没有包含反斜杠0,但是实际操作的时候会访问,所以也要把空间给出来,否则会出现非法访问的问题。
string(const char* str="")
:_size(strlen(str))
{
_capacity = _size == 0 ? 3 : _size;
_str = new char[_capacity+1];//这里要把反斜杠0的位置加上
strcpy(_str, str);//这里会把反斜杠0拷贝进去
}
2.析构的模拟实现
(注意new和delete 配合使用)
~string()
{
delete[]_str;//new 和 delete 配对使用
_str = nullptr;
_size = _capacity = 0;
}
3.拷贝的模拟实现
因为对于string的自定义类型,有char*的存在,所以需要进行深拷贝:即内存要独立拷贝一份,否则会出现两个指针指向同一块空间的问题,会带来很多错误。
string(const string& s)
:_size(s._size),
_capacity(s._capacity)
{
_str = new char[_capacity + 1];
strcpy(_str, s._str);
}
2. 基本功能函数实现
1.c_str
返回指向数组的指针,并且末尾带反斜杠0。
const char* c_str()
{
return _str;
}
2.[ ]方括号操作的实现
一开始我们认识数组的时候,常用的a[i](a是数组名,i是下角标) ,现在我们知道其实这是一种运算符重载:
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
仔细想想,其实这里还有一点不足的地方,如果调用[ ]的对象是有const修饰的,但是*this却是可写可读的,会导致权力的放大问题,所以我们需要加上尾部const的修饰来解决这个问题。
想法很好,但是这里又出现了问题,如果加上后置的const,那么如果我们需要a[i]++,这种操作的时候就无法改变值了,所以综上我们就需要函数重载,编译器会自动选择当前对合适的。
const char& operator[](size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
3.赋值操作实现
赋值分为两种情况:s1=s2(这种不同的对象的赋值),还有s1=s1这种自己给自己赋值,所以我们要判断是否是第二种,如果是则不用处理。
//s1=s1自己给自己赋值要处理一下
string& operator=(const string& s)
{
//s1=s1自己给自己赋值要处理一下
if (this != &s)
{
char* tmp = new char[s._capacity + 1];//先用一个临时变量接收所开的空间大小
//这样就能避免破坏,如果抛异常就直接结束
strcpy(tmp, s._str);
delete[]_str;
_str = tmp;
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
但是有可能会有这样的写法:
就是与上面不同的就是:先删除释放了_str的空间,这样化,如果下面的new失败了抛异常,那么前面的删除操作就堆元数据产生了破坏,简单说:赋值没完成,反而把调用赋值的对象弄丢了。所以写的时候我们可以将new放在开始,先用临时变量接收,这样就可以避免破坏了。
if (this != &s)
{
delete[]_str;
_size = s._size;
_capacity = s._capacity;
_str = new char[_capacity + 1];//如果这里抛异常了,会导致this破坏
strcpy(_str, s._str);
return *this;
4. 扩容操作(reserve)
参数即是一个无符号整数n,但是这个数有要求:n>_capacity,否则变成缩容,或者无意义操作,并且同样采取先new的策略,防止破坏。
先用临时变量接收空间和内容,再将临时变量给到_str,最后记得调整_capacity的值
void reserve(size_t n)//扩容
{
if(n > _capacity)//但是依然有一点问题,如果n<_capacity的话是不是就变成缩容了,所以这里我们加一个判断
{
char* tmp = new char[n + 1];//这里要包含进'\0'
strcpy(tmp, _str);
delete[]_str;
_str = tmp;
_capacity *= 2;
}
}
5.尾插操作(push_back 插入单个字符)
首先既然是插入,那么就需要考虑是否需要扩容;然后再_size位置插入;最后记得补上反斜杠0(因为原来的反斜杠0被新插入的值覆盖了)
void push_back(char ch)//尾插可以服用插入
{
if (_size + 1 > _capacity)
{
reserve(_capacity * 2);
}
_str[_size] = ch;
_size++;
_str[_size] = '\0';//此处要继续让\0继续存在作为结束,否则后面会出现乱码
}
6.尾插操作,插入一串字符
要将字符串整体移动拷贝,我们会想到strcpy这个库函数,所以思路就很清晰了,不过前提依然是要检查是否要进行扩容。
void append(const char* str)
{
int len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
strcpy(_str + _size, str);
_size += len;
}
7.string的便捷尾插操作: +=
string类中有一种十分方便的尾插操作+=,所以我们将上面两种复用起来,构成运算符重载operator+=;加返回值可能连续插入。
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
string& operator+=(const char* str)
{
append(str);
return *this;
}
8.pos位置插入操作实现(在pos位置插入单个字符)
可以加一个断言,然后检查内是否扩容,然后挪动数据,最后再pos位置插入,所以肯恩给就会写出以下的代码:
void insert(size_t pos , char ch)
{
assert(pos <= _size);
if (_size + 1 > _capacity)
{
reserve(_capacity * 2);
}
size_t end = _size;
while (end >= pos)
{
_str[end + 1] = _str[end];
}
_str[pos] = ch;
_size++;
}
这个函数实现,插入哪里都没有问题,但是唯独无法在0位置插入,为什么呢?
我们分析一下:当最后依次有效移位执行完毕,end会落在pos的位置上(就是0的位置) ,那么接下来end要减一,所以end的值会变成-1,这里看起来并没有问题end<pos了,循环不能再执行了,但是这是有符号数的情况下,此时的end是size_t类型的无符号数-1相当于是整型的最大值,最大值非常大,会大于pos(0)循环又进行了下去,造成无限的循环。
处理方法:将end最终减1后的落点控制在0,而不是-1即可。
详细解释:end=_size+1; _str[end]=_str[end-1]; 这样既可以将0位置的数据往后移动一位,也可以使得end减1后的落点不为-1.
更改代码展示:
void insert(size_t pos , char ch)
{
assert(pos <= _size);
if (_size + 1 > _capacity)
{
reserve(_capacity * 2);
}
size_t end = _size + 1;
while (end > pos)
{
_str[end] = _str[end - 1];
--end;
}
_str[pos] = ch;
_size++;
}
9.pos位置插入一串字符串
同上面一样,只不过len的大小不再是1,取决于字符串的长度。原来挪动数据的条件是(end>pos+1-1),变成(end > pos+len -1),也可以是(end >= pos+len),前面的写法只是为了和单个字符插入的形式统一
代码:
void insert(size_t pos, const char* str)
{
assert(pos <= _size);
size_t len = strlen(str);
size_t end = _size + len;
if (_size + len > _capacity)
{
reserve(_size + len);
}
while (end > pos + len - 1)
{
_str[end] = _str[end - len];
end--;
}
}
10.删除操作的实现
删除erase中的参数有pos(开始删除的位置),len删除字符的长度(个数)
删除有三种情况:1.删除的长度<pos位置后面字符的个数
2.删除的长度>pos位置后面字符的个数
3.删除的长度没给,这里会自动补npos(值为-1,无符号下为整型的最大值) :默认全部pos位置后面的全部删除。
void erase(size_t pos,size_t len =npos)
{
assert(pos <= _size);
if (len == npos || len > _size - pos)
{
_str[pos] = '\0';
_size = pos;
}
else
{
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
}
npos是静态成员变量,要在类外面进行赋值操作 :(此处代码展示不完全,仅用来说明展示)
private: char* _str; size_t _size; size_t _capacity; static size_t npos;//静态成员变量不能给缺省值,因为缺省值是给初始化列表用的 //初始化列表是初始化成员变量的,而不初始化静态成员变量(后者属于整个类,属于所有对象) }; size_t string::npos = -1;
3.字符串的比较
想到比较,第一时间会想到是比_size,还是比_capacity,单其实都不是,字符串的大小比较用的是库函数strcmp,这样ilu就清晰了,写出大于等于或者小于等于之后就可以复用
代码展示:
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 || s ==* this;
}
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);
}
到此,string基本的模拟框架就完成了,堆string底层的理解又进了一步!