正文开始@边通书
string
类实际上是basic_string
这个类模板的实例化 ——
template<class T>
class basic_string
{
// ...
private:
T* _str; //动态申请的
size_t _size;
size_t _capacity;
// ...
};
可能令人疑惑的是,难道字符串类型中不都是字符吗,为什么还要有类模板呢?这就要说到不同的编码规则。
在ascii编码表中,将值和符号建立映射关系,1byte空间可以表示256个英文字符;再说unicode,是为了表示全世界文字的编码表,其中的utf-16方案,所有字符,无论中英还是啥,都是两字节表示(这样计算字符个数很方便,但是能表示字符也受限)。你可以认识的到,字符可不简单的是char,还可以是wchar宽字符等等。
(关于编码,不是这里的重点,话说昨天的CSAPP课上,老师忽然讲起了编码,讲了好久演示了好多,真的很有意思!让我忽然觉得这门课好棒,然而后来他讲起了执行三条汇编指令计算机发生了什么,太tnd底层了,都把我讲磕头了哎哈哈)
下面介绍string类常用的接口❤️ ,要熟练掌握,其余的用时查阅即可。在使用string类时,需要包含头文件#include
以及展开命名空间using namespace std;
❤️ 1. 构造函数
其余的接口简单演示,主要为了演示如何查阅文档。
功能:从pos开始取对象的一部分(len)。
substring (3) string (const string& str, size_t pos, size_t len = npos);
其中len给了缺省值npos
,npos
是string类的一个静态成员变量,值为-1,在补码中就是全1,赋给了无符号数size_t,就是整型的最大值4,294,967,295。因此,如果不传参采用缺省值,那就是有多少取多少。因为这个数字太大了42亿9千万,一个字符串就4G,可能吗?
演示 ——
注:string类对象支持直接用cin和cout进行输入和输出,因为重载了流插入>>
和流提取<<
操作符(后文详谈)。
取字符串前n个
from sequence (5) string (const char* s, size_t n);
填充初始化
fill (6) string (size_t n, char c);
❤️ 2. 析构函数
自动调用释放资源,不用管了。
❤️ 3. 赋值重载
❤️字符串中有效字符长度,即不包含最后作为结尾标识符的\0
两者底层实现完全一致(length的存在是历史原因),但强烈推荐使用size
. 这是为了和后序各种容器接口保持一致(各种容器接口表示多少个数据都用size
,没有说你求二叉树的length的吧)
❤️ 容量:能存多少个有效字符(注意\0
没算),要记得string类的底层是顺序表结构
演示 ——
reserve 和 resize 都是改变容量,申请至少n个字符的空间(字符串涉及对齐问题,后续详谈) ,但有所不同 ——
❤️ 1. resize - 开空间,并可以对空间初始化
如果是将元素个数减少,会把多出size
的字符抹去,这很符合resize这个函数的名字
如果是将元素个数增多,void resize (size_t n);
用\0
来填充多出的元素空间,void resize (size_t n, char c);
用字符c来填充多出的元素空间
注:resize在改变元素个数时,如果是将元素个数增多,可能会改变容量的大小;如果是将元素个数size减少,容量不变。
调试可见 ——
#include
using namespace std;
int main()
{
string s1("more than words");
s1.resize(5); // 1. size缩小到5,capacity不变
string s2("more than words");
s2.resize(100); // 2.1 填充\0 size->100, capacity ->111
string s3("more than words");
s3.resize(100,'!'); // 2.2 填充! size->100, capacity ->111
return 0;
}
❤️ 2. reserve - 开空间。在已知需要多少空间时,调用reserve,可以避免频繁增容的消耗。
为字符串预留空间,改变容量。当然了不会改变有效元素个数size。
当给reserve的参数n小于string的容量时,是无效请求,并不会改变容量大小。
调试可见 ——
#include
using namespace std;
int main()
{
string s1;
s1.reserve(100); // size - 0,capcacity->111
string s2("more than words");
s2.reserve(5); // capacity和size仍为15
return 0;
}
❤️ 重载了[]
,使得string类可以像数组一样访问字符。不同的是,数组访问本质是解引用,而这里是调用函数。
它提供了两个版本 ——
❤️ operator[]返回的是每个字符的引用,这使得它可读可写。
引用,可以减少拷贝,但这里并不是。这里是做输出型参数,是为了支持修改返回对象。
1. 【遍历 + 修改】方法一 ——
#include
#include
using namespace std;
// 方式1:[下标]
int main()
{
string s("more than words");
// 1.可读
for (size_t i = 0; i < s.size(); i++)
{
cout << s[i] << ' ' ;
//等价于
//cout << s.operator[](i) << " " <<;
}
cout << endl;
// 2.可写
for (size_t i = 0; i < s.size(); i++)
{
s[i] += 1;
}
cout << s << endl;
for (size_t i = 0; i < s.size(); i++)
{
s.at(i) -= 1;
}
cout << s << endl;
return 0;
}
注意:下面这两个函数功能一致(at的存在还是历史原因),只不过二者检查越界的方式不同,推荐使用[]
——
本节将介绍第二种【遍历 + 修改】的方式:迭代器。迭代器是STL的六大组件之一,用来访问和修改这些数据结构。
看完本节你可能有这样的疑惑,对于string类,无论正着还是倒着走,[下标]
的方法都足够好用,为什么还要有迭代器?
事实上,迭代器是一种通用的遍历方式,所有容器都可以使用迭代器这种方式去访问修改,而list、map/set不支持[下标]
遍历。结论是,对于string类,我们得会用迭代器,但是我们更喜欢用[下标]
。
正向迭代器提供了两个成员函数 ——
❤️ 迭代器是内嵌类型,想象成像指针一样,但又不一定是指针
#include
#include
using namespace std;
// 2.迭代器
int main()
{
string s("more than words");
// 1.可读
string::iterator it = s.begin();
while (it != s.end())
{
cout << *it << " ";
it++;
}
cout << endl;
// 2.可写
it = s.begin();
while (it != s.end())
{
*it += 1;
it++;
}
cout << s <<endl;
return 0;
}
const
成员函数,!=
可不可以写成<
:答案是可以但不建议。对于string类可以,是因为它的物理空间是连续的,其他容器就不一定了。❤️ 倒着遍历字符串 ——
#include
#include
using namespace std;
// 反向迭代器 - 倒着遍历
int main()
{
string s("more than words");
// 1.可读
string::reverse_iterator rit = s.rbegin();
//auto rit = s.rbegin(); //太长了,可自动推导类型
while (rit != s.rend())
{
cout << *rit << " ";
rit++;
}
cout << endl;
// 2.可写
rit = s.rbegin();
while (rit != s.rend())
{
*rit += 1;
rit++;
}
cout << s << endl;
return 0;
}
所谓const迭代器,实际上是上面那些成员函数重载的第二个版本。
T*
);const T*
)const迭代器也分正向迭代器和反向迭代器,且就是给const对象用的。这是因为const对象才能调用这里的const成员函数,返回const迭代器,不可写;是普通对象就直接调用普通的重载接口(因为两个重载函数同时存在),返回普通迭代器,可读可写。
它出现的情况往往是这样 ——
#include
#include
using namespace std;
void func(const string& s)
{
// const正向迭代器 - 可读不可写
string::const_iterator it = s.begin();
while (it != s.end())
{
cout << *it << " ";
it++;
}
cout << endl;
// const反向迭代器 - 可读不可写
string::const_reverse_iterator rit = s.rbegin();
while (rit != s.rend())
{
cout << *rit << " ";
rit++;
}
cout << endl;
}
int main()
{
string s("more than words");
func(s);
return 0;
}
传参进func中,s是const对象,自动调用第二个接口,返回的是const_iterator,要用const迭代器类型接收,且不能修改。
C++11为了区分const迭代器和普通迭代器还提供了以下接口,不然调用时容易混淆,实际上用的不多。
顺便介绍【遍历 + 更改】的第三种,范围for是C++11提供的语法糖,实际上底层编译器也会替换成迭代器。
把s
中的每个字符取出来,赋值给e
#include
#include
using namespace std;
int main()
{
string s("more than words");
for (auto& e : s)
{
cout << e << " ";
}
cout << endl;
for (auto& e : s)
{
e += 1;
}
cout << s << endl;
return 0;
}
注:
&
。若s中的每个对象比较大,也最好加&
auto
,直接写类型也可。❤️ +=
更常用,因为既可以追加字符、也可追加字符串 ——
int main()
{
string s("more than words");
s.append(" is all you have to do to make it real");
s.push_back('~');
cout << s << endl;
s += "then you wouldn't have to say that you love me, cause I'd already know";
s += "~";
cout << s << endl;
return 0;
}
❤️ 下面来探究尾插扩容容量变化 ——
#include
#include
using namespace std;
int main()
{
string s;
//s.reserve(1000);
size_t sz = s.capacity();
cout << "capacity:" << sz << endl;
for (size_t i = 0; i < 1000; i++)
{
s += '~';
// 若容量发生变化
if (capa != s.capacity())
{
capa = s.capacity();
cout << "capacity changed:" << sz << endl;
}
}
return 0;
}
可以看到在vs下,第一次是2倍,后面是约等于1.5倍的增容 ——
注:在知道需要是多少空间,可以调用reserve预留空间,避免频繁增容的消耗。
尽量少用头部和中间的插入删除,因为要挪动数据,O(N)效率低。
❤️ 返回C格式字符串
打印字符串,都能打印,但意义不同 ——
前者是string类的流插入运算符的重载,size是多少打印多少;后者是按字符串类型打印,遇到\0结束。
主要作用还是与函数接口接合,like this——
string file("test.txt");
FILE* fout = fopen(s.c_str(), "w");
❤️ 取当前串的一个子串
len
:如果len比能取到的串长或使用缺省值npos,都是能取多少取多少。
❤️ 1. 从字符串pos位置从前向后找字符c/字符串,返回该字符在字符串中的位置
❤️ 2. 从字符串pos位置从后向前找字符c/字符串,返回该字符在字符串中的位置
现在我要取file的后缀名 ——
#include
#include
using namespace std;
int main()
{
string file("test.txt");
// 获取file后缀
size_t pos = file.rfind('.');
if (pos != string::npos)
{
//string suffix = file.substr(pos, file.size() - pos);
string suffix = file.substr(pos);
cout << suffix << endl;
}
return 0;
}
解析出网址的这三个部分:协议 - 域名 - 资源
#include
#include
using namespace std;
int main()
{
string url("https://cplusplus.com/reference/string/string/find/");
size_t pos1 = url.find(':');
string proctol = url.substr(0, pos1); //取协议子串
size_t pos2 = url.find('/', pos1 + 3);
string domain = url.substr(pos1 + 3, pos2 - (pos1+3)); //取域名
string uri = url.substr(pos2); //取资源
cout << proctol << endl;
cout << domain << endl;
cout << uri << endl;
return 0;
}
注意,流插入和流提取都是以空格、回车作为结束标志的。这意味着如果想要输入一个字符串,最终可能只读入了一个单词。
于是我们引入getline.题目中就会遇到。
持续更新~@边通书