String类
1. string类的价值
在C语言中,C标准库提供了一系列str开头的库函数,这些库函数可以管理以\0
结尾的一些字符的集合,但是这些库函数与字符串是分离的,不符合OOP思想,而且底层空间需要用户自行管理,粗心大意可能会越界访问。而string类则不会出现这样的问题。
在许多OJ题中,有关字符串的题目基本以string类的形式出现;在常规工作中,为了方便快捷,基本都会使用string类,很少使用C语言库中的字符串操作函数。
2. 标准库中的string类
2.1 string类的介绍
字符串是表示字符序列的类
标准的字符串类提供了对此类对象的支持,其接口类似于标准字符容器的接口,但添加了专门用于操作单字节字符字符串的设计特性。
string类是使用char(即作为它的字符类型,使用它的默认char_traits和分配器类型(关于模板的更多信息,请参阅basic_string)。
string类是basic_string模板类的一个实例,它使用char来实例化basic_string模板类,并用char_traits和allocator作为basic_string的默认参数(根于更多的模板信息请参考basic_string)。
注意,这个类独立于所使用的编码来处理字节:如果用来处理多字节或变长字符(如UTF-8)的序列,这个类的所有成员(如长度或大小)以及它的迭代器,将仍然按照字节(而不是实际编码的字符)来操作。
总结:
string是表示字符串的字符串类
该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。
3. string在底层实际是:basic_string模板类的别名,typedef basic_string<char, char_traits, allocator>string;不能操作多字节或者变长字符的序列
在使用string类时,必须包含#include头文件以及using namespace std;
2.2 string类的常用接口
由于string类的接口很多,但常用的只有二十多个,因此下面仅介绍部分接口,更多接口请查看文档!
-
常见构造函数(constructor)
constructor 功能 string ()
默认构造函数 string (const char\* s)
用c-string构造 string (const string& s)
拷贝构造 string(size_t n, char c)
用n个字符c构造 string s1; // 默认构造 string s2("hello"); // 字符串构造 string s3(s2); // 拷贝构造
-
常见容量操作接口
函数 功能 size_t size() const;
返回字符串有效字符长度 size_t length() const;
返回字符串有效字符长度 size_t capacity() const;
返回总空间大小 bool empty() const
检测是否为空字符串 void clear()
清空有效字符 void reserve (size_t n = 0);
调整空间大小 void resize (size_t n);
void resize (size_t n, char c);
调整有效字符个数 说明:
size
和length
在底层的实现完全相同。历史原因,string比STL先问世,先有length
接口,后来STL的容器中都有size
接口,string为与STL保持一致,也增加了size
接口。clear
仅清空有效数据(改变size),不改变空间大小(capacity)。reserve
可以扩容,但是不能缩容。假设传入的参数是n,当n>capacity时,会扩容,将capacity变为n;其余情况空间不做改变。- 当
resize
调整个数n比原来有效字符长度size小,会删除多余的字符(size减小),但空间capacity不变;当调整个数n比原来有效字符长度size大时,会自动填充多余的空间,若capacity小于n,则自动扩容至n。
(把pos位置直接变成’\0’,并将size变成pos)
-
访问和遍历操作
函数 功能 char& operator[] (size_t pos);
const char& operator[] (size_t pos) const;
返回pos位置的字符 iterator begin();
const_iterator begin() const;
获取第一个字符的迭代器 iterator end();
const_iterator end() const;
获取最后一个字符的下一个位置的迭代器 rbegin
和rend
rbegin获取第一个字符的前一个位置的迭代器
rend获取最后一个字符的迭代器范围for
C++11支持的遍历方式,其底层本质是迭代器 string s1("hello world!"); cout << s1[0] << endl; // operator[] - 输出h string::iterator it = s1.begin(); // begin()和end() - 输出hello world! while(it != s1.end()) { cout << *it; it++; } string::reverse_iterator rit = s1.rbegin(); // rbegin()和rend() - 输出!dlrow olleh while (rit != s1.rend()) { cout << *rit; rit++; } for (char ch : s1) // 范围for - 输出hello world! { cout << ch; }
-
string类对象的修改操作
函数 功能 void push_back (char c);
在字符串后面尾插一个字符 append
系列在字符串后面追加一串字符 operator+=
在字符串后面追加字符或字符串 const charT* c_str() const;
返回C格式的字符串 find
从pos位置往后找字符串,找到则返回该位置,否则返回npos rfind
从pos位置往前找字符串,找到则返回该位置,否则返回npos string substr (size_t pos = 0, size_t len = npos) const;
从pos位置开始,截取len长度的字符串并返回 string s1; s1.push_back('a'); s1.append(1,'b'); s1.append("cde"); s1 += 'f'; s1 += "ghi"; cout << s1.c_str() << endl; // 输出为abcdefghi cout << s1[s1.find('e', 0)] << endl; // 输出e string newstr = s1.substr(0, 3); cout << newstr << endl; // 输出abc
说明:
对于尾插,一般使用
operator+=
比较多,因为它不仅可以尾插一个字符,也可以尾插一串字符串,甚至还能尾插一个string类。 -
string类的非成员函数
函数 功能说明 operator+
在字符串后面加一个字符或字符串,返回一个拷贝。
尽量少用,因为传值返回,导致深拷贝效率低operator>>
输入运算符重载 operator<<
输出运算符重载 getline
获取一行字符串(不会因空格或’\0’而终止) relational operators
比较大小(各种比较运算符的重载) swap
交换两个string类
3. 模拟实现string类
#include<iostream>
#include<assert.h>
using namespace std;
/*
* 构造/赋值/析构
* begin/end
* size/capacity/clear/empty/resize/reserve/shrink_to_fit
* operator[]/at/back/front
* operator+=/append/push_back/assgin/insert/erase/pop_back
* c_str/find/rfind/substr
* operator+/relation operators/swap/operator>>/operator<</npos
*
*/
namespace Myspace
{
class string
{
public:
typedef char* iterator;
typedef const char* const_iterator;
public:
//
// 构造/赋值/析构
string(const char* s = "")
: _size(strlen(s))
, _capacity(_size)
, _str(new char[_capacity + 1])
{
memcpy(_str, s, _capacity + 1);
}
/* 传统写法*/
string(const string& str)
: _size(str._size)
, _capacity(str._capacity)
, _str(new char[_capacity + 1])
{
memcpy(_str, str._str, str._capacity + 1);
}
/* 现代写法,有一种情况会出现问题,就是string是"hello\0world",
此时只会拷贝hello,后面的丢失了。所以还是用传统写法好一点。
string(const string& str)
// 这里要初始化列表处理一下,有的编译器会处理,但有的编译器不会处理
: _size(0)
, _capacity(0)
, _str(nullptr)
{
string tmp(str._str);
swap(tmp);
}
*/
string& operator=(const string& str)
{
if (&str != this)
{
string tmp(str);
swap(tmp);
}
return *this;
}
string& operator=(char c)
{
_str[0] = c;
_size = _capacity = 1;
_str[1] = '\0';
return *this;
}
~string()
{
if (_str)
{
delete[] _str;
_size = _capacity = 0;
_str = nullptr;
}
}
//
// begin/end
iterator begin()
{
return _str;
}
const_iterator begin() const
{
return _str;
}
iterator end()
{
return _str + _size + 1;
}
const_iterator end() const
{
return _str + _size + 1;
}
//
// size/capacity/clear/empty/resize/reserve/shrink_to_fit
size_t size()
{
return _size;
}
size_t capacity()
{
return _capacity;
}
void clear()
{
_size = 0;
_str[_size] = '\0';
}
bool empty()
{
return _size == 0;
}
void resize(size_t n, char c = '\0')
{
// 1. n < _size
if (n < _size)
{
_size = n;
_str[_size] = '\0';
}
// 2. n > _capacity
else if (n > _capacity)
{
reserve(n);
memset(_str + _size, c, n - _size);
_size = n;
}
// 3. _size <= n <= _capacity
else
{
memset(_str + _size, c, n - _size);
_size = n;
}
}
void reserve(size_t n = 0)
{
// 1. n > _capacity
if (n > _capacity)
{
char* newspace = new char[n + 1];
memcpy(newspace, _str, _capacity + 1);
delete[] _str;
_str = newspace;
_capacity = n;
}
// 2. 其余情况不做处理
}
void shrink_to_fit()
{
if (_capacity > _size)
{
char* newspace = new char[_size + 1];
memcpy(newspace, _str, _size + 1);
delete[] _str;
_str = newspace;
_capacity = _size;
}
}
//
// operator[]/at/back/front
char& operator[] (size_t pos)
{
assert(pos < _size);
return _str[pos];
}
const char& operator[] (size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
char& at(size_t pos)
{
assert(pos < _size);
return _str[pos];
}
const char& at(size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
char& back()
{
return _str[_size - 1];
}
const char& back() const
{
return _str[_size - 1];
}
char& front()
{
return _str[0];
}
const char& front() const
{
return _str[0];
}
//
// operator+=/append/push_back/pop_back/insert/erase
string& operator+= (const string& str)
{
append(str);
return *this;
}
string& operator+= (char c)
{
push_back(c);
return *this;
}
string& append(const string& str)
{
int len = str._size;
if (_size + len > _capacity)
{
// 扩容
reserve(_size + len);
}
memcpy(_str + _size, str._str, len + 1);
_size += len;
return *this;
}
void push_back(char c)
{
if (_size + 1 > _capacity)
{
reserve(_size + 1);
}
_str[_size++] = c;
_str[_size] = '\0';
}
void pop_back()
{
assert(_size);
_str[_size - 1] = '\0';
_size--;
}
string& insert(size_t pos, const string& str)
{
assert(pos <= _size);
int len = str._size;
if (len + _size > _capacity)
{
reserve(len + _size);
}
size_t i = _size - 1;
while (i >= pos && i != npos)
{
_str[i + len] = _str[i];
--i;
}
_size += len;
memcpy(_str + pos, str._str, len);
_str[_size] = '\0';
return *this;
}
string& insert(size_t pos, size_t n, char c)
{
assert(pos <= _size);
if (n + _size > _capacity)
{
reserve(n + _size);
}
size_t i = _size - 1;
while (i >= pos && i != npos)
{
_str[i + n] = _str[i];
--i;
}
_size += n;
memset(_str + pos, c, n);
_str[_size] = '\0';
return *this;
}
string& erase(size_t pos = 0, size_t len = npos)
{
assert(pos < _size);
if (pos + len >= _size || len == npos)
{
_str[pos] = '\0';
_size = pos;
}
else
{
size_t i = pos;
while (i + len <= _size)
{
_str[i] = _str[i++ + len];
}
_size -= len;
}
return *this;
}
//
// c_str/find/rfind/substr
const char* c_str() const
{
return _str;
}
size_t find(const string& str, size_t pos = 0) const
{
assert(pos < _size);
char* ptr = strstr(_str + pos, str._str);
if (!ptr)
return npos;
else
return ptr - _str;
}
size_t find(char c, size_t pos = 0) const
{
assert(pos < _size);
for (size_t i = pos; i < _size; i++)
{
if (_str[i] == c)
return i;
}
return npos;
}
size_t rfind(const string& str, size_t pos = npos) const
{
if (pos >= _size || pos == npos)
{
pos = _size - 1;
}
int fast = pos;
int slow = pos;
int cur = str._size - 1;
while (slow >= str._size - 1)
{
while (cur >= 0 && _str[fast] == str[cur])
{
fast--;
cur--;
}
if (cur >= 0)
{
slow--;
fast = slow;
cur = str._size - 1;
}
else
{
return slow - str._size + 1;
}
}
return npos;
}
size_t rfind(char c, size_t pos = npos) const
{
if (pos >= _size || pos == npos)
{
pos = _size - 1;
}
int i = pos;
while (i-- >= 0)
{
if (_str[i] == c)
return i;
}
return npos;
}
string substr(size_t pos = 0, size_t len = npos) const
{
assert(pos < _size);
int end = pos + len - 1;
if (pos + len > _size || len == npos)
{
end = _size - 1; // 不包括'\0'
}
string tmp;
for (int i = pos; i <= end; i++)
{
tmp += _str[i];
}
return tmp;
}
//
// swap/relation operators/operator>>/operator<</getline/npos
void swap(string& str)
{
std::swap(str._size, _size);
std::swap(str._capacity, _capacity);
std::swap(str._str, _str);
}
bool operator>(const string& str)
{
int ret = memcmp(_str, str._str, _size < str._size ? _size : str._size);
return ret == 0 ? _size > str._size : ret > 0;
}
bool operator==(const string& str)
{
return _size == str._size && memcmp(_str, str._str, _size) == 0;
}
bool operator<(const string& str)
{
return !(*this > str || *this == str);
}
bool operator>=(const string& str)
{
return *this > str || *this == str;
}
bool operator<= (const string & str)
{
return !(*this > str);
}
bool operator!= (const string& str)
{
return !(*this == str);
}
friend istream& operator>>(istream& in, string& str);
friend ostream& operator<<(ostream& out, const string& str);
static size_t npos;
private:
size_t _size;
size_t _capacity;
char* _str;
};
size_t string::npos = -1;
istream& operator>>(istream& in, string& str)
{
str.clear();
char tmp[128] = { 0 };
char ch = in.get();
while (ch == ' ' || ch == '\n')
{
ch = in.get();
}
int i = 0;
while (ch != ' ' && ch != '\n')
{
tmp[i++] = ch;
ch = in.get();
if (i == 127)
{
str += tmp;
i = 0;
}
}
if (i)
{
tmp[i] = '\0';
str += tmp;
}
return in;
}
ostream& operator<<(ostream& out, const string& str)
{
for (auto ch : str)
{
out << ch;
}
return out;
}
}
4. 深浅拷贝
所谓浅拷贝,就是仅仅拷贝数值,不会考虑是否要开辟空间。
而深拷贝,则会考虑开辟空间,并且把数值和空间的指针一起拷贝。
请看下面两张图:
上面的图就很好的阐释了什么是深拷贝、什么是浅拷贝。
浅拷贝:仅拷贝地址,将某块已存在的空间的地址赋值给指针,而不是创建新的空间后再将地址赋值给指针。
深拷贝:创建和原空间一模一样的新空间,空间内容通过拷贝保持一致,然后将新空间的地址赋值给指针。
在string类中,因为存在动态开辟的空间,我们模拟实现string类的时候一定要使用深拷贝,以免浅拷贝出现差错!
5. string类部分接口示例
一、遍历string
-
cout
string类后面的’\0’,可以被访问到,只是有些编译器不会显示它。且size()函数不会把它算进去。 -
operator[ ] 可以访问、修改
int main()
{
string s1;
string s2("Hello world");
s2 += '!';
cout << s2 << endl;
// 遍历
// 1. 下标+[]
for (size_t i = 0; i <= s2.size(); ++i)
{
cout << s2[i] << ' ';
}
cout << endl;
// 1)其实是访问到'\0'了,只是有些编译器不会显示它
cout << s2.size() << endl;
// 2)可以看出,size()不算'\0'
// 3)两个[]的区别
char s3[] = "hello world";
s3[1]; // -> *(s3 + 1)
s2[1]; // -> s2.operator[](1)
return 0;
}
- 迭代器
也是一种遍历访问的方法。
- 像指针一样的类型,有可能是指针,但不一定是指针(封装的指针)。
begin()开头的位置,end()最后一个数据的下一个位置,也就是\0。
int main()
{
string s1 = "hello world";
string::iterator it = s1.begin();
while (it != s1.end())
{
cout << *it << ' ';
it++;
}
cout << endl;
return 0;
}
- 范围for
int main()
{
string s1("hello world");
for (auto ch : s1) // for (char ch : s1)
{
cout << ch << ' ';
}
cout << endl;
return 0;
}
-
范围for的底层就是迭代器,所以不支持迭代器的类就不支持范围for,比如说stack(栈先进后出,不支持遍历,不支持迭代器,不支持范围for)
-
任何容器都支持迭代器,且用法都是类似的。
对于一些结构,比如链表、树等等,无法用下标+[ ]的方式访问,但是可以用迭代器访问。
int main()
{
vector <int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
vector<int>::iterator vit = v.begin();
while (vit != v.end())
{
cout << *vit << ' ';
vit++;
}
cout << endl;
list<int> lt;
lt.push_back(10);
lt.push_back(20);
lt.push_back(30);
lt.push_back(40);
list<int>::iterator lit = lt.begin();
while (lit != lt.end())
{
cout << *lit << ' ';
lit++;
}
cout << endl;
return 0;
}
-
总结:iterator迭代器,提供了一种统一的方式访问和修改容器的数据。
-
迭代器可以跟算法进行配合:
reverse/sort/…
算法可以通过迭代器去处理容器中的数据。 -
范围for只能正向遍历,不能反向遍历。
-
反向迭代器
reverse_iterator
int main()
{
string s1("hello world");
string::reverse_iterator rit = s1.rbegin();
while (rit != s1.rend())
{
cout << *rit << ' ';
++rit;
}
cout << endl;
return 0;
}
rbegin()在最后一个数据位置,rend()在第一个数据的前一个位置。
-
const迭代器
const_iteratorconst对象只能用const迭代器,防止权限放大
-
const反向迭代器
const_reverse_iterator
二、容量相关
-
STL属于标准库,string属于标准库,string产生的比较早,它并不在STL的容器中。
-
size和length,一样的。length出来的比较早,当STL出来之后,又加了一个size,后面STL中都是size。
-
max_size,这个接口在不同的编译器中的结果可能是不同的,所以在实际使用中不用它。
-
capacity,VS2022下的PJ版和Linux下的SGI版的结果不一定相同。因为STL仅仅是一个规范,具体底层是如何实现的,每个版本的是不一样的,它们仅仅是实现的功能接口一样,底层是不同的。
可以测试,在VS下,大概是1.5倍扩容,Linux下大概是2倍扩容。int main() { string s1; size_t sz = s1.capacity(); for (int i = 0; i < 200; ++i) { if (sz != s1.capacity()) { sz = s1.capacity(); cout << "change capacity: " << sz << endl; } s1.push_back('c'); } return 0; }
同样的一段代码,在Linux下(SGI版本的STL)和微软的VS2022下(P.J.版本的STL),结果是不同的。
Linux:
VS2022:
-
clear,清掉数据,只会让size为0,capacity不会变。因为如果capacity也变了0,那么如果接下来又要用了,又得开辟空间,这样浪费效率。
-
empty,判断空
-
resize,扩容 + 初始化。
- 如果 n 小于当前字符串长度,则当前值将缩短为其第一个 n 个字符,删除第 n 个字符之外的字符。capacity不变。
- 如果 n 大于当前字符串长度,则通过在末尾插入任意数量的字符来扩展当前内容,以达到 n 的大小。如果指定了 c,则新元素将初始化为 c 的副本,否则,它们是值初始化的字符(空字符)。
-
reserve,申请开辟空间,适用于知道需要多少空间,然后提前开好空间,就不必去扩容了。
当我输入100时,实际上不一定是开辟100个字节的空间,它会根据编译器自动给我们开辟一个差不多的空间,一定比100大。- 如果 n 大于当前字符串容量,则该函数会导致容器将其容量增加到 n 个字符(或更大)。
- 在所有其他情况下,它被视为缩小字符串容量的非约束性请求:容器实现可以自由地进行优化,并使字符串的容量大于 n。
-
strink_to_fit,主动缩容,将容量缩容至合适大小
-
总结:string基本上不缩容,只有在clear清掉内容之后,再reserve,才会缩容。
三、读取元素
-
operator[ ], 越界了断言
-
at, 越界了抛异常
四、修改
- append
- push_back
- operator+=
- assign,从第一个位置开始覆盖,重新写入,但capacity不会缩
- insert
- erase
- replace
五、string操作
-
c_str
-
find
int main() { // string url = "https://legacy.cplusplus.com/reference/string/string"; string url = "https://www.huya.com/991111"; size_t pos1 = url.find("://"); string protocol; if (pos1 != string::npos) { protocol = url.substr(0, pos1); } cout << protocol << endl; size_t pos2 = url.find('/', pos1 + 3); string domain; if (pos2 != string::npos) { domain = url.substr(pos1 + 3, pos2 - pos1 - 3); } cout << domain << endl; string uri; uri = url.substr(pos2 + 1); cout << uri << endl; return 0; }
-
rfind
-
find_first_of,找某个字符串中任意一个,在一个string类中第一次出现的位置。
-
find_last_of,与上面相反,从后往前走
-
find_first_not_of,找不属于某个字符串中任意一个,在一个string类中第一次出现的位置。
-
find_last_not_of,与上面相反
-
substr,切取string类的一段
-
getline,读一行,(遇到空格继续读),也可以自行控制结束标志。
如果要读一个字符串,它其中有空格的话,用cin
是读不到的,遇到空格,就会把剩下的东西放到缓冲区,下次读才能读到剩下的。 -
stoi/stod/…/to_string
-
rfind
-
find_first_of,找某个字符串中任意一个,在一个string类中第一次出现的位置。
-
find_last_of,与上面相反,从后往前走
-
find_first_not_of,找不属于某个字符串中任意一个,在一个string类中第一次出现的位置。
-
find_last_not_of,与上面相反
-
substr,切取string类的一段
-
getline,读一行,(遇到空格继续读),也可以自行控制结束标志。
如果要读一个字符串,它其中有空格的话,用cin
是读不到的,遇到空格,就会把剩下的东西放到缓冲区,下次读才能读到剩下的。 -
stoi/stod/…/to_string