C++ 之string常用接口功能解析
关于string的背景
我们可以看到string的本质就是一个类模板!
**为什么使用的是类模板因为字符串涉及到了编码的问题!——有ACCII ,utf-8 utf-16 utf-32....**为了匹配多个不同的编码的形成的字符串!所以使用了类模板!
utf - 8 是使用了8bit位的编码方式!utf - 16 使用了 16比特位 .....
因为计算机中存储都是使用的是二进制!所以不同的编码方式就使用了不同二进制组合来匹配!不同的字符!
string 对应的就是utf-8 也是应用最广泛的编码方式!utf-8同时也兼容ascii编码!
string的接口解析!
string的构造函数!
string的初始化形式
#include <iostream>
#include<string>
using namespace std;
int main()
{
string s1;//创建一个空字符串!——重点
string s2 = "hello world!";//将一个字符串构造成一个对象!再使用拷贝构造!
string s3(s2);//拷贝构造
string s4("helle world");//———重点!
string s5("hello world", 5);//使用指向这个字符串的,第前n个字符
string s6(s2, 0, 5);//拷贝s2的子串,从零这个位置开始,往后5个
string s7(s2, 0, 10000);//如果过长遇到结尾就是停下来!
string s8(10,'a');//使用10个a字符填充这个字符串!——重点!
return 0;
}
string的运算符和赋值重载!
+ / = / += / []
int main()
{
string s1 = "hello world ";
string s3 = "no";
s1 = s1 + 'a';
s1 = s1 + "add ";
s1 = s1 + s3;
string s2("hello world");
s2 += "how are you?";
string s4;
s4 = s1;
for (int i = 0; i < s1.size(); i++)
{
cout << s1[i];//string类型也可以想数组一样正常的使用[],因为[]也是被重载了!
//支持静态和非静态
}
return 0;
}
string类比起正常的数组它的检查更加的严格!只要发生了越界访问!那必定报错!
at
at和[]的功能类似,就是返回所在位置的引用!
区别在于[]越界会直接报错!at越界会抛出异常!
int main() { try { string s1 = "hello"; cout << s1.at(100) << endl; } catch (exception& e) { cout << e.what() << endl; } return 0; }
string的迭代器!
我们遍历数组的时候一般都是使用下标[]来完成!string也继承了这个方式!与此同时,也引入了一种全新的变量方式迭代器!
==这里我们必须先明确一个概念!迭代器的行为类似指针!但是不代表迭代器就是指针!==
==迭代器是一种对于容器通用的遍历方式,像是string,vector我们可以使用[]来进行遍历!但是stack或queue就不再支持!但是它们都是支持以迭代器的方式来进行遍历!==
==每一种容器的迭代器的底层实现方式都是不同的!==,==且迭代器的底层也不一定是指针!==
而且迭代器的接口都是相同的!
迭代器的类型
迭代器的类型为 类 :: iterator
string s1 = "hello world ";
string::iterator st1 = s1.begin();
//iterator可能是一个类中类,也可能是指针!
begin
返回对象的第一个迭代器!
end
返回对象的最后一个数据的下一个位置的迭代器!
int main()
{
string s1 = "1234";
string::iterator st1 = s1.begin();
//迭代器的类型是一个类中类!
//begin接口!是返回这个队形的第一个迭代器!
while (st1 != s1.end())
{
*st1 += 1;
cout << *st1;
++st1;
}
return 0;
}
反向迭代器!
反向迭代器和迭代器是类似的,用法都是一样!
int main() { string s1 = "1234"; string::reverse_iterator st1 = s1.rbegin(); //反向迭代器的类型是reverse_iterator while (st1 != s1.rend()) { *st1 += 1; cout << *st1; ++st1; } return 0; }
rbegin和rend是配合反向迭代器使用的!
静态迭代器
对于静态的类也有配套的迭代器!
cbegin和cend就是配合静态迭代器使用的!
==但是其实begin和end里面已经包含了cbegin和cend的功能!==
==crbegin 和crend也是同理==
int main() { const string s1 = "1234"; string::const_iterator st1 = s1.cbegin(); //string::const_iterator st1 = s1.begin();//这也是可行的! while (st1 != s1.cend()) { cout << *st1 << ' '; st1++; } return 0; }
==或许会有疑惑为什么静态的迭代器不是下面这样==
const string s1 = "1234"; const string::iterator st1 = s1.cbegin();
因为如果像这样的其实修饰的是st1,st1会变成一个无法修改的值但是静态类保护的不是st1,保护的是迭代器里面的内容不被修改!
这样的话st1就不能进行迭代了!
同样的也有静态反向迭代器!
int main() { const string s1 = "1234"; string::const_reverse_iterator st1 = s1.crbegin(); while (st1 != s1.crend()) { cout << *st1 << ' '; } return 0; }
迭代器总结
迭代器分为四种
- 正向迭代器——string::iterator
- 反向迭代器——string::reverse_iterator
- const正向迭代器——string::const_iterator——只能读取,不能修改容器数据
- const反向迭代器——string::const_reverse_iterator——只能读取,不能修改容器数据
string类对象的容量操作
size/lenth
返回该字符串的长度!这两个的接口其实是一样的!
#include<iostream>
#include<string>
using namespace std;
int main()
{
string s1 = "hello world";
cout << s1.size() << endl;
cout << s1.length() << endl;
return 0;
}
capacity
返回该字符串所占的空间!
可能有很多人对于这个函数的含义有点误解!该字符串所占的空间和该字符串的长度不是等价!
string类的底层是一个动态开辟的数组!每次都是按照一定的倍数去开辟的!
#include<iostream>
#include<string>
using namespace std;
int main()
{
string s1 = "hello world";
cout << s1.size() << endl;
cout << s1.capacity() << endl;
return 0;
}
empty
检测字符串释放为空串,是返回true,否则返回false
int main()
{
string s1 = "hello";
string s2;
cout << s1.empty() << endl;
cout << s2.empty() << endl;
return 0;
}
clear
清空有效字符
int main()
{
string s1 = "hello";
cout << s1.empty() << endl;
s1.clear();
cout << s1.empty() << endl;
return 0;
}
reserve(重点!)
该接口的作用是一般为字符串预留空间!也就是会==去改变string的capacity!==
但是也有人会尝试使用缩减其capacity的大小!
但是这个缩减不是一个强制性的请求!它可能缩小,也可能不缩小!所以我们一般不这么使用这个函数接口!
但是一旦reserve后的capacity小于size那么就一定不会去执行这个函数接口!
也就是说reserve只会影响capacity 但是一定不会影响size!
无论扩大还是缩小都不会去改变size!
int main() { string s1 = "hello world"; cout << "capacity:" << s1.capacity() << ' ' << "size::" << s1.size() << endl; s1.reserve(100); cout << "capacity:" << s1.capacity() << ' ' << "size::" << s1.size() << endl; s1.reserve(15); cout << "capacity:" << s1.capacity() << ' ' << "size::" << s1.size() << endl; s1.reserve(1); cout << "capacity:" << s1.capacity() << ' ' << "size::" << s1.size() << endl; return 0; }
这个函数接口的疑惑
我们看到虽然我们reserve(100),但是实际上capacity并没有100,而是111,这牵扯到string的扩容!
接下里我们会使用下列代码对string的扩容进行直观的看待! 这里以vs2022环境下为例
void TestPushBack() { string s; size_t sz = s.capacity(); cout << "making s grow:\n"; for (int i = 0; i < 100; ++i) { s.push_back('c'); if (sz != s.capacity()) { sz = s.capacity(); cout << "capacity changed: " << sz << '\n'; } } } int main() { TestPushBack(); return 0; }
我们可以看到Windows系统下,string类的扩容除了了第一次,都是按照1.5倍来进行扩容的!(一般linux系统下的gcc是2倍扩容!)
所以我们reserve的值和实际的值可能略有出入!但是一般的都是差不多的!一般多出来的也是为了进行内存的对齐
reserve的意义!
如果我们一开始就知道我们要插入的字符的大小!那么我们就可以一开始就使用这个函数开辟出足够多的空间来避免开辟空间造成的资源浪费!
void TestPushBack() { string s; size_t sz = s.capacity(); s.reserve(700); cout << "making s grow:\n"; for (int i = 0; i < 1000; ++i) { s.push_back('c'); if (sz != s.capacity()) { sz = s.capacity(); cout << "capacity changed: " << sz << '\n'; } } } int main() { TestPushBack(); return 0; }
可以看到空间开辟的次数减少了!
resize
将有效字符的个数该成n个,多出的空间用字符c填充
resize这个接口和reserve接口是相对应的**!resize改变的是size!**但是也可能会改变capacity!
int main()
{
string s1 = "hello wrold";
cout << "capacity:" << s1.capacity() << ' ' << "size::" << s1.size() << endl;
//s1.resize(15);//默认使用\0进行填充!
s1.resize(15, 'a');
cout << "capacity:" << s1.capacity() << ' ' << "size::" << s1.size() << endl;
cout << s1 << endl<< endl;
// n > capacity
s1.resize(100);//当原先的capacity小于n的时候!就会进行扩容!
cout << "capacity:" << s1.capacity() << ' ' << "size::" << s1.size() << endl;
// size < n <capacity
s1.resize(15);//当改变的n小于capacity的时候,就不变!
cout << "capacity:" << s1.capacity() << ' ' << "size::" << s1.size() << endl;
// n < size
s1.resize(1);//当size的值小于原来字符串长度的时候!会将数据进行删除!
cout << "capacity:" << s1.capacity() << ' ' << "size::" << s1.size() << endl;
cout << s1 << endl << endl;
return 0;
}
shrink_to_fit
用于缩减string类的多出来的capacity!
我们上面知道string类的size不等于capacity可能,容量可能会多出了造成浪费!所以可以使用该接口缩减多余容量!
缩减的容量不一定完全等于size,因为为了空间的内存对齐,可能会多出一些来!
int main()
{
string s1 = "hello world";
s1.reserve(1000);
cout << "size :" << s1.size() << "capacity :" << s1.capacity() << endl;
s1.shrink_to_fit();
cout << "size :" << s1.size() << "capacity :" << s1.capacity() << endl;
return 0;
}
string类对象的修改操作
push_back
像string进行尾插!
int main()
{
string s1 = "hello world";
s1.push_back('a');
s1.push_back('b');
cout << s1 << endl;
return 0;
}
append
因为使用push_back一个个的插入太麻烦了,string还提供了append来整个整个的插入!
append的使用方式有很多种!但是我们一般都是使用1与3
int main() { string s1 = "hello world"; string s2 = "hello"; s1.append(s2);//1,插入一个对象 cout << s1 << endl; s1.append(" new");//2,插入一个字符串! cout << s1 << endl; s1.append(s2, 0, 2);//3,插入一个对象的子字符串!第二个参数是开始位置,第三个参数是长度! cout << s1 << endl; s1.append("happy", 2);//4,插入这个字符串的前两个 cout << s1 << endl; s1.append(5, 'a');//5,插入5个字符'a' cout << s1 << endl; s1.append(s2.begin(), s2.begin() + 3);//6,支持迭代器! cout << s1 << endl; return 0; }
assign
assign更像是一种赋值!就是将一个对象里面的内容完全替换掉!
使用的接口和append的是一样的!
int main() { string s1 = "hello world"; string s2 = "hello"; s1.assign(s2); cout << s1 << endl; s1.assign(" new"); cout << s1 << endl; s1.assign(s2, 0, 2); cout << s1 << endl; s1.assign("happy", 2); cout << s1 << endl; s1.assign(5, 'a'); cout << s1 << endl; s1.assign(s2.begin(), s2.begin() + 3); cout << s1 << endl; return 0; }
但是即使是有append我们其实更多是是使用+=这个赋值运算符重载!
insert
insert可以在字符串的任意位置进行插入!同样的string也支持迭代器!
int main()
{
string s1 = "hello world";
string s2 = "hello!";
s1.insert(0, s2);//在 位置0 插入s2
cout << s1 << endl;
s1.insert(0, s2, 2, 2);//可以调整插入对象的长度和位置!插入s2的2下标开始的长度为2的子串
cout << s1 << endl;
s1.insert(0, "new");//在0插入字符串!
cout << s1 << endl;
s1.insert(0, 5, 'a');//在零位置插入5个字符a
cout << s1 << endl;
s1.insert(s1.begin(), 5, 'a');//支持迭代器!
cout << s1 << endl;
s1.insert(s1.begin(), 'a');
cout << s1 << endl;
s1.insert(s1.begin(),s2.begin(), s2.begin() + 2);//插入是一个[s2.begin(),s2.begin()+2)的区间!
cout << s1 << endl;
return 0;
}
不过因为时间复杂度为O(N),不建议频繁的去使用这个接口!
erase
用来删除特定位置的字符!
#include<iostream>
#include<string>
using namespace std;
int main()
{
string s1 = "hello world";
s1.erase(0, 5);//1,从位置0开始,删除5个字符!
//s1.erase(0,10000)//给多了删到结尾就结束!
cout << s1 << endl;
s1.erase(s1.begin());//2,支持迭代器
cout << s1 << endl;
s1.erase(s1.begin(), s1.begin() + 2);//3,支持迭代器范围!
cout << s1 << endl;
return 0;
}
replace
将字符串的某个区间进行替换!(区间的范围是一个左闭右开的范围!)[i1,i2)
int main()
{
string s1 = "hello world";
string s2("repalce ");
s1.replace(0, 1, s2);//从0下标开始,范围是1的区间进行替换![0,1)
cout << s1 << endl;
s1.assign("hello world");
s1.replace(s1.begin(), s1.begin() + 1,s2);//支持迭代器区间!
cout << s1 << endl;
s1.assign("hello world");
s1.replace(0, 5, s2, 0, 5);//也支持对插入的区间进行控制!
cout << s1 << endl;
s1.assign("hello world");
s1.replace(0, 5, "world");//可以直接插入插入字符串!
cout << s1 << endl;
s1.assign("hello world");
s1.replace(0, 5, "world", 3);//可以选择插入前三个!//也支持迭代器范围区间!
cout << s1 << endl;
s1.assign("hello world");
s1.replace(0, 5, 5, 'a');//前五个都替换成a!//支持迭代器范围区间!
cout << s1 << endl;
return 0;
}
replace的时间复杂度为O(N),所以不建议频繁的使用!
find ——重要!
从字符串pos位置开始往后找字符c,返回该字符在字符串中的位置
如果找不到就会返回一个npos ——npos是string里面的一个公共成员变量!类型为size_t (unsigned long long) 值为 -1
无类型的 -1 实际大小为 2^32 -1;
int main()
{
string s1 = "hello world hello world ";
size_t pos = s1.find(' ');//找到' '就返回' ' 的位置!
cout << pos << endl;
pos = s1.find(' ', pos + 1);//从pos+1这个位置开始找' ' ,前面忽略!
cout << pos << endl;
string s2 = "helle";
pos = s1.find(s2);//可以用来寻找字串!
cout << pos << endl;
pos = s1.find("helle");
cout << pos << endl;
if (pos == string::npos)
{
cout << "npos" << endl;
}
pos = s1.find("helle", 0, 4);//0是s1的位置! 4是要匹配的字串的长度!
cout << pos << endl;
return 0;
}
应用
int m ain() { string s1 = "hello world hello world "; size_t pos = s1.find(' '); while (pos != string::npos) { s1.replace(pos, 1, "#"); pos = s1.find(' ',pos+1); } cout << s1 << endl; return 0; }//将 ‘ ’都替换成#
rfind
和find的作用相同!但是find是从左往右找!rfind是从右往左找!
c_str
**c_str就是返回string类中的那个数组的指针!**主要作用就是为了能和与C语言编程进行配合!例如使用C语言的库!或者使用C语言的文操作!
int main() { string file = "源.cpp"; FILE* fout = fopen(file.c_str(), "r");//c语言库中不支持string类! char ch = fgetc(fout); while (ch != EOF) { cout << ch; ch = fgetc(fout); } fclose(fout); char a[50]; strcpy(a, s1.c_str()); cout << a << endl; return 0; }
substr
取出字符串的一部分,然后将其构造成对象进行返回!
substr的实际使用!一般都是配合find或者rfind!来提取一部分的字串!
int main()
{
string s1 = "hello world";
string suffix = s1.substr(0, 5);
cout << suffix << endl;
string s2 = "test.txt.zip";
size_t pos = s2.find('.');
string suffix2 = s2.substr(pos);
cout << suffix2 << endl;
pos = s2.rfind('.');
string suffix3 = s2.substr(pos);
cout << suffix3 << endl;
return 0;
}
find_first_of
从左往右开始匹配传入字符串或在对象中的任意一个的字符,一旦匹配就返回这个地址!
找不到就返回npos
int main()
{
string s1 = "hello world hello China hello new world";
string s2 = "aebch";
size_t pos = s1.find_first_of(s2);//只要有s2中存在的任意一个字符就算是匹配!
cout << pos << endl;
pos = s1.find_first_of(s2,5);//从pos+1的位置开始查找
cout << pos << endl;
pos = s1.find_first_of("!", pos + 1);
if (pos == string::npos)
{
cout << pos << endl;
}
return 0;
}
配合string使用的一些函数
getline
获取一行字符串
这个的作用相当于c语言中的gets!
我们使用scanf 和 cin都是遇到 空格就会截止!所以当我们想要把空格也录入的时候就要使用getline,因为c语言的gets不支持string类型!
int main()
{
string s1;
getline(cin, s1);
cout << s1 <<endl;
return 0;
}
string支持各种运算符!
标准库中已经为我们重载好了!但是都是按照字符来进行比较的!