一、string类相关博文
- 对于C++中string类的学习,为避免文章过长显得冗杂,我分成以下三个部分进行总结(可直接点击下方链接进行查看):
二、string::operator[]
- 参考文档:string::operator[] - C++ Reference (cplusplus.com)
- 能让一个类,像数组一样被访问
//operator[]实现了两个重载-->系统会去调用最匹配的,差别在返回值
char& operator[] (size_t pos); //①普通的
const char& operator[] (size_t pos) const;//②const的
operator[]
的大概实现:- 引用做返回的意义:
- ①减少拷贝(但此处若拷贝,
s1[0]
字节也才1个,故意义相对不大) - ②修改返回对象(
s1[0]='x';
可将原来s1[0]
位上的‘h’,修改成‘x’)==>可读可写每个字符
- ①减少拷贝(但此处若拷贝,
- 引用做返回的意义:
- 场景:想遍历string,并每个字符相隔空格地输出
void test_string2()
{
//场景①修改string中特定位置的字符
string s1("hello world");
cout << s1[0] << endl;
s1[0] = 'x';
cout << s1[0] << endl;
cout << s1 << endl;
//场景②要求遍历string,每个字符+1
for (size_t i = 0; i < s1.size(); ++i)
//for (size_t i = 0; i < s1.length(); ++i)//①
{
s1[i]++;
}
cout << s1 << endl;
//场景③重载const版本的意义
const string s2("world");
for (size_t i = 0; i < s2.size(); ++i)
{
//s2[i]++;//不可写
cout << s2[i] << " ";//只可读
}
cout << endl;
cout << s2 << endl;
//s2[6]; //内部会检查越界(内部assert断言检查),s2[6]超出了范围
}
三、 .size()
和.length()
- 这两个虽然在上方(见①处),都能帮助实现遍历(size求个数,length求长度),但推荐使用
.size()
- 原因(究其历史):
- string的实现是先于STL的产生的,最早是仅有
.length()
。 - 但为了和后面产生的(如树、链表等)保持一致(如对于链表length勉强合适,但对于树而言
.length()
便不再合适了),故这些数据结构的容器要求个数,应用.size()
更一致。
- string的实现是先于STL的产生的,最早是仅有
四、string::at
char& at (size_t pos);
const char& at (size_t pos) const;
operator[]
和at
的区别operator[]
越界是会断言,程序直接终止掉at
越界是抛异常,捕获后程序还能继续跑- (在日常使用中
operator[]
用的多于at
)
五、front
和back
front
:返回第一个字符;==>str[0]
back
:返回最后一个字符;==>str[str.size()-1]
- 也不怎么需要熟悉,直接用
operator[]
访问就行
六、在string后加载新的字符、字符串
push_back
、append
和operator +=
append
中调用模板的意义在于,未必是string的迭代器,还可以是其他的迭代器(比如vector),只要满足数据类型匹配。
void test_string6()
{
string s("hello");
s.push_back('-');
s.push_back('-');
s.append("world");
cout << s << endl;//hello--world
string str("coming");
s += '@';
s += str;
s += "!!!";
cout << s << endl;//hello--world@coming!!!
//迭代器区间==>了解即可,不怎么使用
//迭代器区间不一定是string的迭代器,还可以是其他的迭代器(比如vector),只要数据类型匹配
s.append(++str.begin(), --str.end());
cout << s << endl;//hello--world@coming!!!omin
string copy1(++s.begin(), --s.end());
cout << copy1 << endl;//ello--world@coming!!!omi
string copy2(s.begin() + 5, s.end() - 5);
cout << copy2 << endl;//--world@coming!!
return 0;
}
七、string的增容reserve
和resize
-
max_size
:一般都是写死的固定的,没有什么意义。 -
capacity
:容量的大小capacity changed:15中15
表示:最初容量实际是16,有一个是给\0
(\0
是标准字符,不属于有效字符)的,剩余15个是能存储有效字符的空间
-
存在问题:若扩的容量大于需求:浪费空间;若小于需求,需要扩容次数多,频繁扩容
-
示例:对于同一份代码,vs和g++扩容机制的对比
- 由表格可得结论:数量较大时,Linux(右侧)越扩越少(更接近需求),vs反而越扩越多(多于需求)
- 🔺由以上示例侧面也可得:不能依赖其底层实现的东西,因为代码要在不同的编译器下跑,它都有可能会有变化,如以下两种情况:
- 情况1:下方同样代码在vs和Linux下扩容结果不同(结果如上图)
- VS中对string做了优化:导致第一次扩容近2倍,而后面扩容近1.5倍
- 它认为你字符串比较短的时候,不断向堆申请小空间的时候,会有一些内存碎片等问题。
- 故它进行优化,在库的String内部多加了一个
char _buff[16]
:<16字符串时,存在buff数组中(实际只能最多存15个有效字符);>= 16 存在_str
指向堆空间上 - ==> 优点:小数据时直接放在string中,不会向堆申请;缺点:string会变大
- 部分Linux中会采取这样的方案:
- 背景:认为深拷贝的代价太大了,偏向采用浅拷贝。但浅拷贝存在两个问题:①析构多次会出错;②一个对象修改影响另外一个对象
- 解决方案:
- 对于问题①:增加一个引用计数。每次对象析构时,引用计数就–,最后一个析构的对象释放空间
- 对于问题②:采用写时拷贝。本质属于“延迟拷贝”,谁去写,谁做深拷贝(没人写的场景下就赚了)
- VS中对string做了优化:导致第一次扩容近2倍,而后面扩容近1.5倍
- 情况2:无法访问底层的_str,因为在另一个版本下,未必叫_str,也有可能是str_或其他。
- 情况1:下方同样代码在vs和Linux下扩容结果不同(结果如上图)
-
对于扩容存在的扩多了和扩少了的问题,解决方式如下:
- ①
保留:reserve
:开空间- 提前预知所需空间,就可运用此接口来提前开好空间,避免扩容,减少消耗,提高效率。单纯只开空间,不会填size大小
- 但编译器为了对齐,有时也会多给一点(如要s.reserve(1000)会给1007)
- 区分
反转/逆置:reverse
- 提前预知所需空间,就可运用此接口来提前开好空间,避免扩容,减少消耗,提高效率。单纯只开空间,不会填size大小
- ②
resize
:开空间 + 初始化- 开空间:不仅改变capacity值,也会改变size值。并且默认都初始化为0
resize
使用示例如下:
- ①
//s.resize(1000);//开1000个空间,默认都初始化成0
s.resize(1000, 'x');//开1000个空间,并都初始化成x
八、 string::insert
- 参考文档:string::insert - C++ Reference (cplusplus.com)
- 不是特别推荐使用insert,因为其底层是连续的数组,每插入一次,后面的数据就要依次挪动位置==>效率低
string::insert
的接口介绍:- 使用场景:要求实现“在空格位置插入
%
”的功能
string str("wo lai le");
for(size_t i=0;i<str.size();++i)
{
if(str[i]==' ')
{
str.insert(i,"20%");
i+=3;//插入后,原空格的位置也往后推了3个位,故i要+3,否则一直检测的都是同一个空格
}
}
cout<<str<<endl;
九、string::erase
string::erase
的接口介绍:- 使用场景:要求实现“在空格位置插入
%
,并删除空格”的功能
string str("wo lai le");
for(size_t i=0;i<str.size();++i)//先插入
{
if(str[i]==' ')
{
str.insert(i,"20%");
i+=3;
}
}
cout<<str<<endl;
for(size_t i=0;i<str.size();++i)//再删除
{
if(str[i]==' ')
{
str.erase(i,1);
}
}
cout<<str<<endl;
//以上采用insert和erase的方法效率低,不建议O(N²);也不建议自己去挪动,若自己去挪动就要预估好大小,先resize,再挪动,否则[]会检查是否溢出
//以下采用空间O(N)换时间O(N)的方式,效率更高
string str("wo lai le");
for(size_t i=0;i<str.size();++i)
{
if(str[i]!=' ')
{
newstr+=str[i];
}else{
newstr+="20%";
}
}
cout<<newstr<<endl;
十、c_str
- 为了兼容C语言,返回c形式
const char*
的字符串
string filename("test.cpp");
cout <<filename << endl;//打印方式①走string流的输出
cout <<filename.c_str() << endl;//打印方式②C形式的打印:返回const char*的字符串
FILE* fout = fopen(filename.c_str(),"r");
assert(fout);
char ch = fgetc(fout);
while (ch != EOF)
{
cout << ch;
ch = fgetc(fout);
}
- 如上中,打印方式①和②一般情况下都是一样的,但在如下+=后就有区别了
- ③因为string对象,是以size为准的
- 监视窗口此处也有个bug,只显示到test.cpp,但查看详细时,就能看到所有的
- ④常量字符串对象,是以
\0
为准
- 监视窗口此处也有个bug,只显示到test.cpp,但查看详细时,就能看到所有的
- ③因为string对象,是以size为准的
string filename("test.cpp");
filename += '\0';
filename += "string.cpp";
cout << filename << endl;//③test.cpps tring.cpp
cout << filename.c_str() << endl;//④test.cpp
十一、 find
和rfind
- 使用场景①:要求实现“找到字符串str中的某个字符”的功能
string filename("test.cpp.tar.zip");
//size_t pos = filename.find('.');//从左到右找第一个后缀.cpp
size_t pos = filename.rfind('.');//从右到左找第一个后缀.zip
if(pos != string::npos)
{
//string suff = filename.substr(pos,filename.size()-pos);
string suff = filename.substr(pos);//默认取到末尾
cout<<suff<<endl;//.cpp
}
- 使用场景②:把网址切割成协议、域名、动作三部分
- 分析图例:
- 分析图例:
void DealUrl(const string& url)
{
//部分1:协议
size_t pos1 = url.find("://");
if (pos1 == string::npos)
{
cout << "非法url" << endl;
return;
}
string protocol = url.substr(0, pos1);
cout << protocol << endl;
//部分2:域名
size_t pos2 = url.find('/', pos1 + 3);//从pos1 + 3往后找第一个'/'
if (pos2 == string::npos)
{
cout << "非法url" << endl;
return;
}
string domain = url.substr(pos1 + 3, pos2 - pos1 - 3);//取pos2-pos1-3长度
cout << domain << endl;
//部分3:动作
string uri = url.substr(pos2 + 1);//从pos2 + 1位置往后取到结尾
cout << uri << endl << endl;
}
int main()
{
string url1 = "https://cplusplus.com/reference/string/string/";
string url2 = "ftp://ftp.cs.umd.edu/pub/skipLists/skiplists.pdf";
DealUrl(url1);
DealUrl(url2);
return 0;
}
十二、string相关的一些类型转换
1、回顾
- 之前接触过类似的:如
atoi
和itoa
- 字符串转整型(很好用,括号内直接提供字符串):atoi - C++ Reference (cplusplus.com)
- 整型转字符串(括号内需提供成分较多,相对没那么好用):itoa - C++ Reference (cplusplus.com)
2、string中的to_string
- 参考文档:to_string - C++ Reference (cplusplus.com)
- 相当于string中的一个全局函数
- 转成字符串
3、stoi
- 把字符串转回成整型
- 部分演示:
void test_string()
{
//转成字符串
int ival=1234;
double dval=12.34;
string istr = to_string(ival);
string dstr = to_string(dval);
cout << istr << endl;//1234
cout << dstr << endl;//12.340000,c++默认保留小数点后6位的精度
//把字符串转回整型和浮点型
istr = "9999";
dstr = "9999.99";
ival = stoi(istr);//9999
dval = stod(dstr);//9999.9899999999998是因为有些浮点数(如dstr)无法精确存储,精度存进去可能有精度损失,只能无限精确、保留多少位
}