文章目录
这篇文章主要带大家来使用C++中的string类。以及了解string类在部分编译器中的底层实现。
C++ string类
1. 为什么学习string类?
1.1 C语言中的字符串
C语言中,字符串是以’\0’结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,但是这些库函数与字符串是分离开的,不太符合OOP(面向对象编程)的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。
1.2 面试中
在OJ中,有关字符串的题目基本以string类的形式出现,而且在常规工作中,为了简单、方便、快捷,基本都使用string类,很少有人去使用C库中的字符串操作函数。
2. 标准库中的string类
2.1 string类
在使用string类时,必须包含#include头文件(<string>)以及using namespace std;
2.2 如何创建一个string类的对象
这个时候我们可以查看文档中string类的构造函数
如下就是文档中的string构造函数的申明:
以及对上面申明的解释:
但是具体怎么定义还是取决于编译器。
这里对第3种方式做格外补充,当传输实参给len的长度超过字符串的长度,那么将直接截取从pos位置到’\0’,如果不传实参给len的话,编译器采用缺省值npos,npos的值为-1,而-1的补码是七十多亿,而采用size_t 类型存储的-1也就成了最大值,所以结果是直接截取从pos位置到’\0’。
2.3 如何遍历一个string类的对象
这里提供三种方法
1. for循环遍历
string s1("hello world");
for (int i = 0; i < s1.size(); i++)
{
cout << s1[i];
}
s1.size()就是返回字符串s1的长度。
s1[i]用到了运算符重载,将[]达到了类似用数组一样的访问效果。
2. 迭代器
-
正向遍历
string s1("hello world"); string::iterator it = s1.begin(); while (it != s1.end()) { cout << *it; // 可读也可写 it++; } cout << endl;
const string s2("hello world"); string::const_iterator it = s2.begin(); while (it != s2.end()) { cout << *it; // 只读不能写 it++; }
-
反向遍历
string s1("hello world"); string::reverse_iterator rit = s1.rbegin(); while (rit != s1.rend()) { cout << *rit; // 可读也可写 rit++; } cout << endl;
string s1("hello world"); string::reverse_iterator rit = s1.rbegin(); while (rit != s1.rend()) { cout << *rit; // 只读不能写 rit++; } cout << endl;
这里迭代器的类型很多且很长,我们如果用auto
就可以一劳永逸。
3. 范围for
const string s2("hello world");
for (auto& ch : s2)
{
cout << ch;
}
cout << endl;
2.3 string类的常用接口说明
1. string类对象的容器操作
capacity的扩容法则:
由于标准库并没有规定capacity该如何扩容,所以各大编译器呈现不一样,下面主要介绍两种主流的编译器。(下面的capacity都不包含’\0’)
VS2022编译器的扩容法则:
如下就是一个扩容操作,容量的变化:
下面来简单的解释一下:
VS会将大小小于16的字符串将放到一个_Buf字符数组里面,如果需要扩容初次扩容将以其_Buf的两倍扩容,之后以1.5倍扩容。
Linux的g++编译器的扩容法则:
如下就是一个扩容操作,容量的变化:
我们可以看到,g++编译器是两倍扩容
reverse的使用
通过文档,我们可以知道reverse有以下几点法则
- 当扩容值n > capacity;进行扩容
- 当扩容值n < capacity
- 当容量值n > 字符串长度size;具体是否进行缩小看编译器
- 当扩容值n < 字符串长度size;capacity肯定不会缩小到小于size,具体是否进行缩小看编译器
下面依然介绍两种主流的编译器
VS2022
当容量值n > 字符串长度size;不进行缩小
当扩容值n < 字符串长度size;不进行缩小
总结:VS2022采用空间换时间的方式,都不对capacity进行收缩
Linux的g++
当容量值n > 字符串长度size;进行缩小,缩小的大小满足扩容的整数对齐
当扩容值n < 字符串长度size;进行缩小但不会小于size,依然满足扩容的整数对齐
总结:g++考虑空间上的消耗,都尝试进行收缩
resize的使用
通过文档,我们可以知道resize有以下几点法则
- 当 n < size 时;将当前的字符串的值只取前 n 个,但是capacity具体是否变化不确定
- 当 n > size 时;将超过的部分赋值为字符c (如果没有传参的话,默认为’\0’)
- n < capacity 时;是否扩容不确定
- n > capacity 时;进行扩容,按扩容整数对齐扩容
2. string类对象的访问
operator[] 和 at 的区别
这两者使用方式类似
string s("hello world");
s[6]; // w
s.at(6); // w
两者主要的不同是越界访问的时候:
operator[] 会断言at 会抛异常
3. string类对象的修改操作
push_back
push_back就是尾插操作尾插一个字符,不能是一个字符串(用append)
string s("hello world");
s.push_back('a');
append
append的用法有很多,这里就不依依举例,append的最重要的一个用法是尾插字符串
string s("hello world");
s.append("abc");
operator+=
push_back与append都可以被operator+=替代,并且operator+=也比较方便好用,所以一般我们都直接采用operator+=
string s("hello world");
s += 's';
s += "abc";
operator+=其实就是一个运算符重载,我们在后面的string模拟实现会有具体体现。
insert
insert就是对字符串插入操作。(尽量不要经常使用)
string s("hello world");
s.insert(0, "abc");
s.insert(0, 1, 'd'); // 在0位置上插入1个'a'
//s.insert(0, 'a'); // 错误的,没有对应的接口
s.insert(s.begin(), 'e');
s.insert(s.end(), 'x');
erase
insert就是对字符串删除操作。(尽量不要经常使用)
string s("hello world");
s.erase(0, 2); // 从0位置往后删除2个字符
s.erase(2); // 删除从2位置往后的所有字符,len默认缺省为最大值
s.erase(s.begin()); // 头删
s.erase(--s.end()); // 尾删
replace
string s("hello world");
s.replace(5, 1, "%%"); // 将第5个位置其往后的1个字符替换成"%%"
cout << s << endl; // hello%%world
swap
string s1("hello");
string s2("world");
s2.swap(s1);
c_str
由于C++兼容C语言,有时候需要调用C语言的接口(比如数据库),但是C语言没有引入string类类型,所以需要用c_str将一个string类型的对象转换成const char*
类型
std::string str("hello world");
char * cstr = new char [str.length()+1];
std::strcpy (cstr, str.c_str()); // 将str中的字符拷贝到cstr中,但是这个接口是C语言的
find
- str 、s、c 是需要查找的内容
- pos 是从对象第pos的位置开始查找,默认为0(即从头找)
- n 是取内容 s的前n个字符用来查找
- 如果查找到内容的话,返回第一个找到的内容在对象的位置
- 如果没查找到内容的话,返回string::npos(用来遍历每一个查找内容时很有用)
// 将' ' 替换成 "%%"
string s("hello worldhello worldhello worldhello worldhello worldhello world");
int pos = s.find(' ');
while (pos != string::npos)
{
s.replace(pos, 1, "%%");
pos = s.find(' ', pos + 2);
}
cout << s << endl;
// 当然直接定义另一个string,然后进行存储变化之后的字符,这种空间换时间的方法更快
rfind
同上,就是反过来查找
find_first_of
和find的功能有点相似,不同的就是它是将 str、s、c中的每个字符作为查找标准来查找。而find是以整个 str、s、c为查找标准来查找。
find_last_of
同上,就是反过来查找
find_first_not_of
查找不属于find_first_of范畴内的
find_last_not_of
查找不属于find_last_of范畴内的
substr
这个其实和python的切片差不多,在str中从pos位置开始,截取n(默认为无穷大)个字符,然后将其返回
string s("hello world");
string subs1 = s.substr(6); // "world"
string subs2 = s.substr(6, 3); // "wor"
getline
我们在用cin输入给string的时候,会发现 ’ ’ 就是一个分隔符,如果我们要获取比如"hello world"的时候,实际上确实 “hello”,这个时候就得使用getline。
string s;
cin >> s; // 输入hello world
cout << s << endl; // 输出hello
getline用法:
用法一:只传两个参数时,默认 ‘\0’ 为分隔符
string s;
getline(cin, s); // 输入hello world
cout << s << endl; // 输出hello world
用法二:传三个参数,最后一个传需要以什么为结束的分隔符
string s;
getline(cin, s, '*');
cout << endl;
cout << s << endl;
4. 常用的类型转换接口
记忆技巧:
字符串转换成其他类型就是 string to (需要的类型)
比如转整型就是 string to int 简写就是 stoi,其他同理
其他类型转字符串只需要 to_string就行了
string s1("123");
int n1 = stoi(s1);
long n2 = stol(s1);
unsigned int n3 = stoul(s1);
long long n4 = stoll(s1);
unsigned long long n5 = stoull(s1);
string s2("123.123");
float n6 = stof(s2);
double n7 = stod(s2);
long double n8 = stold(s2);
int i = 123;
string s(to_string(i));
3. string类的模拟实现
4. 扩展阅读
下面有两篇陈皓大佬的关于string的文章