今天我们来讲解一下c++中常用的string类,了解其使用,并且对它进行模拟实现。
目录
标准库中的string类
在讲解c++的string之前,我们先回顾一下C语言中的字符串.
C语言中,字符串是以’\0’结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,但是这些库函数与字符串是分离开的,不太符合OOP的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。
什么是String类(了解即可)
那么在c++中的string 类是什么?
- 字符串是表示字符序列的类
- 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。
- string在底层实际是:basic_string模板类的别名
string类的常用接口
string类对象的常见构造
函数名称 | 功能说明 |
---|---|
string() | 构造空的string类对象,即空字符串 |
string(const char* s) | 用C-string来构造string类对象 |
string(size_t n, char c) | string类对象中包含n个字符c |
string(const string&s) | 拷贝构造函数 |
string类对象常用容器操作
字符串的遍历
1. 使用 size()
for (size_t i=0;i< s2.size();i++)
{
s2[i] += i;
}
cout << endl;
2. 使用auto ch
for (auto ch : s2)
{
cout << ch << " ";
}
cout << endl;
3. 使用迭代器
迭代器是一个像指针的东西,有可能是指针,也可能不是。其好处,可以用统一类似的方式去访问修改容器。
begin()函数返回第一个有效数据位置的迭代器
end()函数返回最后一个有效数据的下一个位置的迭代器
1.所有的容器都支持迭代器(容器就是数据结构)
2.vector/string 这种结构支持下标+[]去访问,像list,map就不支持了
string::iterator it = s2.begin();
while (it != s2.end())
{
cout << *it <<" ";
++it;
}
cout << endl;
除了 begin()和end()之外,iterator 还提供了其他的函数:
- rebegin() 和 rend()
简单来说,使用这一组函数就是反向遍历字符串
auto rit = s1.rbegin();
while (rit != s1.rend())
{
*rit += 1;
cout << *rit << " ";
++rit;
}
cout << endl;
- cbegin() 和 cend()
当我们对 字符串 只能读 不能写的时候,为了防止字符串被误改,我们可以使用这组函数替代 begin()和end()。这组函数在我们要对字符串进行输出打印时十分有用。
同时,这里要注意。我们对const对象使用迭代器是要是用 const迭代器。
void Print(const string& s)
{
//const 对象要使用const迭代器,只读,不能写
string::const_iterator it = s.cbegin();
while (it != s.cend())
{
cout << *it << " ";
++it;
}
cout << endl;
}
- crbegin() 和 crend()
看了上面两组函数,这个函数的意义就很简单了:倒过来读const 对象
计算空间相关
1. size() 和 length()
两者都是计算字符串的实际长度(不包括‘\0’).
在这里,相比于size()来说,length()有些老旧了,所以我比较推荐大家使用size()
2. capacity()
我们调用s.capacity()可以知道我们给string对象分配的存储空间大小。
c++在默认初始化capacity的时候给的并不是0:
我们可以发现capacity直接被初始化为15(实际上是16)。
我们再看一下capacity是如何扩容的:
我们将size改为20:
我们可以发现capacity扩容成了31。
那么,capacity到底是怎么增长的呢?
我们使用一段程序测试一下:
由此可以看出,在vs编译器中,capacity每次以1.5倍去增容。
但不是所有的环境下都是按这样的规律去增容,比如在Linux中,capacity被初始化为1,每次以2倍去增容。
3. resize() 和 reserve()
- reserve()函数
根据文档,reserve函数是用来改变capacity的,如果我们知道需要空间的大小,就可以使用reserve直接一次性开好,避免增容,提高效率
我们可以试着使用一下:
我们发现capacity不是100,而是111,这与vs内的自带对齐模式有关,暂时不用深入了解,这种机制并不会影响我们的使用,有兴趣的同学可以去查看其源码。
如果我们使用reseve函数开辟的空间小于原有空间的话,vs中是不会做出回应的,也就是内存不会变小。
这里我附上部分源码(vs2019):
void reserve(_CRT_GUARDOVERFLOW const size_type _Newcap = 0) { // determine new minimum length of allocated storage
if (_Mypair._Myval2._Mysize > _Newcap) { // requested capacity is not large enough for current size, ignore
return; // nothing to do
}
if (_Mypair._Myval2._Myres == _Newcap) { // we're already at the requested capacity
return; // nothing to do
}
if (_Mypair._Myval2._Myres < _Newcap) { // reallocate to grow
const size_type _Old_size = _Mypair._Myval2._Mysize;
_Reallocate_grow_by(
_Newcap - _Old_size, [](_Elem* const _New_ptr, const _Elem* const _Old_ptr, const size_type _Old_size) {
_Traits::copy(_New_ptr, _Old_ptr, _Old_size + 1);
});
_Mypair._Myval2._Mysize = _Old_size;
return;
}
if (_BUF_SIZE > _Newcap && _Mypair._Myval2._Large_string_engaged()) {
// deallocate everything; switch back to "small" mode
_Become_small();
return;
}
// ignore requests to reserve to [_BUF_SIZE, _Myres)
}
- resize()函数
resize,顾名思义,就是调整字符串的长度
- 如果 n 比当前字符串长度小,则当前值将缩短为其第一个 n 字符,删除 n th 以外的字符。
2. 如果 n 大于当前字符串长度,则当前内容通过在末端插入尽可能多的字符来扩展,以达到 n 的大小。
如果我们不指定增加的字符,那么编译器吧自动给我添加’\0’
当然,如果size>capacity,capacity会判断并且增容。
所以,如果我们既要开好空间,还要对这些空间初始化,就可以用resize
增删查改相关
这一部分的内容较多,我只挑选比较常用的讲解:
1. operator+=
这是一个十分使用的运算符重载,可以实现字符串的连接。
而且在c++11中,这个函数重载除了几个版本:
2. append
append是一个对字符串进行尾插的函数
其实之前我们讲的+=在底层是用append和push_back实现的:
那么apend和+=有什么区别呢?
观看下列一段程序即可:
// appending to string
#include <iostream>
#include <string>
int main ()
{
std::string str;
std::string str2="Writing ";
std::string str3="print 10 and then 5 more";
// used in the same order as described above:
str.append(str2); // "Writing "
str.append(str3,6,3); // "10 "
str.append("dots are cool",5); // "dots "
str.append("here: "); // "here: "
str.append(10u,'.'); // ".........."
str.append(str3.begin()+8,str3.end()); // " and then 5 more"
str.append<int>(5,0x2E); // "....."
std::cout << str << '\n';
return 0;
}
Edit &
3. push_back
push_back是对字符串尾插一个字符的函数,由于比较简单,这里不做过多解释
4. insert
insert 插入函数,我们可以选择任意位置插入字符或者字符串。
我们这里也是通过一段程序来学习其用法:
#include <iostream>
#include <string>
int main ()
{
std::string str="to be question";
std::string str2="the ";
std::string str3="or not to be";
std::string::iterator it;
// used in the same order as described above:
str.insert(6,str2); // to be (the )question
str.insert(6,str3,3,4); // to be (not )the question
str.insert(10,"that is cool",8); // to be not (that is )the question
str.insert(10,"to be "); // to be not (to be )that is the question
str.insert(15,1,':'); // to be not to be(:) that is the question
it = str.insert(str.begin()+5,','); // to be(,) not to be: that is the question
str.insert (str.end(),3,'.'); // to be, not to be: that is the question(...)
str.insert (it+2,str3.begin(),str3.begin()+3); // (or )
std::cout << str << '\n';
return 0;
}
5. erase
erase 删除函数
我们这里依然通过一段程序来学习其用法:
#include <iostream>
#include <string>
int main ()
{
std::string str ("This is an example sentence.");
std::cout << str << '\n';
// "This is an example sentence."
str.erase (10,8); // ^^^^^^^^
std::cout << str << '\n';
// "This is an sentence."
str.erase (str.begin()+9); // ^
std::cout << str << '\n';
// "This is a sentence."
str.erase (str.begin()+5, str.end()-9); // ^^^^^
std::cout << str << '\n';
// "This sentence."
return 0;
}
其实String类的功能还有很多,这里不一一讲解,当我们见到不会的函数,我们可以通过查看文档来自行学习。
在 【C++】深入理解String类(二),我将尝试去模拟实现String类,使我们的理解更加深刻。