深入篇【C++】string类的常用接口介绍:标准库中的string类
- Ⅰ.string类介绍
- Ⅱ.string类的常用接口
- ①.string类对象的常用构造
- 1.string()
- 2.string(const char*ch)
- 3.string(const string& str)
- 4.string(size_t n,char c)
- 5.string(const string& str,size_t pos,size_t len=npos)
- 【总结】
- ②.string类对象的容量操作
- 1.size
- 2.capacity
- 3.clear
- 4.empty
- 5.reserve
- 6.resize
- 【总结】
- ③.string类的对象的访问与遍历
- 1.opeartor[]
- 2.begin/end
- 3.rbegin/rend
- 4.范围for
- 【总结】
- ④.string类对象查看与修改操作
- 1.push_back
- 2.append
- 3.operator+=
- 4.insert
- 5.erase
- 6.replace
- 7.c_str
- 8.substr
- 9.find
- 10.rfind
- 【总结】
- ⑤.string类非成员函数
- Ⅲ.牛刀小试:练习string类
- 1.字符串相加
- 2.字符串最后一个单词的长度
Ⅰ.string类介绍
string类文档介绍----cplusplus
1.C语言中,strxxx 是系列库函数而在C++中string是一个类。
2.string是管理字符数组的类
3.标准的字符串类提供了对此类对象的字符类型,其接口类似于标准字符容器的接口,但添加了专门操作单字节字符字符串的设计特性。
4.string类其实是basic_string模板类的一个实例,它使用char来实例化basic_string模板类,并用char_traits和allocator作为basic_string的默认参数。
5.这个类独立于所使用的编码来处理字节:如果用来处理多字节或变长字节的序列,这个类的所有成员以及它的迭代器,将仍然按照字节来操作,而不是实际编码的字符。
总结:
1.string其实表示字符串的字符串类
2.string在底层其实是basic_string模板的实例化。
typedef basic_string<char, char_traits, allocator> string;
3.在使用string类时必须要包含头文件和using namespace std;
Ⅱ.string类的常用接口
①.string类对象的常用构造
1.string()
无参构造函数,不需要参数就可以构造出一个对象,该对象其实就是一个空字符串。
所以可以用空字符串来构造string对象。
int main()
{
//无参构造函数
string s1;
cout << s1 << endl;
}
2.string(const char*ch)
该构造函数的参数是一个字符串。这说明我们可以用一个字符串来构造string对象。
int main()
{
string s2("小陶");//用字符串来构造对象
string s3("hello world");
cout << s2 << endl;
cout << s3 << endl;
}
3.string(const string& str)
该构造函数的参数是string类对象,这说明我们可以用一个string对象来构造string对象。
int main()
{
string s2("小陶");
string s3("hello world");
string s4(s2);
cout << s4 << endl;
string s5(s3);
cout << s5 << endl;
}
4.string(size_t n,char c)
该构造函数的参数有两个,一个是正数n一个是字符c。
该构造函数实现的功能是用n个字符c构造对象。这说明我们可以用n个字符c来构造string对象。
int main()
{
string s4(10, '*');
//用10个'*'来构造对象s4
cout << s4 << endl;
string s5(10, 'x');
//用10个'x'来构造对象s4
cout << s5 << endl;
}
5.string(const string& str,size_t pos,size_t len=npos)
从参数上我们就可以推断出该构造函数的功能是什么了。
从字符串str第pos位置上拷贝长度为len的字符串用来构造对象
注意最后一个参数给了缺省值npos,npos是一个很大的数值,当我们给定长度时,就从pos位置截取len长度,当我们不给定长度时,就默认从pos位置一直往后截取完。
int main()
{
string s3("hello world");//字符串
string s6(s3, 6, 5);///从某个字符串某个位置拷贝n各字符
//从字符串s3第六个位置往后拷贝五个字符
cout << s6 << endl;
//当给定长度为5时,就截取5个字符,当不给定长度时,从pos位置一直往后截取
//因为缺省参数是一个很大的数值。
string s7(s3, 6);
cout << s6 << endl;
//
}
【总结】
常见构造函数 | 功能 |
---|---|
string() | 用空来构造string对象,即空字符串 |
string(const char* ch) | 用C-string来构造string对象 |
string(const string& str) | 用string类对象来构造string对象,即拷贝构造函数 |
string (size_t n,char c) | 对象中包含n个字符c,即用n个字符c来构造对象 |
string(const stirng&str,size_t pos,size_t len=npos) | 从对象str的pos位置上截取长度为len的字符串来构造 |
②.string类对象的容量操作
1.size
用来计算对象的有效长度。它是string类的成员函数,所以调用它只需要对象即可。
C++中其实还有一个成员函数length也是用来计算对象长度的,与size功能是完全一样,只是名字不一样。
类似的成员函数还有max_size,这是用来计算对象可以拥有的最大长度。
int main()
{
string s1("hello xiaotao");
cout << s1.size() << endl;
cout << s1.length() << endl;
cout << s1.max_size() << endl;
}
2.capacity
用来计算对象的容量大小,它是string类的成员函数,对象可以直接调用。
int main()
{
string s1("hello xiaotao");
cout << s1.size() << endl;
cout << s1.capacity() << endl;
string s2;
cout << s2.size() << endl;
cout << s2.capacity() << endl;
}
我们可以发现对象s1和对象s2的容量都为15,这说明对象的起始容量就为15。
当一个对象被创建时,系统就会给它申请15大小的空间。而当我们的对象的长度大于对象本身容量的大小时,就需要对象扩容。
那string类对象的扩容方式是如何的呢?每次对象扩容多少呢?
int main()
{
string s1;
cout <<"大小:"<< s1.size() << endl;
cout <<"起始容量:"<< s1.capacity() << endl;
size_t old = s1.capacity();
for (size_t i = 0; i < 100; i++)
{
s1 += 'x';
if (old != s1.capacity())
{
cout << "扩容" << s1.capacity() << endl;
old = s1.capacity();
}
}
}
我们可以看到在VS下string对象的每次扩容申请的空间都是不一样的,比较奇怪。
第一次扩容16大小,第二次扩容26大小,第三次扩容23大小,第4次扩容35大小。
3.clear
用来清空对象的有效字符
要注意clear只是清空了有效字符的大小(size)变成0,而容量空间大小(capacity)是不变的。
int main()
{
string s1("hello xiaotao");
cout << s1.size() << endl;
cout << s1.capacity() << endl;
s1.clear();
//清理数据,但内存空间还在
cout << s1.size() << endl;
cout << s1.capacity() << endl;
}
4.empty
用来检查字符串是否被释放成空串,如果是空串则返回true、否则返回false。
int main()
{
string s1("hello xiaotao");
cout << s1.empty() << endl;
s1.clear();
//清理数据,但内存空间还在
cout << s1.empty() << endl;
}
5.reserve
用来为对象预留空间。
可以提前为对象申请n大小的空间。
当n比原空间(capacity)大时,就会进行扩容。
当n比原空间(capacity)小时,不一定会缩容,这取决于不同的平台。
而reserve的好处就是当我们知道需要多少空间时,就可以提前将空间开好,这样就可以避免不断扩容,就不存在扩容问题了。
int main()
{
string s1("hello world");
//开空间
s1.reserve(100);
cout << s1.size() << endl;
cout << s1.capacity() << endl;
}
用reserve提前开辟的空间不一定准确,可能会开大点,但不可能开小的。就比如reserve(100)提前申请100个空间,系统分配了111个空间给它。
void TestPushBackReserve()
{
string s;
s.reserve(100);//提前开辟100大小空间
size_t sz = s.capacity();
cout << "now capacity:" << sz << '\n';
cout << "making a grow:'\n";
for (int i = 0; i < 100; i++)
{
s.push_back('c');
if (sz != s.capacity())
cout << "capacity changed:" << sz << '\n';
}
cout << "later capacity:" << sz << '\n';
s.reserve(10);
cout << s.capacity() << endl;
}
可以通过上面的代码知道reserve确实为对象开辟的空间,并且当开辟空间小于原空间大小时,原空间大小是不变的。
6.resize
将对象原来的有效字符的个数改成n个,多出来的空间用字符c来填充。
该成员函数有两种重载形式,一种是resize(size_t n)一种是resize(size_t n,char c);
这两个区别在于多出来的空间,resize(size_t n)是用0来填充
resize(size_t n,char c)是用字符c来填充。
int main()
{
string s1("hello world");
s1.resize(20);
cout << s1 << endl;
string s2("hello world");
s2.resize(20,'x');
cout << s2 << endl;
}
reserve和resize都是可以操纵空间大小的,那它们有什么区别呢?
1.reserve是单纯开空间
2.resize是开空间+填值初始化
3.reserve是只能影响到capacity容量大小,影响不到size的大小。
而resize既能影响capacity容量大小,又能影响size的大小。
因为当resize中的n大于size时,则会扩大size的大小至n,并且容量capacity也会跟着变化。
但当resize中的n小于size时,则只会缩数据个数,不会缩空间大小,也就是只缩小size大小,不改变capacity的大小。
int main()
{
string s1("hello world");
//单纯开空间
s1.reserve(100);
cout << s1.size() << endl;
cout << s1.capacity() << endl;
//开空间+填值初始化
s1.resize(200, 'x');
cout << s1 << endl;
//当resize在改变元素个数时,如果元素的个数增多,可能会改变
//底层容量的大小,如果是将元素个数减少。底层空间总大小不变
cout << s1.size() << endl;
cout << s1.capacity() << endl;
s1.resize(20);
cout << s1.size() << endl;//这个数据大小改变
cout << s1.capacity() << endl;//但容量不会改变
}
【总结】
成员函数 | 功能 |
---|---|
size | 计算字符串的字符长度 |
capacity | 计算空间总大小 |
clear | 清空对象数据 |
empty | 判断字符串是否被清空 |
reserve | 为字符串预留空间 |
resize | 将字符串的大小更改成n |
1.size()和length()底层实现原理是一样的,出现size()原因是为了于其他容器的接口保持一致,一般都用size().
2.clear()只是清空数据的个数,不会改变capacity的大小的。
3.resize(n)和resize(n,char c)都是用来更改字符大小的。不同的是,当n大于size时,多出来的空间rezie(n)用0初始化,resize(n,char c)用字符c来初始化。当n小于size时,size的大小会改变,但capacity的大小不会改变
4.想单纯的开空间就使用reserve、要想即开空间又初始化,那就用resize。
③.string类的对象的访问与遍历
1.opeartor[]
该成员函数其实就是对[]进行运算符重载。返回pos位置的字符,使用方法跟数组是一样的。都是根据下标来进行访问。
int main()
{
char ch[] = "hello world";
cout << ch[2] << endl;
string s1("hellor world");
cout << s1[2] << endl;
}
虽然看起来很像,但它们的底层实现是不一样的,不能混成一块。
char ch[2]的底层实现的原理是*(ch+2).
而s1[2]底层实现的原理是s1.operator[](2).
//如何遍历string对象呢?
cout << s1.size() << endl;
for (int i = 0; i < s1.size(); i++)
{
s1[i]++;
}
s1[0]--;//可以根据下标一样来修改string对象
//下标+[]
for (int i = 0; i < s1.size(); i++)
{
cout << s1[i] << endl;;
}
注意:下标+[ ]这种访问方式只能用于string和vector,list是无法使用的,因为只有顺序表类型的才可以用这种方式访问,而链表类型的是无法使用该方式访问。但是使用迭代器访问是通用的,什么类型都可以使用迭代器进行访问。
这样还要注意一点,当使用下标访问时,如果访问越界了,即非法访问了。它是会断言报错的。
int main()
{
string s1("hello world");
for (int i = 0; i < 20; i++)
{
s1[i]++;
}
}
int main()
{
try {
string s1("hello world");
s1.at(0) = 'x';//在某个位置修改
cout << s1 << endl;
s1[0] = 'h';
cout << s1 << endl;
//s1[15];//如果越界非法访问了,这样会assert警告的。
///暴力处理
s1.at(15);//温和的错误处理
}//会抛异常
catch (const exception& e)
{
cout << e.what() << endl;
}
}
2.begin/end
begin可以获得第一个字符的迭代器,end可以获得最后一个字符下一个位置的迭代器。
什么是迭代器呢?
迭代器(iterator)就像一个指针,我们可以暂且将它看作指针类型,但有时候不一定是指针类型。
使用迭代器的方法:
int main()
{
string s1("hello xiaotao");
//迭代器--->指针
string::iterator it = s1.begin();
while (it != s1.end())
{
//写
(*it)--;
++it;//类似于指针
}
while (it != s1.end())
{
//读
cout << *it << " ";
++it;
}
}
begin和end又称为正向迭代器,是从开头到结尾进行访问的。
3.rbegin/rend
rbegin是获取最后一个字符的迭代器,rend是获取第一个字符前一个位置的迭起器。
rbegin和rend可以用来进行从后往前遍历,所以又称为反向迭代器。
int main()
{
string s1("hello world");
string::reverse_iterator rit = s1.rbegin();//反向迭代器
while (rit != s1.rend())
{
cout << (*rit) << endl;//反向遍历
++rit;
}
}
4.范围for
范围for是C++11支持更简洁的新遍历方式。
int main()
{
string s1("hello xiaotao");
for (auto ch : s1)
{
cout << ch << endl;
}
}
它会依次将s1中的字符赋给ch,ch会自动识别类型。
1.范围for的底层本质上其实就是替换成迭代器。如果不支持迭代器的用法就不能支持范围for。
为什么范围for可以使用呢?
因为任何容器都支持迭代器的用法,并且用法都是类似的。
任何容器都支持迭代器,并且用法是类似的。
vector<int> v;
vector<int>::iterator vit = v.begin();
while (vit != v.end())
{
cout << *vit << endl;
++vit;
}
list<int> lit;
list<int>::iterator lit = lit.beign();
while (lit != lit.end())
{
cout << (*lit) << endl;
++lit;
}
2.所以迭代器(iterator)提供了一种统一的方式进行访问和修改容器的数据。
3.迭代器可以跟算法进行配合。
因为数据封装在容器里面。算法是无法对容器进行修改的,所以要利用迭代器。这样算法就可以通过迭代器去处理容器中的数据,比如reverse和sort。
//迭代器跟算法进行配合
reverse(s1.begin(), s1.end());
sort(s1.beign(), s1.end());
4.迭代器的类型有多种。
比如const修饰的对象就无法使用普通的迭代器来遍历。
而需要使用const迭代器。
void Func(const string& s)
{
string::const_iterator it = s.begin();
while (it !=s.end())
{
cout << *it << endl;
++it;
}
void Func(const string& s)
{
string::const_reverse_iterator rit = s.rbegin();
auto rit = s.rbegin();
while (rit != s.rend())
{
cout << *rit << endl;
++rit;
}
}
【总结】
成员函数 | 功能 |
---|---|
operator[ ] | 返回pos位置的字符,利用下标进行访问和遍历 |
begin/end | 正向迭代器,可以用来遍历和访问 |
rbegin/rend | 反向迭代器,可以倒着遍历 |
范围for | C++11支持的更简洁的遍历方式,任何容器都可以使用 |
④.string类对象查看与修改操作
1.push_back
在字符串后面尾插一个字符
int main()
{
//增操作
string s1("helllo");
//尾插一个字符
s1.push_back('6');
}
2.append
在字符串后面尾插一个字符串
int main()
{
//增操作
string s1("helllo");
//尾插一个字符
s1.push_back('6');
//尾插一个字符串
s1.append("world");
}
3.operator+=
operator+=是对+=运算符重载
它比push_back和append尾插更方便。而且接口很多
可以尾插一个字符,一个字符串,一个stirng对象等。
int main()
{
//增操作
string s1("helllo");
//尾插一个字符
s1.push_back('6');
//尾插一个字符串
s1.append("world");
//push_back是用来尾插字符的
//append是用来插入字符串的
//但是还有一种方法直接尾插,+=
s1 += '6';
s1 += "6666";
问题: 如何将整形转化成string类型呢?
int main()
{ //要求将x转化为string对象?
size_t x = 0;
cin >> x;
string xstr;
while (x)
{
size_t val = x % 10;
xstr += (val + '0');
x /= 10;
}
//逆转
reverse(xstr.begin(), xstr.end());
cout << xstr << endl;
}
4.insert
在字符串的头部插入字符/字符串
int main()
{
string s1("hello world");
//往头部插入10个’x
s1.insert(0, 10, 'x');
//insert(位置,个数,字符);
cout << s1 << endl;
//从第五个位置插入world
s1.insert(5, "world");
//insert(位置,字符串)
cout << s1 << endl;
s1.insert(0, 10, 'x');
//从第十个位置插入10个y
s1.insert(s1.begin() + 10, 10, 'y');
cout << s1 << endl;
}
5.erase
删除字符串
默认从第第一个位置开始删除。
erase(位置,删除的长度),注意删除的长度给了缺省值npos,这说明当不给定长度时,会默认将pos位置后面的字符全部删除掉。
erase也可以使用迭代器进行删除。删除的是迭代器位置上的字符。
int main()
{
string s1("hello world");
s1.erase(5, 1);
//从第5个位置删除1个字符
cout << s1 << endl;
//从第五个位置往后全部删除
s1.erase(5);
cout << s1 << endl;
//erase(位置,n个字符=nps缺省值很大的数
string s2("hello world");
s2.erase(0, 1);//相当于头删了
cout << s2 << endl;
s2.erase(s2.begin());
//删除这个迭代器位置上的字符
cout << s2 << endl;
}
6.replace
替换字符串中的字符
replace(替换的位置,要替换的长度,替换成的字符串)
int main()
{
//将world替换成xxxxxxxxxxxx
string s1("hello world hello xiaotao");
s1.replace(6, 5, "xxxxxxxxxxxxxxxxxxx");
//replace(位置,替换的个数,替换成的字符串)
s1.replace(6, 20, "666");
cout << s1 << endl;
//将s2中所以空格全部替换成20%
string s2 = "hello world hello xiaotao";
string s3;
for (auto ch : s2)
{
if (ch != ' ')
{
s3 += ch;
}
else
{
s3 += "20%";
}
}
s2 = s3;
cout << s2 << endl;
}
7.c_str
返回C格式的字符串
这个接口有什么用呢?为什么要返回C格式的字符串呢?
因为在做项目时我们要和C的一些接口函数进行配合才可以使用。
比如某个函数必须要使用C格式的字符串,那么就需要将string类型的字符串转化成C格式的字符串
int main()
{
//适用于一些C的一些接口函数配合
string filename = "test.cpp";
filename += ".zip";
FILE* fout = fopen(filename.c_str(), "r");//
//调用这个函数必须要C的字符类型
}
8.substr
从字符串pos位置开始截取n个字符,如何将其返回
substr(位置,长度)注意长度参数给了缺省值,所以如果不给定长度,则默认从pos位置往后一直截取字符。
int main()
{
string s1("hello xiaotao");
string s2, s3;
s2 = s1.substr(6);
//不给位置,则默认从该位置往后一直截取字符,直到截取完毕
cout << s2 << endl;
s3 = s1.substr(6, 7);
//从第6个位置截取7个字符
cout << s3 << endl;
}
9.find
从字符串pos位置开始往后找字符c,返回该字符在字符串中的位置。
find(字符,位置),如果不给位置,默认从第一个位置开始往后找。
int main()
{
string s1("hello xiaotao");
cout << s1.find('x') << endl;
//不给位置,则默认从第一个位置开始往后面找
cout << s1.find('o', 5);
//从第5个位置往后找字符'o'找到后返回该字符的位置
}
我们利用一个题目来深入理解find。
【切割字符串】
将任何一个网站的协议,域名,资源名分割处理:
int main()
{
string url = "ftp://www.baidu.com/?tn=65081411_1_oem_dg";
//协议 域名 资源名
size_t pos1 = url.find("://");
string protocol;
if (pos1 != string::npos)
{
protocol = url.substr(0, pos1);
//从位置0这个地方截取pos1长度的字符串赋给protocol
}
cout << protocol << endl;
size_t pos2 = url.find('/', pos1 + 3);
string domain;//域名
string uri;//资源名
if (pos2 != string::npos)
{
domain = url.substr(pos1 + 3, pos2 - (pos1 + 3));
uri = url.substr(pos2+1);
}
cout << domain << endl;
cout << uri << endl;
}
10.rfind
与find类似但不同的是find是从前往后找,而rfind是从后往前找。
从字符串pos位置往前找字符c,找到了返回该字符所在位置。
如果不给位置,默认从最后一位开始往前找。
int main()
{
string s1("hello xiaotao");
cout << s1.rfind(' ') << endl;
//找到' '返回该字符的位置
}
【总结】
成员函数 | 功能 |
---|---|
push_back | 在字符串后面尾插一个字符 |
append | 在字符串后面追加一个字符串 |
operatro+= | 在字符串后面追加str |
insert | 头插字符/字符串 |
erase | 尾删字符/字符串 |
replace | 替换字符串中的字符 |
c_str | 返回C格式的zifc |
substr | 从pos位置截取长度为n的字符串 |
find | 从pos开始往后查找字符c,返回该字符的位置 |
rfind | 从pos位置开始往前查找字符c,返回该字符的位置 |
⑤.string类非成员函数
非成员函数 | 功能 |
---|---|
operator+ | 对运算符+的重载,尽量少用,因为传值返回,深拷贝效率低 |
operator<< | 输入运算符符重载,使string类对象可以直接输入 |
operator>> | 输出运算符重载,使string类对象可以直接输出 |
getline | 可以获取一行字符串,坚持一行不结束,遇到换行才结束 |
relational operators | 各种大小比较运算符重载,使string对象可以直接比较 |
to_string | 将其他类型转化为字符串类型 |
int main()
{
//将整形转化成字符类型
string stri = to_string(1234);
//将浮点型转化成字符类型
string strd = to_string(6.11);
}
Ⅲ.牛刀小试:练习string类
1.字符串相加
字符串相加—力扣
第一种:头插方式
class Solution {
public:
string addStrings(string num1, string num2) {
int end1=num1.size()-1,end2=num2.size()-1;
int carry=0;
string strRet;
while(end1>=0||end2>=0)
{
int val1=end1>=0?(num1[end1]-'0'):0;
int val2=end2>=0?(num2[end2]-'0'):0;
int ret=val1+val2+carry;
carry=ret/10;
ret%=10;
strRet.insert(strRet.begin(),ret+'0');
--end1;
--end2;
}
if(carry==1)
{
strRet.insert(strRet.begin(),'1');
}
return strRet;
}
};
第二种:尾插+逆转
class Solution {
public:
string addStrings(string num1, string num2) {
int end1=num1.size()-1,end2=num2.size()-1;
int carry=0;
string strRet;
while(end1>=0||end2>=0)
{
int val1=end1>=0?(num1[end1]-'0'):0;
int val2=end2>=0?(num2[end2]-'0'):0;
int ret=val1+val2+carry;
carry=ret/10;
ret%=10;
strRet+=ret+'0';
--end1;
--end2;
}
if(carry==1)
{
strRet+='1';
}
reverse(strRet.begin(),strRet.end());
return strRet;
}
};
2.字符串最后一个单词的长度
字符串最后一个单词的长度—牛客
#include <iostream>
using namespace std;
#include <string>
int main()
{
string s1;
while(getline(cin,s1))
{
size_t pos=s1.rfind(' ');
cout<< s1.size()-(pos+1)<<endl;
}
return 0;
}
不能使用流输入cin来输入s1,因为cin和scan遇到空格和换行都会结束,无法完全读取成功,还有一部分会留在缓冲区。所以必须要使用getline来获取完整的一行字符串。