标准库类型string表示可变长的字符序列,使用string类型必须首先包含string头文件。作为标准库的一部分,string定义在命名空间std中。
char* _str;
size_t _size;
size_t _capacity; //不包含最后做标识的'\0'
static const size_t npos;
_str用来存储字符串,_size表示字符串有效数据个数,_capacity表示容量大小,其中不包含最后做标识的‘\0’。
例如这样一段代码:string str(“hello”);
npos是一个静态成员常量,值为-1,因为size_t是无符号整数类型,所以它表示size_t类型的最大值。
当npos用在做成员函数中len的参数时,表示“直到字符串的结尾”,
例如用来删除字符串中某一部分的函数erase(size_t pos = 0, size_t len = npos),如果没传参数len,那么就会从pos位置直接删到最后。
当作为返回值时,npos通常表示不匹配,例如find函数返回npos,就意味着没找到。
初始化string对象常用的几种方式:
string s1("hello"); //默认初始化,s1是一个空字符串
string s2 = s1; //s2是s1的副本
string s3 = "hello";
string s4(5, 'c'); //s4的内容是ccccc
string(const char* str = "")
:_size(strlen(str))
,_capacity(_size)
{
_str = new char[_capacity + 1];
strcpy(_str, str);
}
这里需要注意的是参数列表的初始化顺序与初始化列表列出的顺序无关,只与它在类中声明顺序有关,由于我们声明成员变量顺序_size在_capacity前面,所以这里_size也要在_capacity前面。
容量_capacity中不包含’\0’,所以申请空间时多申请一位。
重载一个用来初始化s4的构造函数
string(const size_t n, const char ch)
:_size(n)
, _capacity(_size)
{
_str = new char[_capacity + 1];
for (size_t i = 0; i < n; ++i)
{
_str[i] = ch;
}
_str[_size] = '\0';
}
string(const string& s)
:_str(nullptr)
{
string tmp(s._str);
swap(tmp);
}
用s._str去构造临时对象tmp,这里引用传参s是s1的别名,tmp调用构造函数开空间拷贝数据,所以最后tmp和s1是一样的数据一样的大小,而tmp的空间是s2想要的,所以把他们交换。这样s2就达到深拷贝的效果了。
tmp是局部对象,出函数作用域会调用析构函数,而s2的_str指向的位置是随机值,把tmp和s2交换后tmp的_str就变成了随机值,不能对一个随机的位置进行释放,所以先在参数列表中把s2的_str指向nullptr。
另外还需要提供一个swap函数交换两个对象:
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
string& operator=(string s)
{
swap(s);
return *this;
}
string s1(“hello”);
string s2(“world”);
s2 = s1;
s1传值传参给s,调用拷贝构造深拷贝,s和s1是一样的,把s和s2交换,出函数作用域后形参s调用析构函数释放资源。
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
迭代器是一个像指针一样的东西,有可能是指针,也有可能不是指针。
begin()返回第一个有效数据位置的迭代器
end()返回最后一个有效数据的下一个位置的迭代器
vector/string这种底层用连续一段空间存储数据,支持[ ] + 下标访问,迭代器用原生指针即可。
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
typedef const char* const_iterator;
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
也一样提供非const版本和const版本
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
const char& operator[](size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
现在可以创建一个string对象并且遍历了。
reserve()函数用来修改字符串容量的大小。如果申请空间的newcapacity大于当前的capacity,则分配新的存储空间,并使capacity 等于或大于 newcapacity。如果newcapacity小于当前容量,则是一个非绑定收缩请求。
从C++20起如果newcapacity小于或等于当前容量,则没有效果。
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
resize()用来将字符串大小调整为n个字符的长度。
如果 n 小于当前字符串长度,则将当前size缩短为n。
如果 n 大于当前字符串长度,则通过在末尾插入所需数量的字符来扩展当前内容,以达到 n 的大小。 如果指定了字符,则将新元素初始化为该字符,否则初始化为空字符。
void resize(size_t n, char ch = '\0')
{
if (n < _size)
{
_str[n] = '\0';
_size = n;
}
else
{
if (n > _capacity)
{
reserve(n);
}
for (size_t i = _size; i < n; ++i)
{
_str[i] = ch;
}
_size = n;
_str[_size] = '\0';
}
}
将给定字符加到字符串的末尾。
void push_back(char ch)
{
if (_size >= _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
reserve(newcapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
在字符串结尾添加字符串
void append(const char* str)
{
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
strcpy(_str + _size, str);
_size += len;
}
有了push_back()和append()就很方便重载+=
右操作数为字符:
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
右操作数为字符串:
string& operator+=(const char* str)
{
append(str);
return *this;
}
右操作数为对象:
string& operator+=(const string& s)
{
*this += s._str;
return *this;
}
在字符串任意位置插入一个字符或字符串。
push_back()和append()都可以复用insert()。
插入字符:
string& insert(size_t pos, char ch)
{
assert(pos <= _size);
if (_size == _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
reserve(newcapacity);
}
for (size_t i = _size + 1; i > pos; --i)
{
_str[i] = _str[i - 1];
}
_str[pos] = ch;
++_size;
return *this;
}
插入字符串:
string& insert(size_t pos, const char* str)
{
assert(pos <= _size);
size_t len = strlen(str);
if (len + _size > _capacity)
{
reserve(len + _size);
}
for (size_t i = _size + len; i >= (pos + len); --i)
{
_str[i] = _str[i - len];
}
for (size_t i = 0; i < len; ++i)
{
_str[pos + i] = str[i];
}
_size += len;
return *this;
}
删除字符串的一部分,减少它的长度,如果没给参数len就会从pos位置直接删到最后。
string& erase(size_t pos = 0, size_t len = npos)
{
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;
}
return *this;
}
查找从pos位置开始第一个给定字符或字符串。找到了返回对应字符或子串第一个字符第一次出现的位置,没找到返回npos。
查找字符:
size_t find(char ch, size_t pos = 0)
{
for (size_t i = pos; i < _size; ++i)
{
if (_str[i] == ch)
return i;
}
return npos;
}
查找字符串:
size_t find(const char* sub, size_t pos = 0)
{
const char* p = strstr(_str + pos, sub);
if (p)
return p - _str;
return npos;
}
ostream& operator<<(ostream& out, const string& s)
{
for (size_t i = 0; i < s.size(); ++i)
{
out << s[i];
}
return out;
}
istream& operator>>(istream& in, string& s)
{
s.clear();
char ch = in.get();
while (ch != ' ' && ch != '\n')
{
s += ch;
ch = in.get();
}
return in;
}
其他运算符重载和一些简单的函数在完整代码给出。
模拟实现string类完整代码