唤起一天明月
照我满怀冰雪
浩荡百川流
鲸饮未吞海剑气已横秋
目录
契机 ✨
我们在 C语言 阶段中,字符串是以 \0 为结尾的一些字符的集合,为了操作方便,C标准库 中提供了一些 str 系列的库函数(比如说:strlen、strcpy), 但是这些库函数与字符串是分离开的,不太符合 OOP (面向对象)的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。
举个栗子~比如 strcpy 在 C语言 中我们知道这个函数的作用是将一块空间拷贝到另一块空间
首先两个空间都需要自己提供,而且要保证目标的空间和拷贝空间是一样大的或者比它大的
以上的例子我们将 s1 拷贝到 s2 中,如果 s2 的空间小于 s1,strcpy 是不会管的,这个时候就会发生越界访问,像这类我们既要管空间又要管方法的函数用起来是很麻烦的
为了方便、快捷的进行我们字符串的编程,在我们 C++ 中就提供了字符串类也就是 string
为了更好的学习 string ,我们最好要学会看 string 库的文档:string 文档
string 库的简介
string 是表示字符串的字符串类 |
该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作 string 的常规操作 |
通过以上的文档信息我们可以知道:
string 其实就是一个模板,而 basic_string 就是模板类的别名:
typedef basic_string<char, char_traits, allocator> string
这里还有个点需要注意:
string 不能操作多字节或者变长字符的序列
剩下的就是别忘记包头文件哦(#include<string>) ~ 还有域限定符
string 的一些小操作
构造函数的使用
我们知道 string 是一个类,那么我们以前在类中的常规操作在 string 中能直接用吗?
-- 比如说:拷贝构造
我们发现 string 的设计其实是有点冗余的,单单一个拷贝构造其形式竟有 7 中之多
先来看一下我们熟悉的那几种的拷贝构造的用法
拷贝构造的常规使用
#include<iostream>
#include<string>
using namespace std;
void TestString()
{
// 创建一个 string 类
string s1;
// 构造
string s2("hello world");
cout << s2 << endl;
// 拷贝构造
string s3(s2);
cout << s3 << endl;
// 赋值构造
string s4;
s4 = s2;
cout << s4 << endl;
// 隐式类型转化,将字符串类型传化成自定义类型
string s5 = "hello world";
cout << s5 << endl;
}
int main()
{
TestString();
return 0;
}
因为 string 库中已将帮我们写好了,我们放心用即可~
指定拷贝内容的拷贝构造
string (const string& str, size_t pos, size_t len = npos);
我们先来分析以上代码, 参数提供了一个字符串和两个无符号整型,结合文档我们不难理解:
从 str 字符串的第 pos 位置开始拷贝,直到拷贝完 npos 个字符结束
如果 npos 超出了 str 后面的字符长度就拷贝到结尾,如果不写 npos 默认拷贝考结尾
我们来验证一下:
void TestString()
{
string s1("I Love You");
string s2(s1, 2, 4);
cout << s2 << endl;
string s3(s1, 2, 15);
cout << s3 << endl;
string s4(s1, 2);
cout << s4 << endl;
}
先分析:
s1 是我们从第二个位置开始拷贝的,拷贝长度为 4 个字节也就是 Loves2 是我们从第二个位置开始拷贝的,但是拷贝的字符长度超出了 str 后面的长度,所以是拷贝剩下的所有字符,也就是 Love You
s3 是我们从第二个位置开始拷贝的,没有写 npos 参数,所以是拷贝剩下的所有字符,也就是 Love You
注意:这里传的 pos 位置是下标哦 ~
这里可能有老铁会问,为什么 npos 不传参就是拷贝剩下的全部呢?
我们这里先看一下文档:
因为不传参,编译器就会给系统默认的值,也就是 -1 |
static const size_t npos = -1;
注意:这里的 -1 并不是真正的 -1,为什么无符号整会有负数?因为 -1 存的是它的补码就是全 F 。整型的范围大概是 21亿 多,而 size_t 就是 42亿 多。可想而知 npos 的值有多大!
我们简化来看其实不传 npos 的本质和传超过 str 的长度的 npos 是一样的,但是会比较方便
拷贝字符串开始的前 n 个字符
string (const char* s, size_t n);
这个比较简单我们直接测试一下吧
void TestString()
{
string s1("I Love You", 6);
cout << s1 << endl;
}
用 n 个字符初始化
string (size_t n, char c);
这个比较简单我们直接测试一下吧
void TestString()
{
string s1(10, 'x');
cout << s1 << endl;
}
string 设计的有些许冗余,我们只要掌握常规的构造就行,其他的方法最好也要了解一下
计算字符串的长度
为了更好的学习这两个函数,我们先参考一下文档:
我们发现这两个函数的功能都是一样的,都是返回 \0 之前的字符串长度:
void TestString()
{
string s1("Hello World");
cout << s1.size() << endl;
cout << s1.length() << endl;
}
那么有两个都可以用,那么最好用哪一个呢? -- 建议用 size(与 STL 库对应)
在这里穿插讲个题外的小故事,C++ 委员初次设计 string 的时候是朝着 STL 方向去做的,当时惠普工作室已经有了一套模板库 -- STL,C++ 委员会觉得很好就规定为标准了。
大家可以看到 string 的许多设计和 STL 是一样的,比如说 -- 迭代器
length 算是 string 的专属,对于 STL 库中计算长度就只有 size
为了与 STL 同步,我们最好在 string 也用 size
string 的三种遍历方式
常规的for循环
void TestString()
{
string s1("hello world");
for (int i = 0; i < s1.size(); i++)
{
cout << s1[i];
}
}
operator[]运算符重载
这里我想问各位老铁一个问题为什么字符串能 [ ] 访问单个字符呢?
-- 因为 [ ] 符号重载了
其实本质是这样的
s1.operator[](i)
这样就可以像数组一样访问
我们可以看看 operator[ ] 的底层逻辑
class string
{
public:
char& operator[](size_t i)
{
assert(i < _size);
return _str[i];
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
这里的 & 除了减少拷贝外,还有修改返回对象的良效
我们来看:
void TestString()
{
string s1("hello world");
for (int i = 0; i < s1.size(); i++)
{
s1[i]++;
cout << s1[i];
}
}
这样我们不仅能访问字符串的元素还能修改!!!
还有一点如果我们的数组发生了越界,string 是可以检查出来的
错误示范:
void TestString()
{
string s1("hello world");
s1[20];
}
string 会报错提示,如果是平时我们数组越界是检查不出来的访问的是其他空间的值
所以 string 有点香 ~
迭代器遍历
我们先来了解一下迭代器的基本原理(先来参考一下文档)
现阶段我们这样片面的理解为 :就是类似于两个指针,begin() 指向字符串的首字符,end()指向字符串的末字符 ~
注意:begin()、end() 并不是指针(只是用法类似),它们的主要内容我们在下篇 STL库 再来讲:
我们可以先看看它们的类型:(typeid().name()显示一个对象的类型)
cout << typeid(it1).name() << endl;
打印出来就是这一长串 “东西” ,别急以后会解释,接下来我们实现一下迭代器的遍历方法
void TestString()
{
string s1("hello world");
string::iterator it1 = s1.begin();
while (it1 != s1.end())
{
cout << *it1;
it1++;
}
cout << endl;
}
这样就写完了 ~ 感觉不如第一种写法简洁,但是以后的用处很大比如:可以遍历链表和树形等
我们可以先来感受一下:
#include<iostream>
#include<list>
using namespace std;
void Test()
{
list<int> lt1;
lt1.push_back(1);
lt1.push_back(2);
lt1.push_back(3);
list<int>::iterator it = lt1.begin();
while (it != lt1.end())
{
cout << *it << " ";
it++;
}
}
int main()
{
Test();
return 0;
}
或者以后写快排的时候也可以写迭代器:
sort(a.begin(), a.end());
表示对整个数据元素进行排序
void TestString()
{
string s1("hello world");
string::iterator it1 = s1.begin() + 6;
while (it1 != s1.end())
{
cout << *it1;
it1++;
}
cout << endl;
}
我们还可以通过改变迭代器的位置来达到我们想要的遍历效果
总结:
begin:任何容器返回第一个数据位置的iterator
end:任何容器返回最后数据的下一个位置的iterator
我们可以通过改变迭代器的始末位置来达到遍历需求
auto 自动类型推导
void TestString()
{
string s1("hello world");
for (auto i : s1)
{
cout << i;
}
cout << endl;
}
最简单的遍历方式,不过也有局限性 -- 只能从头遍历到尾(注意:底层是迭代器哦 ~ )
string 的追加字符或字符串
从 string 末尾插入一个字符
void TestString()
{
string s1("hello world");
s1.push_back('!');
cout << s1 << endl;
}
相当于我们在链表中学的尾插,不过只能插入一个字符哦 ~
string 的追加一个字符串
个人感觉 string 的这个设计也有些冗余,其实 push_back 传个 char*s 就够了(疯狂吐槽)
我们来看一下 append 的用法:
void TestString()
{
string s1("hello ");
string s2("world");
cout << s1.append(s2) << endl;
}
讲到这里我再来介绍一下 另一种方式 (运算符重载 += )
我们来看一下,这里追加 字符 或者 字符串 都是可以的
void TestString()
{
string s1("hello ");
s1 += "world";
cout << s1 << endl;
s1 += '!';
cout << s1 << endl;
}
先介绍到这里啦~
有不对的地方请指出💞