string 是最常用的STL容器。
一、内部实现
C++程序员编码过程中经常会使用string(wstring)类,你是否思考过它的内部实现细节。比如这个类的迭代器是如何实现的?对象占多少字节的内存空间?内部有没有虚函数?内存是如何分配的?构造和析构的成本有多大?笔者综合这两天阅读的源代码及个人理解简要介绍之,错误的地方望读者指出。
首先看看string和wstring类的定义:
- typedef basic_string<char, char_traits<char>, allocator<char> > string;
- typedef basic_string<wchar_t, char_traits<wchar_t> allocator<wchar_t> > wstring;
从这个定义可以看出string和wstring分别是模板类basic_string对char和wchar_t的特化。
再看看basic_string类的继承关系(类方法未列出):
最顶层的类是_Container_base,它也是STL容器的基类,Debug下包含一个_Iterator_base*的成员,指向容器的最开始的元素,这样就能遍历容器了,并定义了了两个函数
- void _Orphan_all() const; // orphan all iterators
- void _Swap_all(_Container_base_secure&) const; // swaps all iterators
Release下_Container_base只是一个空的类。
_String_base类没有数据成员,只定义了异常处理的三个函数:
- static void _Xlen(); // report a length_error
- static void _Xran(); // report an out_of_range error
- static void _Xinvarg();
_String_val包含一个alloctor的对象,这个类也非常简单,除了构造函数没有定义其它函数。
上面三个基类都定义得很简单,而basic_string类的实现非常复杂。不过它的设计和大多数标准库一样,把复杂的功能分成几部分去实现,充分体现了模块的低耦合。
迭代器有关的操作交给_String_iterator类去实现,元素相关的操作交给char_traits类去实现,内存分配交给allocator类去实现。
_String_iterator类的继承关系如下图:
这个类实现了迭代器的通用操作,比如:
- reference operator*() const;
- pointer operator->() const
- _String_iterator & operator++()
- _String_iterator operator++(int)
- _String_iterator& operator--()
- _String_iterator operator--(int)
- _String_iterator& operator+=(difference_type _Off)
- _String_iterator operator+(difference_type _Off) const
- _String_iterator& operator-=(difference_type _Off)
- _String_iterator operator-(difference_type _Off) const
- difference_type operator-(const _Mybase& _Right) const
- reference operator[](difference_type _Off) const
有了迭代器的实现,就可以很方便的使用算法库里面的函数了,比如将所有字符转换为小写:
- string s("Hello String");
- transform(s.begin(), s.end(), s.begin(), tolower);
char_traits类图如下:
这个类定义了字符的赋值,拷贝,比较等操作,如果有特殊需求也可以重新定义这个类。
allocator类图如下:
这个类使用new和delete完成内存的分配与释放等操作。你也可以定义自己的allocator,msdn上有介绍哪些方法是必须定义的。
再看看basic_string类的数据成员:
_Mysize表示实际的元素个数,初始值为0;
_Myres表示当前可以存储的最大元素个数(超过这个大小就要重新分配内存),初始值是_BUF_SIZE-1;
_BUF_SIZE是一个enum类型:
- enum
- { // length of internal buffer, [1, 16]
- _BUF_SIZE = 16 / sizeof (_Elem) < 1 ? 1: 16 / sizeof(_Elem)
- };
从这个定义可以得出,针对char和wchar_t它的值分别是16和8。
_Bxty是一个union:
- union _Bxty
- { // storage for small buffer or pointer to larger one
- _Elem _Buf[_BUF_SIZE];
- _Elem *_Ptr;
- } _Bx;
为什么要那样定义_Bxty呢,看下面这段代码:
- _Elem * _Myptr()
- { // determine current pointer to buffer for mutable string
- return (_BUF_SIZE <= _Myres ? _Bx._Ptr : _Bx._Buf);
- }
这个函数返回basic_string内部的元素指针(c_str函数就是调用这个函数)。
所以当元素个数小于_BUF_SIZE时不用分配内存,直接使用_Buf数组,_Myptr返回_Buf。否则就要分配内存了,_Myptr返回_Ptr。
不过内存分配策略又是怎样的呢?看下面这段代码:
- void _Copy(size_type _Newsize, size_type _Oldlen)
- { // copy _Oldlen elements to newly allocated buffer
- size_type _Newres = _Newsize | _ALLOC_MASK;
- if (max_size() < _Newres)
- _Newres = _Newsize; // undo roundup if too big
- else if (_Newres / 3 < _Myres / 2 && _Myres <= max_size() - _Myres / 2)
- _Newres = _Myres + _Myres / 2; // grow exponentially if possible
- //other code
- }
_ALLOC_MASK的值是_BUF_SIZE-1。这段代码看起来有点复杂,简单描述就是:最开始_Myres每次增加_BUF_SIZE,当值达到一定大小时每次增加一半。
针对char和wchar_t,每次分配内存的临界值分别是(超过这些值就要重新分配):
char:15,31,47,70,105,157,235,352,528,792,1188,1782。。。
wchar_t:7, 15, 23, 34, 51, 76, 114, 171, 256, 384, 576, 864, 1296, 1944。。。
重新分配后都会先将旧的元素拷贝到新的内存地址。所以当处理一个长度会不断增长而又大概知道最大大小时可以先调用reserve函数预分配内存以提高效率。
string类占多少字节的内存空间呢?
_Container_base Debug下含有一个指针,4字节,Release下是空类,0字节。_String_val类含有一个allocator对象。string类使用默认的allocator类,这个类没有数据成员,不过按字节对齐的原则,它占4字节。basic_string类的成员加起来是24,所以总共是32字节(Debug)或28字节(Relase)。wstring也是32或28,至于原因文中已经分析。
综上所述:string和wstring类借助_String_iterator实现迭代器操作,都占32(Debug)或28(Release)字节的内存空间,没有虚函数,构造和析构开销较低,内存分配比较灵活。
实际使用string类时也有很多不方便的地方,笔者写了一个扩展类,欢迎提出宝贵意见。
扩展类链接:http://blog.youkuaiyun.com/passion_wu128/article/details/38354541
二、用法详解
之所以抛弃char*的字符串而选用C++标准程序库中的string类,是因为他和前者比较起来,不必担心内存是否足够、字符串长度等等,而且作为一个类出现,他集成的操作函数足以完成我们大多数情况下的需要。我们尽可以把它看成是C++的基本数据类型。
首先,为了在我们的程序中使用string类型,我们必须包含头文件。如下:
- 1
- 1
1、声明一个C++字符串
声明一个字符串变量很简单: string str;
这样我们就声明了一个字符串变量,但既然是一个类,就有构造函数和析构函数。上面的声明没有传入参数,所以就直接使用了string的默认的构造函数,这个函数所作的就是把str初始化为一个空字符串。string类的构造函数和析构函数如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
2、string与C字符数组的比较
string串要取得其中某一个字符,和传统的C字符串一样,可以用s[i]的方式取得。比较不一样的是如果s有三个字符,传统C的字符串的s[3]是’\0’字符,但是C++的string则是只到s[2]这个字符而已。
1、C风格字符串
- 用”“括起来的字符串常量,C++中的字符串常量由编译器在末尾添加一个空字符;
- 末尾添加了‘\0’的字符数组,C风格字符串的末尾必须有一个’\0’。
2、C字符数组及其与string串的区别
- char ch[ ]={‘C’, ‘+’, ‘+’}; //末尾无NULL
- char ch[ ]={‘C’, ‘+’, ‘+’, ‘\0’}; //末尾显式添加NULL
- char ch[ ]=”C++”; //末尾自动添加NULL字符 若[ ]内数字大于实际字符数,将实际字符存入数组,其余位置全部为’\0’。
3、string对象的操作
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
当进行string对象和字符串字面值混合连接操作时,+操作符的左右操作数必须至少有一个是string类型的:
- 1
- 2
- 3
- 1
- 2
- 3
4、字符串操作函数
1、string类函数
1) =, s.assign() // 赋以新值
2) swap() // 交换两个字符串的内容
3) +=, s.append(), s.push_back() // 在尾部添加字符
4) s.insert() // 插入字符
5) s.erase() // 删除字符
6) s.clear() // 删除全部字符
7) s.replace() // 替换字符
8) + // 串联字符串
9) ==,!=,<,<=,>,>=,compare() // 比较字符串
10) size(),length() // 返回字符数量
11) max_size() // 返回字符的可能最大个数
12) s.empty() // 判断字符串是否为空
13) s.capacity() // 返回重新分配之前的字符容量
14) reserve() // 保留一定量内存以容纳一定数量的字符
15) [ ], at() // 存取单一字符
16) >>,getline() // 从stream读取某值
17) << // 将谋值写入stream
18) copy() // 将某值赋值为一个C_string
19) c_str() // 返回一个指向正规C字符串(C_string)的指针 内容与本string串相同 有’\0’
20) data() // 将内容以字符数组形式返回 无’\0’
21) s.substr() // 返回某个子字符串
22) begin() end() // 提供类似STL的迭代器支持
23) rbegin() rend() // 逆向迭代器
24) get_allocator() // 返回配置器
2、函数说明
1、s.assign();
- 1
- 2
- 3
- 4
- 5
- 6
- 1
- 2
- 3
- 4
- 5
- 6
2、大小和容量函数
一个C++字符串存在三种大小:
1) 现有的字符数,函数是s.size()和s.length(),他们等效。s.empty()用来检查字符串是否为空。
2) max_size(); 这个大小是指当前C++字符串最多能包含的字符数,很可能和机器本身的限制或者字符串所在位置连续内存的大小有关系。
3) capacity()重新分配内存之前string所能包含的最大字符数。
这里另一个需要指出的是reserve()函数,这个函数为string重新分配内存。重新分配的大小由其参数决定,默认参数为0,这时候会对string进行非强制性缩减。
3、元素存取
我们可以使用下标操作符[]和函数at()对元素包含的字符进行访问。但是应该注意的是操作符[]并不检查索引是否有效(有效索引0~str.length()),如果索引失效,会引起未定义的行为。而at()会检查,如果使用at()的时候索引无效,会抛出out_of_range异常。
有一个例外不得不说,const string a;的操作符[]对索引值是a.length()仍然有效,其返回值是’\0’。其他的各种情况,a.length()索引都是无效的。
4、比较函数
C ++字符串支持常见的比较操作符(>,>=,<,<=,==,!=),甚至支持string与C-string的比较(如 str<”hello”)。在使用>,>=,<,<=这些操作符的时候是根据”当前字符特性”将字符按字典顺序进行逐一的比较。字典排序靠前的字符小,比较的顺序是从前向后比较,遇到不相等的字符就按这个位置上的两个字符的比较结果确定两个字符串的大小。
另一个功能强大的比较函数是成员函数compare()。他支持多参数处理,支持用索引值和长度定位子串来进行比较。他返回一个整数来表示比较结果,返回值意义如下:0-相等 、>0-大于、<0-小于。
5、插入字符
也许你需要在string中间的某个位置插入字符串,这时候你可以用insert()函数,这个函数需要你指定一个安插位置的索引,被插入的字符串将放在这个索引的后面。
s.insert(0,”my name”);
s.insert(1,str);
这种形式的insert()函数不支持传入单个字符,这时的单个字符必须写成字符串形式。为了插入单个字符,insert()函数提供了两个对插入单个字符操作的重载函数:
insert(size_type index, size_type num, chart c)和insert(iterator pos, size_type num, chart c)。
其中size_type是无符号整数,iterator是char*,所以,你这么调用insert函数是不行的:
insert(0, 1, ‘j’);这时候第一个参数将转换成哪一个呢?
所以你必须这么写:insert((string::size_type)0, 1, ‘j’)!
第二种形式指出了使用迭代器安插字符的形式。
6、提取子串s.substr()
- 1
- 2
- 3
- 1
- 2
- 3
5、字符串流stringstream操作
Iostream标准库支持内存中的输入输出,只要将流与存储在程序内存中的string对象捆绑起来即可。此时,可使用iostream输入和输出操作符读写这个stream对象。使用stringstream,我们必须包含头文件#include。
1、string s
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 1
- 2
- 3
- 4
- 5
- 6
- 7
2、stringstream特定的操作
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
3、string到int的转换
stringstream通常是用来做数据转换的,如果你打算在多次转换中使用同一个stringstream对象,记住在每次转换前要使用clear()方法。在多次转换中重复使用同一个stringstream(而不是每次都创建一个新的对象)对象最大的好处在于效率。stringstream对象的构造和析构函数通常是非常耗费CPU时间的。
string到int的转换(与其他类型间的转换一样大同小异):
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
6、C字符串、string串、stringstream之间的关系
首先必须了解,string可以被看成是以字符为元素的一种容器。字符构成序列(字符串)。有时候在字符序列中进行遍历,标准的string类提供了STL容器接口。具有一些成员函数比如begin()、end(),迭代器可以根据他们进行定位。注意,与char*不同的是,string不一定以NULL(‘\0’)结束。string长度可以根据length()得到,string可以根据下标访问。所以,不能将string直接赋值给char*。
1、string转换成const char *
如果要将字面值string直接转换成const char *类型。string有2个函数可以运用:一个是.c_str(),一个是data成员函数。
c_str()函数返回一个指向正规C字符串的指针,内容与本string串相同。这是为了与C语言兼容,在c语言中没有string类型,故必须通过string类对象的成员函数c_str()把string 对象转换成C中的字符串样式。注意:一定要使用strcpy()函数等来操作方法c_str()返回的指针
- 1
- 2
- 3
- 1
- 2
- 3
此时,ch1与ch2的内容将都是”Hello World”。但是只能转换成const char*,如果去掉const编译不能通过。
2、string转换成char *
C++提供的由C++字符串得到对应的C_string的方法是使用data()、c_str()和copy(),其中
1) data()以字符数组的形式返回字符串内容,但并不添加’\0’。
2) c_str()返回一个以’\0’结尾的字符数组,返回值是const char*。
3) copy()则把字符串的内容复制或写入既有的c_string或字符数组内。
C++字符串并不以’\0’结尾。我的建议是在程序中能使用C++字符串就使用,除非万不得已不选用c_string。
如果要转换成char*,可以用string的一个成员函数strcpy实现。
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
此时,data中的内容为”Hello World”使用c_str()要么str赋给一个const指针,要么用strcpy()复制。
3、char *转换成string
string类型能够自动将C风格的字符串转换成string对象:
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
不过这个是会出现问题的。有一种情况我要说明一下。当我们定义了一个string类型之后,用printf(“%s”,str);输出是会出问题的。这是因为“%s”要求后面的对象的首地址。但是string不是这样的一个类型。所以肯定出错。
用cout输出是没有问题的,若一定要printf输出。那么可以这样:
- 1
- 1
4、char[ ] 转换成string
这个与char*的情况相同,也可以直接赋值,但是也会出现上面的问题,需要同样的处理。
- 字符数组转化成string类型:
- 1
- 2
- 1
- 2
或者
- 1
- 2
- 3
- 1
- 2
- 3
5、string转换成char[ ]
string对象转换成C风格的字符串:
- 1
- 1
这是因为为了防止字符数组被程序直接处理c_str()返回了一个指向常量数组的指针。
由于我们知道string的长度可以根据length()函数得到,又可以根据下标直接访问,所以用一个循环就可以赋值了,这样的转换不可以直接赋值。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
6、stringstream与string间的绑定
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
- 1
- 2
- 1
- 2