目录
为什么学习string类?
string类常用接口介绍
constructor(构造)函数介绍
string()
string(const char* s)
string(size_t n,char c)
string(const string& str)
string(const string& str, size_t pos, size_t len = npos)
string类对象的容量操作
size
length
capacity
empty
clear
reserve
resize
reserve和resize进一步分析
reserve的空间小于原来空间时会怎样?
resize的n小于原来字符串长度时会怎样?
reserve扩容规则
VS系列
linux系列
string类对象的访问及遍历操作
operator[]
begin + end
rbegin + rend
string类对象的修改操作
push_back
pop_back
append
string没有pop_front和push_front,但是可以使用insert和erase来操作
insert
erase
operator+=(常用)
c_str
特殊情况
find + npos
rfind
substr
string类非成员函数 (函数重载实现的)
operator+
operator>>
operator<<
getline
relational operators (string)
总结
C语言中,字符串是以'\0'结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,但是这些库函数与字符串是分离开的,不太符合OOP的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。
C++中STL库中提供了string这样专门用于操作单字节字符、字符串的设计特性。
1. 字符串是表示字符序列的类。
2. 标准的字符串类提供了对此类对象的支持,其接口类似于标准字符容器的接口,但添加了专门用于操作单字节字符字符串的设计特性。
3. string类是使用char(即作为它的字符类型,使用它的默认char_traits和分配器类型(关于模板的更多信息,请参阅basic_string)。
4. string类是basic_string模板类的一个实例,它使用char来实例化basic_string模板类,并用char_traits和allocator作为basic_string的默认参数(根于更多的模板信息请参basic_string)。
5. 注意,这个类独立于所使用的编码来处理字节:如果用来处理多字节或变长字符(如UTF-8)的序列,这个类的所有成员(如长度或大小)以及它的迭代器,将仍然按照字节(而不是实际编码的字符)来操作。
总结:
1. string是表示字符串的字符串类
2. 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。
3. string在底层实际是:basic_string模板类的别名,typedef basic_string
4. 不能操作多字节或者变长字符的序列。
在使用string类时,必须包含#include头文件以及using namespace std;
构造空的string类对象,即空字符串。这里也好比是构造了一个匿名对象。
举个栗子:
int main()
{
string s;
return 0;
}
调试来看:
用C-string来构造string类对象
举个栗子:
int main()
{
string s("hello world!");
return 0;
}
调试一下:
string类对象中构造n个字符c
举个栗子:
int main()
{
string s(5,'m');
return 0;
}
调试一下看看结果:
拷贝构造函数
举个栗子:
int main()
{
string s1("hello world!\n");
string s2(s1);
cout << s2 << endl;
return 0;
}
运行结果:
从str的pos下标位置开始,共len个长度来构造一个string对象。
举个栗子:
int main()
{
string s1("hello world!\n");
string s2(s1, 6, 4);
cout << s2 << endl;
return 0;
}
如果第三个参数不传的话,默认从pos位置开始直到该字符串结束时候终止构造。
运行结果如下:
我们从s1下标为6的字符开始,往后构造4个字符(worl)来构造初始化s2。
其余的构造函数我们暂时先不讲了,其实常用的就那么几个,当我们用到哪个构造函数,再来官网查就好了。
返回字符串中有效字符长度。
int main()
{
string s1("hello world!\n");
cout << s1.size() << endl;
return 0;
}
运行如下:
注意,这里计算长度时是把'\n'计算在内的。 string定义对象时会在最后自动补上一个'\0'。
返回字符串中有效字符的长度。
这个用法是和上面的size是一样的。只不过length是最早STL库中string类中的函数接口。对于vector、list...这些容器,length不太符合这种计算容器中个体数量的习惯,于是后来就出现了size这样的函数接口。为了向前兼容,length也是被保留下来了。
举个栗子:
int main()
{
string s1("hello world!\n");
cout << s1.length() << endl;
return 0;
}
运行结果如下:
返回空间总大小
举个栗子:
int main()
{
string s("hello\n");
cout << s.capacity() << endl;
return 0;
}
运行结果如下:
这里的容量是15,在windows下也是有原因的,待会会介绍其中的原因。
检测字符串中是否为空串,是返回true,不是返回false
举个栗子:
int main()
{
string s1("hello\n");
string s2;
cout << "s1:" << s1.empty() << endl;
cout << "s2:" << s2.empty() << endl;
return 0;
}
运行结果如下:
s1不为空,则返回false,0。
s2为空,则返回true,1。
清除有效字符
举个栗子:
int main()
{
string s1("hello world\n");
cout << "s1:" << s1 << endl;
s1.clear();
cout << "s1:" << s1 << endl;
return 0;
}
运行结果如下:
我们发现调用s1.clear()之后,s1对象中的字符都被清空了。 VS2019环境下:capacity(空间)并不会缩小。
为字符串预留开辟空间,也可以叫做扩容。
举个栗子:
int main()
{
string s1("hello world\n");
cout << s1.capacity() << endl;
s1.reserve(20); //string:至少为有效存储的字符预留20个字节空间
cout << s1.capacity() << endl;
return 0;
}
运行结果如下:
将有效字符的个数改成n个,多出来的用c来填充。c不传参时,默认用'\0'填充
举个栗子:
int main()
{
string s1("hello world");
cout << "s1.size():" << s1.size() << endl; //resize前的size
s1.resize(20);
cout << "s1:" << s1 << endl;//resize前的size
cout << "s1.size():" << s1.size() << endl << endl; //resize后的size
string s2("hello world");
cout << "s2.size():" << s2.size() << endl; //resize前的size
s2.resize(20, '!');
cout << "s2:" << s2 << endl; //resize前的size
cout << "s2.size():" << s2.size() << endl << endl;//resize后的size
return 0;
}
运行结果如下:
当c不显示传参时
举个栗子:
int main()
{
string s1("hello world");
s1.resize(20);
return 0;
}
调试看下结果:
当我们不传c时,我们发现默认会用'\0'来填充。
举个栗子:
int main()
{
string s1("hello world\n");
cout << s1.capacity() << endl;
s1.reserve(10);
cout << s1.capacity() << endl;
return 0;
}
运行结果:
答案:空间容量不会变,不会存在缩容的情况。(VS2019环境下)
举个栗子:
int main()
{
string s1("hello world\n");
cout << "s1.size():" << s1.size() << endl;
s1.resize(3, 'm');
cout << s1 << endl;
cout << "s1.size():" << s1.size() << endl;
return 0;
}
运行结果:
这时候相当于把字符串截取到下标为n的地方。
void TestPushBackReserve()
{
string s;
size_t sz = s.capacity();
cout << "capacity changed: " << sz << '\n';
cout << "making s grow:\n";
for (int i = 0; i < 500; ++i)
{
s.push_back('c'); //在插入过程中capacity()会改变
if (sz != s.capacity())
{
sz = s.capacity();
cout << "capacity changed: " << sz << '\n';
}
}
}
int main()
{
TestPushBackReserve();
return 0;
}
运行结果:
我们此时可以看到,对象s大概是按照1.5倍的方式增容。
我们可以看到这时候是按照2倍的方式来扩容的。
返回pos位置的字符,const string类对象的调用。(实际上是函数重载实现的)
举个栗子:
int main()
{
string s("hello world");
for (size_t i = 0; i < s.size(); i++)
{
cout << s[i] << " ";
}
cout << endl;
return 0;
}
运行结果如下:
begin获取第一个字符的迭代器,end获取最后一个字符下一个位置的迭代器。
cbegin和cend是重载的const版本。
举个栗子,我们来使用迭代器遍历一下字符串:
int main()
{
string s("hello world");
string::iterator it = s.begin();
while (it != s.end())
{
cout << *it << " ";
it++;
}
cout << endl;
return 0;
}
运行结果:
在之前介绍的语法糖范围for的底层实际上就是迭代器。
rbegin获取最后一个字符的位置,rend获取第一个字符的前一个位置。
举个栗子,我们来使用反向迭代器遍历一下字符串:
int main()
{
string s("hello world");
string::reverse_iterator it = s.rbegin();
while (it != s.rend())
{
cout << *it << " ";
it++;
}
cout << endl;
return 0;
}
运行结果如下:
在字符串后面追加一个字符
举个栗子:
int main()
{
string s("hello world");
s.push_back('!');
cout << s << endl;
return 0;
}
运行结果:
删除字符串中最后一个字符
举个栗子:
int main()
{
string s("hello world!");
s.pop_back();
cout << s << endl;
return 0;
}
运行结果:
在字符串后追加一个字符串
举个栗子:
int main()
{
string s("hello world!");
s.append(" wmm"); //追加一个字符串
cout << s << endl;
string s1("wmm ");
s1.append(s); //追加一个string类对象
cout << s1 << endl;
return 0;
}
运行结果如下:
在任意pos位置可以插入字符或字符串
举个栗子:
int main()
{
//string& insert (size_t pos, size_t n, char c);
string s1("hello world!");
s1.insert(2, 1, 'm');
cout << s1 << endl << endl;
//string& insert (size_t pos, const string& str);
string s2("wmm");
s2.insert(2, s1);
cout << s2 << endl << endl;
//string& insert(size_t pos, const char* s);
string s3("wmm");
s3.insert(0, "wmm ");
cout << s3 << endl;
return 0;
}
运行结果如下:
string& insert (size_t pos, size_t n, char c); //这里在pos位置之前插入一个字符时,需要指定个数n。
注意:传pos时注意边界问题!
从pos位置开始,删除之后len长度的字符。
举个栗子:
int main()
{
string s1("hello world!");
s1.erase(0, 6); //从下标为0开始删除6个字符
cout << s1 << endl << endl;
string s2("wmm");
s2.erase(1); //从下标为1开始,往后删除所有的字符
cout << s2 << endl;
return 0;
}
运行结果:
在字符串后面追加字符串str
举个栗子:
int main()
{
//string& operator+= (char c);
string s1("hello world");
s1 += 'm'; //s1后面追加一个字符
cout << s1 << endl << endl;
//string& operator+= (const char* s);
string s2("hello ");
s2 += "wmm"; //s2后面追加一个字符串
cout << s2 << endl << endl;
//string& operator+= (const string& str);
string s3("cyq ");
s3 += s2; //s3后面追加s2对象
cout << s3 << endl << endl;
return 0;
}
运行结果:
返回C格式的字符串。遇到'\0'就终止。
举个栗子:
int main()
{
string s1("hello world");
s1.resize(20); //size扩大到20(0~19可以用)
s1 += "wmm";
cout << s1 << endl; //按size个数打印,遇到'\0'不打印显示
cout << s1.c_str() << endl; //遇到'\0'就终止
return 0;
}
运行结果:
当我们resize后,后面会自动用'\0'来补齐到size个,这时候我们再在字符串后面追加字符串后,会从s1[20]位置开始插入。这时候两种打印方式是有区别的:
cout << s1 << endl; //按size个数打印,遇到'\0'不打印显示
cout << s1.c_str() << endl; //遇到'\0'就终止
我们不能显示得去传'\0',否则两种形式都遇到'\0'就终止了。
举个栗子:
int main()
{
string s1("hello\0world");
s1 += "wmm";
cout << s1 << endl;
cout << s1.c_str() << endl;
return 0;
}
运行结果如下:
这时候我们发现,两者都是从\0位置开始插入。
我们调试一下来看细节 :
从字符串pos位置开始查找字符c,找到后返回该字符在该字符串中的位置。
找不到时返回-1(npos)。
注意:当我们pos位置的参数不传时,就默认从该字符串中第一个字符的下标开始查找。
举个栗子:
int main()
{
//size_t find (char c, size_t pos = 0) const;
string s1("test.cpp");
size_t pos1 = s1.find('.', 0);
cout << pos1 << endl;
//size_t find (const char* s, size_t pos = 0) const;
string s2("test.cpp.zip");
size_t pos2 = s2.find(".zip", 0);
cout << pos2 << endl;
//size_t find (const string& str, size_t pos = 0) const;
string s3(".zip");
size_t pos3 = s2.find(s3, 0);
cout << pos3 << endl;
return 0;
}
运行结果如下:
find可以查找字符也可以查找字符串。
从pos位置开始往前查找字符c,找到时返回该字符所在字符串中的位置(下标)。
找不到,返回-1(npos)。
注意:当我们pos位置的参数不传时,就默认从该字符串中最后一个字符的下标开始查找。
举个栗子:
int main()
{
//size_t rfind (char c, size_t pos = npos) const;
string s1("test.cpp");
size_t pos1 = s1.rfind('.');
cout << pos1 << endl;
//size_t rfind (const char* s, size_t pos = npos) const;
string s2("test.cpp.zip");
size_t pos2 = s2.find(".zip");
cout << pos2 << endl;
//size_t rfind (const string& str, size_t pos = npos) const;
string s3(".zip");
size_t pos3 = s2.find(s3);
cout << pos3 << endl;
return 0;
}
运行结果如下:
在str中从pos位置开始,截取n个字符,然后将其返回(字符串)。
举个栗子:
我们用substr来分割字符串,可能比较复杂~ 需要对find有深刻的理解。
int main()
{
string s("http://www.cplusplus.com/reference/string/string/substr/");
size_t pos1 = s.find(':');
string str1 = s.substr(0, pos1 + 1);
size_t pos2 = s.find('/',pos1+3);
string str2 = s.substr(pos1+3, pos2 - (pos1+3));
size_t pos3= s.find('/', pos2 + 1);
string str3 = s.substr(pos2 + 1, pos3 - (pos2 + 1));
size_t pos4 = s.find('/', pos3 + 1);
string str4 = s.substr(pos3 + 1, pos4 - (pos3 + 1));
size_t pos5 = s.find('/', pos4 + 1);
string str5 = s.substr(pos4 + 1, pos5 - (pos4 + 1));
size_t pos6 = s.find('/', pos5 + 1);
string str6 = s.substr(pos5 + 1, pos6 - (pos5 + 1));
cout << str1 << endl;
cout << str2 << endl;
cout << str3 << endl;
cout << str4 << endl;
cout << str5 << endl;
cout << str6 << endl;
return 0;
}
运行结果:
看到这里,读者是不是对下标的减法把握不好,在这里博主来用图展示一下:
可能我的表述不太准确,但是我是这样去记得。"我们要的字符串的长度"意思是我们要目标字符串的长度。
和算术运算符中的 + 类似。使用于字符串的相加减。
建议少用,因为涉及深拷贝(之后在讲string模拟实现的时候会详细介绍)的问题,效率是比较低的。
举个栗子:
int main()
{
string s1("cyq ");
string s2 = s1 + "wmm";
cout << s2 << endl;
return 0;
}
运行结果:
输入运算符重载。每次输入时,之前对象里面的字符串会被清空。
举个栗子:
int main()
{
string s1("cyq ");
cin >> s1;
cout << s1 << endl;
return 0;
}
运行结果:
在这里,cin是左操作数,s1是右操作数,上面截图是有两个操作数的。operator+也是类似这样的。
输出运算符重载
举个栗子(实际上我们在上面不断的用这个运算符了):
int main()
{
string s1("cyq ");
cout << s1 << endl;
return 0;
}
运行结果:
cout 作为左操作数,s1作为右操作数。 >>是重载的运算符
获取一行字符串,遇到换行就终止,遇到空格不终止。
举个栗子:
int main()
{
string s;
getline(cin, s);
cout << s << endl;
return 0;
}
运行结果:
在这里我们发现我们输入的有空格,但是在读取过程中,并没有遇到空格就终止。而是当我们按下回车键时才终止接受的。
scanf 、cin...:遇到空格或换行就停止读取。
getline:在OJ题中是经常用到的。
字符串间的大小就比较。
举个栗子:
int main()
{
string s1("cyq");
string s2("wmm");
cout << (s1 == s2) << endl;
cout << (s1 != s2) << endl;
cout << (s1 > s2) << endl;
cout << (s1 < s2) << endl;
return 0;
}
运行结果:
在这里比较大小并不是比较字符串的长度,而是按照strcmp的比较方式来的。(ASCII码表来进行比较) 。
博主就不一一举例所有情况了,使用方法都类似。
只不过在这里注意的是:
比如:cout << (s1 == s2) << endl; //由于==、!=、>、<... 这些运算符的优先级比 << 是低的,这里一定要记住加()。防止因为优先级问题而导致语法错误。
string在做题的时候是经常用到的。上面使我介绍的一些常用接口,如果读者们实际中遇到了个别没见过的可以去官网查看相关用法就好了。我们只需要把常用的接口记住就差不多了,毕竟一个人的精力是有限的~
在这里推荐网站:Reference - C++ Reference
下一篇博客我就要模拟实现string了。希望大家支持~ (由于篇幅原因我就不写在一篇博客里面了。)