前言
为什么学习 string 类呢?
C语言中是没有字符串类型的,字符串是以 ‘\0’ 结尾的一些字符的集合(即字符数组),为了操作方便,C 标准库 <string.h> 中提供了一些 str 系列的库函数,但是这些库函数与字符串是分离开的,不太符合面向对象 OOP 的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。
C++ STL string 是对字符串进行管理的类。实际上就是一个管理字符数组的顺序表。
在常规工作中,为了简单、方便、快捷,基本都使用 string 类,很少有人去使用 C 语言库中的字符串操作函数。
一、STL - string 的介绍
文档介绍:string - C++ Reference (cplusplus.com)
- 字符串是表示字符序列的对象
- 标准的字符串类提供了对此类对象的支持,其接口类似于标准字符容器的接口,但添加了专门用于操作单字节字符字符串的设计特性。
- string 类是使用 char (即作为它的字符类型,使用它的默认 char_traits 和分配器类型(关于模板的更多信息,请参阅 basic_string)。
- string 类是 basic_string 模板类的一个实例,它使用 char 来实例化 basic_string 模板类,并用 char_traits 和 allocator 作为 basic_string 的默认参数(关于更多的模板信息请参考 basic_string)。
- 注意,这个类独立于所使用的编码来处理字节:如果用来处理多字节或变长字符(如UTF-8)的序列,这个类的所有成员(如长度或大小)以及它的迭代器,将仍然按照字节(而不是实际编码的字符)来操作。
总结:
- string 是表示字符串的字符串类
- 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作 string 的常规操作。
- string 在底层实际是 basic_string 模板类的别名
typedef basic_string<char> string;
- 不能操作多字节或者变长字符的序列。
补充:
编码:计算机中只存储二进制数(0 / 1),那如何去表示文字呢,需要制定对应的编码表,规定用哪些二进制数字表示(映射)哪个符号,当然每个人都可以约定自己的一套,而大家如果要想互相通信而不造成混乱,那么大家就必须使用相同的编码规则,比如美国有关的标准化组织就出台了 ASCII 码表(基于拉丁字母的一套电脑编码系统,主要用于显示现代英语和其他西欧语言)
ASCII 码使用指定的 7 位或 8 位二进制数组合来表示 128 或 256 种可能的字符,到目前为止共定义了 128 个字符。
所以早期的计算机中只能表示英文,不能表示其它国家的文字,当全世界各个国家都开始使用计算机了,就需要建立出对应语言的编码表。
UTF - 8 是对不同范围的字符使用不同长度的编码,这样能够适应不同的语言,比如有些语言单字节编码就够了,有些语言需要多字节编码才够。
![]()
还有其它编码表,GBK码(对多达2万多的简繁汉字进行了编码,简体版的Win95和Win98都是使用GBK作系统内码)等等。
注意:使用 string 类需要包含头文件
二、string 的使用(常用接口介绍)
string 类的成员函数(接口)非常的多,我们学习一些常用的就行了,其它不常用的,需要时去查文档就好了。
2.1 常见构造
构造函数:
string(); // 默认构造,构造空的string类对象,即空字符串"" ⭐
string (const string& str); // 拷贝构造,用已有的string类对象去构造string类对象 ⭐
string (const char* s); // 用c-string来构造string类对象 ⭐
string (const char* s, size_t n); // 用c-string前n个字符来构造string类对象
string (size_t n, char c); // 用n个c字符来构造string对象
template <class InputIterator> // 用迭代器[first,last)范围内的字符序列构造string类对象
string (InputIterator first, InputIterator last);
👉Example:
// string constructor
#include <iostream>
#include <string>
int main()
{
std::string s0 ("Initial string");
std::string s1; // s1: ""
std::string s2 (s0); // s2: Initial string
std::string s4 ("A character sequence"); // s4: A character sequence
std::string s5 ("Another character sequence", 12); // s5: Another char
std::string s6 (10, 'x'); // s6: xxxxxxxxxx
std::string s7 (s0.begin(), s0.begin() + 7); // s7: Initial
return 0;
}
👉思考:空串是什么都没有吗,存储空间为空吗?
2.2 容量操作
函数名称 | 功能说明 |
---|---|
size⭐ | 返回字符串有效字符长度(为了统一设计,所有容器都是用 size 表示有效元素个数) |
length | 返回字符串有效字符长度(这是早期提供的接口) |
resize⭐ | 将字符串大小调整为 n 个有效字符的长度 |
capacity | 返回有效字符的最大容量(即已分配 size 的大小) |
reserve⭐ | 更改容量(capacity)的大小 |
clear⭐ | 清空字符串的内容,变为空字符串(size 变为 0,不改变 capacity 的大小) |
empty⭐ | 检测字符串是否为空串,是返回 true,否则返回 false |
👉Example1:
resize 函数的两种重载形式:
void resize (size_t n);
void resize (size_t n, char c);
void Test1()
{
string s("hello");
// 将s中有效字符个数增加到10个,多出位置用'a'进行填充
// "helloaaaaa"
s.resize(10, 'a');
cout << s.size() << endl; // 10
cout << s.capacity() << endl; // 15
cout << s << endl; // s: "helloaaaaa"
// 将s中有效字符个数增加到20个,多出位置用缺省值'\0'进行填充
// 如果resize参数大于原有 capacity 大小,会进行增容
// "helloaaaaa\0\0\0\0\0\0\0\0\0\0"
s.resize(20);
cout << s.size() << endl; // 15
cout << s.capacity() << endl; // 31
cout << s << endl; // s: "helloaaaaa"
// 如果resize参数x原有 capacity 大小
// 将s中有效字符个数缩小到2个
// "he"
s.resize(2);
cout << s.size() << endl; // 2
cout << s.capacity() << endl; // 31
cout << s << endl; // s: "he"
// 将s的内容清空,注意清空时只是将size置0,不改变capacity的大小
s.clear();
cout << s.size() << endl; // 0
cout << s.capacity() << endl; // 31
cout << s << endl; // s: ""
return 0;
}
👉Example2:
reserve 函数介绍:
void reserve (size_t n = 0);
-
如果 n 大于当前字符串容量,则该函数使容器将其容量增加到 n 个字符(或更大)。
-
在所有其他情况下,缩小字符串容量被视为非绑定请求:容器可以自由实现优化,但要保留容量大于 n 的字符串。
-
此函数对字符串长度没有影响,并且不能更改其内容。
void Test2()
{
string s("hellohellohello");
// 如果reserve参数大于原有 capacity 大小,会进行增容
s.reserve(20);
cout << s.size() << endl; // 15
cout << s.capacity() << endl; // 31
// 测试reserve参数小于原有 capacity 大小,是否会将空间缩小呢?
// VS2019下,如果字符串有效长度size大于参数10,不会缩小,如果字符串长度小于参数10,会缩小
// 当然,这个也和编译器平台有关系
s.reserve(10);
cout << s.size() << endl; // 15
cout << s.capacity() << endl; // 31(VS2019)
}
测试:reserve 是如何进行增容呢?
void Test3()
{
string s;
cout << "initial value: " << s.capacity() << endl;
size_t sz = s.capacity();
for (size_t i = 0; i < 500; i++)
{
s.push_back('a');
if (s.capacity() != sz)
{
sz = s.capacity();
cout << "capacity changed: " << sz << endl;
}
}
}
经过测试,VS2019 下大概是 1.5 倍增容:
![]()
Linux g++下是 2 倍增容:
![]()
思考:resize 和 reserve 的意义在哪里呢?
reserve 的作用:如果知道需要多大的空间,可以利用 reserve 提前一次性把空间开好,避免增容带来的开销。
resize 的作用:既要开好空间,还要对这些空间初始化,就可以使用 resize
2.3 访问操作
⭐有了这个运算符重载,我们就可以像使用数组一样去使用 string 类对象。
函数名称 | 功能说明 |
---|---|
operator[]⭐ | 返回对字符串中 pos 位置处的字符的引用(string 类对象支持随机访问)(一般物理地址是连续的才支持) |
operator[] 函数的两种重载形式:
char& operator[] (size_t pos);
const char& operator[] (size_t pos) const;
operator[] 函数会检查越界(pos 必须小于 size)
2.4 迭代器及遍历操作
⭐所有容器都有迭代器,迭代器提供了用统一类似的方式去访问容器。
函数名称 | 功能说明 |
---|---|
begin(iterator) | 返回指向第一个有效字符的迭代器 |
end | 返回指向字符串末尾字符的迭代器(即最后一个有效字符的下一个位置) |
rbegin(reverse_iterator) | 反向迭代器(可以反向遍历对象) |
rend | 反向迭代器 |
范围 for | C++11支持更简洁的范围 for 的新遍历方式(底层其实是被替换成迭代器,所以支持迭代器就支持范围 for) |
迭代器有两个版本:普通迭代器和 const 迭代器
iterator begin(); // 可读可写
const_iterator begin() const; // 只读
👉Example:
void test(const std::string& s) {
// const对象必须要用const迭代器
std::string::const_iterator it = s.begin();
while (it != s.end()) {
std::cout << *it;
it++;
}
}
int main()
{
std::string s1;
std::string s2("hello");
// for+operator[]遍历
for (size_t i = 0; i < s.size(); ++i)
cout << s2[i] << endl;
// 正向迭代器遍历
// 注意:这里不建议写成it < s2.end(),比如链式结构的容器,就没法用了
// 所以统一写成 it != s2.end()
std::string::iterator it = s2.begin();
while (it != s2.end()) {
std::cout << *it;
it++;
}
// 反向迭代器遍历
for (std::string::reverse_iterator rit = s2.rbegin(); rit != s2.rend(); ++rit)
std::cout << *rit;
for (auto rit = s2.rbegin(); rit != s2.