前言:
1 string是表示字符串的字符串类
2 string类是basic_string模板类的一个实例,它使用char来实例化basic_string模板类,并用char_traits和allocator作为basic_string的默认参数,所以string在底层实际是:basic_string模板类的别名,typedef basic_string<char, char_traits, allocator> string
3 使用string类时,必须包含#include<string>头文件以及using namespace std
4 string类对象支持直接用cin和cout进行输入和输出,因为重载了流插入和流提取
在c语言阶段,我们要想输入字符串,首先就需要确定数组的大小,毫无疑问是静态数组
int main()
{
char str[16];
scanf("%s", str);
return 0;
}
固定长度的数组限制了自由,但在c++中string类对象实现了动态增长的数组
int main()
{
string s;
cin >> s;
cout << s;
return 0;
c++中string类对象可以轻易实现字符串拼接:因为重载了运算符+
int main()
{
string s("hello ");
string s2("world");
string ret = s + s2;
string ret2 = s + "world";
cout << ret<<endl<<ret2<<endl;
return 0;
}
而在c语言中字符串的拼接需要用到strcat,使用十分繁琐,且不会自动扩容
string类的常用接口
1. string类对象的常见构造
1 string():默认构造函数,无参,即构造空字符串
2 string (const char* s):用常量字符串初始化string对象
也有另一种写法:
int main()
{
string s("aaaa");
string s2 = "aaaa";//二者等价
return 0;
}
因为单参数的构造函数支持隐式类型的转换:
生成的一个string类型的临时对象用"aaaa"去构造,再将临时对象拷贝构造给s2
构造+拷贝构造--->编译器优化为直接构造
3 string (size_t n, char c):用n个字符c初始化string对象
4 string (const string& str):拷贝构造函数,用已存在的string对象去拷贝构造新的string对象
了解:
用str对象的一部分去拷贝构造,从pos位置开始取len长度
len给了一个缺省值npos,即当我们没有给len传参时,默认从pos位置开始有多少取多少,也就是一直取到字符串结束
无符号整型的-1是最大值,-1的补码是全1,看成无符号整型就是42亿9千万,也就是4g,但是字符串不会有那么长,所以npos可以看作是有多少取多少,不会越界访问
也可以用一段迭代器区间去初始化,但是用的不多
int main()
{
string s("hello");
string s2(s.begin(), s.end());
cout << s << endl << s2 << endl;
return 0;
}
赋值:operator=
2. string类对象的容量操作
1 size:返回字符串有效字符长度,不包括\0
2 capacity:返回分配的存储空间的大小,不包括\0,当此容量耗尽并且需要更多容量时,对象会自动扩展它
在vs2019中string对象的初始capacity是15,但实际空间是16,因为要存储\0
不断向string对象去插入数据,容量不够会自动扩容
3 empty:检测字符串是否为空串,是返回true,否则返回false
4 clear:清空有效字符,但空间还在
5 reserve :为字符串预留空间
在vs2019中,reserve后的空间数,会比原先指定的预留空间数多
在g++中,指定要开多少,reserve就会开多少
那么在我们明确要多少空间的情况下,reserve可以一次性开好空间,避免多次扩容,提高效率,因为扩容是需要付出代价的,尤其是异地扩容
//测试vs2019中string对象的扩容
int main()
{
string s;
size_t sz = s.capacity();
cout << "原本容量: " << sz << endl;
int i = 0;
for (i = 0; i < 100; i++)
{
s.push_back('A');
if (sz != s.capacity())
{
sz = s.capacity();
cout << "扩容: " << sz << endl;
}
}
return 0;
}
所以这时候reserve就有巨大作用了,在你知道需要多少空间的前提下:
int main()
{
string s;
s.reserve(100);
size_t sz = s.capacity();
cout << "原本容量: " << sz << endl;
int i = 0;
for (i = 0; i < 100; i++)
{
s.push_back('A');
if (sz != s.capacity())
{
sz = s.capacity();
cout << "扩容: " << sz << endl;
}
}
cout << s << endl;
return 0;
}
reserve的使用:
在vs2019中reserve参数小于string的底层空间大小时,不会将空间缩小,在标准中,reserve是否会缩小容量,并没有规定:
6 resize (size_t n):将有效字符的个数改为n个,多出的用'\0'来填充
resize的参数n分三种情况:
一:n>capacity() 先扩容再插入
二: size()<n<capacity() 无需扩容直接插入
三:n<size() 删除作用,将有效字符个数size缩短至n
一:插入作用,先扩容再插入,默认插入'\0'
二: 插入作用
resize (size_t n, char c):将有效字符的个数改为n个,多出的用字符c来填充
三:删除作用,若是n小于当前字符串长度,则是缩短效果
元素访问:
string类对象使用运算符[]的重载就很方便,这个访问方式检查越界很严格,一旦越界直接终止程序
也可以用at函数:越界会抛异常,有解决机会,程序不会直接崩掉
有const版本和非const版本
int main()
{
string s("aaaa");
s.at(0)++;
cout << s << endl;
return 0;
}
3. string类对象的访问及遍历操作
访问:
operator[]:返回pos位置的字符,const对象和非const对象都可以调用,因为在库中重载了:
int main()
{
string s("hello");
const string s2("Hello");
cout << s << endl << s2 << endl;
cout << s[0] << endl << s2[0] << endl;
s[0] = 'A';
cout << s << endl;
//s2[0] = 'S';//不可以修改,因为是const对象
return 0;
}
string的3种遍历方式:
以下三种方式除了遍历string对象,还可以遍历时修改string中的字符
string类中重载了[],因为string类对象存储字符串的是连续的物理空间,所以可以像遍历普通数组一样,用[]来遍历string,但这种遍历方式并不适配所有容器
1 for+operator[]
int main()
{
string s("hello");
size_t i = 0;
for (i = 0; i < s.size(); i++)
{
cout << s[i];
}
return 0;
}
遍历时修改:
int main()
{
string s("hello");
size_t i = 0;
for (i = 0; i < s.size(); i++)
{
s[i]++;
cout << s[i];
}
return 0;
}
2 迭代器
迭代器是可以适配所有容器的遍历的,是主流
在string类中,迭代器可以看作是指针,符合访问逻辑
一个string类的对象可以看成有三个成员变量:_str,_size,_capacity
迭代器iterator都是定义在类域里的,要使用它需要指明类域
int main()
{
string s("hello");//非const对象的迭代器遍历
string::iterator it = s.begin();
//也可以用auto自动推导it的类型:
// auto it = s.begin();
while (it != s.end())
{
cout << *it;
it++;
}
cout << endl;
return 0;
}
注意:结束条件不可以写成:it<s.end() 不通用, 在vector,string这种有连续物理空间的容器中是可行的
但是在list中是不可以的,list是一个带头双向循环链表,连接着的节点们的地址之间的大小是未知的,无法比较,所以s.end() 不一定比it大
遍历时修改:
int main()
{
string s("hello");
string::iterator it = s.begin();
while (it != s.end())
{
(*it)++;
cout << *it;
++it;
}
return 0;
}
begin:返回指向字符串第一个字符的迭代器
非const对象调用非const版本的迭代器,const对象调用const版本的迭代器
int main()
{
const string s("hello");//const对象的迭代器遍历
string::const_iterator it = s.begin();//法一
//auto it = s.begin();//自动推导it类型:string::const_iterator 法二
while (it != s.end())
{
cout << *it;
it++;
}
cout << endl;
return 0;
}
const对象的迭代器遍历需要调用const的迭代器,那么it的类型就必须是const_iterator
end:返回最后一个有效字符的下一个位置的迭代器
迭代器反向遍历字符串:
rbegin:返回指向字符串最后一个字符的反向迭代器
rend:返回指向字符串第一个字符的前一个位置的反向迭代器
反向迭代器的++是倒着走的,正向迭代器的++是从左往右走,那么方向迭代器的++是从右往左走的
int main()
{
string s("hello");//非const对象的反向遍历
string::reverse_iterator rit = s.rbegin();
while (rit != s.rend())
{
cout << *rit;
rit++;
}
cout << endl;
return 0;
}
int main()
{
const string s("hello");//const对象的反向遍历
string::const_reverse_iterator rit = s.rbegin();
while (rit != s.rend())
{
cout << *rit;
rit++;
}
cout << endl;
return 0;
}
总结:迭代器总共有4种,由正向迭代器,反向迭代器,与非const对象,const对象的两两组合
3 范围for
范围for的底层原理就是迭代器,但是使用更加简洁
注意:范围for不支持倒着遍历
编译器编译时会替换成迭代器
*it会赋值给e
int main()
{
string s("hello");
for (auto e : s)
{
cout << e;
}
cout << endl;
return 0;
}
遍历时修改:
因为底层原理是迭代器,*it会赋值给e,e只是*it的一份拷贝,对e做修改是改变不了原字符串的
&e,e就是*it的别名,就可以对原字符串进行修改了
int main()
{
string s("hello");
for (auto &e : s)
{
e++;
cout << e;
}
cout << endl;
return 0;
}
4. string类对象的修改操作
push_back (char c):在字符串后尾插字符c
int main()
{
string s("hello");
s.push_back('A');
cout << s << endl;
return 0;
}
append:
int main()
{
string s("hello ");
string s2("world");
s.append(s2);
cout << s << endl;
return 0;
}
int main()
{
string s("hello ");
s.append("world");
cout << s << endl;
return 0;
}
int main()
{
string s("hello ");
s.append(10,'A');
cout << s << endl;
return 0;
}
operator+=:
int main()
{
string s("hello ");
string s2("world");
s += s2;
cout << s << endl;
return 0;
}
int main()
{
string s("hello ");
s += "world";
cout << s << endl;
return 0;
}
int main()
{
string s("hello ");
s += 'A';
cout << s << endl;
return 0;
}
assign:
为字符串分配一个新值,替换其当前内容
int main()
{
string s("aaaaaaaaaaaaaaaaaa");
string s2("bbb");
s.assign(s2);
cout << s << endl;
return 0;
}
insert:
在pos位置之前插入
int main()
{
string s("hello");
s.insert(0, 1, 'C');//头插
s.insert(s.begin(), 'C');//头插
cout << s << endl;
return 0;
}
erase:
第一个接口比较常用,但是无论是erase还是insert都要少用,因为涉及挪动数据,效率低
int main()
{
string s("hello");
s.erase(1);
cout << s << endl;
return 0;
}
replace:
接口设计复杂繁多,需要时再去查一下文档即可
替换字符串的一部分
int main()
{
string s("hello world");
s.replace(5, 1, "%20");
cout << s << endl;
return 0;
}
replace同样涉及挪动数据的问题,效率低
刷题过程中可能会碰到过以下一个题目:将字符串中的所有空格替换成%20
可以用find函数找到空格+replace替换,但是效率很低,所以可以采用以下方法:
int main()
{
string s("hello world hello world");
string s2;
for (auto e : s)
{
if (e != ' ')
{
s2 += e;
}
else
{
s2 += "%20";
}
}
//s = s2;法一
//s.assign(s2);法二
s.swap(s2);//法三
cout << s << endl;
return 0;
}
让s中的字符串变成s2中的字符串有如上三种方式,法一法二都是赋值
法三的效率相较于法一法二提高不少,原因如下:
string类的对象中可以看成是这样的结构:_str _size _capacity
s.swap(s2) 交换的是双方的_str
证明:
int main()
{
string s("hello world hello world");
string s2;
for (auto e : s)
{
if (e != ' ')
{
s2 += e;
}
else
{
s2 += "%20";
}
}
cout << "交换前:"<<endl;
cout <<(void*)s.c_str() << endl;//c++char*的打印比较特殊,需要强转为其他类型,否则打印的是字符串形式的内容
cout << (void*)s2.c_str() << endl;
s.swap(s2);//法三
cout << "交换后:"<<endl;
cout << (void*)s.c_str() << endl;
cout << (void*)s2.c_str() << endl;
cout << s << endl;
return 0;
}
其实也可以直接用swap函数,不过此swap不是库里的swap,而是string类中的全局swap
对于库中的swap函数模板,和string类中现成的全局swap函数,会优先调用string类中的全局swap函数 ,效率就和调用成员函数swap一样高
所以其实有三个swap:
库中的swap:是一个函数模板
若是用库中的swap完成两个string类对象的交换,代价比较大,要完成一次拷贝构造,两次赋值
所以string类中设计了一个全局的swap:
它的行为就像调用string类对象的成员函数swap一样,效率很高
string类中的成员函数swap:
c_str:
返回c格式字符串
int main()
{
string s("hello ");
cout<<s.c_str()<<endl;
cout << s << endl;
return 0;
}
find:
pos的缺省值为0,默认从头开始找,pos若是给定值,则从给定位置开始查找
返回值:
返回第一个匹配项的第一个字符的位置。
如果未找到匹配项,则该函数返回 string::npos
find通常可以substr搭配使用:
实例1:如取后缀
int main()
{
string s("test.cpp");
size_t i = s.find('.');
string s2 = s.substr(i);
cout << s2 << endl;
return 0;
}
实例2:分割网址的协议、域名、资源名
int main()
{
string s("https://legacy.cplusplus.com/reference/string/string/rfind/");
string sub1;
string sub2;
string sub3;
size_t i1 = s.find(':');
if (i1 != string::npos)
{
sub1 = s.substr(0, i1);
}
else
{
cout << "找不到" << endl;
}
size_t i2 = s.find('/', i1 + 3);
if (i2 != string::npos)
{
sub2 = s.substr(i1 + 3, i2 - (i1 + 3));
}
else
{
cout << "找不到" << endl;
}
sub3 = s.substr(i2 + 1);
cout << sub1 << endl;
cout << sub2 << endl;
cout << sub3<< endl;
return 0;
}
1.
int main()
{
string s("hello ");
string s2("hello world");
size_t pos = s.find(s2);
if (pos == string::npos)
{
cout << "找不到" << endl;
}
else
{
cout << pos << endl;
}
return 0;
int main()
{
string s("aaaaahello ");
string s2("hello ");
size_t pos = s.find(s2);//默认从头开始查找
if (pos == string::npos)
{
cout << "找不到" << endl;
}
else
{
cout << pos << endl;
}
return 0;
}
2
int main()
{
string s("aaaaahelloworld");
size_t pos = s.find("world");//默认从头开始查找
if (pos == string::npos)
{
cout << "找不到" << endl;
}
else
{
cout << pos << endl;
}
return 0;
}
3
int main()
{
string s("helloworld");
size_t pos = s.find('e');//默认从头开始查找
if (pos == string::npos)
{
cout << "找不到" << endl;
}
else
{
cout << pos << endl;
}
return 0;
}
rfind:
从字符串的后面开始向前查找
如有多个后缀,要取到最后一个后缀:
int main()
{
string s("test.cpp.tar.zip");
size_t i = s.rfind('.');
string s2 = s.substr(i);
cout << s2 << endl;
return 0;
}
如果未找到匹配项,则该函数返回 string::npos
int main()
{
string s("worldworld");
size_t pos = s.rfind("world");//默认从尾开始查找
if (pos == string::npos)
{
cout << "找不到" << endl;
}
else
{
cout << pos << endl;
}
return 0;
}
substr:
从pos位置开始截取len个字符生成子串
若是len没有传参,则从pos位置开始,截取pos位置及之后的所有内容
int main()
{
string s("hello world");
string str = s.substr(6);
cout << str << endl;
return 0;
}
int main()
{
string s("hello world");
string str = s.substr(6,3);
cout << str << endl;
return 0;
}
getline:
获取一行
不论是cin还是scanf遇到空格或者换行就会停止读取
所以当一行字符串中有空格时,用cin是无法完整读取到的,但是getline是获取一行,它可以做到,除此之外,若是给getline传你指定的分隔符,那么就可以以你想要的方式读取结束
输入:
以'!'结束读取
5. string类非成员函数
operator+:
能少用这个运算符重载就少用,传值返回,且有多次拷贝的代价
int main()
{
string s;
string ret1 = s + '#';
string ret2 = s + "world";
cout << ret1 << endl;
cout << ret2 << endl;
return 0;
}
operator>>:
输入运算符重载
operator<<:
输出运算符重载
relational operators:
比较大小
6. vs和g++下string结构的说明
vs下string的结构
看下面一段代码,试猜一下在32位平台下vs中,一个string类的对象的大小是多少呢?
int main()
{
string s;
cout << sizeof(s) << endl;
string s2("hello world");
cout << sizeof(s2) << endl;
return 0;
}
string总共占28个字节
string内部有一个联合体,联合体用来定义string中字符串的存储空间:
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;
1 当字符串长度小于16时,使用内部固定的字符数组来存放
2 当字符串长度大于等于16时,从堆上开辟空间
因为大多数情况下字符串的长度都小于16,那string对象创建好之后,内
部已经有了16个字符数组的固定空间,不需要通过堆创建,效率高
实际上,vs中一个string类对象的真实结构如下:
_buff[16]
_str
_size
_capacity
16+3*4 = 28
其中的字符数组_buff共有16个空间,能存储有效字符的空间个数是15,剩下的一个要存储'\0'
所以,当字符串长度小于16时,存储到buff数组里面
当字符串长度大于等于16时,存储到_str指向的空间里
实例:
int main()
{
string s("aaaaaa");
string s2("hello world1111111111111111111111");
return 0;
}
g++下string的结构
写时拷贝(了解):
写时拷贝是在浅拷贝的基础之上增加了引用计数的方式来实现的
用string类的s1去拷贝构造s2,若是浅拷贝
浅拷贝的问题:1 析构两次 2 一个对象的修改会影响另一个对象
问题1的解决:增加一个变量,引用计数,拷贝的时候++引用计数,析构的时候先--引用计数,引用计数减到0时说明是最后一个对象,可以释放空间,否则不释放空间,因为还有其他对象在使用这块空间
问题2的解决:写时拷贝(全称是引用计数的写时拷贝,写的时候再拷贝,即延迟拷贝)
当要对空间上的数据进行修改且不能影响其他对象时,需要深拷贝
引用计数的写时拷贝的意义:在只是浅拷贝的基础上,如果没有修改,那就很赚了,没有开空间,没有深拷贝
G++下,string是通过写时拷贝实现的,32位平台下,string对象总共占4个字节,64位平台下,string对象总共占8个字节,内部只包含了一个指针,该指针将来指向一块堆空间,内部包含了如下字段:
1 容量 2 字符串有效字符个数 3 引用计数
struct _Rep_base
{
size_type _M_length;//容量
size_type _M_capacity;//有效字符个数
_Atomic_word _M_refcount;//引用计数
};
4 用于存储字符串的空间
存储n个有效字符,表面上是开n个空间,实际上需要开n+12+1个空间
实例:g++下的写时拷贝