STL——string剖析
文章目录
- STL——string剖析
- 1. C语言中的字符串
- 2. 标准库中string的使用
- 2.1 构造函数
- 2.2 string的容量操作
- resize和reserve
- 2.3 string的增删查改
- 插入操作
- push_back:
- insert:
- 删除操作
- pop_back:
- erase
- 查找操作
- find
- find_first_of
- 修改操作
- operator[]
- at
- front
- back
- 2.3 字符串其他操作
- 字符串的拼接
- operator+=(重要)
- append
- 字符串的替换
- assign
- replace
- 字符串的比较
- compare
- 提取字串
- substr
- string转const char *
- data
- c_str(重要)
- 2.4 重要的非成员函数
- operator+(重要)
- swap
- 字符串IO
- operator>>
- operator<<
- getline
- relational operators
- 3. string在不同编译器下的底层构造
- 3.1 vs下string的结构
- 3.2 g++下string的结构
- 4. 写时拷贝
- 5. 拓展阅读
- 6. 两个常见面试题
1. C语言中的字符串
在C语言中,字符串是一段以’\0’结尾的字符集合,为了操作方便,C标准库定义了许多对字符串操作的库函数,但是这些库函数和字符串是分离的,和我们的"OOP"(面向对象编程)思想不太符合,而且底层空间需要用户自己维护,容易产生越界访问等错误。因此C++声明字符串实现有两个头文件,是C++对C语言头文件<string.h>的另一层封装,而C++真正重新定义实现string的标准是在头文件中。
2. 标准库中string的使用
string其实是模板类basic_string的一种实例化:
typedef basic_string<char> string;
2.1 构造函数
default(1) | string(); | string的默认构造函数,管理的字符串初始化为空串 |
---|---|---|
copy (2) | string (const string& str); | string的拷贝构造函数,是深拷贝 |
substring (3) | string (const string& str, size_t pos, size_t len = npos); | 从str字符串中下标为pos的字符作为首字符,长度为len的子串构造新string,len有缺省参数npos,npos是一个size_t的常量(值非常大),用在长度中到表示字符串的结尾,也就是将pos - end的子串截取 |
from c-string (4) | string (const char* s); | 将C风格的字符串构造string |
from sequence (5) | string (const char* s, size_t n); | 将C风格的字符串的前n个字符构造string |
fill (6) | string (size_t n, char c); | 将n个字符c构造string |
range (7) | template <class InputIterator> string (InputIterator first, InputIterator last); | 使用迭代器模板构造string |
2.2 string的容量操作
string管理字符串是通过动态开辟空间进行管理的,那么就需要合理的最利用空间,什么时候开辟空间,什么时候缩小空间就很重要了。因此也就有了一系列为了管理字符串内存空间的变量和函数:
成员 | 解释 | 类型 |
---|---|---|
capacity | 成员变量,表示string的容量,存放的有效字符个数的上限,capacity >= size | size_t |
size | 成员变量,表示有效字符个数 | size_t |
length | 成员变量,同样表示有效字符个数 | size_t |
max_size | 成员函数,表示string最多存储多少有效的字符个数,返回值是一个很大的数,一般比容量大得大得多 | size_t () |
empty | 成员函数,表示字符串是否为空串 | bool () |
clear | 成员函数,将字符串清空,不会释放空间,不会改变容量,但会改变有效字符个数 | void () |
resize | 成员函数,用来将字符串的有效个数改成n | void (size_t n, char c) void (size_t n) |
reserve | 成员函数,用来更改字符串的容量 | void (size_t n ) |
resize和reserve
resize:
void resize(size_t n)
void resize(size_t n, char c)
resize根据n分为四种情况:
-
当n < size时:
字符串会截断,有效长度减少至n;
-
当n == size时:
什么都不做;
-
当size < n <= capacity时:
将把字符串的有效长度增长到n,如果给出了c,则添加的字符全部用c填充,没给出则默认用’\0’;
-
当n > capacity时:
会先根据string的扩容规则(reserve函数)进行扩容,然后再将字符串的size填充至n。
resize的作用一般是来填充字符串和截断字符串的,但是当填充的长度超过容量时,就要使用reserve函数进行扩容了。
reserve:
void reserve(int n)
reserve根据n也会分两种情况:
-
当n <= capacity时:
什么都不会做; -
而当n > capacity时:
容量进行扩容至n或者大于n,具体实现根据对应的编译器下的标准库的扩容规则(vs的规则一般为扩1.5倍,g++一般为扩2倍)
2.3 string的增删查改
插入操作
字符串的插入操作一般有两个:
- push_back(尾插),只能用来在尾部插入单个字符,效率高;
- insert,可以在任意位置插入字符或者字符串。
push_back:
void push_back (char c);
尾插函数,并且只允许插入一个字符,但是尾插效率高,时间复杂度为O(1)。
insert:
string& insert (size_t pos, const string& str);
string& insert (size_t pos, const string& str, size_t subpos, size_t sublen);
string& insert (size_t pos, const char* s);
string& insert (size_t pos, const char* s, size_t n);
string& insert (size_t pos, size_t n, char c);
iterator insert (const_iterator p, size_t n, char c);
iterator insert (const_iterator p, char c);
insert实现的重载比较多,允许各种形式的参数,但是参数的意义比较固定。
参数解析:
- pos:插入的下标,插入时是将原下标和其右边的字符串右移,再将要插入的内容填充;
- str:要插入的string对象管理的字符串;
- subpos:表示我们要插入str的子串,该子串的起始下标为subpos;
- sublen:表示str子串的长度,和subpos配合,表示要插入str中的其实位置下标为subpos,长度为sublen的子串;
- s:要插入的字符串;
- n:要插入的字符串s的前n个字符;
- c:要插入的字符,如果和n配合,表示要插入n个字符;
- p:指向要插入位置上字符的迭代器,和pos差别不大,通常我们想要表示第i个位置字符的迭代器可以用string.begin() + i。
删除操作
pop_back:
void pop_back ();
尾删函数,删除字符串结尾的一个字符,将有效字符减小一个,时间复杂度也是O(1)。
注意:如果调用时字符串是空串,是一个未定义行为,因此调用前检查字符串是否为空串很重要。
erase
string& erase (size_t pos = 0, size_t len = npos);
iterator erase (iterator p);
iterator erase (iterator first, iterator last);
参数解析:
- pos:要删除的字符串的起始下标;
- len:要删除的字符串的长度;
- p:删除字符串的单个字符的迭代器;
- first:要删除的字符串的起始位置字符的迭代器;
- last:要删除的字符串的结尾位置字符的迭代器。
返回值:
string&:删除后的字符串对象的*this;
iterator:删除的迭代器或者迭代器区间的下一个元素的迭代器。
注意:迭代器的使用都是左闭右开,也就是说,last位置的字符不会被删除,而是到last结束。
查找操作
字符串的查找操作主要依赖find家族,这些函数属于算法一类,并不是string的成员函数,并且对很多容器都适用。
这里只举例常用的find和find_first_of的操作:
find
size_t find (const string& str, size_t pos = 0) const;
size_t find (const char* s, size_t pos = 0) const;
size_t find (const char* s, size_t pos, size_t n) const;
size_t find (char c, size_t pos = 0) const;
参数说明:
- ch: 要查找的字符;
- str: 要查找的子字符串;
- s: 要查找的 C 风格字符串;
- pos: 开始查找的位置,默认为 0,表示从字符串的开头开始查找;
- n: 要查找的字符数组的长度(仅在使用字符数组时适用)。
返回值:
返回值是 size_t 类型,表示找到的字符或子字符串的索引位置。如果未找到,返回 std::string::npos,作为返回值,这是一个常量,表示无效位置。
这是一个从左向右查找的函数,用于查找第一个匹配的字符串的下标。
find_first_of
size_t find_first_of (const string& str, size_t pos = 0) const noexcept;
size_t find_first_of (const char* s, size_t pos = 0) const;
size_t find_first_of (const char* s, size_t pos, size_t n) const;
size_t find_first_of (char c, size_t pos = 0) const noexcept;
功能: 查找在给定的 std::string 中的任意字符首次出现的位置。
参数解析:
- str: 包含要查找的字符集合的 std::string;
- pos: 开始查找的位置,默认为 0。
返回值:
返回找到的字符的索引位置,如果未找到则返回 std::string::npos。
其它的find家族函数:
- rfind:和find方向相反,这是一个从右往左查找的函数,用于反向查找一个匹配的字符串的下标;
- find_last_of:查找在给定的字符串或字符任意字符最后一次出现的位置;
- find_first_not_of:查找第一个不在给定的字符串或字符的任意字符的出现的位置;
- find_last_not_of:查找最后一次不在给定的字符串或字符的任意字符的出现的位置。
修改操作
string对指定元素进行修改,需要获取到字符位置的地址或者引用,而string通过重载运算符operator[]实现了获取指定下标元素的引用,从而让程序员直接可以修改。
operator[]
char& operator[](size_t pos);
const char& operator[](size_t pos) const;
string类型可以通过下标运算符的重载获取对应下标的字符的引用,也就是说可以直接对字符串的指定下标的字符进行修改,注意还同样重载了const版本的operator[]。
同样的,string还实现了at成员函数,作用和operator[]一样。
at
char& at (size_t pos);
const char& at (size_t pos) const;
at和operator[]的效果一样,都是获取指定下标的字符的引用,可以直接对字符串进行修改。
返回值同样是char&的还有front和back:
front
char& front();
const char& front() const;
取出首字符的引用。
back
char& back();
const char& back() const;
取出最后一个字符的引用。
2.3 字符串其他操作
字符串的拼接
operator+=(重要)
string& operator+= (const string& str);
string& operator+= (const char* s);
string& operator+= (char c);
string类型可以通过+=实现直接在原来字符串的基础上增加新字符,重载有三个版本,分别可以和string类型、字符串、字符进行+=,并且返回的是+=完后的原string类型(*this)的引用。
append
string& append (const string& str);
string& append (const string& str, size_t subpos, size_t sublen);
string& append (const char* s);
string& append (const char* s, size_t n);
string& append (size_t n, char c);
参数的意义和insert一样,但是它不需要像insert一样给出插入位置字符的下标或者迭代器,因为它固定插入位置是在最后一个字符后,返回值均为调用该函数的string对象的*this引用。
字符串的替换
assign
string& assign (const string& str);
string& assign (const string& str, size_t subpos, size_t sublen);
string& assign (const char* s);
string& assign (const char* s, size_t n);
string& assign (size_t n, char c);
参数解析:
- str:将原string对象的字符串完全替换成str的字符串;
- subpos:表示将原string对象的字符串完全替换成str的子串,该子串的起始位置为subpos;
- sublen:该子串的长度;
- s:将原字符串替换成指定的C风格字符串,\0也会赋值过去,但是不会算进有效字符;
- n:和c一起配合,表示要替换成n个c字符;
- c:要替换的字符。
返回值:
*this。
replace
string& replace (size_t pos, size_t len, const string& str);
string& replace (iterator i1, iterator i2, const string& str);
string& replace (size_t pos, size_t len, const string& str, size_t subpos, size_t sublen);
string& replace (size_t pos, size_t len, const char* s);
string& replace (iterator i1, iterator i2, const char* s);
string& replace (size_t pos, size_t len, const char* s, size_t n);
string& replace (iterator i1, iterator i2, const char* s, size_t n);
string& replace (size_t pos, size_t len, size_t n, char c);
string& replace (iterator i1, iterator i2, size_t n, char c);
和assign不同,assign是把原字符串全部替换,而replace可以只替换一部分子串,但是一定要注意pos和len是否合法,编译器不会判断是否越界,一旦越界,访问到了释放的空间,严重会造成程序终止。
这里参数就不展开讲了,了解了前面的函数,其实会对字符串参数有个大概的了解,如果要指定位置,通常用下标和迭代器,指定字符串长度有len,指定字符有c,指定字符的个数有n,指定C风格字符串有s,指定string类型有str,这里的i1、i2和前面的first和last差不多。
字符串的比较
字符串之间的比较,其实就是将对应下标位置上的字符分别对应比较,由于char类型的字符实际就是一个整形(由ASCLL码表示),因此根据ASCLL码值的大小进行比较,并且顺序从左往右,直到一方字符串字符的ASCLL码值更小或者长度更短。
compare
int compare (const string& str) const noexcept;
int compare (size_t pos, size_t len, const string& str) const;
int compare (size_t pos, size_t len, const string& str, size_t subpos, size_t sublen) const;
int compare (const char* s) const;
int compare (size_t pos, size_t len, const char* s) const;
int compare (size_t pos, size_t len, const char* s, size_t n) const;
参数解析:
- const std::string& str:要比较的另一个 std::string 对象。
- size_t pos1:当前字符串中开始比较的起始位置。
- size_t len1:当前字符串中要比较的字符数。
- const char* s:要比较的 C 风格字符串。
- size_t n:要比较的 C 风格字符串的长度。
返回值:
一个int类型,表示比较的结果。
比较规则:(我们将原string对象的字符串的字符作为左,要比较的对象的字符作为右)
- 当左字符的ASCLL码值 > 右字符的ASCLL码值,返回一个大于0的数(通常用1表示)
- 当左字符的ASCLL码值 < 右字符的ASCLL码值,返回一个小于0的数(通常用-1表示)
- 当左字符的ASCLL码值 == 右字符的ASCLL码值,继续下一个下标的字符的比较
- 如果左右字符串一模一样,也就会一直比较直到下标走到了字符串结尾,那么就会返回0
- 当有一方的下标先到了字符串结尾,也认为是小于对方(这里巧妙的点在于,如果我们对string对象的字符串结尾都处理以\0结尾,\0的ASCLL码值就为0,跟任何字符比较都是更小,也就省去了判断长度)
提取字串
substr
string substr (size_t pos = 0, size_t len = npos) const;
提取pos位置为起始位置,len为长度的子串,两个参数都有缺省值,如果都没给,表示提取string对象自身的字符串
返回值不是一个引用而是一个拷贝。
注意:由于 npos 的值是 size_t 的最大值,它可以被视为一个无效的长度值。在某些情况下,使用 npos 作为长度可以表示一个超出有效范围的长度。也就是说当npos作为字符串长度时,由于字符串不能提取超出有效长度之外的字符,那么只会把从指定位置到结尾的所有字符选中,所以npos常用来选中指定位置到结尾的子串。
string转const char *
data
const char* data ();
c_str(重要)
const char* c_str ();
两者没有明显不同,返回的指针都指向原string对象的字符串在内存中的起始地址,唯一的不同是在C++11以前data返回的字符串并不一定以’\0’结尾,而c_str始终返回一个以\0结尾的字符串,因此c_str更常用。
2.4 重要的非成员函数
operator+(重要)
string operator+ (const string& lhs, const string& rhs);
string operator+ (const string& lhs, const char* rhs);
string operator+ (const char* lhs, const string& rhs);
string operator+ (const string& lhs, char rhs);
string operator+ (char lhs, const string& rhs);
operator+是string类外重载的一个运算符,它允许string类型可以和多种类型的变量运算:
- string + string
- string + const char*
- string + char
- const char* + string
- char + string
注意:string和其他变量运算是不会修改自身的值的,而是会生成一个新的拷贝,因此尽量少用,因为是传值返回(一旦字符串长度比较长,一个字符就是一个字节),会导致深拷贝的效率低。
swap
void swap (string& x, string& y);
令x和y的字符串交换。
字符串IO
operator>>
std::istream& operator>>(std::istream& is, std::string &str);
流插入运算符重载,正是因为重载了>>我们才能实现std::cin >> str;从终端命令行给str赋值(is实际上可以是任意的输入流),但是is遇到空格就会结束,因此不能把带有空格的字符串赋值给str。
返回值:
是is的引用,这样可以实现 is >> str >> str1 >> str2;多个字符串的输入。
operator<<
ostream& operator<< (ostream& os, const string& str);
流提取运算符重载,可以将str输出到指定的输出流当中。
getline
istream& getline (istream& is, string& str, char delim);
istream& getline (istream& is, string& str);
由于is不能实现带有空格字符串的赋值,因此getline实现了
参数解析:
- is:读取流对象的引用;
- str:读取的字符串赋值的字符串;
- delim:分隔符,默认为\0,当读到delim时停止,不会把delim读进str。
relational operators
除了[]、+=、+外,标准库还重载了一些关于string的操作符函数:
bool operator== (const string& lhs, const string& rhs);
bool operator!= (const string& lhs, const string& rhs);
bool operator< (const string& lhs, const string& rhs);
bool operator<= (const string& lhs, const string& rhs);
bool operator> (const string& lhs, const string& rhs);
bool operator>= (const string& lhs, const string& rhs);
这是一些特定的比较运算符的重载,字符串的比较规则和compare一样。
3. string在不同编译器下的底层构造
注意:下述结构是在32位平台下进行验证,32位平台下指针占4个字节。
3.1 vs下string的结构
string总共占28个字节,内部结构稍微复杂一点,先是有一个联合体,联合体用来定义string中字 符串的存储空间:
- 当字符串长度小于16时,使用内部固定的字符数组来存放 ;
- 当字符串长度大于等于16时,从堆上开辟空间。
union _Bxty
{ // storage for small buffer or pointer to larger one
value_type _Buf[_BUF_SIZE];
pointer _Ptr;
char _Alias[_BUF_SIZE]; // to permit aliasing
} _Bx;
这种设计也是有一定道理的,大多数情况下字符串的长度都小于16,那string对象创建好之后,内部已经有了16个字符数组的固定空间,不需要通过堆创建,效率高;
其次:还有一个size_t字段保存字符串长度,一个size_t字段保存从堆上开辟空间总的容量 ;
最后:还有一个指针做一些其他事情。 故总共占16+4+4+4=28个字节。
3.2 g++下string的结构
g++下,string是通过写时拷贝实现的,string对象总共占4个字节,内部只包含了一个指针,该指 针将来指向一块堆空间,内部包含了如下字段:
- 空间总大小
- 字符串有效长度
- 引用计数
struct _Rep_base
{
size_type _M_length;
size_type _M_capacity;
_Atomic_word _M_refcount;
};
- 指向堆空间的指针,用来存储字符串。
4. 写时拷贝
写时拷贝(COW) 是一种优化技术,允许多个对象共享同一块内存,直到其中一个对象需要修改数据时,才会创建该数据的副本。这种方式可以节省内存和提高性能,尤其是在处理大量相同数据的情况下。
写时拷贝就是一种拖延症,是在浅拷贝的基础之上增加了引用计数的方式来实现的。
引用计数:用来记录资源使用者的个数。在构造时,将资源的计数给成1,每增加一个对象使用该资源,就给 计数增加1,当某个对象被销毁时,先给该计数减1,然后再检查是否需要释放资源,如果计数为1,说明该 对象时资源的最后一个使用者,将该资源释放;否则就不能释放,因为还有其他对象在使用该资源。
写时拷贝
写时拷贝在读取时的缺陷
5. 拓展阅读
面试时string的一种正确写法
STL中的string类怎么了
6. 两个常见面试题
LCR 192. 把字符串转换成整数 (atoi) - 力扣(LeetCode)
字符串相加