string类介绍
STL定义
是c++标准库的重要组成部分
为什么要学习string类
c语言中字符串是以\0结尾的字符的集合,为了操作方便,c语言中提供了一些str库函数,但是这些库函数和字符串是分开的,不太符合oop的思想,而且底层空间需要用户自己维护。
string类的介绍
1 string是表示字符串的字符串类
2 该类的接口与一些常见的容器接口相同,添加了一些专门操作string的常规操作
3在使用string类的时候,必须加上#include<stdio.h>和using namespace std;
string的常见构造函数
string()
构造一个空的string对象,也就是一个空字符串
#include<iostream>
#include<string>using namespace std;
int main()
{string s1;cout<<"s1的长度:"<<s1.size()<<endl;return 0;
}
string (const char* s)
用c风格的字符串(以"\0"结尾的字符数组)来构造string对象
string s2("hello world");
string(size_t n,char c)
构造一个字符对象,其中包含n个字符c
string s4(5,'a');
string(const string& s)
用已有的string对象来构造新的string对象
string s1("hello world");
string s2(s1);
string对象的容量操作
size
返回字符串有效字符的长度,也就是字符串中所包含实际字符的个数,不包括"\0"
string s("hello");
cout<<"s的size"<<s.size()<<endl;
length()
和size()功能完全相同
capacity()
返回当前已分配的总空间大小,包括未分配的和已分配的,也就是字符串底层数组能容纳的字符个数。
empty()
判断当前字符串是否为空串,如果是空,返回true,如果不是,返回false。
clear()
清空字符当中的有效字符,使字符串变成空串,但不改变底层空间的大小。
reserve()
为字符串预留空间,如果参数小于字符串当前容量,则容量不会发生改变。
s.reserve(100);
resize()
将字符串的有效个数改成n个。如果n比原来字符串多,就将多出的部分用'c'填充(如果没有指定c则用'\0'填充),如果n比原来的字符串少,则截断。
string s="hello";
s.resize(8,'x');
s.resize(3);
string对象的访问以及遍历操作
operate[]
返回字符串pos位置的字符。对于const string类型对象调用,返回字符的常引用,不能修改;而对于非const string类对象调用,可以修改。
string s="hello";
s[0]='H';//可以修改const string s2="hi";//不能修改
begin+end
begin函数获取指向第一个字符串的迭代器,end函数获取指向最后一个字符下一个位置的迭代器,通过迭代器可以遍历字符串。
string s="hello";
for(string::iterator it=s.begin();it!=s.end();it++)
{cout<<*it;
}
cout<<end;
return 0;
rbegin+rend
rbegin函数获取指向最后一个字符串的反向迭代器,rend函数指向第一个字符前一个位置的反向迭代器,通过反向迭代器可以遍历字符串
for(string::reverse_iterator rit=s.rbegin();rit!=s.rend();rit++)
{cout<<*rit;
}
范围for
遍历容器中的数组
string s="hello";
for(char c:s)
{cout<<c;
}
cout<<endl;
string对象的修改
push_back
在字符串的末尾插入一个字符c
s.push_back('!');
append
在字符串的末尾追加一个字符串
s.append("world");
operator+=
在字符串的末尾追加字符串str
string s="hello";
s+=“world”;
c_str
返回c格式的字符串(即const char*类型的字符串),可用于与c语言的函数进行交互
string s="hello";
const char* str=s.c_str();
find+npos
从字符串的pos位置开始往后查找字符c,返回该字符在字符串中的位置,如果未找到,返回string::npos。
string s="helo world":
size_T pos=s.find('o');
if(pos!=string::npos)
{cout<<"n的位置是"<<pos<<endl;
}
else
{cout<<"未找到";
}
rfind
从字符串的pos位置,开始往前查找字符c,返回字符在字符串中的位置,如果找不到,返回string::npos。
string s="hello";
size_t pos=s.rfind('o');
if(pos!=string::npos)
{cout<<"从后往前找o的位置是"<<"pos"<<endl;
}
else
{cout<<"未找到\n"<<endl;
}
substr
从pos位置开始,截取n个字符
string s="hello";
string sub=s.substr(6,5);
注意
在string尾部追加字符的时候,用s.push_back(),s.append(1,c),s+='c'的效果都差不多。一般情况下,string类用+=比较多,+=不仅可以连接字符,还可以连接字符串
string非成员函数
operator+
用于连接两个字符串,返回新的字符串。
注意尽量少使用,因为他是传值返回,会进行深拷贝,效率比较低。如果需要频繁进行字符串连接操作,建议使用operator+=。
string s1="hello";
string s2="world";
string s3=s1+s2;
operator<<
输入运算符重载,用于从输入流中读取字符串到string中。默认遇到空格、制表符、换行符就停止。
string s;
cout<<"请输入字符串";
cin>>s;
cout<<"你输入的字符串"<<s<<endl;
请输入字符串: hello world
你输入的字符串: hello
operator>>
输出运算符重载,用于将string对象中的字符串输出到输出流当中。
string s="hello";
cout<<"输出字符串:"<<s<<endl;
getline
获取一行字符串,包括空格,直到换行符为止
string s;
cout<<"请输入一行字符串:";
getline(cin,s);
cout<<"你输入的一行字符:"<<s<<endl;
return 0;
relational operators
用于字符串之间的比较
包括==,!=,<,>。比较是按照字符的ascii码依次进行的。
vs下string的结构
在32位平台下,vs的string总共占28个字节,其内部采用了一种较为复杂的设计结构,通过联合体(union)来管理字符串的存储空间。
联合体_Bxty的作用
联合体_Bxty用于定义string的存储空间:
1当字符串长度小于16时,使用内部固定的字符数组_Buf来存放字符串。这样的设计很高效,因为大部分的字符串长度都比较小,不需要额外从堆上分配内存,这样减少了内存分配的开销,提高了效率
2 当字符串长度大于16时,从堆上开辟空间来存储字符串,此时会使用指针_Ptr指向堆伤分配的内存
各部分所占字节
联合体部分,_Buf、_Ptr、_Alias共享同一块内存空间,_Buf 是长度为 _BUF_SIZE(这里 _BUF_SIZE 为 16)的字符数组,在 32 位平台下,字符占 1 个字节,所以这部分占 16 个字节。
size_t :在32位平台下,size_t是4个字节
一个字段size_t保留从堆上开辟开辟空间的总容量size_t,是4个字节
用于其他操作的指针:在32位平台下,占4个字节
所以总字节数是16+4+4+4=28字节
g++下string的结构
g++下的string结构采用写时拷贝优化,目的是减少不必要的内存复制
其核心是:1 读共享:多个string对象可以共享同一个堆内存
2写分离:当只有某个对象需要修改字符,才会复制一份内存
string对象总共占4个字节,内部只包含了一个指针,该指针用来指向一块堆空间,内部包含了如下字段,
1空间总大小 2字符串有效长度 3引用计数
string对象的模拟实现
c++中,当类未显示定义拷贝构造函数和赋值运算符重载的时候,编译器会自动生成默认版本,默认的拷贝构造和赋值操作是浅拷贝
错误的实现(浅拷贝)
#include<cstring>
#include<cassert>
#include<iostream>
//存在浅拷贝的String类
class String
{public:String(const char* str=" "){if(nullptr==str){assert(false);return;}_str=new char[strlen(str)+1];strcpy(_str,str);}
//析构函数:释放动态分配的内存
~String(){if(_str){ delete[] _str;_str=nullptr;}private:char* _str;
}
void test//触发浅拷贝问题
{String s1("hello");String s1(s2);//程序运行到此处s1和s2的_str指向同一块资源
//当函数结束时,s2先析构,s1再析构
//导致double free,程序崩溃}
错误原因分析:
1默认拷贝构造函数的浅拷贝:编译器生成的默认拷贝构造函数只会复制_str指针本身,导致s1._str和s2._str指向同一块堆内存
2重复释放内存:函数结束时,s2和s1依次析构,两次调用 delete[ ] _str释放同一块内存
string的正确实现
#include<cstring>
#include<cassert>
#include<iostream>
using namespace std;
class String
{public:String(const char* str=""){if(nullptr==str){assert(fasle);return;}_str=new char[strlen(str)+1];strcpy(_str,str);}
//拷贝构造函数
String(const String& s)
{_str=new str[strlen(s._str)+1];strcpy(_str,s._str);
}
//赋值运算符重载(深拷贝)
String& operator=(const String& s)
{if(this!=&s)//避免自赋值,比如s1==s2{//先分配新内容,复制内容,防止原内存释放后拷贝失败char* newStr=new char[strlen(s._str)+1];strcpy(newStr,s._str);delete[] _str;_str=newStr;//指向新内存}return *this;
}
~String()//析构函数
{if(_str){delete[] _str;_str=nullptr;}}
const char* c_str() const{return const_str;
}
private:
char* _str;
};
void test()
{String s1="hello";String s2(s1);cout<<s1.c_str()<<endl;//输出hellocout<<s2.c_str()<<endl;//输出helloString s3("world");s2=s3;cout<<s2.c_str()<<endl;//输出worldcout<<s3.c_str()<<endl;//输出world(不受s2影响)
}
修正说明:
1 显示定义拷贝构造函数:为新对象分配独立内存,并复制原对象的字符串内容,确保s1和s2的_str指向不同内存
2 显示定义运算符重载,先分配新内存复制内容,再释放旧内存,避免自赋值问题和内存泄漏