String的认识和使用
string我会结合英文文档来进行剖析
网站是cplusplus.com
string是一个管理字符数组的顺序表 ,string的本质也是模板,只是库给你typedef了
首先我们来看string的构造函数
void test1()
{
string s1;
string s2("yeah");
cin >> s1;
cout << s1 << endl;
cout << s2 << endl;
char str[16];
}
int main()
{
test1();
return 0;
}
C语言的不能做到很好的按需申请空间,而string就可以根据动态申请来很好的管理空间
那我们想把两个字符串拼接到一起怎么办
string重载了运算符+
string ret = s1 + s2;
cout <<ret << endl;
string ret2 = s1 +"好快乐";
cout << ret2 << endl;
这是类比C语言中strcat的知识,strcat只管把那个字符串拷贝到另一个字符后面,空间不够得自己开好,而且找'\0'也会耗时间,string就很好的解决了这个问题
前面我们都是浅浅的看一下,接下来我们来看string的访问
void test2()
{
string s1("yeah");
string s2 = "yeah";
}
上面这个是单参数的构造支持隐式类型的转换
接下来我们来看string的遍历
这里包括可读和可写两个版本的
那么我们如何知道它的大小呢
for (size_t i = 0; i < s1.size(); i++)
{
//读
cout << s1[i] << " ";
}
cout << endl;
for (size_t i = 0; i < s1.size(); i++)
{
//写
s1[i]++;
}
cout << s1 << endl;
这是第一种遍历方式
第二种遍历方式叫作迭代器
迭代器可以想象成像指针一样的东西
每个容器的迭代器都是在它里面定义的,直接用是用不了的,得去到类域里面找
begins是开始的位置,end是最后一个数据的下一个位置
这是一个左闭右开的区间
string::iterator it = s1.begin();
while (it != s1.end())
{
cout << *it << " ";
++it;
}
it = s1.begin();
while (it != s1.end())
{
*it = 'a';
++it;
}
一个是读一个是写
那我们上面的不等于可不可以改成小于呢,这里是可以的但是不建议,迭代器作为一种主流的容器遍历方式,当我在链表的时候就会发现这个小于是用不了的,因为物理空间不是连续的
这两个其实是反向迭代器的意思
string s1("yeah");
string::reverse_iterator rit = s1.rbegin();
while (rit != s1.rend())
{
cout << *rit << " ";
++rit;
}
cout << endl;
它的++就是倒着走的
那我们想简化一下还可以用auto来识别类型
auto rit = s1.rbegin();
那我们再用范围for来进行一个遍历,这个其实是最舒服的
for (auto ch:s1)
{
cout << ch << " ";
}
这个就可以自动判断结束,自动++了,这个原理其实就是迭代器,跟上面的代码差不多
也包含了begin和end
但是范围for不支持倒着遍历和修改
for (auto ch:s1)
{
ch++;
}
cout << s1 << endl;
因为本质是赋值给给它,ch的改变并不会影响s1
for (auto&ch:s1)
{
ch++;
}
cout << s1 << endl;
这样子就可以了
void func(string s)
{
string::iterator it = s.begin();
while (it != s.end())
{
cout << *it << " ";
++it;
}
}
string s1("yeah");
func(s1);
这里我们这样子传值会涉及到深拷贝的问题
我们又不期望修改所以
void func(const string& s)
但是这样子编译就通过不了了
因为我迭代器是支持可读可写的,但是我这里加上了const就不能写了
这里是const对象,const对象返回的是const的begin,所以我们应该用const迭代器
void func(const string& s)
{
string::const_iterator it = s.begin();
while (it != s.end())
{
cout << *it << " ";
++it;
}
}
这里也能体现的出auto的重要性
我们再回头看一下拷贝构造函数
比如说这个就是从第几个位置往后去拷贝几个字符
string s2(s1, 2, 1);
cout << s2 << endl;
那假如说这个后面很长但是我们又想拷贝所有字符怎么办,我们可以看到上面有给一个缺省值npos
类型结合它的长度来看,无符号整型的-1就是整型的最大值,42亿9千万这个铁铁的是造成越界
它会取到这个字符串的结束
string s1("yeahxxxxxxxxxxxxxxxxxxxxxxxxxx");
string s2(s1, 2);
cout << s2 << endl;
所有我们不给就好了
那我们其实也不需要去数去算就好了
string s3(s1, 2,s1.size()-2);
cout << s3 << endl;
初始化也是有好几种方式
string s4(10,'a');
cout << s4 << endl;
string s5(++s4.begin(), --s4.end());
cout << s5 << endl;
第二个就是第一个和最后一个不要了
这两个其实是没有区别的,因为string其实出现的比stl要早,最早开始就叫length,但是后面stl出来了,为了迎合大众需求,因为大家都用size,所以string也添加了size
这个是不会释放空间的
string s1("yeah");
s1.clear();
s1 += "mmm";
cout << s1.size() << endl;
cout << s1.capacity() << endl;
这个其实是不准的,因为它是写死了,假如我没有剩那么多空间它也不会跟着改变
那么我们可以想出一个问题,为什么不把’\0‘删去呢,因为C++需要向下兼容C语言
我们再来看一下它的扩容机制,这个大概是1.5倍的扩容
void test6()
{
string s;
size_t old = s.capacity();
cout << "初始化" << s.capacity() << endl;
for (size_t i = 0; i < 100; i++)
{
s.push_back('x');
if (s.capacity() != old)
{
cout << "扩容" << s.capacity() << endl;
old = s.capacity();
}
}
}
这主要是和编译器所运行的环境来看
这也是涉及空间,reserve这个单词涉及保留的意思,reserve一般会去申请扩容
s.reserve(100);
不过vs编译器下一般会开的比100大
reserve的意义在哪里呢,确定大概知道要多少空间,提前开好,减少扩容,提高效率
那reserve会不会缩呢
s.reserve(10);
cout<< s.capacity() << endl;
一般的编译器都是不会缩了,有些编译器可能会缩,那要是缩是怎么缩呢,比如我有一块100的空间,内容是50,那么我一般都会缩到50,这个一般都是异地缩
s1.resize(7,'x');
cout << s1 << endl;
cout << s1.size() << endl;
cout << s1.capacity() << endl;
s1.resize(12, 'x');
cout << s1 << endl;
cout << s1.size() << endl;
cout << s1.capacity() << endl;
s1.resize(3);
cout << s1 << endl;
cout << s1.size() << endl;
cout << s1.capacity() << endl;
这里是resize的三种情况
at就是越界了以后它会抛异常,它和operator[]的区别就是,前者会给你提醒,后者直接就进行报错
但是日常还是operator[]用的比较多
s1[0]++;
s1.at(0)++;
cout << s1 << endl;
还是第一行的看的最符合我们日常需求
这个是插入字符串所需的
string s1("hello world");
string s;
s.push_back('*');
s.append("hello");
s.append(s1);
cout << s << endl;
第二行就是插入一个对象的一部分,但是我们最好用的还是+=
s += '*';
s += "hello";
s += s1;
cout << s << endl;
所以string的设计是非常冗余的
有+=自然也有+,+其实是全局函数
string ret1 = s1 + '*';
string ret2 = s1 + "hello";
string ret3 = s1 + s;
但是+是一个传值返回就是要构造一个对象,至少都有两次拷贝
assign有赋值的意思
string s1("hello world");
string s2;
s2.assign(s1);
cout << s2 << endl;
那么我已经有一些数据呢,就会把它覆盖掉
string s1("hello world");
string s2("xxx");
s2.assign(s1);
cout << s2 << endl;
string s1("hello world");
string s2("xxx");
s2.assign(s1,2,3);
cout << s2 << endl;
也可以指定某些位置多少值,这里就是第二个位置往后三个值
那如果我们想要头插的话就需要用到insert
string s1("hello world");
s1.insert(0, 1, 'x');
s1.insert(s1.begin(), 'x');
cout << s1 << endl;
这是插入一个字符下的场景,具体我们还得看对应的接口
s1.erase(5);
cout << s1 << endl;
npos就是后面有多少删多少
insert和erase能不用就不用,因为他们都涉及挪动数据,效率不高
replace有替换的意思
s1.replace(5, 1, "****");
cout << s1 << endl;
这段代码的意思就是把第五个位置的一个字符替换成上面这串
这个其实也跟上面一样能不用就不用,因为接口设计复杂繁多,需要时查一下即可
string s1("hello world daily room");
string s2;
for (auto e:s1)
{
if (e !=' ')
{
s2 += e;
}
else
{
s2 += "%20";
}
}
s1.assign(s2);
cout << s1 << endl;
s1 = s2;
cout << s1 << endl;
swap(s1, s2);
cout << s1 << endl;
这个是库里面的swap
会经历3次深拷贝
string s1("hello world daily room");
string s2;
for (auto e:s1)
{
if (e !=' ')
{
s2 += e;
}
else
{
s2 += "%20";
}
}
s1.assign(s2);
cout << s1 << endl;
s1 = s2;
cout << s1 << endl;
swap(s1, s2);
cout << s1 << endl;
那我们string里面的这个交换时怎么实现的呢
把它们两个指针一交换就ok了
我们验证一下发现也确实是如此
printf("s1:%p\n", s1.c_str());
printf("s2:%p\n", s2.c_str());
s1.swap(s2);
printf("s1:%p\n", s1.c_str());
printf("s2:%p\n", s2.c_str());
但是编译器就怕你用到上面那个深拷贝的swap
所以编译器提前弄好了这个,在有现成的和模板之间它就会先选择现成的
printf("s1:%p\n", s1.c_str());
printf("s2:%p\n", s2.c_str());
swap(s1,s2);
printf("s1:%p\n", s1.c_str());
printf("s2:%p\n", s2.c_str());
所以改成这个也是一样的
string s("test.cpp");
size_t i = s.find('.');
那我们怎么把后面的给取出来呢
string s1 = s.substr(i);
cout << s1 << endl;
string s("test.cpp.tar.zip");
size_t i = s.rfind('.');
string s1 = s.substr(i);
cout << s1 << endl;
像这样我们要找到它最后一个.的位置就需要用到rfind
就是在子字符串中找相匹配的任意一个字符
这里我们直接借用官网来看一下
std::string str("Please, replace the vowels in this sentence by asterisks.");
std::size_t found = str.find_first_of("aeiou");
while (found != std::string::npos)
{
str[found] = '*';
found = str.find_first_of("aeiou", found + 1);
}
std::cout << str << '\n';
这个是倒着找的
这个是找不是子字符串里面的
我们平时cin输入字符串的时候中间难免会出现空格换行等字符,所以跟scanf一样都只能识别到一部分,所以我们需要用到getline
string的大部分接口我就介绍到这里了,接下来是string的模拟实现,后面是各种容器,但是其实这些容器都有很强的相似性,所以后面会显得越来越简短,主要还是放在模拟实现上面,有什么不好的地方欢迎大家指出