string是c++中表示字符串的字符串类,要使用需要包头文件:#include<string>
先了解一下string的一些信息
string看起来是一个类,但实际上是typedef的模板。
在cplusplus.com网站上,string的相关信息
模板的实例化结果有以下几个,其中string最常用,有些情况下也会用到u16string,u32string,wstring
由图可以看出,这些实例化的结果都是由basic_string模板实例化得来的,至于为什么要用不用的类型来实例化出这些类,就涉及到编码了。
世界上第一个编码是ASCII码,Unicode(utf-8,utf-16,utf-32),gbk也是编码
有了ASCII码表,就能通过计算机储存的二进制数字表示出信息。
但是ASCII码表仅能表示英文,随着经济全球化,计算机上需要显示其他国家的语言。
比如汉语,汉语的复杂,一个字符无法表示清楚(一个字符的大小时1字节,仅能表示256个不同信息),所以就用2个字符表示。不常见的汉字可能会用到3~4个字节。gbk就是针对汉语的编码表。而Unicode则是针对全世界的文字设计的编码表,其内容更复杂。Unicode和gbk都是要兼容ASCII码的。
Windows下的编码一般使用gbk,Linux下的编码一般使用utf-8。
string为什么要设计成模板?
因为string是char实例化出来的,能很好的对应英文,但是gbk中的字符,也就是汉字,需要两个字节,一个字符不在对应一个char,所以设计出新的的类型char16_t(对应utf16),wchar_t(对应gbk),模板是为了应用到不同的场景(即存在不同的编码)中,比如在char16_t中一个字符就不是一个字节了。所以对不同类型(char16_t等)的字符串进行处理时,需要用到不同的类(u16string等)。
string类具有一些优秀的功能,比如string类中字符串的连接,直接+=就可以。
int main()
{
string sql = "hello ";
sql += "world"
cout << sql << endl;
return 0;
}
会打印出:hello world
cplusplus.com
cppreference.com
c++的学习要学会看文档,上面两个是看文档的网站
第一个内容更合理,看起来比较舒服。
第二个是c++的官方网站,语法都是最新的。但是整体内容相对第一个比较乱。
官方网站叫这个的原因是第一个网站名被抢注了。
这两个网站都是英文文档,没有好的中文文档,从这里开始要学会看英文文档了,这是必备的技能之一。
英语不好的同学可以建立一个常见英文库,毕竟这是某一领域的英文文献,词汇量并不大。
下面使用的图片都是第一个网站的。
string这样的知识点通常分为几个板块
最前面的:
头文件包含。
第一个,类的声明:介绍string
第二个,对string的一些说明
重点关注的是下面几个板块:
成员函数:成员函数的种类为4个。第一个包含了构造函数和拷贝构造函数。
迭代器:所有数据结构容器都有
容量相关的接口:
访问相关的接口:
还有修改相关函数接口(modifiers),操作相关函数接口(
String operations)
还有非成员函数的重载:
毕竟不可能记住全部的函数,所以查文档是程序员必备的技能。
string的使用
string是管理动态增长的字符数组,这个字符串以\0结尾。
#include<string>
int main()
{
//string s;这样会报错,编译器不认识string,string未声明
std::string s;
//因为string属于std,c++标准库的内容都是放到std的
return 0;
}
首先看看构造函数的情况,单击框起来的部分
并不是每个函数都要学,string相关函数就有100+个,每个都学根本记不住,学常用的就可以了。遇到不常用的情况才查文档。
常用的如上图
int main()
{
string s1;
//构造一个无参的string,但是会有一个\0
string s2("hello world");
//构造一个常量字符串初始化的string
s2 += "!!!";
//动态增长意为着可以增删查改,string的空间是从堆上申请的,增的方式就是+=
string s3(s2);
string s4 = s3;
//s3,s4则是拷贝构造函数
//下面介绍一些不常用的
string s5("
https://cplusplus.com", 5);
//第5个函数,意为使用字符串的前5个字符来进行初始化
string s6(10, 'a');
//第6个函数,意为使用10个字符'a'来初始化
string s7(s2, 6, 3);
//第3个函数,意为使用s2的一部分来初始化,6代表从下标为6的位置开始,3代表从开始位置的后三个字符,len有一个npos的缺省值,npos是属于string类里面的一个静态成员变量,大小为整形最大值,len如果超过字符串的长度了,就有多少取多少。
return 0;
}
每个板块的下面都是有相应的函数说明,所以不用担心函数中出现的变量不知道什么意思。
析构函数是自动调用的,没什么需要特别说明的
赋值重载函数:
int main()
{
string s1("hello");
string s2("!!!");
s2 = s1;
//赋值后,s2会变成hello,而不是!!!lo
s1 = "wor";
//
赋值后,s1会变成wor,而不是worlo
s1 = 'a';
//
赋值后,s1会变成a,而不是aorlo
return 0;
}
后两个函数用的不多,string设计的比较早,所以很多设计不成熟,出现了冗余。
简单展示一下string的底层原理
template T
class basic_string
{
public:
basic_string(const T* str)
//这只是介于string的简单实现,实际情况要考虑各种类型,比这个复杂
{
size_t len = strlen(str);
_str = new T[len + 1];
strcpy(_str, str);
//不能不开辟空间,直接赋值,即:_str = str;
//如果str是一个常量字符串的指针,_str就指向了一个常量字符串,这样就不能扩容了
}
private:
T* _str;
size_t size;
size_t capacity;
};
对于传统的数组,检查越界是一种抽查;而对于string,只要越界就会报错。
如何遍历string中的每个字符?
在C语言中,我们使用指针来遍历字符串,在c++中还使用指针吗?
int main()
{
string s1("hello world");
//方式一:下标+[]
//c++有[]运算符重载
for(size_t i = 0; i < s1.size(); i++)
//s1.size()是一个函数,计算字符串长度,不包含\0
{
cout << s1[i] << " ";
//相当于调用函数s1.operator[](i);
}
//方式二:迭代器
string::iterator it = s1.begin();
//it是迭代器名称,和变量名类似,s1.begin()函数返回开始位置的迭代器
//iterator是内嵌在string中的,iterator是string的内嵌类型,iterator是在string中定义或者typedef出来的
//通过这样的方式来取迭代器的类型,如果是vector的迭代器:vector<int>::iterator
while(it != s1.end())
//s1.end()函数返回结束位置(最后一个字符)的下一个位置(\0)的迭代器,是一个左闭右开的关系,为了方便遍历,所有迭代器都符合这个关系,
it != s1.end()
是迭代器的标准用法,有人会想用
it < s1.end(),这个只能在地址空间连续的情况使用
{
cout<< *it << " ";
it++;
}
//迭代器是像指针一样的东西,或者就是指针,具体看底层实现
//所有的迭代器都是差不多的,基本上学会string的迭代器用法,就会其他迭代器的用法了。
//自己写的数据结构也可以有自己写的迭代器,符合某一行为的都可以称为迭代器
//迭代器前期最好自己写,后期熟悉了可以用auto自动推导。
//方式三:范围for,c++11
for(auto a : s1)
//范围for其实就是被替换的迭代器
{
cout << a <<" ";
//自动取字符,自动++
}
return 0;
}
由图中可知,operator[]是引用返回,引用返回处了可以减少拷贝,还有一个作用:支持修改返回对象,比如在交换string中的两个字符时,s[a]和s[b]直接就交换了,不用取地址,再解引用。
在早期,还有一个at函数接口,其功能和[]一样。取下标位置为0的字符:s[0] / s.at(0)
但是还是存在区别,比如访问越界时,[]直接断言报错,s.at()则是抛异常
迭代器分四种
第一个就是之前写的正向迭代器
string s1("hello world");
string::iterator it = s1.begin();
while(it != s1.end())
{
cout<< *it << " ";
it++;
}
第二种迭代器:反向迭代器
string s1("hello world");
string::reverse_iterator rit = s1.rbegin();
//rbegin()在字符串\0的前一个位置
while(rit != s1.rend())
//rend()在字符串首元素的前一个位置
{
cout << *rit << " ";
rit++;
//反向迭代器的++是按反的方向走
}
单链表就没有反向迭代器
迭代器的功能和下标类似,现在的string和后面的vector都可以选择下标,到list就只能选择迭代器了。
迭代器类似指针,所以迭代器除了读取数据,还可以修改数据,如果string引用传参给const string类型,就意味着不能用string类型的对象来写迭代器了,必须用const string类型的对象来写迭代器。
void func(const string& rs)
//只能读的正向迭代器
{
string::const_iterator it = rs.begin();
while(it != rs.end())
{
cout<< *it << " ";
it++;
}
string::const_reverse_iterator rit = rs.rbegin();
//只能读的反向迭代器
while(rit != rs.rend())
{
cout << *rit << " ";
rit++;
}
}
int main()
{
string s1("hello world");
func(s1);
return 0;
}
这就是剩下的第三,第四种迭代器。
c++11为了更规范化,添加了cbegin等函数,用来解决被const修饰的string,但是begin()函数的自动识别就挺好用的。
size的功能是返回有效字符的个数,length也一样。因为string诞生的时间比STL早,所以当时使用length表示字符个数更合适,STL出现后,因为树等数据结构不适合用length表示,所以用size表示元素个数。string也是管理字符串的数据结构,就有了size函数,实际应用中size使用的更多一点。
上面代码是观察扩容规律的。
除了第一次,其余都是1.5倍扩。
扩容是存在损耗的,有没有办法减少扩容?
这个函数可以减少扩容。注意:reserve不是reverse
reserve可以根据需要提前开好足够的空间(容量会被修改,不是多扩1000)
string s;
s.reserve(1000);
size_t sz = s.capacity();
cout << "making s grow:\n";
……
涉及对齐问题,所以实际中会多开一点。
还有一个resize函数
s.resize(1000);
和reserve相比,resize除了会扩空间外,还会将空间内容初始化为\0。如果不想是\0,还可以自己定义初始化内容
s.resize(1000, 'x');
观察不同函数带来的效果
string s("hello");
s.reserve(100);
s.resize(100);
//s.reserve(100);已被注释掉
已经存在5个元素,补95个'x'。
在s.resize(100);的情况下,
再添加两行代码:
s.reserve(10);
//结果无变化,说明reserve不会缩
s.resize(10);
//容量无变化,元素个数改变了
string增删查改
插入一个字符可以用push_back,插入字符串可以用append,append很像构造函数。图示两个最常用。
但尾插最好用的是+=。
头插或者中间插要用到insert,删除则要用到erase。
图示的两个为最常用的两个。
第二个函数只会删除一个字符
swap函数
string s1("hello");
string s2("world");
s1.swap(s2);
swap(s1,s2);
这两个swap有什么区别吗?
第一个swap效率高,第二个swap效率低,这是c++98,c++11情况又不一样了。
因为s1,s2分别指向一个空间,所以第一个swap的交换,改变的是指向空间的方向,第二个swap的交换是深拷贝交换。
c_str函数
c_str就是返回c形式的字符串,即返回指向字符串的指针。一般的string对象包含三个成员:指针,size,capacity,c_str就是把指针单独拿出来使用。
主要的功能是和c的一些接口(函数)形成配合。
cout << s1 <<endl;
cout << s1.c_str() << endl;
两者都是打印s1中存储的字符串,但第一个是string的流插入重载,第二个则是字符串打印。
有时我们需要取出文件的后缀名
string file("test.cpp");
这时就需要find函数,确定要找到的位置
string file("test.cpp");
size_t pos = file.find('.');
if(pos != string::npos)
{
string suffix = file.substr(pos, file.size() - pos);
cout << file.c_str << "后缀:" << suffix.c_str <<endl;
}
else
{
cout << "没有后缀" << endl;
}
substr是取字符串子串的函数
取出pos位置后len个位置的子串。
但是存在包含多个字符 '.' 的文件,比如:test.c.tar.zip
这时就需要反着找第一个字符 '.'
string file("test.c.tar.zip");
size_t pos = file.rfind('.');
if(pos != string::npos)
{
string suffix = file.substr(pos);
cout << file.c_str << "后缀:" << suffix.c_str <<endl;
}
else
{
cout << "没有后缀" << endl;
}
统一资源定位符(Uniform Resource Locator,URL)是对可以从互联网上得到的资源的位置和访问方法的一种简洁的表示,是互联网上标准资源的地址。互联网上的每个文件都有一个唯一的URL,它包含的信息指出文件的位置以及
浏览器应该怎么处理它。
考虑的再复杂一点,分离网址中的域名
string url1("
https://www32.cplusplus.com/reference/string/string/rfind/");
string url2("
力扣");
://之前的部分称为协议,://到第一个/之间的部分称为域名,剩下的部分是类似目录一样的东西,
string url1("
https://www32.cplusplus.com/reference/string/string/rfind/");
string url2("
力扣");
string& url = url1;
//这里用引用url1,再需要分离其他网址的域名,就不需要修改下面的url,在这里改变引用对象就行
string protocol;
size_t pos = url.find("://");
//返回的是字符 ':' 位置的下标
if(pos != string::npos)
{
protocol = url.substr(0, pos);
cout << "protocol: " << protocol << endl;
}
else
{
cout << "网址非法" << endl;
}
string domain;
size_t pos1 = url.find('/', pos + 3);
//下面有说明
if(pos1 != string::npos)
{
domain = url.substr(pos + 3, pos1 - (pos + 3));
cout << "domain: " << domain << endl;
}
find函数还有一个pos参数,可以决定从哪开始找
getline函数
适用于手动输入包含字符 ' ' 的字符串,getline函数只认换行符,不像cin,遇到字符 ' ' 就停止了。
getline的第一个函数是输入流,即cin。
C语言中有让字符串转变为整型的函数:atoi和让整数变成字符串的函数:itoa
value代表要转换成字符串的整数, str是内存中的数组,用于存储结果以 null 结尾的字符串,base是基于什么进制对value进行转换,范围:2~36,返回值是 指向生成的以 null 结尾的字符串的指针,与参数str相同。
c++提供和了string配合更便捷的函数
在头文件<string>中可以看到
//size_t* idx可以暂时不用管
string在一些地方还有一种设计叫:引用技术的浅拷贝或者叫显式拷贝,不过快被淘汰了,了解一下就行
大致思路:
string s1("hello world");
string s2(s1);
需要深拷贝s1,代价太大,就进行浅拷贝。
进行浅拷贝需要解决析构问题,解决方案是引入一个引用计数的概念,s1,s2指向相同空间,s1引用计数为1,s2引用计数为2。有几个对象指向这块资源,引用计数就为几。s2结束时,引用计数为2,引用计数自减1,当结束时引用计数为1,说明是最后一个使用该资源的对象,才会释放空间。
还要解决一个对象修改,会影响其他对象的问题。解决方案是看引用计数,不为1则说明还要其他对象要用,这时进行深拷贝。这叫写时拷贝,也称为延时拷贝。
也就是说,只有不修改对象,这个设计的作用才能最大化。