0%

STLstring

体会string 的强大。现今世界的数据处理大部分就是字符串处理,作为使用STL的程序员自然应该去了解 string 这个特殊容器。

这里就不同篇介绍string 了,只给出常用部分函数接口的内部实现。在应用string 的时候,只需要包含头文件 就行了

C++ 标准中string 类的特性与 vector<> 很相似,有以下几点区别:string 总会在末尾存放NULL 字符;string 需要借助 char_traits<>::assign,char_traits<>::copy 和char_traits<>::move 来复制元素,另外string 还额外提供了一些接口函数。

string(basic_string)类空间的分配(构造和析构)和vector<> 很相似,这里就不说了。这里只介绍几个重要函数

1
2
3
4
5
6
7
8
9
10
11
// ------------------------------------------------------------
// Class basic_string.
// Class invariants:
// (1) [start, finish) is a valid range.
// (2) Each iterator in [start, finish) points to a valid object
// of type value_type.
// (3) *finish is a valid object of type value_type; in particular,
// it is value_type().
// (4) [finish + 1, end_of_storage) is a valid range.
// (5) Each iterator in [finish + 1, end_of_storage) points to
// unininitialized memory.

一、string 赋值和追加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
//basic_string 赋值运算符重载
//对象也是string类型
basic_string& operator=(const basic_string& __s) {
if (&__s != this)//不允许自我赋值
assign(__s.begin(), __s.end());
return *this;
}
//参数是char*类型,将字符串__s 赋值给string类型
basic_string& operator=(const _CharT* __s)
{ return assign(__s, __s + _Traits::length(__s)); }
//单一字符赋值
basic_string& operator=(_CharT __c)
{ return assign(static_cast<size_type>(1), __c); }
/*赋值操作:拷贝指定区间的字符[__f,__l)*/
/*assign函数要区别是调用string的还是_Traits::assign*/
template <class _CharT, class _Traits, class _Alloc>
basic_string<_CharT, _Traits, _Alloc>&
basic_string<_CharT, _Traits, _Alloc>::assign(const _CharT* __f,const _CharT* __l)
{
const ptrdiff_t __n = __l - __f;//待拷贝字符的大小
//如果待赋值的字符数小于等于原有的
if (static_cast<size_type>(__n) <= size())
{
_Traits::copy(_M_start, __f, __n);//拷贝目标字符串
erase(_M_start + __n, _M_finish);//擦出原有空间大于的部分字符
}
//如果拷贝的字符数大于原有的字符数
else {
_Traits::copy(_M_start, __f, size());//先拷贝原有大小数目的字符
append(__f + size(), __l);//然后追加剩下的字符
}
return *this;
}
/*拷贝指定数目—__n的单一字符*/
template <class _CharT, class _Traits, class _Alloc>
basic_string<_CharT, _Traits, _Alloc>&
basic_string<_CharT, _Traits, _Alloc>::assign(size_type __n, _CharT __c)
{
//数目小于原有字符个数,就需要擦除多余的字符
if (__n <= size()) {
_Traits::assign(_M_start, __n, __c);//这里调用的char_traits<>::assign
erase(_M_start + __n, _M_finish);
}
//大于,则拷贝原有的之后,再追加多余的
else {
_Traits::assign(_M_start, size(), __c);
append(__n - size(), __c);
}
return *this;
}

追加append()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
/*末尾追加[__first, __last)区间字符*/
template <class _Tp, class _Traits, class _Alloc>
basic_string<_Tp, _Traits, _Alloc>&
basic_string<_Tp, _Traits, _Alloc>::append(const _Tp* __first, const _Tp* __last)
{
if (__first != __last) {
const size_type __old_size = size();//记录现有元数个
ptrdiff_t __n = __last - __first;//追加的字符个数
//长度检查和异常处理
if (__n > max_size() || __old_size > max_size() - __n)
_M_throw_length_error();
//预分配的内存空间不够
if (__old_size + __n > capacity()) {
const size_type __len = __old_size + max(__old_size, (size_t)__n) + 1;//新分配长度
pointer __new_start = _M_allocate(__len);//分配内存
pointer __new_finish = __new_start;
//初始化新分配的内存区域,异常处理
__STL_TRY{
__new_finish = uninitialized_copy(_M_start, _M_finish, __new_start);//原有的
__new_finish = uninitialized_copy(__first, __last, __new_finish);//追加的
_M_construct_null(__new_finish);
}
__STL_UNWIND((destroy(__new_start, __new_finish),
_M_deallocate(__new_start, __len)));
//销毁原有的元素
destroy(_M_start, _M_finish + 1);
_M_deallocate_block();//释放内存
_M_start = __new_start;//重新调整水平位
_M_finish = __new_finish;
_M_end_of_storage = __new_start + __len;
}
//原分配的内存满足需求,无需分配新内存区域
else {
const _Tp* __f1 = __first;
++__f1;//后移
uninitialized_copy(__f1, __last, _M_finish + 1);//拷贝除首位置的其余元素
__STL_TRY{
_M_construct_null(_M_finish + __n);
}
__STL_UNWIND(destroy(_M_finish + 1, _M_finish + __n));
//这里拷贝首位置元素
_Traits::assign(*_M_finish, *__first);//调用char_traits<>::assign,单一赋值*_M_finish = *__first
_M_finish += __n;//调整位置
}
}
return *this;
}
/*末尾追加__n个数目的字符__c*/
template <class _CharT, class _Traits, class _Alloc>
basic_string<_CharT, _Traits, _Alloc>&
basic_string<_CharT, _Traits, _Alloc>::append(size_type __n, _CharT __c)
{
//max_size():容器允许的最大元素数(很大),size():目前空间元素个数
//追加数目大于允许的最大数或大于剩余空间,则抛出长度异常
if (__n > max_size() || size() > max_size() - __n)
_M_throw_length_error();
//capacity():预分配的内存空间,参见 http://blog.csdn.net/wenqian1991/article/details/19540385
//已有元素+追加元素 >预分配的空间,则需要重新分配内存空间
if (size() + __n > capacity())
reserve(size() + max(size(), __n));
//初始化
if (__n > 0)
{
//http://blog.csdn.net/wenqian1991/article/details/19676159
uninitialized_fill_n(_M_finish + 1, __n - 1, __c);
//捕捉异常
__STL_TRY{
_M_construct_null(_M_finish + __n);
}
__STL_UNWIND(destroy(_M_finish + 1, _M_finish + __n));
_Traits::assign(*_M_finish, __c);//赋值
_M_finish += __n;//调整目前使用空间的尾
}
return *this;
}

有了前面 assign 和 append 两个函数,下面的接口基本上都是基于上面两个函数而来的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//string += 运算符重载,实际是调用append函数
basic_string& operator+=(const basic_string& __s) { return append(__s); }
basic_string& operator+=(const _CharT* __s) { return append(__s); }
//push_back 内部也是转调用append()
basic_string& operator+=(_CharT __c) { push_back(__c); return *this; }
//追加string类对象
basic_string& append(const basic_string& __s)
{
return append(__s.begin(), __s.end());
}
//追加一个string,从pos位置开始的最多n个字符
basic_string& append(const basic_string& __s, size_type __pos, size_type __n)
{
//边界异常
if (__pos > __s.size())
_M_throw_out_of_range();
return append(__s.begin() + __pos,
__s.begin() + __pos + min(__n, __s.size() - __pos));
}
//追加n个字符s
basic_string& append(const _CharT* __s, size_type __n)
{
return append(__s, __s + __n);
}
//追加一个字符数组
basic_string& append(const _CharT* __s)
{
return append(__s, __s + _Traits::length(__s));
}
/*下面两个函数类似于vector<> 中的push_back 和 pop_back*/
void push_back(_CharT __c)
{
if (_M_finish + 1 == _M_end_of_storage)
reserve(size() + max(size(), static_cast<size_type>(1)));
_M_construct_null(_M_finish + 1);//null调整
_Traits::assign(*_M_finish, __c);//最后一个元素的后一个位置赋值
++_M_finish;
}
void pop_back() {
_Traits::assign(*(_M_finish - 1), _M_null());//最后一个元素置位NULL
destroy(_M_finish);//销毁原有的
--_M_finish;
}

二、插入函数 insert

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/*插入函数:指定位置position插入[first,last)字符*/
template <class _CharT, class _Traits, class _Alloc>
void basic_string<_CharT, _Traits, _Alloc>::insert(iterator __position, const _CharT* __first,
const _CharT* __last)
{
if (__first != __last) {
const ptrdiff_t __n = __last - __first;//插入字符个数
if (_M_end_of_storage - _M_finish >= __n + 1) //剩余内存空间是否满足需求
{
const ptrdiff_t __elems_after = _M_finish - __position;//指定插入位置之后字符的个数
iterator __old_finish = _M_finish;//记录原始的末尾位置
/*随手画个草图,就很清楚的知道数据的拷贝过程了*/
//这里有两个数据拷贝过程
if (__elems_after >= __n) //如果插入位置之后的字符个数大于等于插入字符的个数
{
//将末尾__n个字符拷贝到末尾开始的地方,即将末尾__n个字符后移(复制)__n个位置
uninitialized_copy((_M_finish - __n) + 1, _M_finish + 1, _M_finish + 1);

_M_finish += __n;//调整末尾位
//将position后面(__elems_after - __n) + 1个字符拷贝到position+n位置
//这样在position后面预留出n个位置给待插入字符
_Traits::move(__position + __n, __position, (__elems_after - __n) + 1);
_M_copy(__first, __last, __position);//将待插入字符复制过去
}
//如果插入位置之后的字符个数小于插入字符个数
//这里也是两次复制操作
else {
const _CharT* __mid = __first;
advance(__mid, __elems_after + 1);//iterator加减,将mid迭代器位置移动elems_after+1
uninitialized_copy(__mid, __last, _M_finish + 1);//[mid,last) -> [M_finish+1, )
//待插入字符数组第一次复制进去,后面部分
_M_finish += __n - __elems_after;//第一次调整位置
//这次正好拷贝__n-__elems_after个字符
__STL_TRY{
uninitialized_copy(__position, __old_finish + 1, _M_finish);//position后面的元素拷贝到末尾之后
_M_finish += __elems_after;//第二次调整位置,正好是__n个字符大小
}
__STL_UNWIND((destroy(__old_finish + 1, _M_finish), _M_finish = __old_finish));
_M_copy(__first, __mid, __position);//待插入字符数组第二次复制进去,没复制的前面部分
}
}
else//剩余空间不满足插入需求,则需要重新分配空间
{
const size_type __old_size = size();
//新分配空间大小
const size_type __len = __old_size + max(__old_size, static_cast<size_type>(__n)) + 1;
pointer __new_start = _M_allocate(__len);//分配
pointer __new_finish = __new_start;
//数据的拷贝初始化,就是将原有数据和待插入数据按照对应位置拷贝过去
__STL_TRY{
__new_finish = uninitialized_copy(_M_start, __position, __new_start);
__new_finish = uninitialized_copy(__first, __last, __new_finish);
__new_finish = uninitialized_copy(__position, _M_finish, __new_finish);
_M_construct_null(__new_finish);//字符串尾端特殊处理
}
__STL_UNWIND((destroy(__new_start, __new_finish), _M_deallocate(__new_start, __len)));
destroy(_M_start, _M_finish + 1);//销毁原来空间
_M_deallocate_block();
_M_start = __new_start;//调整位置
_M_finish = __new_finish;
_M_end_of_storage = __new_start + __len;
}
}
}

\三、删除 erase**

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
basic_string& erase(size_type __pos = 0, size_type __n = npos) {
if (__pos > size())
_M_throw_out_of_range();
erase(_M_start + __pos, _M_start + __pos + min(__n, size() - __pos));
return *this;
}
iterator erase(iterator __position) {
// The move includes the terminating null.
_Traits::move(__position, __position + 1, _M_finish - __position);
destroy(_M_finish);
--_M_finish;
return __position;
}
iterator erase(iterator __first, iterator __last) {
if (__first != __last) {
// The move includes the terminating null.
_Traits::move(__first, __last, (_M_finish - __last) + 1);
const iterator __new_finish = _M_finish - (__last - __first);
destroy(__new_finish + 1, _M_finish + 1);
_M_finish = __new_finish;
}
return __first;
}

需要注意的是线性存储方式的容器中,erase 一个迭代器所指元素或所指区域元素的方法是将其erase 位置后面的元素拷贝到删除位置处,然后容器最后的元素(该元素其实是容器末端的重复元素),然后返回删除元素的下一个位置,其实迭代器还是指向原来的位置,只不过该位置上的元素变成了删除元素的下一个元素。

窥探string 的插入函数 insert 函数,你会发现将待插入元素插入指定位置之后,其指定位置后的元素也是整个拷贝到后续位置,事实上内部怎么操作的,我们不感兴趣,但我们需要了解的是经过删除和插入操作之后原指定位置的迭代器如何变化。

对于迭代器要理解这一点,对于线性存储方式的容器,如 vector、deque 尤其得注意这一点,贸然erase元素,处理不当极易造成程序崩溃。

建议这样处理:

1
2
3
4
5
6
7
for (pos = str.begin(); pos != str.end();)
{
if ((a[*pos - 'a']) == min)
pos = str.erase(pos);
else
++pos;
}

string 的其余部分函数这里就不介绍了,具体可以查看 STL 源码 string,另外string 一些接口函数内部会调用 char_traits<> 的成员函数。