string是用来存储字符类型数据的容器,由于string比STL出现的早,所以在C++官方网string并不在container分类中,但是我们也可以称它为容器。
string有两个类模板,我们主要学习的是 basic_string这个类模板实例化出来的 string (basic_string<char>的重命名)类。我们在C语言中学的字符都是以ASCII码的形式来表示和存储的,但是ASCII码只能表示128个字符。对于不同的语言不同的字符,比如我们的汉字,是无法只用ASCII码就能完全表示的,所以在计算机科学中,还有很多种编码方式用来表示字符,比如utf-8、utf-16编码,不同的编码方式自然不能用同一个类来表示,以basic_string模板就能实例化多个不同的类,用来存储这些不同编码方式的字符。我们学习的string就是存储char类型的字符的类。
1.string的结构
string本质上是一个动态增长的数组,与顺序表类似,只是它是专门用来管理字符类的。
string实际上是对 basic_string<char>的重命名,char就是用来存utf-8编码,也可以存储表示ASCII码。
首先我们需要了解一个string对象的成员变量有哪些
在vs编译器中,一个string对象的内部有一个size和capacity变量,还有allocator空间配置器,我们目前可以将空间配置器直接理解为动态开辟数组,用来存储数据,我们直接以顺序表的方式来理解就行了。
2.接口及模拟实现
首先是string的构造函数,string的构造函数有很多个,但是我们实际上常用的也就三四个
使用起来也很简单,我们只要参数能够与构造函数匹配上就行了
string s1;//默认构造
string s2("abcdefg");//常量字符串构造
string s3(s2);//拷贝构造
string s4(s2, 3, 2);//用s2的字符串第三个位置开始的2个字符进行构造
string s5("abcdefg", 5);//用常量字符串的前5 个字符进行构造
我们可以看到上面的额有一些构造函数中有一个缺省值 npos ,这是什么呢?
我们可以在文档中看到npos是一个string类的const修饰的静态成员,他的值是size_t的-1,也就是一个32位二进制全是1的正数,大约是四十二亿,她表示的是一个对象的最大数据个数,也就是_size的最大值。那么我们的string类就可以这样定义
private:
char* _str;
size_t _size;
size_t _capacity;//表示可存储的数据个数,比实际容量要小 1 ,确保最后要存储一个 \0
const static size_t npos = -1;//C++的一个特例,就是const static的整型成员变量可以通过缺省值初始化。
我们在这里直接用缺省值对npos进行初始化,这是C++的一个特殊情况,对于const static的整型类型的成员变量可以直接用缺省值进行初始化。
我们自己实现的string类,不一定按照vs的扩容规则来,我们可以按照自己的想法来进行扩容和初始化。
string(const char* str)
{
int len = strlen(str);
_capacity = len;
_size = len;
_str = new char[_capacity+1];
strcpy(_str, str);
}
string(const string& s)
{
_size = s._size;
_capacity = s._capacity;
_str = new char[_capacity + 1];
strcpy(_str, s._str);
}
string()
{
_size = 0;
_capacity = 0;
_str = new char[1];
_str[_capacity + 1] = '\0';
}
同时我们可以将无参构造和常量字符串的构造写成一个构造函数,无参无非就是只有一个'\0'。
string(const char* str="")
{
int len = strlen(str);
_capacity = len;
_size = len;
_str = new char[_capacity+1];
strcpy(_str, str);
}
三种遍历方式:
对于string对象的遍历我们有三种方式,第一种就是用方括号,以数组的形式进行遍历。我们可以在文档中看到对方括号的重载
我们可以看到,方括号的重载有两个函数重载,一个是针对普通的string对象的,它返回的是可读可写的引用,另一个是针对const对象的,它返回的是只读的引用。这种遍历方式我们需要知道string对象的size来控制循环条件
第二种方式就是范围for,范围for我们在之前就已经用过了,他的底层就是用迭代器来实现的。
第三种遍历方式就是直接使用迭代器来实现,而string的迭代器的接口如下
目前我们可以把迭代器当成指针来使用,它的用法和指针类似,但是他的实现并不一定是指针。
begin返回的是指向数据开头的迭代器,而end返回的是指向数据结尾的后一位的迭代器,也就是'\0'的位置。我们可以当成指针来理解,而string类的迭代器我们是可以直接用指针来实现的,因为对迭代器++就是指向下一个位置了,而对于其他的容器的迭代器则不一定使用指针来实现的。迭代器是容器的一种通用的遍历方式,后面我们见到的容器基本都是使用迭代器来遍历的。
同时我们还要实现size函数来支持 循环遍历
文档中说明 size接口返回的就是有效数据的个数,也就是字符串的长度。
我们可以重载方括号和迭代器来实现遍历
//方括号重载
//普通对象
char& operator[](size_t pos)
{
return this->_str[pos];
}
//const对象
const char& operator[](size_t pos)const
{
return this->_str[pos];
}
//迭代器接口实现
typedef char* iterator;
//普通对象
iterator begin()
{
return &this->_str[0];
}
//const对象
const iterator begin()const
{
return &this->_str[0];
}
//普通对象
iterator end()
{
return &this->_str[_size];
}
//const对象
const iterator end()const
{
return &this->_str[_size];
}
//size接口,返回_size
size_t size()
{
return _size;
}
string还有很多关于大小和容量的接口。
重点的其实就是上面用红色框起来的几个接口
size和length其实是完全一样的功能。capacity和empty以及clear接口我们也不难理解,resize和reserve接口的作用我们可以看文档来了解
resize 的功能就是调整数据的个数,如果 n 比原来的 size 小,那么就相当于丢掉后面的数据,只保留 n 个,而如果 n 大于原来的size,则多出来的空间用我们传的参数来补充 ,如果我们没有指定字符,那么就用空字符,一般是 '\0' 。
要注意的一点就是,resize影响的是数据的个数,如果是减少数据个数,则不会所容,如果增加数据个数同时容量不同了,就会扩容。
而string的扩容函数就是reserve函数,既能缩容,也能扩容。
对于reverse,如果n比原来的空间大的话,则扩容到可以存储 n 个数据,也就是实际空间 n+1,如果比 原来的空间小,则缩容到 n ,也就是实际空间 n+1 ,多出来的则截断。
void resize(size_t n, char ch = '\0')
{
if (n >= _size)//扩容
{
reserve(n);
for (size_t begin = _size; begin < n; begin++)
{
_str[begin] = ch;
}
_capacity = n;
}
//缩容不改_capacity
_str[n] = '\0';
_size = n;
}
void reserve(size_t n)
{
char* tmp = new char[n+1];
//扩容
if (n > _size)
{
strcpy(tmp, _str);
}
//缩容
else
{
strncpy(tmp, _str, n);
_size = n;
}
tmp[n] = '\0';
delete[]_str;
_str = tmp;
_capacity = n;
}
void clear()
{
delete[]_str;
_size = 0;
_capacity = 0;
_str = new char[1];
_str[0] = '\0';
}
size_t capacity()
{
return _capacity;
}
bool empty()
{
return _size == 0;
}
at 接口与方括号有一些类似,返回相应位置的字符,也要实现const对象和非const对象的重载。
char& at(size_t pos)
{
assert(pos < _size);
return _str[pos];
}
const char& at(size_t pos)const
{
assert(pos < _size);
return _str[pos];
}
修改的接口也有很多,但是我们常用的也只有几个
+=有三个重载函数,在实现+=的时候最重要的是控制扩容。
//尾插对象
string& operator+=(const string& s)
{
//扩容
if(_size+s._size>_capacity)
{
reserve(_size + s._size);
_capacity = _size + s._size;
}
//尾插
strcpy(_str + _size, s._str);
_size += s._size;
return *this;
}
//尾插字符串
string& operator+=(const char* str)
{
size_t len = strlen(str);
//扩容
if (_size + len > _capacity)
{
reserve(_size + len);
_capacity = _size + len;
}
//插入
strcpy(_str + _size, str);
_size += len;
return *this;
}
//插入字符
string& operator+=(char ch)
{
//扩容
if (_size == _capacity)
{
_capacity *= 2;
reserve(_capacity);
}
_str[_size] = ch;
_size++;
return *this;
}
append函数的实现我们可以完全复用 += 来实现。
string& append(const string&s)
{
*this += s;
return *this;
}
string& append(const char* str)
{
*this += str;
return*this;
}
string& append(const string& s, size_t begin, size_t num)
{
//扩容
if (_size + num > _capacity)
{
reserve(_size + num);
_capacity=_size + num;
}
size_t end = _size;
_size += num;
for (size_t i = 0; i < num; i++)
{
if (begin > s._size)
break;
(*this)[end++] = s[begin++];
}
(*this)[_size] = '\0';
return *this;
}
string& append(size_t n, char ch)
{
while (n--)
{
*this += ch;
}
return *this;
}
push_back我们直接复用 +=
void push_back(char ch)
{
*this += ch;
}
insert接口:
insert的实现最重要的地方就是挪数据的循环控制以及扩容
string& insert(size_t pos,const string& s)
{
//当s为空对象,直接返回
if (s._size == 0)
return *this;
assert(pos <= size());
//扩容
if (_size + s._size > _capacity)
{
reserve(_size + s._size);
_capacity = _size + s._size;
}
//移动数据
_size += s._size;
size_t end = _size;
while (end >= pos + s._size)
{
_str[end] = _str[end - s._size];
--end;
}
//插入数据
size_t begin = 0;
while (begin < s._size)
{
_str[begin + pos] = s._str[begin];
begin++;
}
return *this;
}
string& insert(size_t pos, const char* str)
{
assert(pos <= _size);
size_t len = strlen(str);
if (len == 0)
return *this;
//扩容
if (_size + len > _capacity)
{
_capacity = _size + len;
reserve(_capacity);
}
//挪数据
size_t end = _size + len;
_size += len;
while (end >= pos+len)
{
_str[end] = _str[end - len];
end--;
}
//插入
size_t begin = 0;
while (begin < len)
{
_str[begin + pos] = str[begin];
begin++;
}
return *this;
}
string& insert(size_t pos, size_t n, char ch)
{
assert(pos <= _size);
if (n==0)
return *this;
//扩容
if (_size + n > _capacity)
{
_capacity = _size + n;
reserve(_capacity);
}
//挪数据
size_t end = _size + n;
_size += n;
while (end >= pos + n)
{
_str[end] = _str[end - n];
--end;
}
//插入数据
size_t begin = 0;
while (begin < n)
{
_str[begin + pos] = ch;
++begin;
}
}
erase函数的接口我们就实现一个就行了 ,如果第二个参数不传的话,默认把这个位置及以后的全部删除。
string& erase(size_t pos, size_t len=npos)
{
assert(pos <= _size);
if (len == npos||pos+len>_size)//npos要单独讨论,会溢出
{
_str[pos] = '\0';
_size = pos;
return *this;
}
else
{
//挪数据,要把'\0'也一起挪
size_t begin = pos;
while (begin <= _size-len)
{
_str[begin] = _str[begin + len];
++begin;
}
_size -= len;
return *this;
}
}
find函数我们要注意的就是pos缺省值为0。找不到返回npos
size_t find(const string& s, size_t pos = 0)const
{
assert(pos < _size);
char* ret=strstr(_str + pos, s._str);
if (ret == NULL)
return npos;
return ret - _str;
}
size_t find(const char*str, size_t pos = 0)const
{
assert(pos < _size);
char* ret = strstr(_str + pos, str);
if (ret == NULL)
return npos;
return ret - _str;
}
size_t find(char ch, size_t pos = 0)const
{
assert(pos < _size);
size_t ret = 0;
while (ret < _size)
{
if (_str[ret] == ch)
return ret;
ret++;
}
return npos;
}
重载流插入和流提取
流插入重载我们要注意的就是不要直接打印 c_str ,因为我们存储的字符数据中间可能有 \0 , \0是可以作为数据存储的,所以我们要逐字符打印。
ostream& operator<<(ostream& out, const string& s)
{
size_t begin = 0;
size_t end = s.size();
while (begin < end)
{
cout << s[begin++];
}
return out;
}
而对于流提取的重载,首先要清除数据,然后我们要注意,如果是单纯用>>来逐个读取字符然后+=到后面的话
istream& operator>>(istream& in, string& s)
{
s.clear();
char ch;
in >> ch;
while(ch!='\n')
{
s += ch;
in >> ch;
}
return in;
}
这样写的话,当读取到空格时就会停止读取,导致 ch 一直是空格,死循环了。而且这样写的话会频繁调用 += ,效率很低。
为了将空格也读取进来同时避免频繁调用 += ,我们可以用一个数组先存储读取到的内容,然后一次性 += 到对象中。但是这时候又有一个问题,就是我们无法预知到要输入的数据的个数,也就无法确定数组的大小。 一种思路就是设置一个固定大小的数组,当数组存满的时候就 += 到对象中,然后继续从头覆盖存储,用一个下标记录。
为了将空格也读取进来,我们可以用 istream 类的get函数
istream& operator>>(istream& in,string& s)
{
s.clear();
char buffer[128]={'\0'};
size_t i = 0;
char ch;
in.get(ch);
while (ch != '\n')
{
if (i == 127)//满了就先存进去
{
s += buffer;
i = 0;
}
buffer[i++] = ch;
in.get(ch);
}
//剩余的数据放进去
buffer[i] = '\0';
s += buffer;
return in;
}
赋值重载
string& operator=(const string& s)
{
_size = s._size;
_capacity = s._capacity;
reserve(_capacity);
strcpy(_str, s._str);
return *this;
}