STL(standard template libaray-标准模板库):是C++标准库的重要组成部分,不仅是一个可复用的组件库,而且是一个包罗数据结构与算法的软件框架。
STL六大组件:
网上有句话说:“不懂STL,不要说你会C++”。STL是C++中的优秀作品,有了它的陪伴,许多底层的数据结构以及算法都不需要自己重新造轮子,站在前人的肩膀上,健步如飞的快速开发。
string出现较早,本不是STL容器,但是它和STL容器有很多相似的操作,因此我们把它和其他STL容器放到一起学习。
C语言中,字符串是以’\0’结尾的一些字符的集合,虽然C标准库中提供了一些str系列的库函数,但是这些库函数与字符串是分离开的,不太符合OOP的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。
C++中,我们有了string类,操作更加方便,快捷。
string实际上是模板实例化后typedef出来的:
string类的文档介绍👉string - C++ Reference (cplusplus.com)
string
typedef basic_string<char> string;
可见其底层的类模板名叫basic_string,指定模板参数char实例化的类就是string类
类似的还有:
u16string
typedef basic_string<char16_t> u16string;
u32string
typedef basic_string<char32_t> u32string;
wstring
typedef basic_string<wchar_t> wstring;
由于char是8位字符类型,最多只能表示256种字符,许多外文字符集所含的字符数目超过256个,char型无法表示,所以出现了其他这些字符类型。
为了适应不同的字符类型,于是就有了basic_string这个类模板
string类的一个简单使用:
int main()
{
string s = "hello";
s += " world!";
cout << s << endl;
return 0;
}
//结果:hello world!
构造函数 | 说明 |
---|---|
①string() | string的默认构造,空字符串 |
②string(const char* s) | 用C-string构造string类对象 |
③string(const string& s) | string的拷贝构造 |
④string(const char* s, size_t n) | 用C-string的前n个字符构造 |
⑤string(size_t n, char c) | string类对象中包含n个字符c |
⑥string(const string& str, size_t pos, size_t len = npos) | 用str中pos位置开始的len个字符构造(len默认为npos) |
⑦string(InputIterator first, InputIterator last) | 迭代器区间初始化 |
最常用的:①②③
//默认构造
string s1;
//带参构造
string s2("hello world!");
string s3 = "hello world";
//拷贝构造
string s4(s2);
string s5 = s3;
不那么常用的:④⑤
string s6("ABCDEF", 3);
cout << s6 << endl; //结果:ABC
string s7(10, 'x');
cout << s7 << endl; //结果:xxxxxxxxxx
⑥
string s8(s2, 3, 5);
cout << s8 << endl; //结果:lo wo
string s9(s2, 3, 100);
cout << s9 << endl; //结果:lo world! //len的值大于要拷贝的字符串长度,则直接拷贝到字符串末尾
string s10(s2, 3);
cout << s10 << endl; //结果:lo world!
👆:第三个参数len
默认为npos
,npos
是string类的一个静态成员变量,其原型为static const size_t npos = -1;
-1的补码全为1,转换为无符号整型就是size_t
的最大值,意味着如果不传第三个参数,就直接拷贝到str字符串的末尾。
⑦涉及迭代器,留到后面讲。
string& operator= (const string& str);
string& operator= (const char* s);
string& operator= (char c);
支持string,C-string,char类型赋值
string s1("hello world");
string s2("xxx");
s1 = s2;
s1 = "yyy";
s1 = 'z';
此法使用最多
string类重载了[],我们可以把它当成数组来访问:
char& operator[] (size_t pos);
const char& operator[] (size_t pos) const;
👆:注意这里是引用返回,而不是传值返回,目的不是减少拷贝,而是为了支持修改返回对象;第二个有const修饰的,他与第一个构成函数重载,这样做可以保证如果传入的是const修饰的string,那么返回的引用也具有const修饰。
例:
void test1()
{
string s1("hello world!");
for (int i = 0; i < s1.size(); i++) //size()返回该字符串字符个数
{
cout << s1[i] << " ";
}
cout << endl; //结果:h e l l o w o r l d !
}
👆:string类里面还有个length
成员函数,它的作用和size
一样。它是为早期的string设计的,现在为了和其他STL容器统一,更多使用size
也可以通过at成员函数来访问
char& at (size_t pos);
const char& at (size_t pos) const;
例:
void test4()
{
string s1("hello world!");
for (int i = 0; i < s1.size(); i++)
{
cout << s1.at(i) << " ";
}
cout << endl;
}
at与[]的区别在于,at的越界访问会抛异常,[]的越界访问通过断言来检查。
迭代器遍历虽然麻烦一点,但它是我们遍历STL容器的利器,比如list这种就用不了[],但依然可以用迭代器。
iterator
是一个类所特有的,使用时要指定类域;begin()
返回开始位置的迭代器,end()
返回末尾的下一个位置的迭代器。例:
void test2()
{
string s1("hello world!");
string::iterator it = s1.begin();
while (it != s1.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
👆:while的判断条件it != s1.end()
也可以写成it < s1.end()
,因为这里的迭代器可以当成指针,而且string的存储空间是连续的。
反向迭代器
下标的反向遍历很简单,迭代器的反向遍历需要用到反向迭代器。
reverse_iterator
rbegin()
返回一个反向迭代器,该迭代器指向字符串最后一个字符。rend()
返回的反向迭代器指向第一个字符之前的理论元素。例:
void test5()
{
string s1("hello world!");
string::reverse_iterator rit = s1.rbegin();
while (rit != s1.rend())
{
cout << *rit << " ";
++rit; //此处不能写成--rit
}
cout << endl; //结果:! d l r o w o l l e h
}
👆:正向迭代器和反向迭代器一个是正着走,一个是倒着走,但都是用的++
const迭代器
对于const修饰的对象,以上两种迭代器都不能用,因为它们既可以读也可以写。
const迭代器的类型为const_iterator
或者const_reverse_iterator
const string s2("hello world!");
string::const_iterator it = s2.begin(); //begin传入const对象的指针返回的就是const迭代器
string类型的对象是可迭代的,可以使用范围for。
例:
void test3()
{
string s1("hello world!");
for (auto ch : s1)
{
cout << ch << " ";
}
cout << endl;
}
函数 | 说明 | 函数原型 |
---|---|---|
①size | 返回字符串长度 | size_t size() const; |
②length | 返回字符串长度 | size_t length() const; |
③max_size | 返回字符串可以达到的最大长度 | size_t max_size() const; |
④capacity | 返回已分配的存储空间大小 | size_t capacity() const; |
⑤reserve | 请求更改容量 | void reserve(size_t n = 0); |
⑥resize | 将字符串的长度调整至n | void resize(size_t n); void resize(size_t n, char c); |
⑦clear | 请掉所有数据,容量不变。 | void clear(); |
⑧empty | 判断是否为空 | bool empty() const; |
①②中的①更常用一些,在上文 string类常见接口/遍历/1.下标+[] 使用过。
例:
string s("hello world!");
cout << s.max_size();
//结果:2147483647
我们可以通过以下程序看看编译器如何扩容:
void testCapacity()
{
string s;
size_t sz = s.capacity();
cout << "capacity changed: " << sz << endl;
for (int i = 0; i < 100; i++)
{
s.push_back('c'); //尾插
if (sz != s.capacity())
{
sz = s.capacity();
cout << "capacity changed: " << sz << endl;
}
}
}
//vs2022下的结果:
//capacity changed: 15
//capacity changed: 31
//capacity changed: 47
//capacity changed: 70
//capacity changed: 105
可以看出在vs2022的环境下,除了第一次,后面都是1.5倍扩容。
为了避免多次扩容造成的开销,我们可以提前使用reserve
开好容量。
在VS下如果要改的容量小于当前容量,则此函数无效,即reserve能扩不能缩
例:
void testReserve()
{
string s;
s.reserve(1000);
cout << s.capacity() << endl;
}
//结果:1007
它改变的是字符串实实在在的大小,不仅要开空间,还要对新开的空间初始化。
默认对新开的空间初始化为'\0'
,也可以自己传入要初始化的参数。
在VS下如果要改的字符串大小小于当前大小,则舍弃后面的,只保留前n个字符,capacity保持不变。
例:
void testResize()
{
string s1("hello world!");
s1.resize(1000); //通过监视窗口可以看到后面都是'\0'
string s2("hello world!");
s2.resize(1000, 'x'); //后面都是'x'
}
⑦⑧比较常用,使用也非常简单,这里不多演示
函数 | 说明 |
---|---|
①push_back | 在字符串后面加一个字符 |
②append | 在字符串后面加一个字符串 |
③operator+= | 在字符串后面加一个字符串 |
④insert | 插入到字符串中 |
⑤erase | 从字符串中删除字符 |
⑥assign | 为字符串指定新值,替换其当前内容。 |
⑦replace | 替换字符串的一部分 |
⑧swap | 交换两个字符串 |
①只能添加单个字符,②可以添加字符串,使用方式类似构造函数
③operator+=是最常用的,可读性最好
函数原型:
string& operator+= (const string& str); //string (1)
string& operator+= (const char* s); //c-string (2)
string& operator+= (char c); //character (3)
例:
void test()
{
string s1("a");
string s2("efg");
s1 += 'b';
s1 += "cd";
s1 += s2;
cout << s1 << endl;
}
//结果:abcdefg
函数原型:
string& insert (size_t pos, const string& str); //string (1)
string& insert (size_t pos, const string& str, size_t subpos, size_t sublen); //substring (2)
string& insert (size_t pos, const char* s); //c-string (3)
string& insert (size_t pos, const char* s, size_t n); //buffer (4)
string& insert (size_t pos, size_t n, char c); //fill (5)
void insert (iterator p, size_t n, char c);
iterator insert (iterator p, char c); //single character (6)
template <class InputIterator> //range (7)
void insert (iterator p, InputIterator first, InputIterator last);
👆:它的函数重载太多了,但也不必刻意地去记,有需要的时候可以查文档,这里挑选常见的进行讲解。
插入单个字符可以用(5)或(6)
例:
void testInsert()
{
string s("hello world!");
s.insert(0, 1, 'x'); //(5)头插一个'x'
cout << s << endl; //xhello world!
s.insert(s.begin(), 'y'); //(6)头插'y'
cout << s << endl; //yxhello world!
s.insert(3, 1, 'x'); //在下标为3的位置插入一个'x'
cout << s << endl; //yxhxello world!
s.insert(s.begin() + 3, 'y'); //迭代器当成指针来用
cout << s << endl; //yxhyxello world!
}
插入一个字符串可以用(1)或(3)
例:
void testInsert()
{
string s1("hello world!");
string s2("yes ");
s1.insert(0, "ok "); //头插一个C-string
cout << s1 << endl; //ok hello world!
s1.insert(0, s2); //头插s2
cout << s1 << endl; //yes ok hello world!
}
函数原型:
string& erase (size_t pos = 0, size_t len = npos); //sequence (1)
iterator erase (iterator p); //character (2)
iterator erase (iterator first, iterator last); //range (3)
例:
void testErase()
{
string s("hello world!");
s.erase(s.begin()); //删除一个字符
cout << s << endl; //ello world!
s.erase(3, 2); //删除下标3开始的2个字符
cout << s << endl; //ellworld!
s.erase(3); //删除下标3开始的所有字符
cout << s << endl; //ell
}
⑥⑦用得较少,此处省略。。。
⑧swap
函数原型:
void swap (string& str);
这个swap是成员函数,另外库里还有一个swap是函数模板。
例:
void testSwap()
{
string s1("hello world!");
string s2("goodbye world!");
s1.swap(s2);
swap(s1, s2);
cout << s1 << " " << s2 << endl;
}
那么这两个swap有什么区别呢?
函数原型:
const char* c_str() const;
例:
void testCstr()
{
string s("hello world!");
const char* sp = s.c_str(); //转换为了C风格的字符串
cout << sp << endl;
}
函数原型:
string substr (size_t pos = 0, size_t len = npos) const;
返回从pos位置开始的长度为n的字符串
③find
函数原型:
size_t find (const string& str, size_t pos = 0) const; //string (1)
size_t find (const char* s, size_t pos = 0) const; //c-string (2)
size_t find (const char* s, size_t pos, size_t n) const; //buffer (3)
size_t find (char c, size_t pos = 0) const; //character (4)
string::npos
比如我们给一个文件名的字符串,要取出该文件名的后缀:
void testFind()
{
string file("string.cpp");
size_t pos = file.find('.');
if (pos != string::npos) //找到
{
string suffix = file.substr(pos, file.size() - pos);
cout << file << "后缀:" << suffix << endl;
}
else //未找到
{
cout << "没有后缀" << endl;
}
}
//结果:string.cpp后缀:.cpp
rfind和find的区别:find是从前往后找,rfind是从后往前找。
如果把上面的文件名换成string.c.tar.zip
,用find
找的就是第一个.
,最后打印结果为.c.tar.zip。只想取出最后一个后缀可以用rfind
void testRfind()
{
string file("string.c.tar.zip");
size_t pos = file.rfind('.');
if (pos != string::npos)
{
string suffix = file.substr(pos, file.size() - pos);
cout << file << "后缀:" << suffix << endl;
}
else
{
cout << "没有后缀" << endl;
}
}
//结果:string.c.tar.zip后缀:.zip
利用上面的字符操作函数,如何将一个网站的协议,域名,uri分别取出呢?
void test()
{
string url1("https://blog.csdn.net/CegghnnoR/article/details/125471285");
string& url = url1;
string protocol; //协议
size_t pos1 = url.find(':');
if (pos1 != string::npos)
{
protocol = url.substr(0, pos1);
cout << "protocol:" << protocol << endl;
}
else
{
cout << "非法url" << endl;
}
string domain; //域名
size_t pos2 = url.find('/', pos1 + 3); //指定从pos1+3位置开始找
if (pos1 != string::npos)
{
domain = url.substr(pos1 + 3, pos2 - pos1 - 3);
cout << "domain:" << domain << endl;
}
else
{
cout << "非法url" << endl;
}
string uri = url.substr(pos2 + 1);
cout << "uri:" << uri << endl;
}
//结果:
//protocol:https
//domain:blog.csdn.net
//uri:CegghnnoR/article/details/125471285
string支持比较,可以string和string比较,也可以string和C-string比较。
void testRelational()
{
string s1("hello world!");
string s2("string");
s1 < s2;
s1 < "hello";
"hello" < s1;
s1 < string("hello"); //匿名对象
}
👆:有人吐槽,支持string和C-string比较是多此一举,C-string也可以构造成匿名对象参与比较。
+的重载,不建议多用,因为它是传值返回,增加了拷贝。
operator>> (string)、operator<< (string)
已经用了很多次了,不多说。
istream& getline (istream& is, string& str, char delim);
istream& getline (istream& is, string& str);
C语言中要通过键盘把带空格的字符串输入到字符数组中要用到gets,因为scanf是以空格和换行作为分隔符,gets只以换行为分隔符。
类似于gets,C++中可以使用getline。
void testGetline()
{
string s;
getline(cin, s); //输入hello world!
cout << s << endl; //打印hello world!
}
使用getline和rfind可以轻松解决👉:字符串最后一个单词的长度_牛客题霸_牛客网 (nowcoder.com)
参考代码:
#include <iostream>
using namespace std;
int main()
{
string s;
getline(cin, s);
size_t pos = s.rfind(' ');
cout << s.size() - pos - 1 << endl;
return 0;
}