我们在上半部分,简单的讲述了一下string类的具体用法,我们本节就从底层模拟实现string,能够更加清晰的学习string的底层。我会将模拟实现的代码放到文章最后。
目录
string成员变量
上一节,我们已经了解到string的底层就是一个字符数组,因此我们很容易定义出string的类成员
string的构造函数
我们先来实现无参的构造函数,利用初始化列表
无参构造函数
注意 ,这个无参的构造函数有潜在的风险,还需要进行改进,在下面的c_str函数我们会具体的讲
再实现带参的构造函数
带参构造函数
这里有一个问题,我们能按照下面的方式进行初始化吗?
string(const char* str)
:_size(strlen(str))
,_str(new char[_size+1])
,_capacity(_size)
{}
如果你还没有发现问题,不妨再去看看string成员变量的顺序
我们在类和对象的章节讲过初始化列表,并不按照初始化列表的顺序进行构造,而是按照类变量的定义顺序进行构造的,像上面的代码就会先初始化_str,而此时_size是一个随机值,那么就完蛋了,因此这里不太适合用初始化列表,我们还是老老实实的正常写即可。
拷贝构造
拷贝构造如果不手动实现的话,编译器会自动生成一份浅拷贝的版本,但是对于string类对象,浅拷贝显而是不够用的,很容易出现问题,因此,我们手动写一份深拷贝的版本。
拷贝构造的逻辑也并不复杂,如下所示
这种拷贝构造的实现是传统的写法,现在有现代的写法
利用标准库中的swap函数
我们构造一个临时的对象,利用构造函数进行构造,再利用swap交换二者的数据,就像是掠夺资源一样,将其的数据全部转移到我身上,然后在函数结束后,会自动销毁临时对象,而自身的数据还是保存的。
同样的思路还可以使用在operate=
赋值重载
赋值操作,要首先将原有的空间释放掉,不然会导致内存泄漏
还要提前判断是否二者相等,不然自己给自己赋值,就会删掉原有的数据,直接就变成随机值了
现代写法
c_str
c_str就是返回字符串的首地址,直到遇到 \0 才会终止
、
如果我们直接按照上面的写法,会有一个风险
如果我们创建了一个字符串对象,并用无参的构造函数进行构造,这样它的_str就是nullptr进行初始化的,如果用c_str()直接解引用的话,就直接导致程序崩溃。因此,我们可以改变一下我们的无参构造函数,将其的_str修改为带一个字符'\0'的字符串,_size和_capacity都不用修改
这里我们只需要传入缺省参数,就可以将带参和无参的构造函数合并
string的析构函数
在类里面定义的函数,默认是内联展开的,因此,我们可以将这种频繁调用且代码量小的函数,直接放到类里面来定义,而代码量比较大的函数则可以进行声明定义的分离。
string中频繁调用的短小函数
这些函数都是我们直接定义在类中的
我们可以看到operate[],我们模拟的和库里面的实现一样,定义了两种,一种是const类,另一种是正常的情况,并且是用断言来判断越界的情况的,然后直接返回访问元素的引用,可以进行直接的修改。
clear
只清掉数据,不清空间
string中的迭代器实现
我们看看目前我们实现的函数是否能用范围for进行遍历
从这里我们就可以看到没有办法使用范围for,而原因就是我们没有实现迭代器,这也就再次间接的证明了,范围for的底层就是用迭代器来实现的。
这里我们要注意,实现迭代器的时候必须严格按照库里面的命名风格,不然范围for是走不过去的
迭代器的底层实现还是有些复杂的,但是在这里,我们可以用一种简单的方式对迭代器进行实现,复杂的迭代器,我们在之后的list部分再进行切入。
当然简单的实现,就避免不了它有所缺陷,不过目前够用。
我们直接用原生指针就能简单的实现迭代器,我们也可以将迭代器和库中的迭代器一致,实现一个const迭代器,一个正常的迭代器,如下图所示
反向迭代器,要复杂的多,要用适配器模式来实现,这里我们先不实现。
其实就是将char*进行了typedef
其实所有的iterate都是由typedef过来的,但是并不一定全是指针,还有一些是自定义类型的
比如
list中的迭代器就是由自定义类型,typedef过来的
迭代器的实现就体现了封装的特性
屏蔽了底层的实现细节,提供了统一的类似访问方式,不需要关心容器底层的结构和实现细节
string中的其他函数
下面的函数,我们都进行定义和声明的分离,将其在类内进行声明,在其他文件中进行定义。
reserve
我们传入参数n,如果大于_capacity,那么我们就开辟一段新的空间,注意是n+1,因为要给'\0'留一个空间,然后拷贝数据到新空间,再进行交换删除即可。不要忘记修改_capacity的值。
push_back
尾插元素,先检查空间是否需要扩容,然后再进行插入,不过一定要记得在插入元素的后面加上'\0',因为我们插入的新元素,占据了原来的'\0',如果不添加一个新的'\0',就会在打印时出现烫烫烫或者屯屯屯。
append
实现append函数时,也是先检查容量,这里我们有两种实现思路,第一种就是需要多少扩容多少,还有一种方式,也就是我们实现的方法,对齐去实现,如果你需要的空间,为了避免频繁的扩容,大于_capacity的二倍,你要多少,我给多少,如果小于2倍,我就按2倍扩。
关于追加字符串部分
我们可以用C语言种的函数strcat,但是不推荐,因为strcat还要自己去找原有字符串str的'\0'在哪,然后再追加,太麻烦了,不如我们直接用strcpy,直接添加。
最后不要忘记修改_size的值
operate+=
两种实现方式,我们都直接调用我们写的push_back和append直接实现,十分的方便
insert
插入一个字符
在某个位置插入一个字符,意味着我们要挪动数据
首先用断言判断是否越界,然后判断是否需要扩容。
然后挪动数据,插入元素,最后修改_size即可
这里注意while()的循环条件,如果写成end>=pos ,对于大部分情况都可以通过,但是如果是头插的话,pos = 0,而end是size_t的类型,会导致死循环。
那有人可能会说,那我把end的类型修改成int不就好了吗?
通过调试我们发现,当end为-1,pos为0 居然还能进入这个循环,是编译器出错了吗?
实际上,这里是C语言中埋下的一个坑,当操作符两端是不同类型时,C语言会让其中一个隐式类型转换,end在比较的时候就悄悄的提升成了size_t类型。
我们可以再把pos改成int,也可以解决问题,但是库里面的pos就是size_t的类型,因为pos肯定不为负数。
所以我们还是老老实实的改变while的终止条件
插入一个字符串
大体逻辑就是先挪动数据,腾开空间,再插入数据
注意边界情况
erase
由于缺省值,只能在声明中定义,在定义函数这里就没法写npos,大家知道即可,声明头为
void erase(size_t pos, size_t len = npos);
删除操作,先判断是否越界,再判断len是否大于_size-pos,如果大于就说明将pos后的元素全部都删了,只需要将pos位置改为'\0',修改_size的值即可。
否则就把数据从前往后向前挪动即可,最后修改_size,注意是从前往后挪动,如果是从后往前挪动会导致数据覆盖
find
find函数是给定一个字符或字符串,然后从pos位置向后找
这两个函数pos的缺省值都为0,声明为
size_t find(char ch, size_t pos = 0);
size_t find(const char* str, size_t pos = 0);
查找一个字符
从pos位置依次寻找,找到就返回下标,找不到就返回npos
查找一个字符串
匹配字符串有好多种写法,第一种就是暴力查找,第二种是KMP算法,第三种还有BF算法,后两种都比较复杂,这两个算法的效率都很高,有兴趣的可以自己去了解一下,还是有一定难度的。
我们在这里就简单一点来实现,用一下C的strstr算法直接匹配过掉即可
substr
如果len大于_size-pos就更新一下有效字符
将len改成有效的长度
然后我们就创一个变量,开好空间,将从pos位置开始的len个字符全都放进去,直接返回即可
重载运算符
有这些接口
bool operator<(const string& s1, const string& s2);
bool operator<=(const string& s1, const string& s2);
bool operator>(const string& s1, const string& s2);
bool operator>=(const string& s1, const string& s2);
bool operator==(const string& s1, const string& s2);
bool operator!=(const string& s1, const string& s2);
ostream& operator<<(ostream& out, const string& s);
istream& operator>>(istream& in, string& s);
上面的六种重载比较符,我们只需要手动实现两个,其他的再复用这两个的逻辑就可以达到目的
如图所示,我们用C语言种的strcmp函数,实现了==和<
其他的运算符,我们就直接复用这两个的逻辑。
流插入流输出
流输出只需要将字符一个一个的提取出来,再依次输出即可
流提取的实现,首先清掉字符串的数据,然后也是一个字符一个字符的输入读取,判断是否为空格和换行符,就终止循环,最后直接接到字符串后面即可。
这里读取字符要用get方法,不然在缓冲区中会自动忽略掉空格和换行符,就导致没法正常的读取数据,而get方法恰好就能解决这个问题。
这里有一个优化,如果我们按照一个字符一个字符的提取,直接加到原有字符串的后面,如果我们输入了很长一段数据,那么就会不断的+=,也会不断的有异地扩容,再拷贝,效率十分的低。
因此,我们不如先开一个大一点字符数组buff,然后将读取的数据都放到数组里,当buff存满,就统一直接+=给原字符串s,如果还有数据,就再次走这个逻辑,大大提升了效率。
不仅如此,buff是一个临时变量,并不会长期占用内存,也能提升效率。
代码如图所示
流插入流提取并不需要全都设为友元函数,因为我们并不需要访问类的私有对象,我们用迭代器和重载[]就能达到目的。
引用计数的写实拷贝
写时拷贝就是一种拖延症,是在浅拷贝的基础之上增加了引用计数的方式来实现的。
引用计数:用来记录资源使用者的个数。在构造时,将资源的计数给成1,每增加一个对象使用该
资源,就给计数增加1,当某个对象被销毁时,先给该计数减1,然后再检查是否需要释放资源,
如果计数为1,说明该对象时资源的最后一个使用者,将该资源释放;否则就不能释放,因为还有
其他对象在使用该资源。
引用计数主要来解决两个问题,如图所示
这个引用计数写实拷贝的实际作用,要用博弈的思想来看待,如果我们只是拷贝,但是没有去修改,这里我们就只需要进行浅拷贝,节省空间。
如果要修改我们再进行深拷贝,也不影响什么。
博弈就在于,如果我们拷贝后,不修改,那就是浅拷贝,我们就赚了。
大致意思就算是这样,这里作了解即可。
模拟实现的代码
string.h
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;
namespace study
{
class string
{
public:
typedef char* iterator;
typedef const char* const_iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
string(const char* str = "")
{
_size = strlen(str);
// _capacity不包含\0
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
string(const string& s)
{
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
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;
}
~string()
{
if (_str)
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
}
const char* c_str() const
{
return _str;
}
size_t size() const
{
return _size;
}
size_t capacity() const
{
return _capacity;
}
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
const char& operator[](size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
void clear()
{
_str[0] = '\0';
_size = 0;
}
void reserve(size_t n);
void push_back(char ch);
void append(const char* str);
string& operator+=(char ch);
string& operator+=(const char* str);
string substr(size_t pos = 0, size_t len = npos);
void insert(size_t pos, char ch);
void insert(size_t pos, const char* str);
void erase(size_t pos, size_t len = npos);
size_t find(char ch, size_t pos = 0);
size_t find(const char* str, size_t pos = 0);
private:
char* _str;
size_t _size;
size_t _capacity;
const static size_t npos = -1;
};
}
string.cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include"string.h"
namespace study
{
void string::reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
void string::push_back(char ch)
{
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
void string::append(const char* str)
{
size_t len = strlen(str);
if (_size + len > _capacity)
{
// 大于2倍,需要多少开多少,小于2倍按2倍扩
reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
}
strcpy(_str + _size, str);
_size += len;
}
string& string::operator+=(const char* str)
{
append(str);
return *this;
}
string& string::operator+=(char ch)
{
push_back(ch);
return *this;
}
void string::insert(size_t pos, char ch)
{
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 string::insert(size_t pos, const char* s)
{
assert(pos <= _size);
size_t len = strlen(s);
if (_size + len > _capacity)
{
// 大于2倍,需要多少开多少,小于2倍按2倍扩
reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
}
size_t end = _size + len;
while (end > pos + len - 1)
{
_str[end] = _str[end - len];
--end;
}
for (size_t i = 0; i < len; i++)
{
_str[pos + i] = s[i];
}
_size += len;
}
void string::erase(size_t pos, size_t len)
{
assert(pos < _size);
if (len >= _size - pos)
{
_str[pos] = '\0';
_size = pos;
}
else
{
for (size_t i = pos + len; i <= _size; i++)
{
_str[i - len] = _str[i];
}
_size -= len;
}
}
size_t string::find(char ch, size_t pos)
{
assert(pos < _size);
for (size_t i = pos; i < _size; i++)
{
if (_str[i] == ch)
{
return i;
}
}
return npos;
}
size_t string::find(const char* str, size_t pos)
{
assert(pos < _size);
const char* ptr = strstr(_str + pos, str);
if (ptr == nullptr)
{
return npos;
}
else
{
return ptr - _str;
}
}
string string::substr(size_t pos, size_t len)
{
assert(pos < _size);
// len大于剩余字符长度,更新一下len
if (len > _size - pos)
{
len = _size - pos;
}
string sub;
sub.reserve(len);
for (size_t i = 0; i < len; i++)
{
sub += _str[pos + i];
}
return sub;
}
bool operator<(const string& s1, const string& s2)
{
return strcmp(s1.c_str(), s2.c_str()) < 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 strcmp(s1.c_str(), s2.c_str()) == 0;
}
bool operator!=(const string& s1, const string& s2)
{
return !(s1 == s2);
}
ostream& operator<<(ostream& out, const string& s)
{
for (auto ch : s)
{
out << ch;
}
return out;
}
istream& operator>>(istream& in, string& s)
{
s.clear();
const int N = 256;
char buff[N];
int i = 0;
char ch;
//in >> ch;
ch = in.get();
while (ch != ' ' && ch != '\n')
{
buff[i++] = ch;
if (i == N - 1)
{
buff[i] = '\0';
s += buff;
i = 0;
}
//in >> ch;
ch = in.get();
}
if (i > 0)
{
buff[i] = '\0';
s += buff;
}
return in;
}
}