一、引入string类
1.1 C语言中的字符串
C语言中,字符串是以’\0’结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,但是这些库函数与字符串是分离开的,不太符合OOP(面向对象)的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。
1.2 C++中的string类
- string是表示字符序列的类
- string类使用char作为它的字符类型。
- string类是basic_string模板类的一个实例,它使用char来实例化basic_string模板类。
- string类的底层实际是一个存储字符元素的顺序表,并且可以根据存入字符的多少动态开辟存储空间。
- 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。
- 注意,这个类独立于所使用的编码来处理字节:如果用来处理多字节或变长字符(如UTF-8)的序列,这个类的所有成员(如长度或大小)以及它的迭代器,将仍然按照字节(而不是实际编码的字符)来操作。因此,string类不能操作多字节或者变长字符的序列。
- 在使用string类时,必须包含头文件#include <string>以及展开命名空间using namespace std;
1.3 其他的实例化版本
以下这篇文章是对字符编码及各字符类型的详细介绍:
二、string类的常用接口说明
2.1 string类接口的学习使用方法
单单string类的常用接口就有上百个之多,我们不可能将每个接口都记住。只需要将一些基本的,重要的记住用熟练,其他接口见过面会用即可。如果在使用过程中对某个函数比较模糊可在下面的几个网站进行查询:
除此之外,STL库中许多函数的设计都是互通的:
学习使用过程中要学会上下联系帮助记忆
2.1 string类对象的常见构造
示范使用构造&拷贝构造:
// constructor
void Test_string1(){
string s1;// 构造空的string类对象s1
string s2("hello world!!!");// 用C字符串构造string类对象s2
string s3 = "bit";// 构造+拷贝构造 优化->直接构造s3
string s4(s2);// 拷贝构造s4
string s5(s2, 6, 5);// 部分拷贝构造:第三个参数缺省取到字符串末尾,大于字符串长度也取到末尾。
string s6("I love China", 5);// 取字符串的前n个字符构造对象s6
string s7(10,'x');// 用n个字符构造对象s7
string s8(s2.begin()+6, s2.end()-3);// 拷贝构造的迭代器区间版本
cin >> s1; //operator>>
cout << s1 << endl; // operator<<
//此处其他对象的输出省略
//......
}
运行结果:
注意:C++string类符合C语言的字符串规范:字符串末尾以’\0’结尾。‘\0’不算有效字符,即使是空字符串(_size==0),也会在第一个位置存储一个’\0’。
2.2 string类对象的容量操作
2.2.1 clear
// clear
void Test_string8(){
cout << "Test_string8:" << endl;
string s1 = "hello world!!";
cout << s1 << endl;
cout << s1.size() << endl;
cout << s1.capacity() <<endl;
s1.clear(); //clear()只是将string中有效字符清空(_size),不改变底层空间大小(_capacity)
cout << "after:" <<endl;
cout << s1 << endl;
cout << s1.lenth() << endl;
cout << s1.capacity() <<endl;
}
运行结果:
提示:
-
clear() 只是将string中有效字符清空(_size),不改变底层空间大小(_capacity)。
-
size()与length()方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一致,一般情况下基本都是用size()。
注意:对于string类,size()返回字符串中有效字符的个数(不包括’\0’);capacity()返回该对象能存储的有效字符的个数(不包括’\0’)。
2.2.2 reserve & resize
// reserve & resize
void Test_string7(){
cout << "Test_string7:" << endl;
string s1;
//s1.reserve(1000); //只开空间:为sz预置1000个字符的内存
s1.resize(10, 'a'); //开空间+初始化:为sz预置10个字符的内存,并将其全部初始化为'a'
size_t cap = s1.capacity();
cout << s1 << endl;
cout << "_capacity: " << cap << endl;
cout << "_size: " << s1.size() << endl;
// 这段代码是为了查看编译器的string类扩容机制
for(int i = 0; i < 1000; i++)
{
s1.push_back('x');
if(s1.capacity() != cap)
{
cap = s1.capacity();
cout << "new capacity: " << cap << endl;
}
}
cout << endl;
}
运行结果:
注意:
-
resize(size_t n) 与 resize(size_t n, char c)都是将字符串中有效字符个数改变到n个,不同的是当字符个数增多时:resize(n)用0来填充多出的元素空间,resize(size_t n, char c)用字符c来填充多出的元素空间。
注意:resize在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小;如果是将元素个数减少,底层空间总大小不变。
-
reserve(size_t res_arg=0):为string预留空间,不改变有效元素个数。对string操作时,如果能够大概预估到放多少字符,可以先通过reserve把空间预留好。
注意:当reserve的参数小于string的底层空间大小时,reserver不会改变容量大小。
string类的扩容机制:
这是同样的一段代码在VS2013下的编译运行结果:
结论:vs2013下大概是1.5倍扩容,而g++下是2倍扩容
注意:由此可以看出,我们编写的代码不能依赖于底层实现,因为不同的编译环境的底层实现不同,编译运行结果也就不同。
2.3 string类对象的访问及遍历操作
2.3.1 operator[] & at 通过下标遍历字符串
示例代码:
//operator[]
void Test_string3(){
string s1 = "hello";
//cout << s1[0] << endl; //调用[]重载访问元素
cout << s1.at(0) << endl; //调用at函数访问元素
s1[0] = 'H';
cout << s1 << endl;
for(int i = 0; i<s1.size(); i++){
s1[i]^=32; //大小写互换
}
cout << s1 << endl;
const string s2 = "operatro[]";
for(int i = 0; i<s2.size(); i++){
//s2[i]++; //error:此处调用的是const[]的重载函数,不能进行++操作
cout << s2[i] << " ";
}
cout << endl;
}
运行结果:
总结:
-
operator[]内部会检查越界,pos必须小于_size;如果越界访问程序运行时会报错。
-
返回引用的作用:1.减少拷贝;2.可以修改元素
-
operatro[]和at的区别:二者都是返回指定位置元素的引用,但他们的越界处理不同。operator[]是函数内断言报错,而at则是抛异常报错。
2.3.2 iterator 迭代器
迭代器作为STL的六大组件之一,为所有容器提供了通用的遍历访问方式,且用法类似便于学习使用。
使用迭代器遍历字符串:
//iterator
void Test_string4(){
//string类的迭代器使用
cout << "string:" << endl;
string s1("hello");
string::iterator it = s1.begin();
while(it != s1.end()) //此处应该使用!=
{
(*it)^=32;
++it;
}
cout << s1 << endl;
//范围for的使用,别着急下面有介绍↓↓↓
for(auto e : s1)
{
//e^=32; //无效,因为e是s1元素的拷贝
cout << e << " ";
}
cout << endl;
//要想修改各元素的值,需取各元素的引用
for(auto &e : s1)
{
e^=32;
}
cout << s1 << endl;
//......
}
运行结果:
迭代器的图形表示:
总结:
- 对于string和vector这些顺序存储的容器不太喜欢使用迭代器,因为operator[]更好用。
- 但是对于list/map/set…这些存储结构不连续的容器,只能使用迭代器遍历访问。
- 因此iterator是所有容器的通用遍历访问方式,且用法类似,一通百通。
- 迭代器iterator的使用类似于指针,像解引用操作(*it)和++操作(++it)。
- 对于string和vector这些类顺序存储的容器,迭代器的底层实现就是原生指针。
- 但是对于list/map这类存储结构不连续的容器,迭代器的底层实现不是指针,因为指针++不能访问到其下一个节点(元素)。
注意:迭代器end()是最后元素的下一个位置(string指向’\0’),也就是说迭代器的开始和结束是一个左闭右开区间。
使用迭代器遍历list链表:
//........
//list类的迭代器使用
cout << "list:" << endl;
list<int> lt(10,2);
list<int>::iterator lit = lt.begin(); //定义了一个链表(存储int类型)的迭代器
while(lit != lt.end())
{
cout << *lit << " ";
++lit;
}
cout <<endl;
//范围for的使用,别着急下面有介绍↓↓↓
for(auto e : lt)
{
cout << e << " ";
}
cout << endl;
运行结果:
iterator遍历list的过程和遍历string类的过程极其相似,这里我们可以看出迭代器的通用性。
list容器实际上是一个带头双向循环链表:
注意:链表的迭代器就不是单纯的指针了,因为链表节点在内存中并不是连续存储的,++不能跳到下一个节点。
2.3.3 reverse_iterator 反向迭代器
//reverse_iterator & const_iterator
void Test_string5(){
//string类反向迭代器的使用
string s1 = "hello";
string::reverse_iterator rit = s1.rbegin();//rbegin返回反向迭代器开始
while(rit!=s1.rend())//rbegin返回反向迭代器结束
{
cout << *rit << " ";
++rit;//++反向迭代器,实际是向前遍历
}
cout << endl;
//隐式类型转换生成的临时对象具有常性,需用const引用接收↓↓↓
PrintString("I love China!!");
}
运行结果:
反向迭代器的图形表示:
注意:反向迭代器++实际上是倒着向前遍历
总结四种迭代器类型:
正向迭代器:iterator / const_iterator
反向迭代器:reverse_iterator / const_reverse_iterator
//这里演示的是const迭代器
void PrintString(const string &str){
//正向迭代器遍历
string::const_iterator cit = str.begin();//const迭代器
while(cit!=str.end())
{
cout << *cit << " ";
++cit;
}
cout << endl;
//反向迭代器遍历
string::const_reverse_iterator crit = str.rbegin();//const反向迭代器
while(crit!=str.rend())
{
cout << *crit << " ";
++crit;
}
cout << endl;
}
运行结果:
begin() & end():
注意:编译器会选择最合适的begin/end进行调用:普通对象调用普通begin/end;const对象调用const begin/const end;
2.3.4 范围for
-
范围for——自动迭代,自动判断结束
-
底层实现:通过查看汇编代码可知,范围for底层其实就是迭代器。
-
工作原理:对于编写迭代器的对象,通过迭代器依次取对象中的每一个元素赋值给定义的自动变量,从而完成遍历操作。
注意:
- 范围for只能正向遍历,不能反向遍历。
- 范围for中定义普通的自动变量,不能在遍历中修改各元素的值;需要定义引用类型的自动变量,才能进行修改。
2.4 string类对象的修改操作
2.4.1 push_back & append & operator +=
//push_back & append & operator +=
void Test_string6(){
string s1("hello");
s1.push_back(' '); //只能用来尾插字符
s1.append("world"); //用于尾插字符串
s1.append(3,'!'); //追加3个'!'
cout << s1 << endl;
string s2 = "hello China";
s1+=' ';
s1+=s2;
s1+="!!!";
cout << s1 << endl;
s1.append(s2.begin()+5, s2.end()); //append的迭代器区间版本
cout << s1 << endl;
}
运行结果:
注意:
- 在string尾部追加字符时,s.push_back(c) / s.append(1, c) / s += 'c’三种的实现方式差不多,一般情况下string类的+=操作用的比较多,+=操作不仅可以连接单个字符,还可以连接字符串。
- +=重载函数的底层封装push_back和append实现字符串的连接功能。
2.4.2 insert & erase
insert & erase 实现指定位置插入和删除字符、字符串的功能。
下面的代码将字符串中的空格替换为+号:
//insert & erase
void Test_string9(){
string str = "wo lai le";
//使用insert & erase成员函数,效率低
int pos = 0;
while((pos = str.find(' ', pos)) != string::npos)
{
str.erase(pos,1);
str.insert(pos,"+");
pos+=1;
}
//典型的空间换时间的做法,高效
string retstr;
for(size_t i = 0; i<str.size(); i++)
{
if(str[i]!=' ')
{
retstr+=str[i];
}
else{
retstr+='+';
}
}
cout << retstr << endl;
}
运行结果:
注意:
- insert & erase函数须通过移动指定位置之后的每一个字符实现插入和删除功能,因此效率较低。
- 例如,如果使用insert函数实现连续的头插复杂度为O(N^2),可以通过push_back尾插再reverse逆置的方法将复杂度降至O(N)。
2.4.3 c_str
c_str返回string对象的C字符串形式:const char*
以下代码使用C方法打开并读取源文件代码:
//c_str
void Test_string10(){
string filename = "test.cc";
FILE *fout = fopen(filename.c_str(), "r");//fopen的第一个参数是const char*
char ch = fgetc(fout);
while(ch != EOF)
{
cout << ch;
ch = fgetc(fout);
}
fclose(fout);
}
C字符串 VS string字符串类
分别输出和拷贝C字符串和string类字符串:
void Test_string11(){
string str = "hello";
str += '\0';
str += "world";
cout << "Cstr out: " << str.c_str() << endl;
cout << "string out: " << str << endl;
char Ccopy[15];
strcpy(Ccopy, str.c_str());
string strcopy = str;
cout << "Cstr copy: " << Ccopy << endl;
cout << "string copy: " << strcopy << endl;
}
运行结果:
总结:
- C字符串以’\0’为字符串结束的标志,不管是输出还是拷贝遇到’\0’就停止。
- string类字符串不以’\0’为结束标志,而是以成员变量_size为准,因此string类字符串可以存储字符’\0’
- 但为了向前兼容C,C++string类字符串仍以’\0’结尾。
2.4.4 find & rfind & substr
-
find从前往后查找指定的字符或字符串,返回其下标;
-
rfind从后往前查找指定的字符或字符串,返回其下标;
-
substr截取指定位置以后的n个字符,并返回该字符串。
以下的代码用于截取文件名后缀:
//find & rfind & substr
void Test_string12(){
//测试一:
string str = "test.cc.zip";
//size_t pos = str.find('.'); //find从前往后查找
size_t pos = str.rfind('.'); //rfind从后往前查找
if(pos!=string::npos)
{
string suff = str.substr(pos);//第二个参数缺省即截取到末尾
cout << suff << endl;
}
//测试二:↓↓↓
string url1 = "https://cplusplus.com/reference/string/string/";
string url2 = "https://blog.csdn.net/zty857016148?spm=1011.2124.3001.5343";
string url3 = "https://www.baidu.com/";
string url4 = "https/www.baidu.com/";
DealUrl(url1);
cout << endl;
DealUrl(url2);
cout << endl;
DealUrl(url3);
cout << endl;
DealUrl(url4);
cout << endl;
}
运行结果:
提示:
- npos是string类中的静态const成员,值为无符号-1即无符号整形的最大值。
- npos取整形最大值的目的在于:1.substr中表示取到字符串末尾;2.find中表示找不到该字符、字符串
以下代码用于分割网址url:
void DealUrl(const string &url){
string protocol;
string domain;
string uri;
size_t pos1 = url.find("://");
if(pos1 == string::npos)
{
cout << "invalid url" << endl;
return;
}
protocol =url.substr(0,pos1);
pos1+=3;
size_t pos2 = url.find('/', pos1);
if(pos2 == string::npos)
{
cout << "invalid url" << endl;
return;
}
domain = url.substr(pos1, pos2-pos1);
uri = url.substr(pos2+1);
cout << "protocol: " << protocol << endl; //网络协议
cout << "domain: " << domain << endl; //域名
cout << "uri: " << uri << endl; //路径及文件名
}
运行结果:
小技巧:字符个数取左闭右开区间的差
2.5 string类非成员函数
2.5.1 数值字符串相互转换
数值转字符串:
to_string:其各个重载版本可以将几乎所有内置类型数值转换为字符串
字符串转数值:
//to_string & stoi & stod
void Test_string13(){
int ival;
double dval;
cin >> ival;
cin >> dval;
string istr = to_string(ival);
string dstr = to_string(dval);
cout << istr << endl; //输出字符串
cout << dstr << endl;
istr = "123456";
dstr = "3.1415";
ival = stoi(istr);
dval = stod(dstr);
cout << ival << endl; //输出数值
cout << dval << endl;
}
运行结果:
2.5.2 getline
从指定流对象中获取输入,直到遇到指定的分割符号delim或者是换行符’\n’(未指定)才停止输入。
int main()
{
string line;
// 不要使用cin>>line,因为会它遇到空格就结束了
// while(cin>>line)
while(getline(cin, line))
{
size_t pos = line.rfind(' ');
cout<<line.size()-pos-1<<endl;
}
return 0;
}
三、vs和g++下string结构的说明
注意:下述结构是在32位平台下进行验证,32位平台下指针占4个字节。
3.1 vs下string的结构
string总共占28个字节,内部结构稍微复杂一点,其中包括:
1._Container_proxy *_Myproxy:指向_Container_proxy对象的指针,该对象用于管理标准STL中的迭代器对象。
2. size_t _Mysize:字符串的有效长度
3. size_t _Myres:字符串的底层容量
4. _Bx:是一个联合体类型,其中包含一个大小为16字节的字符数组 _Buf;和两个指针 _Ptr和_Alias
- 当字符串长度小于15时,使用内部固定的字符数组 _Buf来存放
- 当字符串长度大于等于15时,从堆上开辟空间,由 _Ptr指向这段空间。_Buf闲置不用。
- 这种设计也是有一定道理的,大多数情况下字符串的长度都小于15,那string对象创建好之后,内部已经有了16个字符数组的固定空间,不需要通过堆创建,效率高。
故总共占16+4+4+4=28个字节。
3.2 g++下string的结构
g++下的string结构相对简单:
string对象总共占4个字节,内部只包含了一个指针,该指针将来指向一块堆空间,内部包含了如下字段:
- _M_length:空间总大小
- _M_capacity:字符串有效长度
- _M_refcount:引用计数
- _Ptr:指向堆空间的指针,用来存储字符串。
g++下string的结构图:
引用计数 + 写时拷贝:
- 在进行拷贝构造或是赋值操作的时候,g++下面会先发生浅拷贝,先不开空间。
- 每进行一次浅拷贝,对应的引用计数就会+1。
- 只有当对空间进行写入操作的时候才会发生写时拷贝,即进行空间的深拷贝。
- 每进行一次写时拷贝,对应的引用计数就会-1。
- 每个对象进行析构时,先会看引用计数是否为0,如果不为0引用计数-1即可;如果为0,则释放空间,清理资源。