Ⅰ string的构造函数
这里是string的构造函数和拷贝构造函数,它支持用string,字符串,单个字母,迭代器等去构造。
#include<iostream>
#include<string>
using namespace std;
void TestString()
{
string a("hello world");//用字符串构造string
string s1(a);//用string构造
string s2(a, 0, 5);//用string的部分去构造,这里的 0 是开始拷贝的位置 5 是要拷贝字符串的长度
string s3(a.begin(), a.end());//用迭代器去构造,这里也可以用其他容器的迭代器
string s4;//生成一个空字符串
string s5(10, 'x');//用n个字母去构造
cout << "a : " << a << endl;
cout << "s1: " << s1 << endl;
cout << "s2: " << s2 << endl;
cout << "s3: " << s3 << endl;
cout << "s4: " << s4 << endl;
cout << "s5: " << s5 << endl;
}
int main()
{
TestString();
return 0;
}
要注意这里的用迭代器拷贝构造不仅仅可以用string的还可以用其他的容器。
对于构造函数的实现可以写成这样 ,这里只简单的实现了一下他的默认构造函数和拷贝构造函数。
string(const char* str="")
{
size_t len = strlen(str);
_str = new char[len + 1] {};
strcpy(_str, str);
_size = len;
_capacity = len;
}
string(const string& s)
{
string tmp(s._str);
swap(tmp);
}
这里的拷贝构造函数用的是现代写法,以前我们写sting的拷贝构造是先new一个新空间再把 s 的 _str 中的内容拷贝过来然后更新 _size 和 _capacity 再把这个新空间给 this 的_str , 这样就可以完成拷贝构造了,现代写法是先构造一个 tmp 再把tmp里边的 _str , _size ,_capacity 全部交换,当函数结束的时候tmp会调用析构函数,这样写的话也是一样的完成了对空间的拷贝,现代写法看起来更加的简单,但现代写法离不开对 swap 的实现,我们先看下为什么库里的 swap 不太行。
库中的实现是创造一个string类型的中间量再进行拷贝这种的效率太低了,因为类的拷贝如果涉及到一些空间的拷贝需要我们自己去实现深拷贝付出的代价太大。
void swap( string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity,s._capacity);
}
我们自己写的话会调用三次库里的 swap 去分别解决 _str ,_size, _capacity的问题对于 _str ,_size, _capacity都是内置类型拷贝的代价是比较小的对效率的影响是小的。如果我们去看文档我们可以看见在 string 的库中也实现了 swap 。
Ⅱ string的迭代器
1、 首先我们先看begin:
它是返回_str第一个字符的迭代器。那么我们是任何实现的呢?
typedef char* iterator;
iterator begin()
{
return _str;
}
我们在这先 typedef 把 char* 变成迭代器。这里的实现是一种简单的实现方式。 * 可以对迭代器解引用对指向的对象进行访问和改动。
2、再来看一下end
end 会返回一个最后一个位置的迭代器也就是 ‘\0’ 的位置。
3、rbegin , rend 和 begin,end的区别
rbegin 和 rend 被称为反向迭代器,也就是 rbegin 的指向的是 \0 的位置而 rend 是指向第一个字符的位置。画图来说明的话是这样的。
这里说明一下范围 for 是依靠迭代器去实现的只是编译器把迭代器的遍历披上了一层壳变成了范围 for ,如果要想在我们自己实现的 string 中实现范围 for 的话要先实现迭代器,要记得我们实现的迭代器要与库中的名字一模一样要不然范围for也是用不了的。
Ⅲ 元素访问
对于 operator[] 它将返回一个引用我们可以对 pos 位置进行改动。
back 是返回最后一个字符的引用我们可以对最后一个字符改动。
front 是返回第一个字符的引用我们可以对第一个字符进行改动。
Ⅳ 修改
1、operator +=
operator+= 既可以加等一个字符也可以加等一个string类型的字符串(这里也实现了对字符串的+=的重载其实这里只实现+=string类型就可以不用写对字符串的+=因为传值传参需要创建一个临时变量 ,这样这个字符串可以被构造成一个string类型的临时变量然后再被拷贝构造给 operator += 中)[]的重载可以很形象的像数组一样访问和改动其中的数据。
2、push_back 和 append
push_back 支持去尾插一个字符,而 append 支持尾插一个字符串要注意他们的区别。
string& string::operator+= (const string& c)
{
int len = strlen(c._str);
if (len == 0)
{
return *this;
}
if (len + _size > _capacity)
{
reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
}
for (int i = 0; i < len; i++)
{
_str[_size + i] = c._str[i];
}
_size = _size + len;
_str[_size] = '\0';
return *this;
}
string& string::append(const string& s)
{
*this += s;
return *this;
}
string& string::push_back(const char c)
{
*this += c;
return *this;
}
在实现中是复用了operator +=。
3、insert
insert 支持在字符串的任意位置头插一个字符。
string& string::insert(size_t pos, char c)
{
assert(pos < _size);
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
int end = _size + 1;
while (pos < end)
{
_str[end] = _str[end - 1];
end--;
}
_str[pos] = c;
return *this;
}
string& string::insert(size_t pos, const string& s)
{
assert(pos < _size);
int len = strlen(s._str);
if (_size + len > _capacity)
{
reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
}
int end = _size + len;
while (pos + len - 1 < end)
{
_str[end] = _str[end - len];
end--;
}
for (int i = 0; i < len; i++)
{
_str[pos + i] = s._str[i];
}
return *this;
}
上面这段代码是对 insert 的实现,重载了string和单个字符版本。实现过程中要注意的是要先判断容量是否足够如果不够的话要重新开空间,对于插入单个字符来说只需要把_capacity 扩大两倍就行了但对于插入一个字符串的话不能单单扩2倍一定要先判断字符串的长度然后再扩多大因为字符串如果比_capacity大的话扩2倍的话也会存不下。还有一点要注意 pos 是size_t类型的如果要在0位置头插的话要考虑有一些写法会隐式类型转化和类型提升比如pos = 0,end = -1 end >= pos的情况虽然end 是 int 类型但会被提升到 size_t 类型中 -1 补码全1 当是size_t 类型的话就是最大值这样end>=pos 就会一直为真。
4、erase
erase 如果只给pos 不给len 的话就会把pos 后面的全部删除,如果给的len超过pos后面剩余的个数的话也会把pos后面的全部删除。
下面是对erase的实现:
string& string::erase(size_t pos, size_t len )
{
assert(pos < _size);
if (len >= ( _size - pos))
{
len = _size - pos;
}
if (len == 0)
{
return *this;
}
for (int i = pos; i < _size - len + 1; i++)
{
_str[i] = _str[i + len];
}
_size = pos;
return *this;
}
Ⅴ string的其他的一些接口
1、find
find可以从头找到第一个匹配的位置并且返回这个位置。
2、rfind
rfind是从尾向头找第一个匹配的位置并且返回它。
3、operator>>
istream& operator>>(istream& in, string& s)
{
char* tmp = new char[256] {};
static size_t i = 0;
char ch = in.get();
while (ch != ' ' && ch != '\n')
{
tmp[i] = ch;
ch = in.get();
i++;
if (i == 255)
{
s += tmp;
i = 0;
}
}
if (i != 0)
{
s += tmp;
}
return in;
}
这里是我实现的流提前操作符,我先创建了一个字符数组先把提取到的值放到数组里,这是因为如果直接放到string 里的话如果太长的话会反复扩容这对于效率有很大的影响,但如果我现在栈上开一段空间等空间满了以后再把他们统一放到string中的话这样效率就会好一点因为在栈上开空间的消耗比在堆上释放空间再开空间的消耗要低很多。
这里还有一个要注意的是不能用cin去提取缓冲区里的字符,因为cin会自动忽略 \n ,' ' cin会把空格和换行符认为是分割符会自动忽略,而我们要实现的是当遇到空格和换行符的时候要终止,所以我们用了get ,get不会跳过空格和换行符会把缓冲区里的数据全部拿出来。
文章到此结束,如果有疑问的小伙伴可以在评论区提问记得一键三连哦~~~