String容器
顾名思义String类就是一个管理字符串的一个类,也可以理解为字符串的自定义类型。
String容器的成员变量
与之前c语言实现顺序表的结构很像,一个char类型的数组,一个表示字符串大小的_size变量,一个表示数组容量大小的 _capacity,还有一个静态成员变量 npos
String容器的成员方法
reserve:为字符串数组开辟空间
void reserve(size_t n = 0)
{
if (n <= _capacity)
{
return;
}
if (_size == 0)
{
_str = new char[n + 1];
return;
}
char* temp = new char[n + 1];
strcpy(temp, _str);
delete[] _str;
_str = temp;
_capacity = n;
}
reserve的实现分为三种情况:
1.传过来的n和原本的capacity相等或比他小 : reserve不支持缩容,直接return,不处理
2.n比原本的capacity大 :开辟大小为n的新空间,把旧空间的元素拷贝到新空间里面去,释放旧空间
3._str为空指针,还没有开辟任何空间:直接对_str开辟空间
构造函数
默认构造和c-string构造
问:我们能够把构造函数写成这样吗?
答:不能,因为str是被const char的字符串,不能赋值给char的字符串,这会导致权限放大的问题,编译器会报错,也不能把_str改成const char* 类型这样会导致后面不能执行修改操作。
所以最好的方法是:
string(const char* str = "")
:_size(strlen(str))
{
_capacity = _size == 0 ? 3 : _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
在初始化列表中,用str的字符串大小初始化_size,然后再去初始化_capacity,再去new _str的空间,这里的cacpacity代表的是不包括‘\0’的大小,所以要new _capacity+1,然后再用strcpy对_str初始化,再给传参位置缺省值,即实现c-string构造和默认构造,还顺带实现单个参数的构造函数支持隐式类型转换
拷贝构造
string(const char* str = "")
:_size(strlen(str))
{
_capacity = _size == 0 ? 3 : _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
和默认构造同一个逻辑,但是传参不同拷贝构造传的是常引用,因为会在三种情况中产生临时变量 1.类型转换 2.传值返回 3.传值传参,这三种情况都是产生临时变量后,再把临时变量拷贝到他要赋值的地方。
如下图:
substring构造
string(const string& str, size_t pos, size_t len = npos)
{
_size = len == npos ? str.size() : len;
if (_size > str.size() - pos)
{
_size = str.size() - pos;
}
_capacity = _size;
_str = new char[_capacity + 1];
memcpy(_str, str.c_str() + pos, _size);
}
逻辑:先计算要构造的大小,这里值得注意的是_size的大小不能单单被len赋值,最大只能是pos到传参字符串的末尾,然后下面逻辑默认构造一致
n个val构造
//n个val构造
string(size_t n, char c)
{
reserve(n);
for (int i = 0; i < n; i++)
{
push_back(c);
}
}
逻辑:先给_str开辟n个空间然后再push_back n个c进去
inerst:指定位置插入元素
string& insert(size_t pos, const string& s)
{
assert(pos <= _size);
int len = s._size;
//判断是否需要增容
if (_size+len >_capacity)
{
reserve(_size + len);
}
/* 方法一 */
保存pos后面的值
//char* temp = new char[_size - pos + 1];//错误原因:这里没有算\0的大小
//strcpy(temp, _str + pos);
直接往pos位置插入
//strcpy(_str + pos, s._str);
//strcpy(_str + pos + s._size, temp);
//delete[] temp;
//_size += len;
///* 方法二 */
//memcpy(_str + pos + len, _str + pos, _size - pos + 1);
//memcpy(_str + pos, s._str, len);
//_size += len;
/* 方法三 */
// 挪动数据
size_t newend = _size + len;
size_t oldend = _size;
while (newend >= pos + len)
{
_str[newend] = _str[oldend];
newend--;
oldend--;
}
memcpy(_str + pos,s._str, len);
_size += len;
return *this;
}
方法一:
主要逻辑:
遇到的bug:开辟临时变量的时候没有考虑到斜杠0,导致strcpy的时候写入斜杠0越界
方法二:
对方法一的优化,用memcpy,memcpy和strcpy最大的区别是memcpy可以指定复制多少内容而strcpy要把赋值到src的斜杠0位置,而且还省去了临时变量
方法三:
用while循环挪动数据,然后再用memcpy复制指定内容
bug日志:
size_t end = _size;
while (end >= pos)
{
_str[end + 1] = _str[end];
--end;
}
问:上面的拷贝代码能过吗?
答:在pos = 0 情况下不能,因为size_t类型的end无法小于0,无法退出循环。
erase:指定位置删除元素
string& erase(size_t pos = 0, 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;
}
return *this;
}
逻辑:分为两个情境:
1.len为缺省值或者len的值大于pos后面的字符串长度,就直接把pos后面的元素全部删除,直接在pos位置置为\0。
2.反之则把要len后面的元素覆盖拷贝到pos位置
如图:
resize: 将有效字符的个数改成n个,多出的空间用字符C填充
void resize(size_t n, char c = '\0')
{
if (n <= _size)
{
_str[n] = '\0';
_size = n;
}
else
{
if (n > _capacity)
{
reserve(n);
}
while (_size < n)
{
_str[_size++] = c;
}
_str[n] = '\0';
}
}
流输入、输出运算符重载
//<<重载
ostream& operator<<(ostream& out, const string& s)
{
for (auto c : s)
{
out << c;
}
return out;
}
//>>重载
istream& operator>>(istream& in, string& s)
{
s.clear();//cin流提取会清空之前的数据
char ch = in.get();//如果直接用流提取>>会拿不到空格和换行,因为会把空格和换行当做分隔
char buff[128]{};//避免频繁扩容
size_t i = 0;
while (ch != ' ' && ch != '\n')
{
buff[i++] = ch;
if (i == 127)//buff满了
{
buff[127] = '\0';
s += buff;
i = 0;
}
ch = in.get();
}
if (i != 0)//如果i不等0就代表buff大小在127内,则后加\0并+=
{
buff[i] = '\0';
s += buff;
}
return in;
}
流输出运算符重载:cout<<s1.c_str()<<endl 、cout<<s1<<endl 的区别,cout<<char*类型会输出截止到\0 ,cout<<string类型会输出截止到_size,所以cout<<string类型用范围for实现比较合适。
流输入运算符重载:为了避免反复扩容,用一个buff字符串数组,暂时接收流输入数据,然后等到输入完毕,再把buff内容添加到_str的末尾。
完整代码:代码链接