string类是什么
string类是用来存储字符的类(由类模板实例化出来),
底层是一个动态增长的字符数组
下图是cplusplus.com的内容:

--为什么要有basic_string类模板
实例化出来的string内部存的是char类型.
除此之外还有wstring、u16string、u32string.
以下是cplusplus.com里的截图:

char是字符类型,大小1byte,最多表示2^8 = 256种字符,
但世界上的字符远大于256种,为了表示世界上的文字,出现了各种编码表.
--编码表
内存只存储01序列,如何把这些0/1序列转换成各种符号,显示到屏幕上?
去查编码表,编码表上记录了 内存存的值 与 文字符号 的映射关系.
例:


内存中存的其实是符号对应的数字,字符/字符串的显示要去查编码表.
ASCII编码表
ASCII(American Standard Code For Information Interchange):
即美国标准信息交换码,最先产生,美国的文字只包含英文数字和标点符号,
ascii编码只编了0~127,负的没有编写,包含所有的美国字符.
char类型有1byte,256种状态足够表示所有的美国字符.
但为了表示多个国家的文字,出现了更多编码表.
其它编码表
utf-8:兼容ASCII,char仍然是8bit,但是像汉字一般用2个char来表示
【ASCII只编了0~127,一般以负数/头几个比特位以固定规律开始的就是汉字】
用ASCII没有编码的映射作为前缀
例:

最后打印tmp,内容还是“你好”.
utf-16:无法兼容ASCII编码,不管是什么字符,都用2个字节来映射.
utf-32:不管是什么字符,都用4个字节来映射.
gbk:中国出的针对汉字标准,对繁体字/生僻字的支持更好.
--小结
为了让计算机能用数字映射世界上各种字符,
出现了多种编码表:如ASCII、utf-8、utf-16、utf-32
因此也出现多种字符类型:如char、wchar_t、char16_t、char32_t
而string类是由类模板basic_string实例化出来的,内部的字符类型是char.
string类基本使用
--构造函数
void test1()
{
//1 默认构造string()
string s1;
//2 用字符串初始化string(const char *s)
string s2 = "abcd";//先用"abcd"构造临时对象,再拷贝构造s2 + 编译器优化 -》直接构造
//string s2("abcd");
//3 拷贝构造string(const string& str)
string s3(s2);//同string s3 = s2
//string里面重载了<<和>>运算符,
//可以直接用cout打印string类,用cin输入字符串
cout << s1 << endl;
cout << s2 << endl;
cout << s3 << endl;
cin >> s1;
cout << s1 << endl;
}
注意:它们都存有一个隐藏的'\0'.

4 string (const string& str, size_t pos, size_t len = npos);//部分拷贝构造
从str的pos下标开始,数len个字符,用该子串进行拷贝构造;
另外,如果从pos下标开始第3个参数len超过了后面字符长度,拷贝到结尾
例:
//从string类"hello world"下标为2位置开始数3个字符,用该子串进行拷贝构造
string tmp("hello world");
string s1(tmp, 2, 3);
cout << s1 << endl;
//从string类"hello world"下标为3位置开始数999个字符,超过该字符串,直接到结尾
string s2(tmp, 3, 999);
cout << s2 << endl;

cplusplus.com:

npos是string类里的一个静态const变量,size_t是无符号整数,
size_t npos = -1,此时npos是无符号int整型的最大值,即2^32 - 1
说明如果不传最后一个参数,就是拷贝到字符串结尾,例:
string tmp("newYear");
string s3(tmp, 3);
cout << s3 << endl;//输出Year
void test3()
{
//5 string(const char* s, size_t n);
//用s的前n个字符,来初始化字符串
string s1("abcdef", 3);
cout << s1 << endl; //输出abc
//6 string(size_t n, char c);
//用n个字符c,初始化字符串
string s2(3, 'a');
cout << s2 << endl; //输出aaa
}
--赋值运算符重载
void test4()
{
string s1(5, 'x');//"xxxxx"
cout << s1 << endl;
string s2("abcd");
//1 operator=(const string& str)
s1 = s2;
cout << s1 << endl; //"abcd"
//2 operator=(const char* str)
s1 = "hello";
cout << s1 << endl;//"hello"
//3 operator=(char x)
s1 = 'a';
cout << s1 << endl;//"a"
}
--遍历string里的字符
用重载的[ ]遍历string
string内部的成员变量char*_str(名字不定)指向一块动态开辟的空间,用于存储字符.
虽然它的成员变量char*str或str(名字不定)是私有的,但string类内部实现了[ ]的重载.
即char& operator[](size_t pos)和const char& operator[](size_t pos)const
const string类型会调用const类型operator[]成员函数
普通 string类型会调用普通的operator[]
void test5()
{
string s1("hello world");
//可以读写对应位置的字符,会转换成调用operator[]
cout << s1[0] << endl;
cout << s1.operator[](0) << endl;
s1[0] = 'a';
cout << s1 << endl;
const string s2("ttt");
//s2[0] = 'x';会报错
}
此时[ ]重载可以让string类对象像数组一样,访问内部动态空间指向的字符数组
void test6()
{
string s1(6, 'a');//"aaaaaa"
for (size_t i = 0; i < s1.size(); ++i)//size()会返回当前字符个数
{
cout << s1[i];
++s1[i];
}
cout << endl;
cout << s1 << endl; // 修改后"bbbbbb"
//const string可以遍历,但不可以修改
const string s2("abcd");
for (size_t i = 0; i < s2.size(); ++i)
{
//s2[i]++;会报错
cout << s2[i];
}
}
用迭代器遍历string
在string类里有一个内嵌类型iterator,
迭代器就是该内嵌类型,它是用指针typedef出来的,或是定义的内部类.
在string类中,iterator就是char*,在string类内部如下:
typedef char* iterator
void test7()
{
string s1("abc");
string::iterator it = s1.begin();//begin()会返回第一个位置的迭代器,指向首元素
while (it != s1.end())//end()返回最后一个迭代器位置的下一个位置,指向尾元素的下一个位置
{
//对于string,!=改成<也可以,因为它底层是连续的物理空间,同时迭代器是原生指针
//但如果是其它容器不一定行,例list(链表)就不能
cout << *it << " "; //输出a b c
++it;
}
}

(1) 像指针一样,迭代器类型iterator通过解引用可以拿到string中的每个字符数据
同时++可以指向下一个数据
(2) begin()和end()返回的迭代器位置,遵循[ 左闭右开 )原则
(3)任何一个类型的迭代器,都是在它内部定义的.有些是typedef出来的,有些是内部类.
例如:要使用string的迭代器,要先指定string这个类域 —— string::iterator
(4)迭代器是所有容器的通用访问方式,屏蔽容器底层的细节,遍历容器内部的每个数据,
同时用法都是类似的
(5) 正向迭代器通过begin()和end()正向遍历,
反向迭代器通过rbegin()和rend()支持反向遍历
void test8()
{
string s1("abcd");
//反向迭代器类型名字——reverse_iterator,
//提供rbegin()和rend()用于反向遍历容器
string::reverse_iterator rit = s1.rbegin();
while (rit != s1.rend())
{
cout << *rit << " ";
++rit;//不管正向还是反向,都是++
}
}
(6)普通对象调用begin()返回普通迭代器,const对象调用begin()会返回const迭代器
void test9()
{
const string s1("abcd");
//const正向迭代器类型:const_iterator
string::const_iterator const_it = s1.begin();
while (const_it != s1.end())
{
cout << *const_it << " ";
const_it++;
}
//const反向迭代器类型:const_reverse_iterator
string::const_reverse_iterator const_rit = s1.rbegin();
while (const_rit != s1.rend())
{
cout << *const_rit << " ";
++const_rit;
}
}
因为库里的begin()有2种,构成函数重载:
1 iterator begin(); //普通对象调用
2 const_iterator begin()const;//const对象调用
end()、rend()、rbegin()同理.
(7) 总共4种迭代器类型:
iterator、reverse_iterator;
const_iterator、const_reverse_iterator
补充:
可以用一个迭代器区间构造一个string对象,注意这个区间都是左闭右开
string s1("abcd");
string s2(s1.begin(), s1.end());
cout << s2 << endl;
string s3(s1.rbegin(), s1.rend());
cout << s3 << endl;

--插入字符/字符串
1 调用内部普通函数尾插
void test10()
{
string s1("hello");
//void push_back(char c) 尾插一个字符c
s1.push_back('s');
cout << s1 << endl;
//string& append (const char* s); 尾插一个字符串
s1.append("world");
cout << s1 << endl;
//string& append(const string& str)
string tmp("aaa");
s1.append(tmp);
cout << s1 << endl;
}
2 调用+=运算符重载尾插
void test11()
{
string s1("abc");
//+=一个字符
s1 += 'd';
cout << s1 << endl;
//+=一个常量字符串
s1 += "efg";
cout << s1 << endl;
//+=一个string对象
string s2("hijk");
s1 += s2;
cout << s1 << endl;
}
3 在pos位置插入字符串/字符
string& insert(size_t pos, const char* s)
string& insert(size_t pos, const string& str)
string& insert(size_t pos, size_t n, char c)//在pos位置插入n个字符c
底层是连续的数组空间,在任意位置插入数据要挪动数据,挪动一次是O(n)
void test14()
{
string s1("it is sunny");
//在每个空格处插入@_
for (size_t i = 0; i < s1.size(); )
{
if (s1[i] == ' ')
{
//string& insert (size_t pos, const char* s);在pos位置,插入s
s1.insert(i, "@_");
//注意当前空格的下标改成了i+2
i += 3;//避开已经遍历的空格
}
else
i++;
}
cout << s1 << endl;
}

--删除
void test15()
{
//string& erase(size_t pos = 0, size_t len = npos);
//从pos位置开始删除len个数据,若len大于pos之后的字符数,删到结尾
string s1("abcdefg");
cout << s1.erase(2, 3) << endl;//"abfg"
//iterator erase(iterator p);
//删除迭代器某个位置的字符
string s2("utf-8");
s2.erase(s2.begin() + 1);//删除t
cout << s2 << endl;
//iterator erase(iterator first, iterator last);
//删除某个迭代器区间的字符串[左闭右开),不会删除last对应位置的字符
string s3("utf-16");
s3.erase(s3.begin() + 1, s3.end() - 1);//删除"tf-1"
cout << s3 << endl;
}
--查找
(1) 正向找
size_t find(const string& str, size_t pos = 0)const;//pos位置开始查找一个string对象
size_t find(const char* s, size_t pos = 0)const;//查找一个常量字符串
size_t find(char c,size_t pos = 0)const;//查找一个字符
如果找到,返回找到的第一个字符位置/字符串首位置
如果没找到,返回npos = -1,但由于返回类型size_t,所以实际返回的是无符号整型最大值.
string substr(size_t pos = 0, size_t len = npos)const //取当前string一部分,构造子串
例1:找到文件后缀,并打印出来
void test19()
{
string fileName("test.cpp");
//找到.的位置
size_t ret = fileName.find('.');
if (ret == -1)
cout << "无后缀" << endl;
else
{
//从ret位置开始,到结尾,就是文件后缀
string suff = fileName.substr(ret, string::npos);
cout << suff << endl;
}
}
(2)反向找
size_t rfind(char c, size_t pos = npos)const//返回最后一次出现字符c的位置
例2:若文件有多个后缀,要求找到真后缀(最后一个后缀)
test.cpp.tar
void test20()
{
string fileName("test.cpp.tar");
size_t ret = fileName.rfind('.');
if (ret == string::npos)
cout << "无后缀" << endl;
else
{
string suff = fileName.substr(ret);
cout << suff << endl;
}
}
(3) 应用
将任意的网址url分割成3个部分.
URL是由那3部分组成请列举_百度知道 (baidu.com)
网址url由3部分构成:协议名、存有该资源的主机IP地址、资源的具体地址
例:legacy.cplusplus.com/reference/s…
协议名:https
存有该资源的主机IP地址:legacy.cplusplus.com
资源的具体地址:reference/string/string/?kw=string
void splitUrl(const string& url)
{
//"https://legacy.cplusplus.com/reference/string/string/?kw=string";
//1 先找 :// 或 : 分割出协议
size_t pos1 = url.find(':');
if (pos1 == string::npos)
{
cout << "url非法" << endl;
exit(-1);
}
string agreement = url.substr(0, pos1 - 1);
cout << "协议名:" << agreement << endl;
//2 在://位置之后,找到第一个/,分割出主机ip
size_t begin = pos1 + 3;
size_t pos2 = url.find('/', begin);
if (pos2 == string::npos)
{
cout << "url非法" << endl;
exit(-1);
}
string ip = url.substr(begin, pos2 - begin);
cout << "主机IP地址:" << ip << endl;
//3 分割出资源具体地址
string specificAddress = url.substr(pos2 + 1);
cout << "资源具体地址:" << specificAddress << endl;
}

--容量相关
1 size_t capacity() const; //返回当前string可存储有效字符的容量
2 void reserve(size_t n = 0);//提前开好可存储n个有效字符的容量
如果提前知道要插入的字符数目,可以用reserve提前开好空间,
从而减少扩容的消耗.
void test12()
{
string s3("abcd");
cout << "当前存储的字符数:"<< s3.size() << endl;
cout << "当前容量:" << s3.capacity() << endl;
s3.reserve(1000);//扩容到1000,但因为内存碎片一些问题,会对齐一下出现偏差
cout << "扩容后存储的字符数:" << s3.size() << endl;
cout << "扩容后容量:" << s3.capacity() << endl;
}

3 void resize(size_t n);//修改size的值为n,同时初始化新增的空间为0
void resize(size_t n, char c)//修改size的值为n,同时初始化新增的空间为c
void test13()
{
string s1("abcd");
s1.resize(99);
string s2("aaaa");
s2.resize(1);
cout << s2 << endl;//"a"
string s3("bbbb");
s3.resize(7, 't');
cout << s3 << endl;//"bbbbttt"
}
--c_str函数
1 网络上c接口函数,虽然c++兼容c,但c接口在传参时,不支持用string类对象传参.
const char* c_str()const//返回c形式的字符串,以'\0'结尾
例:
void test16()
{
string fileName("源.cpp");
//假设现在要用c接口读取当前文件 FILE* fopen(const char* _FileName, const char* _Mode)
//fopen(fileName, "r");此时string类型的fileName不能直接传过去,要用c_str返回c形式字符串
//c形式的字符串:const char*类型,指向一个字符串常量 或者 就是一个字符数组(都以'\0'结尾)
FILE* fr = fopen(fileName.c_str(), "r");
char ch = fgetc(fr);
while (ch != EOF)
{
cout << ch;
ch = fgetc(fr);
}
}

2 【cout << string对象】 与 【cout << c字符串】区别
(1)调用不同的运算符重载
void test17()
{
string s1("abcd");
//调用重载的<<运算符 ostream& operator<<(ostream& os,const string& str)
cout << s1 << endl;
//调用重载的<<运算符 ostream& operator<<(ostream& os, const char* s)
cout << s1.c_str() << endl;
}
下图来自cplusplus.com:


(2) cout打印string对象 只认 size,cout打印c字符串 只认 '\0'.
void test18()
{
string s1("abcd");
s1 += '\0';
s1 += "ttt";
cout << "string类型的字符串打印:"<<s1 <<",s1.size()为" <<s1.size() << endl;
cout << "c类型的字符串打印:" << s1.c_str() << endl;
}

--getline函数
当要输入一个包含 空格 的字符串时,要用getline才能输入
void test22()
{
string s1;
//istream& getline(istream & is, string & str),默认以换行符终止输入
getline(cin, s1);
cout <<"输入后:"<< s1 << endl << endl;
string s2;
//istream& getline(istream & is, string & str, char delim);自定义delim符号终止输入
getline(cin, s2,'@');
cout <<"输入后:" << s2 << endl;
}

--字符串转数字 与 数字转字符串
以下函数是文件下的全局函数,不是成员函数.
(1) to_string函数:用于把数字转换成字符串,重载了多种数字类型,如int、double、long long等
(2) 字符串转数字:stoi(字符串转整数)、stod(字符串转浮点数)等函数,以stoi为例:
int stoi (const string& str, size_t* idx = 0, int base = 10)
只需要传指定字符串,就能转换成10进制数字
idx:做输出型参数,可以置空,
若不为空,函数内部会把转换出的数字长度交给*idx
base:进制
void test23()
{
int a = 0;
double b = 3.3;
//string to_string(int a)
string aStr = to_string(a);
//string to_string(double a)
string bStr = to_string(b);
cout << "转换成功:" << aStr << " " << bStr << endl;
string s1 = "123";
string s2 = "13.2";
int tmp1 = stoi(s1);
double tmp2 = stod(s2);
cout << "转换成功:" << tmp1 << " " << tmp2 << endl;
string s3 = "345699 tt";
size_t idx = 0;
int tmp3 = stoi(s3, &idx , 10);
cout << tmp3 <<" 转换的数字长度为:" << idx;
}

string模拟实现
namespace yh
{
class string
{
public:
private:
char* _str;//指向动态开辟的空间
size_t _size;//当前有效字符的个数
size_t _capacity;//当前存储有效字符的空间大小,不包含'\0'
};
}
--默认构造函数和析构函数
(1) 无参构造函数
不能用nullptr初始化_str,因为cout打印c字符串,会首先对每个字符解引用
使得调用c_str()接口后,不一定可以用cout进行打印.
string()
:_str(nullptr)//如果用nullptr初始化会引发上述问题
,_size(0)
,_capacity(0)
{}
string s1;
cout << s1.c_str() << endl;//cout对于char*类型,都会先解引用再打印,如果是nullptr会崩溃
char* tmp = nullptr;
cout << tmp << endl;//都会崩溃
对于string s1,要开1byte用来存储'\0'
string()
:_str(new char[1])
,_size(0)
,_capacity(0)
{
_str[0] = '\0';
}
(2) 用常量字符串构造string
string(const char* str)
首先,不能把str(const char* 类型)直接赋给_str(char* 类型),这会导致权限的扩大
第二,给_str开空间,要多开一个'\0'空间,再把str的数据拷贝过去
string(const char* str)//用常量字符串初始化string
//:_str(str) 权限的扩大
:_str(new char[strlen(str)+1])//开空间,注意'\0'
,_size(strlen(str))
,_capacity(strlen(str))
{
strcpy(_str, str);
}
(3) 把上述2个构造函数合并,并改进为一个全缺省的构造函数
string(const char* str = "");
首先,空字符串""包含了'\0',把它作为缺省值
第二,strlen是O(n)的函数,可以先用size记录,然后再用来初始化初始容量和大小
string(const char* str = "")
{
//只调用一次strlen
int size = strlen(str);
//设置初始容量和大小
_capacity = _size = size;
//开空间
_str = new char[_capacity + 1];
//拷贝数据
strcpy(_str, str);
}
(4) 析构函数
~string()
{
delete[] _str;
}
--拷贝构造函数和赋值运算符重载
(1) 浅拷贝出现的问题
默认的拷贝构造函数和赋值运算符重载,会进行值拷贝(浅拷贝),
此时它们内部的_str会指向同一块空间

问题一:s2修改,s1也会跟着修改,因为它们的_str指向同一块空间
问题二:同一块空间在析构(析构函数清理对象在堆区申请的空间)时,会释放2次,报错
(2) 使用深拷贝方式解决,s2单独申请一块新空间,把s1指向的内容拷贝到新空间里
拷贝构造函数1(传统写法——自己开空间):
string(const string& str)
:_size(str._size)
,_capacity(str._capacity)
{
//开新空间,多开一个给'\0'
_str = new char[_capacity + 1];
//把str的内容拷贝到新空间
strcpy(_str, str._str);
}
拷贝构造函数2:
通过调用已有的构造函数,生成与待拷贝的string相同内容的对象,
最后交换tmp与*this,不需要手动拷贝.
void swap(string& s)
{
//该交换成员函数,用来交换两个对象的所有成员
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
string(const string& str)
{
//调用已有的string(const char* str = "")构造临时tmp
string tmp(str._str);
//然后*this和tmp所有成员进行交换
this->swap(tmp);
//因为tmp是局部对象,出了作用域调用析构函数,
//与*this交换所得的空间是随机值,会崩溃
//所以交换后要修改tmp,阻止上述情况发生
tmp._str = nullptr;//delete[]内部有检查空指针,为空什么都不干
}

赋值运算符重载1(传统写法 —— 手动开空间,手动释放旧空间,手动拷贝):
注意判断自己给自己赋值
被赋值的对象,要注意清理原先申请的空间,否则会有内存泄漏
string& operator=(const string& str)
{
if (this != &str)//如果不是自我赋值
{
//先开新空间拷贝数据,new失败后,不会破坏_str
char* tmp = new char[str._capacity + 1];
strcpy(tmp, str._str);
//把this原来的空间释放
delete[] _str;
_str = tmp;
_size = str._size;
_capacity = str._capacity;
}
return *this;
}
赋值运算符重载2:利用传参的拷贝构造出和s1数据相同的对象,
此时*this只需要与str交换成员即可
出了作用域,str自动销毁,帮助释放旧空间.
//s2 = s1
string& operator=(string str)//调用拷贝构造生成str
{
//*this和str所有成员进行交换
swap(str);
//str是局部对象,出了作用域自动销毁,帮助释放原来s2的旧空间
return *this;
}
--string的迭代器实现
string里的迭代器iterator类型本质就是原生指针char*
在类内部:typedef char* iterator;
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
typedef const char* const_iterator;
const_iterator begin()const
{
return _str;
}
const_iterator end()const
{
return _str + _size;
}
string s1("abcdefg");
string::iterator it = s1.begin();
while (it != s1.end())
{
cout << *it << " ";
++it;
}
cout << endl;
cout << "for循环遍历:";
//for(:)底层调用的就是迭代器,一个类实现了迭代器,就能用for(:)遍历
for (auto x : s1)
{
cout << x << " ";
}


--插入字符/字符串
(1) 插入字符/字符串,一定会涉及扩容
void reserve(size_t n);//提前开好可存储n个有效字符的容量
//开存储n个有效字符的空间,不会缩容
void reserve(size_t n)
{
//如果需要的空间n大于当前容量
if (n > _capacity)
{
//开新空间,把旧空间的数据拷贝过去
char* tmp = new char[n+1];
strcpy(tmp, _str);
//释放旧空间
delete _str;
//更新当前对象*this的成员
_str = tmp;
_capacity = n;
}
}
size_t capacity()const
{
return _capacity;
}
//该函数在命名空间yh中
void test25()
{
string s1("abcd");
cout << s1.capacity() << endl;
s1.reserve(100);
cout << s1.capacity();
}

(2) 尾插字符
void push_back(char x)
{
if (_capacity == _size)
{
//注意_capacity初始值为0的情况
reserve(_capacity == 0 ? 5 : _capacity * 2);
}
//放数据
_str[_size] = x;
++_size;
//注意'\0'
_str[_size] = '\0';
}
string& operator+=(char x)
{
//复用push_back
push_back(x);
return *this;
}
(3) 尾插字符串
void append(const char* str)
{
size_t length = strlen(str);
//此时的容量必须能存下所有字符
if (_capacity < _size + length)
{
reserve(_size + length);
}
//_str+_size 即string最后一个有效数据的下一个位置
strcpy(_str + _size, str);
_size += length;
}
string& operator+=(const char* str)
{
append(str);
return *this;
}
(4) 在任意位置插入字符
//在pos位置插入一个字符
string& insert(size_t pos, char ch)
{
assert(pos <= _size);//=_size相当于尾插
reserve(_size + 1);//判断扩容
//从后往前挪数据
//把前一个位置的字符赋值给当前位置的字符
size_t pre = _size - 1;
size_t cur= _size;//初始位置为'\0'
while(cur > pos)
{
//把 前一个位置数据 挪到 当前位置
_str[cur] = _str[pre];
pre--;
cur--;
}
//修改pos位置数据,并++_size
_str[pos] = ch;
++_size;
//最后一个字符的下一个位置为'\0'
_str[_size] = '\0';
return *this;
}
void test27()
{
string s1("abc");
cout << s1.c_str() << endl;
s1.insert(0, 't');
cout << s1.c_str() << endl;//"tabc"
s1.insert(1, 'g');
cout << s1.c_str() << endl;//"tgabc"
}

(5) 任意位置插入字符串

总思路
定义前后下标pre和cur指向_size和_size+length的位置,
_str[cur] = _str[pre]
然后同时--,将插入位置之后的值全部挪到后面.最后再放入字符串.
细节
1 判断挪动字符终止的条件
当pre < pos 位置时(即pre = pos - 1),
此时pos位置之后(包括pos位置)的字符已经全部挪动,
但是size_t pre,当头插时,pre会刚好减到无符号整型最大值,导致死循环.
所以判断条件用cur:
pre = pos - 1;
cur = pre + length(插入的字符串长度);
-》cur = pos+length - 1,
即当cur减到pos+length-1时,所有字符挪动完成,可以开始插入
2 插入的是空字符,不做处理
此时cur和pre是相等的,头插情况下cur也会一起减到无符号整型最大值.
//在pos位置插入一个字符串
string& insert(size_t pos, const char* str)
{
//插入位置判断
assert(pos <= _size);
//如果是空串,不进行操作
if (strlen(str) == 0)
return *this;
//扩容判断
size_t length = strlen(str);
reserve(length + _size);
//前后两个指针,
size_t cur= _size + length;
size_t pre = _size;
while (cur > pos+length - 1)
{
_str[cur] = _str[pre];
--cur;
--pre;
}
for (int i = 0; i < length; i++)
{
_str[pos + i] = str[i];
}
_size += length;
return *this;
}
void test28()
{
string s2("aaa");
s2.insert(1, "bc");//"abcaa"
cout << s2.c_str() << endl;
s2.insert(5, "gg");//"abcaagg"
cout << s2.c_str() << endl;
s2.insert(0, "uu");//"uuabcaagg"
cout << s2.c_str() << endl;
string s3;
s3.insert(0, "");//插入空串
cout << s3.c_str();
}

--删除字符/字符串
string& erase(size_t pos, size_t len = npos)
(1)npos是string类的const静态成员变量
对于该类成员,c++特殊处理,不需要在类外面定义,直接在声明地方就可以定义

(2)情况分类
情况1:当len = npos时,删到结尾
情况2:当pos + len >= _size时,删到结尾
情况3:删除0个字符,len = 0
情况4:删除某个区间的所有字符
void test29()
{
string s1("abcdefgh");
s1.erase(0, 2);//头删 "cdefgh"
cout << s1.c_str() << endl;
s1.erase(1, 2);//中间删"cfgh"
cout << s1.c_str() << endl;
}
--查找字符/子串
//查找一个字符
size_t find(char c, size_t pos = 0)const
{
for (size_t i = pos; i < _size; i++)
{
if (_str[i] == c)
return i;
}
return npos;
}
size_t find(const char* sub, size_t pos = 0)const
{
//复用c语言库的strstr
//const char* strstr(const char* str1, const char* str2);
//char* strstr(char* str1, const char* str2);
//如果str2是str1的子串,返回str1对应指针位置,否则返回NULL
const char* tmp = strstr(_str + pos, sub);
if (tmp == nullptr)
{
return npos;
}
else
{
return (tmp - _str);//指针-指针起始就是其下标位置,因为它们是连续物理空间
}
}
--string的流插入流提取运算符重载
(1) 关于全局对象cin和cout
库里定义了两个类型,分别叫ostream输出流类和istream输入流类.(在头文件iostream中)
而cout是ostream类型的全局对象,cin是istream类型的全局对象
在ostream类型里,重载了<<运算符;
在istream类型里,重载了>>运算符.
库里写好了运算符重载+各种内置类型构成函数重载.
cplusplus.com:


void testn()
{
int i = 0;
double d = 1.0;
cin.operator>>(i); // cin >> i;
cin.operator>>(d); //cin >> d;
cout.operator<<(i)<<endl; //cout << i << endl;
cout.operator<<(d); //cout << d;
}

(2) 实现string的流插入和流提取运算符
//1 不能实现为成员函数,如果实现为成员函数,this指针抢占左操作数
//2 不一定要实现为string的友元函数,因为不需要访问string的私有成员
ostream& operator<<(ostream& out, const string& str)
{
for (int i = 0; i < str.size(); i++)
{
out << str[i];
}
//支持连续流插入
return out;
}

cout << s1 <<s2 << endl:从左向右执行,先执行cout<<s1,会返回一个ostream&对象,
然后继续用这个对象调用上面实现的string流插入运算符重载,这就实现了连续流插入.
istream& operator>>(istream& in, string& str)
{
char ch;
//in >> ch;
//cin是会忽略掉输入的空格或者换行,不会把空格和换行交给ch
//清空str,也可以调用clear()
str = "";
ch = in.get();//它有一个get()成员函数,可以逐个字符读取
while (ch != ' ' && ch != '\n')
{
str += ch;
ch = in.get();
}
//在接收输入的数据时,cin默认以' '或'\n' 作为数据的分割符,但cin不会去接收它们
return in;
}

但是逐个插入str中,扩容频繁,效率不高.
优化思路:解决扩容频繁问题,在内部定义一个buff数组,一批一批去加
istream& operator>>(istream& in, string& str)
{
//先清空str,也可以调用clear()
str = "";
char ch;
ch = in.get();
char buff[64];
size_t i = 0;
while (ch != ' ' && ch != '\n')
{
buff[i++] = ch;
if (i == 63)
{
buff[i] = '\0';
//如果缓冲区已满,就一次性给str,可以有效减少扩容的消耗
str += buff;
i = 0;
}
ch = in.get();
}
buff[i] = '\0';
str += buff;
return in;
}
--resize函数
void resize(size_t n)
void resize(size_t n, char c)
1 特性
若n < 当前的数据量,尾删数据, 不做任何处理.
若n == 当前数据量,不做任何处理.
若n > 当前数据量,将多出的空间初始化为c或'\0'
std::string s1("abcd");
s1.resize(50, 'x');
cout << "resize(50, 'x')后:s1的数据个数:" << s1.size() << endl;
cout << s1 << endl<<endl;
s1.resize(5);
cout << "resize(5)后:s1的数据个数:"<<s1.size() << endl;
cout << s1 << endl << endl;
s1.resize(4, 'u');
cout << "resize(4,'u')后:s1的数据个数:" << s1.size() << endl;
cout << s1 << endl << endl;
s1.resize(4);
cout << "resize(4)后:s1的数据个数:" << s1.size() << endl;
cout << s1 << endl;

2 模拟实现
void resize(size_t n, char ch = '\0')
{
if (n == _size)//不处理
return;
else if (n < _size)//删除数据
{
_size = n;
_str[_size] = '\0';
}
else//扩容,同时初始化多出的空间
{
reserve(n);
for (size_t i = _size; i < n; i++)
{
_str[i] = ch;
}
_size = n;
_str[_size] = '\0';
}
}