文章目录
前言
计算机的两大核心任务是数值计算和字符处理,分别对应数学语言和自然语言,各种计算机编程语言也分别为数值计算和字符处理提供了丰富的库函数。前篇博文从字符编码和字符类型开始,介绍了C 语言常用的字符串和字符数组操作函数,C++ 由于兼容C 语言的设计,也完全支持前文介绍的字符类型识别、字母大小写转换、数值格式与字符串格式间的相互转换、字符串或字符数组的比较、拼接、复制替换、查找匹配、分割获取子串等操作。
但以字符数组的形式操作字符串也存在一些局限,比如为字符串申请的数组长度都是定长的,如果要拼接或者扩充字符串,可能原来的数组空间不够用,需要开发者重新申请更大的空间以容纳更多的字符,而且还容易发生数组访问越界的问题。回想下前面介绍过的C++ STL(Standard Template Library),不管是vector、deque、list、forward_list 这些线性的序列式容器(包括为应对特殊需求,基于这些序列式容器实现的stack、queue、priority_queue等),还是set、map、unordered_set、unordered_map 这些有序或无序的关联式容器,都支持自动变长特性,而且提供了丰富的操作函数,方便我们直接处理和操作STL 内的元素,我们能否借助C++ STL 容器来处理字符串呢?
最适合用来处理字符信息的容器是vector,既支持变长特性,又支持随机访问,也能高效的顺序遍历容器内各字符。但vector<char> 更侧重对容器内单个元素也即字符的处理和操作,如果涉及到子字符串整体的操作,比如子串查找、子串匹配、字符串格式与数值格式转换等,vector<char> 并没有提供对应的操作函数,处理子串查找匹配及转换问题就显得不方便了。在程序开发中,对字符串的处理是很常用的操作,因此C++ 为字符串处理专门设计了String 类库,除了提供字符串的常用操作函数,还尽可能提高字符串的处理效率。String class 与vector<char> 的主要设计目标分别如下:
- Vector<char>:首要目标是处理和操作容器内的元素,而非容器整体,因此实现时通常会考虑“容器元素的操作行为”的最优化。
- String class:主要是把整个字符串视为整体来处理和操作,因此实现时通常会考虑“整个字符串的赋值和传递”的最优化。
不同的设计目标往往导致完全不同的实现手法,比如String 为了提高字符串的赋值和传递效率,通常采用reference 语义,而Vector<char> 一般采用value 语义。那么,String 算是一种STL 容器么?
String 支持vector 提供的大部分操作,同时还新增了一些字符串处理的操作,而且string 和vector<char> 提供的成员函数名也基本一样,可以像使用STL 容器那样使用vector,但string 并不算STL 容器。因为STL 标准模板库的核心是泛型,也即STL 作为容器可以通过模板支持任意数据类型,而且使用STL 容器时都要按照模板要求声明容器元素的数据类型,比如vector<char>、vector<int> 等。
C++ 相比C 语言增强的部分除了面向对象编程方法外(C 语言也可以实现面向对象的特征,可参考博文:C语言对象化模型),另一个就是泛型编程(C 语言也支持有限的泛型编程,可参考博文:qsort与bsearch 比较函数),STL 正是泛型编程的绝佳实现。String 则是已经限定了操作处理的数据类型为字符类型,而且C++ 已经为其预设了数据类型,我们使用string 时并不需要再声明数据类型,因此string 并不算STL 容器,但string 可以作为任何一种STL 容器的元素被STL 操作处理。
一、C++ String支持的字符操作
String 跟vector 类似,也支持动态调整大小,在初始化字符串时会多申请一部分空间(我测试初始化一个空字符串,查询其容量为15),为以后的变长预留位置。当预留空间用完之后,若要继续插入字符元素就需要再找一块儿更大的连续地址空间(一般为原地址空间的两倍大小),将原地址空间的数据搬移到新的地址空间(字符串需要经常顺序遍历各字符,连续地址空间可以保证遍历效率),继续支持变长特性,这个操作称为变长数组的扩容。由于扩容后,原数据元素搬移导致地址变更,对这些元素的地址引用操作比如引用、指针、迭代器等都将失去作用,且扩容操作比较耗时。要想缓解扩容导致的问题,可以在创建变长数组时,根据我们的需求预留足够的空间(string 提供了reserve 函数预留一定空间),但预留太多空间会导致空间浪费,这个取舍需要平衡。
C++11 string 和vector 的类模板定义对比如下(第一个参数charT 为字符类型;带默认值的第二个参数Traits 指定字符类型上操作的特性类,规定如何复制、比较、查找、转换字符等;带默认值的第三个参数Allocator 定义了string class 所采取的内存模型):
// <vector>
namespace std{
template <typename T,
typename Allocator = allocator<T>>
class vector;
}
// <string>
namespace std{
template <typename CharT,
typename Traits = char_traits<CharT>,
typename Allocator = allocator<CharT>>
class basic_string;
typedef basic_string<char> string;
typedef basic_string<wchar_t> wstring;
typedef basic_string<char16_t> u16string; //(C++11)
typedef basic_string<char32_t> u32string; //(C++11)
}
从上面的类模板定义可以看出,vector 使用时需要声明模板数据类型T,string 则是已经被声明了模板数据类型char,使用时不需要再额外声明数据类型,如果想使用宽字符类型,直接使用wstring、u16string、u32string 即可。本文主要介绍最常用的string 类型,其支持的常用操作如下:
String class 提供的上述字符串操作函数,其中很多往往具有数个重载版本,不同重载版本支持的参数类型要求不同,下面给出不同操作函数支持的实参类型:
为方便和vector<char> 对比,我们按照本文前言部分提供的STL 容器成员函数对照表中vector 支持的操作顺序介绍string 支持的操作,中间会介绍string 相比vector 独有的操作函数。
1.1 构造析构与赋值操作(创建、复制、销毁)
String class 支持的构造与析构函数如下:
String 构造函数允许以整个字符串、字符串的一部分、整个C-string(也即带空终止符的字符数组)、字符数组中的部分字符、多个字符、迭代器区间内的字符、初始化列表等形式来初始化一个字符串,但不能以单个字符初始化一个字符串,比如下面这样:
#include<string>
std::string s('x'); //Error, 不能以单个字符初始化一个字符串
std::string s(1, 'x'); //Ok, 允许使用指定数量个字符初始化一个字符串
std::string s({
'x'}); //Ok, 允许使用初始化列表初始化一个字符串
String class 支持的赋值操作如下:
表达式 | 效果 |
---|---|
s = str s.assign(str) |
将字符串str 的全部元素以copy assignment 方式赋值给字符串s |
s = rvStr s.assign(rvStr) |
将字符串rvalue rvStr 的所有元素以move assignment 方式搬移给字符串s |
s.assign(str, stridx, strnum) | 将str 内从索引stridx 开始最多strnum 个字符赋值给字符串s |
s = cstr s.assign(cstr) |
将C-string cstr 的内容赋值给字符串s,不包括’\0’ |
s.assign(chars, charslen) | 将字符数组chars 内的charslen 个字符赋值给字符串s |
s = c | 将单个字符c 赋值给字符串s |
s.assign(num, c) | 将num 个字符c 赋值给字符串s |
s.assign(beg, end) | 将迭代器[beg, end) 区间内的所有字符赋值给字符串s |
s = initlist s.assign(initlist) |
将初值列表initlist 内的所有字符赋值给字符串s |
s.swap(str) swap(s, str) |
用来交互字符串s 和str 的内容,具有常量复杂度,可使用该操作更快的实现string object 间的赋值 |
#include<string>
std::string s;
s = ' '; //Ok, 允许将单个空格字符赋值给字符串s
s = "hello, world"; //Ok, 允许将C-string cstr 的内容赋值给字符串s
std::string str("world wide web");
s.assign(str, 6, 4); //Ok, 允许将字符串str 中的“wide” 4个字母赋值给字符串s
s.assign(str, 6, std::string::npos); //Ok, 允许将字符串str 中从索引为6的字符开始到末尾的所以字符赋值给字符串s
上例中的std::string::npos 被定义为-1,也可表示size_t 无符号类型的最大值,这里取后一种意思表示取最多size_t 能表示的最大值个字符,也即一直取到字符串的末尾。在后面将要介绍的字符串查找操作中,可使用std::string::npos 取前一种意思,表示没有找到期望的内容。
static const size_type npos = -1;
1.2 元素访问与容量操作
String class支持的容量操作如下:
表达式 | 效果 |
---|---|
s.empty() | 返回字符串s是否为空,等价于s.size() == 0 但可能更快 |
s.size() s.length() |
返回字符串s 的现有字符数,两个函数等效 |
s.max_size() | 返回字符串s 最多能够包含的字符数,返回值一般是索引类型的最大值减1 |
s.capacity() | 返回字符串s 扩容或重新分配内存之前,所能包含的最大字符数 |
s.reserve() s.reserve(size_t num) |
为字符串s 保留至少能容纳num 个字符的内存空间,若未指定num 或num < s.size()则相当于s.shrink_to_fit() |
s.shrink_to_fit() | 降低字符串s 的内存占用以符合当前字符数,是一个“非强制性合身缩减”请求,相当于s.reserve() |
s.resize(size_t num) s.resize(size_t num, char c) |
将字符串 |