【本节目标】
- 1. 标准库中的string类
- 2. string类的模拟实现
- 3. 扩展阅读
目录
【本节目标】
1.标准库中的string类
2. string类对象的常见构造
🍉无参构造
🍉带参构造
🍉拷贝构造
🍉用n字符 # 去初始化
🍉用字符串的前n个字符去初始化
🍉从一个 string 对象的 pos 为值开始,拿 len 个字符去初始化
3. string类对象的容量操作
🍉size
🍉length
🍉max_size
🍉capacity
🍉reserve
🍉resize
🍉clear
🍉empty
4. string类对象的访问及遍历操作
🍉[ ] + 下标
🍉at
🍉begin + end(迭代器 iterator)
🍉rbegin 和 rend(反向迭代器 reserve_iterator)
🍉const 迭代器、const反迭代器(const iterator)、(const_reserve_iterator)
🍉范围for
5. string类对象的操作
🍉operator+=
🍉aappend
🍉push_back
🍉pop_back
🍉insert
🍉erase
🍉replace
🍉swap
6.string类运算符函数
🍉operator=
🍉operator+=
🍉operator+
🍉operator>> 和 operator<<
🍉relational operators (string)
前言:我们为什么要学string类
C语言中的字符串
C语言中,字符串是以'\0'结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,但是这些库函数与字符串是分离开的,不太符合OOP的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。
在OJ中,有关字符串的题目基本以string类的形式出现,而且在常规工作中,为了简单、方便、快捷,基本都使用string类,很少有人去使用C库中的字符串操作函数。
string文档介绍
我们下面的学习,都会以文档内容为标准
string类 就类比数据结构的 字符类型的顺序表 来学习能更好的理解
1.标准库中的string类
1. 字符串是表示字符序列的类
2. 标准的字符串类提供了对此类对象的支持,其接口类似于标准字符容器的接口,但添加了专门用于操作单字节字符字符串的设计特性。
3. string类是使用char(即作为它的字符类型,使用它的默认char_traits和分配器类型(关于模板的更多信息,请参阅basic_string)。
4. string类是basic_string模板类的一个实例,它使用char来实例化basic_string模板类,并用char_traits和allocator作为basic_string的默认参数(根于更多的模板信息请参考basic_string)。
5. 注意,这个类独立于所使用的编码来处理字节:如果用来处理多字节或变长字符(如UTF-8)的序列,这个类的所有成员(如长度或大小)以及它的迭代器,将仍然按照字节(而不是实际编码的字符)来操作。
编码的本质就是:内存当中存的全是 0 和 1,那么如果我要把它显示成对应的文字,比如:英文、中文、日文、韩文等等,只能通过编码显示出来
在显示之前,会通过一张表去对照着查,这个表就是需要显示值的映射表,比如ASCII表
总结:
- 1. string是表示字符串的字符串类
- 2. 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。
- 3. string在底层实际是:basic_string模板类的别名,
typedef basic_string<char, char_traits, allocator> string; - 4. 不能操作多字节或者变长字符的序列
在使用string类时,必须包含#include头文件以及using namespace std;
2. string类对象的常见构造
🍉无参构造
构造函数的string类对象,即空字符串
string(); int main() { string s1; return 0; }
🍉带参构造
用常量字符串来构造string对象
string (const char* s); int main() { string s2("hello world"); return 0; }
🍉拷贝构造
拿一个已经存在的对象去初始化另一个未存在的对象(加引用是为了减少拷贝)
string(const string& str); int main() { string s2("hello world"); string s3(s2); return 0; }
🍉用n字符 # 去初始化
string 类对象内会有n个字符#
string(size_t n, char c); int main() { // 用9个¥去初始化s4对象 string s4(9, '¥'); return 0; }
🍉用字符串的前n个字符去初始化
string(const char* s, size_t n); int main() { // 用字符串的前5个字符去初始化s5对象 string s5("hello world", 5); cout << s5 << endl; return 0; }
string存放的是前5个字符
🍉从一个 string 对象的 pos 为值开始,拿 len 个字符去初始化
string(const string& str, size_t pos, size_t len = npos); int main() { string s6("STL_is_so_hard"); // 从s6对象的第0个位置开始(包括第0个位置的字符),往后数7个字符,去初始化s7对象 string s7(s6, 0, 7); cout << s7 << endl; return 0; }
可以看到,len 里面给了个缺省值 nops,那么这个 nops 是多少嘞?
查看文档可以看到:
npos 其实是 string 类里面的一个静态成员变量,给的值是 -1
但是 size_t 是用无符号来修饰的,也就是说无符号的 -1,它的补码全是1,也就是整形的最大值,所以他的意思就是:从 pos 位置开始,往后取 42 亿多个字符。
也就是代表着如果不给 len 值,那么就是有多少取多少
3. string类对象的容量操作
我们需要学习的以下函数接口:
函数名称 | 功能说明 |
size(重点) | 返回字符串有效字符长度 |
length | 返回字符串有效字符长度 |
capacity | 返回空间总大小 |
empty(重点) | 检测字符串释放为空串,是返回true,否则返回false |
clear(重点) | 清空有效字符 |
reserve(重点) | 为字符串预留空间** |
resize(重点) | 将有效字符的个数该成n个,多出的空间用字符c填充 |
🍉size
返回字符串有效长度
size_t size() const; int main() { string s1("hello world"); cout << s1.size() << endl; return 0; }
🍉length
返回字符串有效字符长度(和 size 一样)
size_t length() const; int main() { string s2("hello world"); cout << s2.length() << endl; return 0; }
size 与 length 方法底层实现原理完全相同,引入 size 的原因是为了与其他容器的接口保持一致,所以一般情况下基本都是用 size
🍉max_size
返回字符串可以达到的最大长度
size_t max_size() const; int main() { string s3("hello world"); cout << s3.max_size() << endl; return 0; }
这个实际上就是告诉你,字符串最大能开多长,但是这个函数在实际中并没有什么太大的用处。大家可以试试运行下看看最大能开多大
🍉capacity
返回分配的存储空间容量大小
size_t capacity() const; int main() { string s4("hello world"); cout << s4.capacity() << endl; return 0; }
可以看到,容量的大小默认为 15 ,那么 string类 是怎样扩容的?
可以看到,VS下,capacity第一次是扩2倍,之后是进行1.5倍的扩容
在Linux平台下进行编译会发现又是2倍来进行扩容的,这是因为他们版本不同。VS是PJ版,它是微软进行维护的;Linux下的g++是开源社区维护的。也正是STL并没有明确的扩容机制,所以每个社区或平台上的是不一样的。
另外扩容也是有代价的,需要开新的空间,那么有什么方法能够减少扩容呢?
这就需要我们的 reserve 函数
🍉reserve
为字符串预留空间
void reserve(size_t n = 0); int main() { string s; s.reserve(1000); // 提前开好空间 size_t sz = s.capacity(); cout << "扩容" << endl; cout << "capacity changed:" << sz << endl; // 因为前面开了1000个空间,现在往小的阔,编译器并不会执行,而是保持之前的大小 for (int i = 0; i < 1000; ++i) { s.push_back('c'); if (sz != s.capacity()) { sz = s.capacity(); cout << "往小的阔_capacity changed: " << sz << endl;; } } return 0; }
当使用 reserve 改变当前对象的容量大小,并提前开辟空间时,
注意:
- 当 n 大于对象当前的 capacity 时,将 capacity 扩大到 n 或大于 n。
- 当 n 小于对象当前的 capacity 时,什么也不做。
- 此函数对字符串的 size 没有影响,并且无法更改其内容。
如下测试:
void TestString() { string s("HelloWorld"); cout << s << endl; //HelloWorld cout << s.size() << endl; //10 cout << s.capacity() << endl; //15 cout << endl; //reverse(n)当n大于对象当前的capacity时,将当前对象的capacity扩大为n或大于n s.reserve(20); cout << s << endl; cout << s.size() << endl; //10 cout << s.capacity() << endl; //31 cout << endl; //reverse(n)当n小于对象当前的capacity时,什么也不做 s.reserve(2); cout << s << endl; cout << s.size() << endl; //4 cout << s.capacity() << endl; //31 }
结果如下:
总结:
void reserve (size_t n = 0):为 string 预留空间,不改变有效元素个数,当 reserve 的参数小于 string 的底层空间总大小时,reserver 不会改变容量大小。
🍉resize
将有效字符的个数改成 n 个,多出的空间用字符 c 填充
void resize(size_t n); void resize(size_t n, char c);
使用 resize 改变当前对象的有效字符个数
- 当 n 大于对象当前的 size 时,将 size 扩大到 n,扩大的字符为 c,若 c 未给出,则默认为 0
- 当 n 小于对象当前的 size 时,将 size 缩小到 n
void TestString() { string s1("HelloWorld"); //resize(n)n大于对象当前的size时,将size扩大到n,扩大的字符默认为'\0' s1.resize(20); cout << s1 << endl; //HelloWorld cout << s1.size() << endl; //20 cout << s1.capacity() << endl; //31 cout << endl; string s2("HelloWorld"); //resize(n, char)n大于对象当前的size时,将size扩大到n,扩大的字符为char s2.resize(20, 'x'); cout << s2 << endl; //HelloWorldxxxxxxxxxxxxxxxx cout << s2.size() << endl; //20 cout << s2.capacity() << endl; //31 cout << endl; string s3("HelloWorld"); //resize(n)n小于对象当前的size时,将size缩小到n s3.resize(2); cout << s3 << endl; //He cout << s3.size() << endl; //2 cout << s3.capacity() << endl; //15 }
结果:
相比于 reserve 只是用来开空间,resize 的作用就是 开空间+初始化
关于 reserve 和 resize,它们在 VS 下都不会缩容量。
但是可以看到我们扩容的时候,明明是申请的 20 个空间,为什么打印出来有 31 个呢?这是因为 capacity 的内存对齐
这里可以简单解释一下:
系统去申请内存,需按照整数倍去对齐,就算我们不对齐,那么系统也会自动去对齐的;
比如这里的31,加上最后的 ' \0 ' 就是32,刚好时4个字节
为什么要申请对齐呢?
是因为和内存的效率以及内存碎片有关。
🍉clear
擦除字符串的内容,该字符串变为空字符串(长度为 0 个字符)
使用 clear 删除对象的内容,删除后对象变为空字符串,但是对象的容量不会被清理掉void clear(); int main() { string s1("hello world"); cout << s1 << endl; //HelloWorld cout << s1.size() << endl; //11 cout << s1.capacity() << endl; //15 cout << endl; s1.clear(); cout << s1 << endl; //空 cout << s1.size() << endl; //0 cout << s1.capacity() << endl; //15 return 0; }
clear 只是将 string 对象中有效字符清空,不改变底层空间大小。
🍉empty
判断字符串是否为空,如果字符串长度为 0,则为 true,否则为 false。
bool empty() const; int main() { string s1("hello world"); string s2; // 字符串不为空,返回0 cout << s1.empty() << endl; // 字符串为空,返回1 cout << s2.empty() << endl; return 0; }
4. string类对象的访问及遍历操作
函数名称 | 功能说明 |
operator[ ](中点) | 返回pos位置的字符,const string类对象调用 |
at | 返回pos位置的字符,const string类对象调用 |
begin + end | begin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器 |
rbegin + rend | begin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器 |
范围for | C++11支持更简洁的范围for的新遍历方式 |
🍉[ ] + 下标
string 类 对 [ ] 运算符做了重载,所以我们可以直接使用 [ ] + 下标 访问对象中的元素
并且,重载用的是 引用返回 ,所以我们可以直接通过 [ ] + 下标 来修改对于位置元素
char& operator[] (size_t pos); const char& operator[] (size_t pos) const; int main() { string s1("hello world!"); //1.使用下标访问对象元素 for (size_t i = 0; i < s1.size(); ++i) { cout << s1[i]; } cout << endl; //2.使用下标修改对象元素 for (size_t i = 0; i < s1.size(); ++i) { s1[i] = 'x'; } cout << s1 << endl; }
[ ] + 下标 也有个const版本,加上const就不能用下标进行修改了
🍉at
at 和 operator[ ] 很相似,区别是:当他们出错时编译器会有不同的警告
int main() { string s1 = "hello world"; s1[100]; s1.at(100); return 0; }
operator[ ]:就是 assert() 那种直接中断的报错警告
at :at是抛异常的警告
抛异常是能被 捕获 到的:
int main() { string s1 = "hello world"; try { s1.at(100); } catch(const exception& a) { cout << a.what() << endl; } return 0; }
结果:程序会继续运行,但会出现提示
实际过程中很少用at
🍉begin + end(迭代器 iterator)
begin 获取第一个字符的迭代器 + end 获取最后一个字符下一个位置的迭代器
void TestString() { string s1("hello world!"); //使用迭代器访问对象元素 string::iterator it = s1.begin(); while (it != s1.end()) { cout << *it << " "; ++it; } cout << endl; //使用迭代器修改对象元素 string::iterator aaa = s1.begin(); while (aaa != s1.end()) { *aaa += 1; ++aaa; } cout << s1 << endl; }
对于迭代器,可以理解为像指针一样的东西,或者说就是指针。
🍉rbegin 和 rend(反向迭代器 reserve_iterator)
rbegin 获取之后一个字符的迭代器 + end 获取第一个字符上一个位置的迭代器
rbegin + rend 打印出来的是数组的反串
void TestString() { string s1("hello world!"); //使用迭代器访问对象元素 string::iterator it = s1.begin(); while (it != s1.end()) { cout << *it << " "; ++it; } cout << endl; string::reverse_iterator rit = s1.rbegin(); while (rit != s1.rend()) { cout << *rit << " "; ++rit; } cout << endl; }
🍉const 迭代器、const反迭代器
(const iterator)、(const_reserve_iterator)
我们可以在C++文档中可以看到的begin、end、rbegin、rend中都有个const修饰的类型
以begin为例:
普通对象用普通迭代器,const对象用const迭代器
void Func(const string& s) { //const迭代器的使用,遍历和读容器的数据,不能修改 string::const_iterator it = s.begin(); while (it != s.end()) { cout << *it << " "; ++it; } cout << endl; } int main() { string s1 = "hello world"; Func(s1); return 0; }
对应的还有const反迭代器 const_reserve_iterator
void Func(const string& s) { string::const_reverse_iterator rit = s.rbegin(); while (rit != s.rend()) { cout << *rit << " "; ++rit; } cout << endl; } int main() { string s1 = "hello world"; Func(s1); return 0; }
🍉四种迭代器:
- iterator
- reserve_iterator
- const_iterator
- const_reserve_iterator
这里我们可以直接用 auto 语法来让编译器自动推导
void Func(const string& s)
{
//string::const_reverse_iterator rit = s.rbegin();
//用auto自动推导
auto rit = s.rbegin();
while (rit != s.rend())
{
cout << *rit << " ";
++rit;
}
cout << endl;
}
🍉范围for
范围for 的底层实现就是通过迭代器实现的。
如果我们是通过范围 for 来修改对象的元素,那么接收元素的变量 e 的类型必须是引用类型,否则 e 只是对象元素的拷贝,对 e 的修改不会影响到对象的元素。
void TestString() { string s1("hello world!"); //使用范围for访问对象元素 for (auto e : s1) { cout << e; } cout << endl; //使用范围for修改对象元素 for (auto& e : s1) { e = 'x'; } cout << s1 << endl; }
5. string类对象的操作
点击查看string类文档
对象的操作有很多,主要可以分为:插入、删除、查找、拼接
🍉operator+=
string 类中对 += 运算符进行了重载,重载后的 += 运算符支持 string 类的复合赋值、字符串的复合赋值以及字符复合的赋值
1)插入一个string对象
string& operator+= (const string& str); int main() { string s1; string s2("hello"); s1 += s2; cout << s1 << endl; return 0; }
2)插入一个常量字符串
string& operator+= (const char* s); int main() { string s1("hello"); s1 += "world"; cout << s1 << endl; return 0; }
3)插入一个字符
string& operator+= (char c); int main() { string s1("hello"); s1 += "!"; cout << s1 << endl; return 0; }
总结:
在 string尾插字符时,有以下三种方式
- s.push_back(c)
- s.append(1, c)
- s += ' c '
三种的实现方式差不多,一般情况下 string 类的 += 操作用的比较多,
+= 操作不仅可以连接单个字符,还可以连接字符串
🍉aappend
在字符串后追加一个字符串(基本用不到,因为用 += 更简单)
1)插入一个string对象
string& append(const string& str); int main() { string s1("hello"); string s2("world"); //直接把s2对象拼接到s1的后面 s1.append(s2); cout << s1 << endl; return 0; }
2)插入一个常量字符串
string& append(const char* s); int main() { string s1("hello"); //在s1字符串后面拼接新的字符串 s1.append("world"); cout << s1 << endl; return 0; }
3)插入 n 个字符 c
string& append(size_t n, char c); int main() { string s1("hello"); //将3个字符拼接到s1对象后面 s1.append(3, '!'); cout << s1 << endl; return 0; }
🍉push_back
尾插,将字符 c 追加到字符串末尾,将其长度增加1
void push_back(char c); int main() { string s1; s1.push_back('h'); s1.push_back('e'); s1.push_back('l'); s1.push_back('l'); s1.push_back('o'); cout << s1 << endl; //hello return 0; }
🍉pop_back
尾删,删除string最后一个字符
void pop_back();
int main()
{
string s1("hello");
s1.pop_back();
cout << s1 << endl;
return 0;
}
🍉insert
可以看到insert有很多结构,但并不是每个都会用到,这里我们选几个用的最多的来演示
1)在 pos 位置插入一个常量字符串
string& insert(size_t pos, const string& str); int main() { string s("hello"); // 在第0个位置插入字符串xxx s.insert(3, "xxx"); cout << s << endl; // helxxxlo return 0; }
2)在 pos 位置插入 n 个字符串 c
string& insert(size_t pos, size_t n, char c); int main() { string s("hello"); // 在第3个位置插入5个字符y s.insert(3, 5, 'y'); cout << s << endl; //helyyyyylo return 0; }
3)使用迭代器来进行插入
iterator insert(iterator p, char c); int main() { string s("hello"); // 从begin开始的5个位置插入字符y s.insert(s.begin() + 5, 'y'); cout << s << endl; //helloy return 0; }
begin(),是s对象的第一个位置的指针
🍉erase
从字符串中删除字符
1)从 pos 位置开始,删除 len 个字符
string& erase(size_t pos = 0, size_t len = npos); int main() { string s1("helloworld"); 从s1下标为5的位置开始,往后删除2个字符 s1.erase(5, 2); // 删除 w 和 o cout << s1 << endl; //hellorld return 0; }
注意:len 参数的缺省值是 npos,不给值会将后面的全删掉,删到' /0 '
2)从迭代器的位置删除字符
iterator erase(iterator p); int main() { string s1("helloworld"); //删除s1字符串第一个位置的元素 s1.erase(s1.begin()); cout << s1 << endl; //elloworld return 0; }
3)从迭代器开始的位置,一直删除到迭代器结束的位置
iterator erase(iterator first, iterator last); int main() { string s1("helloworld"); //从begin+5的位置开始,一直删除到end的位置 s1.erase(s1.begin() + 5, s1.end()); cout << s1 << endl; // hello return 0; }
🍉replace
替换字符串的一部分
替换也有很多接口,我们只了解圈出来的两个即可
1)从 pos 位置开始的第 len 个字符替换为一个字符串
string& replace(size_t pos, size_t len, const char* s); int main() { string s1("helloworld"); //将第6个位置开始的4个字符替换为字符串xxxyyy s1.replace(6, 4, "xxxyyy"); cout << s1 << endl; //hellowxxxyyy return 0; }
2)从 pos 位置开始的第 len 个字符替换为 n 个字符 c
string& replace(size_t pos, size_t len, size_t n, char c); int main() { string s1("helloworld"); //将第5个位置开始的1个字符替换为3个字符x s1.replace(5, 1, 3, 'x'); cout << s1 << endl; //helloxxxorld return 0; }
🍉swap
交换两个字符串的值
void swap(string& str);
int main()
{
string s1("hello");
string s2("world");
cout << "交换前" << endl;
cout << "s1:" << s1 << endl;
cout << "s2:" << s2 << endl;
//使用string类的成员函数swap交换s1和s2
s1.swap(s2);
cout << "交换后" << endl;
cout << "s1:" << s1 << endl;
cout << "s2:" << s2 << endl;
return 0;
}
对于string里面的swap我们还要说一点,我们要知道,在我们全局里面也有一个swap函数,他能完成各种类型的交换,当然也包括string
int main()
{
string s1("hello");
string s2("world");
cout << "交换前" << endl;
cout << "s1:" << s1 << endl;
cout << "s2:" << s2 << endl;
//使用全局swap函数交换s1和s2
swap(s1, s2);
cout << "交换后" << endl;
cout << "s1:" << s1 << endl;
cout << "s2:" << s2 << endl;
return 0;
}
那么这两个swap的区别是什么,用哪个实现起来更效果更高呢?
string 里面的 swap 是直接交换指针的指向,而全局的swap是进行拷贝,
所以用 string 里面的 swap 效率高
6.string类运算符函数
🍉operator=
string类中,对=运算符进行了重载,使其能支持string类的赋值、字符串的赋值以及字符的赋值
1)支持string类对象之间的赋值
int main() { string s1; string s2("hello world"); s1 = s2; cout << s1 << endl; return 0; }
2)支持字符串赋值
int main() { string s1; s1 = "hello world"; cout << s1 << endl; return 0; }
🍉operator+=
上面已经有过介绍、这里就不多赘述了
🍉operator+
string类中,对+运算符进行了重载,使其可以进行+string类、+字符串、字符的操作
int main() { string s11; string s12; string s13; string s2("hello"); string s3("world"); char str[] = "C++"; char a = '!'; //string类 + string类 s11 = s2 + s3; //string类 + 字符串,或者:字符串 + string类 s12 = s11 + str; //s12 = str + s11; //string类 + 字符,或者:字符 + string类 s13 = s12 + a; //s13 = a + s12; cout << s11 << endl; cout << s12 << endl; cout << s13 << endl; return 0; }
结果如下:
🍉operator>> 和 operator<<
string也对>>和<<进行了重载,可以再cin和cout中直接使用string类进行输入和输出
int main()
{
string s1;
//流提取
cin >> s1;
//流插入
cout << s1 << endl;
return 0;
}
🍉relational operators (string)
string还对其他一系列操作符进行了重载,分别是:==,!=,<=,>=,<,>