前言
我们前面已经介绍了C++的基本知识,本期开始我们将进入C++的第二部分,也是非常重要的一个部分!他就是STL!本期我们来先介绍string及其使用!
本期内容介绍
STL介绍
为什么要学习string?
string的常用接口介绍
一、STL介绍
什么是STL?
STL(standarrd template libaray --- 标准模板库)是C++标准库中的重要组成部分,不仅仅是一个可复用的组件库,而且是一个包含数据结构和算法的软件框架!
STL的六大组件
如何学习STL?
我们会分为两个层次来学习STL,一是介绍它的使用,二是模拟实现!这里的模拟实现不是说实现的和库里面一模一样的!不是,我们只是模拟实现我们常用的!
OK让我们进入string的快乐学习吧!
二、为什么要学习string?
在正是的学习string前我们来想一个问题:我们为什么学习string?C语言不是字符数组替代字符串吗?为什么这里又搞出来一个string?
在C语言中,字符串是以'\0'结尾的一些字符的集合,为了方便操作,C语言标准提供了很多str系列的函数例如strcpy,strlen,strstr等,但这些函数是与字符串分离的,需要用户自己控制底层管理,不符合OOP的思想,而且稍不注意会有越界的风险!所以C++提供了string类,来更好的管理字符串!用户此时调用string相关的函数即可,不在关心底层管理,符合了OOP的思想!也更加安全了!
我们先来看看什么是string?
官网上介绍的很清楚,string是一个字符串顺序表的对象,是被typedef过成string的,为什么底层是basic_string<char>这里先不做介绍,先说结论:和编码有关!编码后面专门介绍!
三、string的常用接口介绍
构造和析构
string s;//空构造
cout << s << endl;
string tmp("123456789");
string s1(tmp);//字符串构造
cout << s1 << endl;
string s2(tmp, 3, 5);//用字符串的pos位开始的len个长度的子串构造
cout << s2 << endl;
string s3("hello string");//字符串常量构造
cout << s3 << endl;
string s4("abcdef", 3);//字符串常量的前n进行构造
cout << s4 << endl;
string s5(10, '@');//用n个c进行构造
cout << s5 << endl;
string s6(tmp.begin(), tmp.end());//迭代器区间构造
cout << s6 << endl;
这么多的构造函数,其实在日常的使用中,我们一般常用的是如下:
析构就没啥说的了!清理资源还空间嘛~!
迭代器
迭代器是和指针用法类似,但不一定是指针!!!
迭代器分为:正向、const正向、反向、const反向!后面C++11新增的那4个是为了区别!这里你可能有个一问题就是为什么要搞一个const迭代器?
这里还是权限问题:权限可以平移、可以缩小、但不能放大!假设不提供const版本,const的字符串在用迭代器访问时就权限放大了,我本来是只读的,已访问变成了可读可写的了,这显然不符合常理!这时你可能想那直接提供一个const版本即可,const和非const都可以访问了!但是如果我非const要写呢?是不是解决不了了!所以得提供两个!!!
正向、const正向
也就是说迭代器是左闭右开的!
OK,用一下:
void test_string2()
{
string s("hello iterator");
string::iterator it = s.begin();//正向迭代器
while (it != s.end())
{
cout << *it ;
++it;
}
cout << endl;
const string s1("hello 6666");
string::const_iterator it1 = s1.begin();//const正向迭代器
while (it1 != s1.end())
{
cout << *it1;
++it1;
}
cout << endl;
}
这里指定类域是因为STL容器都有迭代器,
反向、const反向
反向迭代器顾名思义就是说倒着遍历访问的!
OK,还是用一下:
void test_string3()
{
string s("hello world");
string::reverse_iterator rit = s.rbegin();//反向迭代器
while (rit != s.rend())
{
cout << *rit;
++rit;
}
cout << endl;
const string s1("asrdafa");
string::const_reverse_iterator rit1 = s1.rbegin();//const反向迭代器
while (rit1 != s1.rend())
{
cout << *rit1;
++rit1;
}
cout << endl;
}
还有最后的两组的效果和上面的const和反向是一样的,是C++11新增的目的是减少误使用的风险!原因是,上是代码的迭代器变量前的那一堆都可以用auto替代,但代码的可读性就降低了,当出现误操作时,不容易发现!!但是一般开发中很少有人这样搞!!
我就那一个来用一下,其他同理!!
const string s("hello world");
//string::const_reverse_iterator rit = s.rbegin();//正常
//auto rit = s.rbegin();//auto--->这样写你不知道他是不是const
auto rit = s.crbegin();//这样就看起来是哪个了
while (rit != s.crend())
{
//...
}
容量相关的接口
size 和 length
size和length没有区别,都是返回字符串的元素个数!以前是只有length但STL的有些容器的底层不是线性的,length不再合理了!于是就有了size,string为了和其他容器的一致性也就设计了size!!
max_size()这个接口没什么用这里直接略~!
resize
resize的英文含义是:改变大小。它的功能比较多,可以分为三中情况!
1、如果n小于当前字符串的长度(size),保留前n个字符,size 等于 n,后面的字符相当于被删除了!
2、如果n大于当前字符串的容量(capacity)的话,扩容,然后尾插字符c。如果字符c没有指定,就插入'\0',否则插入指定的字符!
3、如果n大于size,但小于capacity的话,直接尾插!
void test_string1()
{
string s("hello world");
cout << s << endl;
cout << s.size() << endl;//11
cout << s.capacity() << endl << endl;//vs上一开始默认是15个容量
//n < _size, 删除字符
s.resize(5);
cout << s << endl;
cout << s.size() << endl;//5
cout << s.capacity() << endl << endl;//15个容量
// _size < n <_capacity, 尾插
s.resize(10, '#');
cout << s << endl;
cout << s.size() << endl;//10
cout << s.capacity() << endl << endl;//15个容量
//n > _capacity,扩容 + 尾插
s.resize(20);//默认不给第二个参数时,默认是'\0'
cout << s << endl;
cout << s.size() << endl;//20
cout << s.capacity() << endl << endl;//31个容量
s.resize(30,'@');//给第二个参数时,后面尾插的是给的字符
cout << s << endl;
cout << s.size() << endl;//30
cout << s.capacity() << endl << endl;//31个容量
}
OK,再来通过调试看看,上面的没指定参数的扩容+尾插的是不是'\0'
而'\0'是字符串结束的标志,当下次插入是,还是在'\0'开始插入,以前是hello#####\0,现在需要将size改到30,需要插入@,以前的size == 20,所以在以前的基础上插入10个@即可,覆盖在\0开始的10个,这就是20个字符,但size是30的原因!!!
reserve
reserve的英文是保留、预留的意思。这里的作用是为字符串预留空间的。分为两种情况:
1、当n > _capacity时, 就会扩容到n个字符的容量或更大(有的编译器可能存在对齐)!
2、当n <= _capacity时,所不缩容是不确定的,但一定不影响size和已经有的内容!
void test_string2()
{
string s("hello world");
cout << s << endl;
cout << s.size() << endl;
cout << s.capacity() << endl << endl;
//n > _capacity ---> 扩容
s.reserve(200);
cout << s.size() << endl;
cout << s.capacity() << endl << endl;
// n <= _capacity, 不影响容量和size,也就是即使你的, n < _size也不会删除
s.reserve(2);
cout << s.size() << endl;
cout << s.capacity() << endl << endl;
}
这里很清楚的看到,我开了200个空间,但这里是207个,原因是人家也说了开到n或更大!VS上是考虑了对齐的!而当小于_capacity时我们发现VS上没有缩容!我们再把这段代码拿到Linux上看看:
这是Linux的代码,我们可以看到Linux下是没有对齐的,开多少就是多少!而且当n <_capacity时,会缩容!但缩到当前字符串的size大小,其内容不改变!!!
OK,介绍这里我们就可以看看string的扩容机制了!
先说结论:
VS上是1.5倍扩,一开始默认是15(底层有个buff数组,大小16,除去'\0',就是15)
Linux上是2倍扩!!!
void test_string3()
{
string s;
size_t num = s.capacity();
cout << s.capacity() << endl;
int i = 0;
while (i < 200)
{
s.push_back('a');
size_t count = s.capacity();
if (count != num)
{
cout << count << endl;
num = count;
}
i++;
}
}
在VS下的结果:
Linux下结果:
OK,我们通过VS调试看看VS下的string的那个buff数组:
他这样设计是因为多数情况下的字符串是小于16的,一开始直接整一个数组就不需要去堆上开辟了,效率高了!这里就先介绍到这里,等吧所有接口介绍完了还会在谈VS下和Linux的底层结构区别的!!!
OK,既然这个reserve可以提前预留空间的话我们是不是在有些情况下,大概提前估算好所需的空间,然后一次性开好,减小了扩容次数,效率是不是会高了?是的,这也是reserve 的一个用很重要的法!!!
例如上述的代码,我们如果这道自己要200个空间的话,提前开好就不需要在扩容了:
Linux同理:
capacity
返回字符串的容量!
clear
清空字符串内容!
void test_string4()
{
string s("hello world");
cout << s << endl;
cout << s.size() << endl;
cout << s.capacity() << endl;
s.clear();
cout << s << endl;
cout << s.size() << endl;
cout << s.capacity() << endl;//不会删掉容量
}
empty
判断字符串是否为空! 如果为空,返回true; 否则,返回false;
shrink_to_fit
这是C++11提供的一个缩容接口!目的是避免多与空间的浪费,例如一个字符串实际占用的空间是5个,但容量是200!是不是有点浪费!我们可以通过这个函数来缩到当前字符串的合适的容量大小!但不影响原来字符串的长度和内容!!!
访问相关的接口和操作
除了这几个外我们上面介绍的迭代器也是元素访问,支持元素的遍历;还有一个是C++11新增的范围for,也是支持的!
[ ] 和 at 、迭代器 、范围for
一看到[],我在你在想这东西不是以前C语言有吗?是的!但成员的那个不是很安全,有时候越界检查不出来!C++又重载了[]操作符!
[]和at的作用一样呢!都是获取字符串中某个字符。会返回那个字符的引用!这里他也有const和非const版本!这里原因不在多介绍了!和上面的一,还是权限问题!
at 和 []的区别是:[]越界的检查是暴力检查(assert), 而at是温柔的检查(报异常)!
void test_string5()
{
string s("hello world");
//[]遍历
for (int i = 0; i < 11; i++)
{
cout << s[i] << " ";
}
cout << endl;
//at遍历
for (int i = 0; i < 11; i++)
{
cout << s.at(i) << " ";
}
cout << endl;
//迭代器
string::iterator it = s.begin();
while (it != s.end())
{
cout << *it << " ";
++it;
}
cout << endl;
//范围for
for (auto c : s)
{
cout << c << " ";
}
cout << endl;
}
这是非const的[]、at、和迭代器。他们,他们是可读可写的!这里的范围for没加引用,只读没即使是修改也不会修改s原本内容!!!const和这个相反!只读不写!
总结:如果访问时不修改,建议是使用cosnt的,这样const的可用,非const的也可以使用!
关于范围for,其实它的底层就是迭代器!!!后面模拟实现再看底层!!!
OK,我们再来看看,[]和at的越界不同点!在看这个之前,我先想介绍的是,[]和C语言[]的访问越界的区别:C语言[]的检查是在一定的位置抽查,有可能越界的远了查不到,但是[]是一定可以检查到的!!!因为底层是断言检查!!!
再来看看C++的[]:
再来看看at:
front 和 back
这两个接口一个是取第一个元素,一个是取最后一个元素!其实有了[]我们可以直接[0]和[size-1]!!!
修改的相关接口
OK,我们按顺序来:
operator+=
它的作用是:可以在字符串结尾;尾插一个字符、追加一个字符串、追加一个常量字符串
string s("hello world");
s += "666";
cout << s << endl;
s += string("heheh");
cout << s << endl;
s += '@';
cout << s << endl;
这个函数其实把我们将要介绍的push_back和append的功能集于一体了!!!非常好用!!!
append
这个和构造函数一样,我就不再一一的演示了!我找两个最常用的举个栗子:
string s("hello world");
s.append("*********");
cout << s << endl;
s.append(string("hehehe"));
cout << s << endl;
push_back
尾插一个字符
string s("hello world");
s.push_back('#');
cout << s << endl;
这个在上面的测试扩容机制时用过了,这里不在多介绍了!!!!
insert
几乎不用的我划掉了,留下的这几个也是很复杂!我们最常用的是1\3\5来演示一下!
string s("hello world");
s.insert(0, string("999999"));//0位置前插入一个字符串
cout << s << endl;
s.insert(2, "###");//2位置前插入一个字符串常量
cout << s << endl;
s.insert(5, 1,'*');//5位置前插入一个*
cout << s << endl;
erase
最常用的是第一个!
string s("hello world");
s.erase(2, 3);//2号位置开始,删除3个
cout << s << endl;
s.erase(0);//全删
cout << s << endl;
s.erase();//不给位置和长度默认全删
cout << s << endl
replace
这个设计的也是很复杂的,我还是介绍最常用的!这个和构造那块的函数很相似!若有其他需求请参照上面的构造!
void test_string1()
{
string s("hello world");
s.replace(0, 5, string("12345678"));//从0位置开始的5个字符,用string("12345678")替代
cout << s << endl;
string s1("xxxxyyyy");
s1.replace(0, 4, "abcd");//从0位置开始的4个字符,用"abcd"替代
cout << s1 << endl;
string s2("aaaaaaaaaa");
s2.replace(2, 5, 10, '@');//从2位置开始的5个字符,用10个'@'替代
cout << s2 << endl;
}
swap
这个函数是成员函数,和算法库里面的那个不一样!!!(后期会介绍比算法库中的高效)!
它的作用就是交换两个字符串!注意这个swap交换的是string的底层的指针!空间复杂度较低!
void test_string2()
{
string s1("hello world");
string s2("123456");
cout << "s1 = " << s1 << endl;//hello world
cout << "s2 = " << s2 << endl << endl;//123456
s1.swap(s2);
cout << "s1 = " << s1 << endl;//123456
cout << "s2 = " << s2 << endl;//hello world
}
我们上面感刚也说了,他是直接交换底层的指针的!我们可以看看他们的size和capacity是否已经交换!
pop_back
作用:尾删
这个函数很简单!不在举栗子了,就是删除最后一个字符!
转换和查找的相关操作
c_str和data:
由于c++是支持C语言的,有时候会混着写!例如在C++中使用fopen等!此时要的是c语言形式的字符串,给C++的string就不行了!
此时我们需要c形式的字符串,我们可以用c_str或data进行转换!
此时s1和s1.c_str()的区别是有无'\0'
find
void test_string4()
{
string s1("hello world world");
string s2("wor");
size_t i = s1.find(s2, 5);//5位置开始找s1
cout << i << endl;
size_t j = s1.find("hello", 0);//0位置开始查找"hello"
cout << j << endl;
size_t x = s1.find("world", 7, 5);//7位置开始查找"world", 的5个字符
cout << x << endl;
size_t s = s2.find('w', 3);//3位置开始查找字符'w'
cout << s << endl;
}
这里我有想到了一个C语言时候的一个题!就是给你一个字符串要求把空格换成20%!ok 这里就可以用find的和replace做!
void test_string5()
{
string s1("hello world world");
cout << s1 << endl << endl;
size_t pos = s1.find(' ', 0);//从第一个位置开始找空格
while (pos != string::npos)
{
s1.replace(pos, 1,"20%");
pos = s1.find(' ', pos + 1);//从当前空个的下一个位置开始找下一空格
}
cout << s1 << endl;
}
这样写这个题就完成了!但时间效率一点也不高!!!一位insert、earse、replace等都是要挪动数据的!有的还有扩容问题!所以一般能不用就不用!!!!
这道题其实还有一个解法就是!再开一个字符串把不是空格的字符尾插到后面,遇到空格了尾插20%即可!!!这样就可以用空间换时间了!!!
string tmp;
for (auto c : s1)
{
if (c == ' ')
{
tmp += "20%";
}
else
{
tmp += c;
}
}
cout << tmp << endl;
rfind
void test_string6()
{
string s1("hello world world");
size_t pos = s1.rfind("world");//最后一个位置向前找"world"在s1中最后出现的首字符的下标
cout << pos << endl;
size_t pos1 = s1.rfind(string("ld"), 13);//从13位置开始向前找这个字符串的最后一次出现的位置的首字符的下标
cout << pos1 << endl;
size_t pos2 = s1.rfind("hello", 6, 3);//从6位置开始向前找"hello"的前三个的最后一次出现的位置的首字符的下标
cout << pos2 << endl;
size_t pos3 = s1.rfind('w');//最后一个位置向前找'w'在s1中最后出现的下标
cout << pos3 << endl;
}
find_first_of
这个函数的名字很神奇,一看是找第一个出现的字符的位置!但其实是从前往后找在一个字符串中第一次出现的某个字符,然后返回其位置, 如果没找到则返回npos这个前面介绍过就是整型的最大值!!!
void test_string7()
{
string s("hello world world");
size_t pos1 = s.find_first_of("aeiou", 1);//找到e
size_t pos2 = s.find_first_of(string("abcd"), 0);//0位置开始找到d
}
这个东西可以干啥呢?其实在我们的日常生活中你见过!比如你的身份证的后六位隐藏,出现一些敏感词的屏蔽等!OK举个栗子:
string s1("我草尼玛");
cout << s1 << endl;
size_t pos = s1.find_first_of("草尼玛");
while (pos != string::npos)
{
s1[pos] = '*';
pos = s1.find_first_of("草尼玛",pos + 1);
}
cout << s1 << endl;
这是一段你在游戏中问候队友的问候语,但系统检测到会给你屏蔽!
find_last_of
这个和上面的一样只不过这个是从后向前找的!举个常见的小栗子:我们得到一个文件的路径后,我们要把它的各个部分取出来此时就可以用这个了!
void test_string8()
{
string str("c:\\win\\test.cpp");
cout << "Splitting: " << str << '\n';
size_t found = str.find_last_of("\\");
cout << " path: " << str.substr(0, found) << '\n';
cout << " file: " << str.substr(found + 1) << '\n';
}
find_first_not_of
这个和find_first_of的功能相反,前者是把存在的从前面找到并返回第一个字符出现的下标,这个是把不存在的第一个的下标返回!
OK,我们还是用上面屏蔽的例子来举栗(这里就是把存在不屏蔽,不存的屏蔽):
void test_string9()
{
string s("jhsdgfuianbxcsjakghwdfuqab kbj");
cout << s << endl;
size_t pos = s.find_first_not_of("aeiou");
while (pos != string::npos)
{
s[pos] = '*';
pos = s.find_first_not_of("aeiou", pos + 1);
}
cout << s << endl;
}
find_last_not_of
这个和find_last_of的相反,前者是找在集合的字符的最后一个的位置,这个是找字符串中最后一个不匹配指定字符集合中任何字符的位置
void test_string10()
{
string str = "Hello, World!";
string chars = " ,!"; // 要查找的字符集合
size_t found = str.find_last_not_of(chars);
if (found != string::npos)
{
cout << "最后一个不在指定字符集合中的字符位置是: " << found << "是" << str[found] << endl;
}
else
{
cout << "字符串中的所有字符均在指定字符集合中" << endl;
}
}
substr
这个是返回字符串中的一个子串!从pos位置开始,len长度的子串!!!
void test_string11()
{
string str = "Hello, World!";
string sub = str.substr(0, 5);
cout << sub << endl;
}
非成员函数介绍
operator+
返回两个字符串拼接的一个字符串!他这里写成非成员函数的原因是成员函数默认第一个参数是this,如果char* + str的话就会有问题!
逻辑比较
这个就不举例子了!很简单,就是比较两个字符串的!
swap
这个函数是交换两个字符串的内容的!底层直接交换的是指针!我们前面也说过有一个成员函数swap和这个个一样,那为什么还有这个呢???有一种情况就是被误操作了,没有str1.swap(str2);而是直接swap(str1,str2),这不就是调算法库里面的那个呢吗?有拷贝不好!这里就是为了防止这种情况的!你可能会说这个在全局,算法库的那个也在全局到底调用哪个?我们在模板那里介绍过!编译器和人一样有现成的迟现成的,没有现成的再去做!!!这里也是一样,swap已经是string的参数了,就不用去实例化了!而这个swap底层实际上是调用的str1.swap()!
流插入和流提取略!
getline
这个函数的意思是从流里面提取字符到字符串,遇到终止符结束,如果终止符没指定就遇到\n结束!也就是终止符默认是\n
string s;
getline(cin, s, '@');//有指定符到指定福结束
cout << s << endl;
string s;
getline(cin, s);//没有指定符到\n结束
cout << s << endl;
to_string
将非字符串转化为字符串!然后返回这个字符串
int a = 10;
string s = to_string(a);
cout << s << endl;
OK,这就是string的基本使用,本期就介绍到这里!好兄弟,我们下期再见!!!